WIP: sh probe grid

This commit is contained in:
Tuomas Katajisto 2026-04-12 09:48:41 +03:00
parent 4099fd4af1
commit 3bcece4bc3
21 changed files with 6827 additions and 5726 deletions

Binary file not shown.

Binary file not shown.

View File

@ -10,8 +10,9 @@ Sky_Config :: struct {
sunHalo : Vector3; sunHalo : Vector3;
sunLightColor : Vector3 = .{1.0, 1.0, 1.0}; sunLightColor : Vector3 = .{1.0, 1.0, 1.0};
sunPosition : Vector3 = #run normalize(Vector3.{0.5, 0.5, 0.5}); sunPosition : Vector3 = #run normalize(Vector3.{0.5, 0.5, 0.5});
sunIntensity : float = 1.0; sunIntensity : float = 1.0;
skyIntensity : float = 10.0; skyIntensity : float = 10.0;
skyDesaturation : float = 0.0;
} }
Trixel_Data :: struct { Trixel_Data :: struct {
@ -49,6 +50,7 @@ tacoma_destroy :: (ctx: *Tacoma_Context) #foreign libtacoma;
tacoma_load_scene :: (ctx: *Tacoma_Context, sky: Sky_Config, ts: Trile_Set, world: World, include_water: s32) #foreign libtacoma; tacoma_load_scene :: (ctx: *Tacoma_Context, sky: Sky_Config, ts: Trile_Set, world: World, include_water: s32) #foreign libtacoma;
tacoma_render_rdm :: (ctx: *Tacoma_Context, world_trile_index: s32, roughness: s32, quality: s32, size: s32) -> *float #foreign libtacoma; tacoma_render_rdm :: (ctx: *Tacoma_Context, world_trile_index: s32, roughness: s32, quality: s32, size: s32) -> *float #foreign libtacoma;
tacoma_render_reference :: (ctx: *Tacoma_Context, width: s32, height: s32, eye: Vector3, target: Vector3, roughness: float, quality: s32) -> *float #foreign libtacoma; tacoma_render_reference :: (ctx: *Tacoma_Context, width: s32, height: s32, eye: Vector3, target: Vector3, roughness: float, quality: s32) -> *float #foreign libtacoma;
tacoma_render_sh_chunk :: (ctx: *Tacoma_Context, origin_x: float, origin_y: float, origin_z: float, probe_n: s32, probe_spacing: float, quality: s32) -> *float #foreign libtacoma;
tacoma_free_result :: (data: *float) #foreign libtacoma; tacoma_free_result :: (data: *float) #foreign libtacoma;

Binary file not shown.

View File

@ -27,6 +27,7 @@ Fetch_Type :: enum {
WORLD_CHUNKS; WORLD_CHUNKS;
RDM_ATLAS; RDM_ATLAS;
RDM_LOOKUP; RDM_LOOKUP;
SHGRID;
} }
Fetch_Request :: struct { Fetch_Request :: struct {
@ -63,6 +64,7 @@ g_asset_manager : Asset_Manager;
#scope_file #scope_file
#load "rdm_loader.jai"; #load "rdm_loader.jai";
#load "sh_loader.jai";
fetch_callback :: (res: *sfetch_response_t) #c_call { fetch_callback :: (res: *sfetch_response_t) #c_call {
push_context,defer_pop default_context; push_context,defer_pop default_context;
@ -139,6 +141,9 @@ handle_fetch_failed :: (req: *Fetch_Request) {
if req.rdm_pending_atlas.id != 0 then sg_destroy_image(req.rdm_pending_atlas); if req.rdm_pending_atlas.id != 0 then sg_destroy_image(req.rdm_pending_atlas);
log_error("RDM: failed to load lookup for chunk %", req.chunk_key); log_error("RDM: failed to load lookup for chunk %", req.chunk_key);
case .SHGRID;
sh_loader_handle_failed(req);
} }
} }
@ -178,6 +183,7 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
if ok { if ok {
set_loaded_world(world); set_loaded_world(world);
rdm_loader_enqueue_world(*get_current_world().world); rdm_loader_enqueue_world(*get_current_world().world);
shgrid_loader_enqueue_world(*get_current_world().world);
log_info("Loaded world (legacy): %", req.world_name); log_info("Loaded world (legacy): %", req.world_name);
} else { } else {
log_error("Failed to parse world '%'", req.world_name); log_error("Failed to parse world '%'", req.world_name);
@ -196,6 +202,7 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
if ok { if ok {
set_loaded_world(world); set_loaded_world(world);
rdm_loader_enqueue_world(*get_current_world().world); rdm_loader_enqueue_world(*get_current_world().world);
shgrid_loader_enqueue_world(*get_current_world().world);
log_info("Loaded world: %", req.world_name); log_info("Loaded world: %", req.world_name);
} else { } else {
log_error("Failed to parse world '%'", req.world_name); log_error("Failed to parse world '%'", req.world_name);
@ -290,6 +297,9 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
sg_destroy_image(req.rdm_pending_atlas); sg_destroy_image(req.rdm_pending_atlas);
} }
case .SHGRID;
sh_loader_handle_completed(req, data);
} }
} }

100
src/assets/sh_loader.jai Normal file
View File

@ -0,0 +1,100 @@
// SH probe grid streaming helpers.
// The unified fetch queue lives in asset_manager.jai; these functions
// add entries to that queue and handle completed/failed fetches.
#scope_export
shgrid_loader_enqueue_world :: (world: *World) {
for *chunk: world.chunks {
if chunk.sh_valid then continue;
af := *g_asset_manager.active[CHANNEL_RDM];
if af.occupied && af.req.type == .SHGRID
&& af.req.world_name == world.name && af.req.chunk_key == chunk.coord
then continue;
already_queued := false;
for g_asset_manager.rdm_queue {
if it.type == .SHGRID && it.world_name == world.name && it.chunk_key == chunk.coord {
already_queued = true;
break;
}
}
if already_queued then continue;
req : Fetch_Request;
req.type = .SHGRID;
req.world_name = world.name;
req.chunk_key = chunk.coord;
req.path = shgrid_filename(world.name, chunk.coord);
array_add(*g_asset_manager.rdm_queue, req);
}
}
sh_loader_handle_failed :: (req: *Fetch_Request) {
log_error("SH: failed to load probe grid for chunk %", req.chunk_key);
}
sh_loader_handle_completed :: (req: *Fetch_Request, data: []u8) {
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name then return;
header_size := cast(s64) size_of(SH_Grid_File_Header);
if data.count < header_size {
log_error("SH: probe grid too small for chunk %", req.chunk_key);
return;
}
sh_header := cast(*SH_Grid_File_Header) data.data;
if sh_header.magic != SH_FILE_MAGIC {
log_error("SH: bad magic for chunk %", req.chunk_key);
return;
}
n := sh_header.probe_n;
expected_floats := cast(s64) n * n * n * 12;
if data.count < header_size + expected_floats * size_of(float) {
log_error("SH: probe grid data too short for chunk %", req.chunk_key);
return;
}
// Pack flat float[n^3 * 12] → 3-RGBA16F-texels-per-probe 2D texture layout.
// Texture dims: (n*3) x (n*n). Probe (px,py,pz) at row (pz*n+py), col (px*3+k).
// Coefficient layout in 3 texels (12 slots):
// t0: R.c0-3 t1: G.c0-3 t2: B.c0-3
tex_w := n * 3;
n_tex := cast(s64) tex_w * n * n;
packed := NewArray(n_tex * 4, u16);
src := cast(*float) (data.data + header_size);
for pz: 0..n-1 {
for py: 0..n-1 {
for px: 0..n-1 {
probe_idx := px + py * n + pz * n * n;
s := src + probe_idx * 12;
for k: 0..2 {
tex_idx := (pz * n + py) * tex_w + (px * 3 + k);
d := packed.data + tex_idx * 4;
for ch: 0..3 {
d[ch] = f32_to_f16((s + k * 4 + ch).*);
}
}
}
}
}
sh_imgdata : sg_image_data;
sh_imgdata.subimage[0][0] = .{ packed.data, cast(u64)(n_tex * 4 * size_of(u16)) };
sh_desc : sg_image_desc = .{
width = tex_w,
height = n * n,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = sh_imgdata,
};
chunk := table_find_pointer(*curworld.world.chunks, req.chunk_key);
if chunk != null {
if chunk.sh_probe_grid.id != 0 then sg_destroy_image(chunk.sh_probe_grid);
chunk.sh_probe_grid = sg_make_image(*sh_desc);
chunk.sh_valid = true;
log_debug("SH: loaded probe grid for chunk %", req.chunk_key);
}
free(packed.data);
}

View File

@ -80,8 +80,6 @@ draw_editor :: () {
#if OS != .WASM { #if OS != .WASM {
if !in_editor_view then return; if !in_editor_view then return;
bypass_postprocess = (current_editor_view == .Trile_Editor);
if current_editor_view == { if current_editor_view == {
case .Trile_Editor; case .Trile_Editor;
draw_trile_editor(); draw_trile_editor();
@ -97,7 +95,6 @@ tick_editor_ui :: () {
if is_action_start(Editor_Action.TOGGLE_EDITOR) { if is_action_start(Editor_Action.TOGGLE_EDITOR) {
in_editor_view = !in_editor_view; in_editor_view = !in_editor_view;
g_mixer.paused = in_editor_view; g_mixer.paused = in_editor_view;
if !in_editor_view then bypass_postprocess = false;
clear_particles(); clear_particles();
} }

View File

@ -264,6 +264,10 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
} }
} }
r.y += r.h; r.y += r.h;
if GR.button(r, "Bake all chunk SH grids", *t_button_selectable(theme, !sh_bake.active)) {
if curworld.valid && !sh_bake.active sh_bake_start(tacomaSamples);
}
r.y += r.h;
if rdm_bake.active { if rdm_bake.active {
total := cast(s32) rdm_bake.jobs.count; total := cast(s32) rdm_bake.jobs.count;
done := rdm_bake.current_job; done := rdm_bake.current_job;
@ -271,6 +275,13 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
GR.label(r, tprint("Baking RDMs: %/% (\%%)", done, total, pct), *t_label_left(theme)); GR.label(r, tprint("Baking RDMs: %/% (\%%)", done, total, pct), *t_label_left(theme));
r.y += r.h; r.y += r.h;
} }
if sh_bake.active {
total := cast(s32) sh_bake.chunk_keys.count;
done := sh_bake.current_chunk;
pct := ifx total > 0 then done * 100 / total else 0;
GR.label(r, tprint("Baking SH grids: %/% (\%%)", done, total, pct), *t_label_left(theme));
r.y += r.h;
}
if current_screenshot.valid { if current_screenshot.valid {
aspect := cast(float)current_screenshot.width / cast(float)current_screenshot.height; aspect := cast(float)current_screenshot.width / cast(float)current_screenshot.height;
@ -304,6 +315,14 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.y += r.h; r.y += r.h;
GR.slider(r, *tacomaSaturation, 0.5, 3.0, 0.1, *theme.slider_theme); GR.slider(r, *tacomaSaturation, 0.5, 3.0, 0.1, *theme.slider_theme);
r.y += r.h; r.y += r.h;
GR.label(r, tprint("Sky Scale (bake): %", formatFloat(bake_sky_scale, trailing_width=2)), *t_label_left(theme));
r.y += r.h;
GR.slider(r, *bake_sky_scale, 0.0, 5.0, 0.05, *theme.slider_theme);
r.y += r.h;
GR.label(r, tprint("Sky Desaturation (bake): %", formatFloat(bake_sky_desaturation, trailing_width=2)), *t_label_left(theme));
r.y += r.h;
GR.slider(r, *bake_sky_desaturation, 0.0, 1.0, 0.05, *theme.slider_theme);
r.y += r.h;
} else { } else {
@ -517,7 +536,7 @@ editor_edit_y :: () -> float {
} }
tick_level_editor :: () { tick_level_editor :: () {
#if FLAG_TACOMA_ENABLED { rdm_bake_tick(); } #if FLAG_TACOMA_ENABLED { rdm_bake_tick(); sh_bake_tick(); }
tick_level_editor_camera(); tick_level_editor_camera();
tick_particles(cast(float)delta_time); tick_particles(cast(float)delta_time);

View File

@ -12,3 +12,33 @@ RDM_FILE_MAGIC :: u32.[0x4D445254][0]; // "TRDM" as little-endian u32
rdm_chunk_filename :: (world_name: string, chunk_key: Chunk_Key, suffix: string) -> string { rdm_chunk_filename :: (world_name: string, chunk_key: Chunk_Key, suffix: string) -> string {
return sprint("%/worlds/%/%_%_%.%", GAME_RESOURCES_DIR, world_name, chunk_key.x, chunk_key.y, chunk_key.z, suffix); return sprint("%/worlds/%/%_%_%.%", GAME_RESOURCES_DIR, world_name, chunk_key.x, chunk_key.y, chunk_key.z, suffix);
} }
// SH probe grid (2 probes per trile per axis = 64x64x64 per 32x32x32 chunk)
SH_FILE_MAGIC :: u32.[0x44475348][0]; // "SHGD" as little-endian u32
SH_PROBE_N :: 64; // probes per axis (2 per trile in a 32-trile chunk)
SH_TEX_W :: SH_PROBE_N * 3; // 192 — 3 RGBA texels per probe along X
SH_Grid_File_Header :: struct {
magic: u32;
version: u16;
probe_n: s32;
}
shgrid_filename :: (world_name: string, chunk_key: Chunk_Key) -> string {
return sprint("%/worlds/%/%_%_%.shgrid", GAME_RESOURCES_DIR, world_name, chunk_key.x, chunk_key.y, chunk_key.z);
}
f32_to_f16 :: (f: float) -> u16 {
b := (cast(*u32) *f).*;
sign := (b >> 31) & 0x1;
exp := cast(s32)((b >> 23) & 0xFF) - 127;
mant := b & 0x7FFFFF;
if exp >= 16 return cast(u16)((sign << 15) | 0x7C00); // clamp to inf
if exp >= -14 return cast(u16)((sign << 15) | (cast(u32)(exp + 15) << 10) | (mant >> 13));
if exp >= -24 {
shift := cast(u32)(-exp - 1);
return cast(u16)((sign << 15) | ((mant | 0x800000) >> shift));
}
return cast(u16)(sign << 15); // underflow → ±0
}

View File

@ -0,0 +1,35 @@
test_f32_to_f16 :: () {
s := begin_suite("f32_to_f16");
check(*s, "0.0 → 0x0000", f32_to_f16(0.0) == 0x0000);
check(*s, "-0.0 → 0x8000", f32_to_f16(-0.0) == 0x8000);
check(*s, "1.0 → 0x3C00", f32_to_f16(1.0) == 0x3C00);
check(*s, "-1.0 → 0xBC00", f32_to_f16(-1.0) == 0xBC00);
check(*s, "0.5 → 0x3800", f32_to_f16(0.5) == 0x3800);
check(*s, "2.0 → 0x4000", f32_to_f16(2.0) == 0x4000);
check(*s, "1.5 → 0x3E00", f32_to_f16(1.5) == 0x3E00);
// Max normal f16 = 65504
check(*s, "65504.0 → 0x7BFF", f32_to_f16(65504.0) == 0x7BFF);
// Min normal f16 = 2^-14
check(*s, "2^-14 → 0x0400", f32_to_f16(cast(float)(1.0 / 16384.0)) == 0x0400);
// Overflow clamps to infinity
check(*s, "65536.0 → 0x7C00 (inf)", f32_to_f16(65536.0) == 0x7C00);
check(*s, "1e10 → 0x7C00 (inf)", f32_to_f16(1.0e10) == 0x7C00);
check(*s, "-65536.0 → 0xFC00 (-inf)", f32_to_f16(-65536.0) == 0xFC00);
// Denormals: 2^-15 is the largest f16 denormal base
check(*s, "2^-15 → 0x0200", f32_to_f16(cast(float)(1.0 / 32768.0)) == 0x0200);
// Smallest representable f16 denormal = 2^-24
check(*s, "2^-24 → 0x0001", f32_to_f16(cast(float)(1.0 / 16777216.0)) == 0x0001);
// Underflow to zero
check(*s, "2^-25 → 0x0000", f32_to_f16(cast(float)(1.0 / 33554432.0)) == 0x0000);
check(*s, "-2^-25 → 0x8000", f32_to_f16(cast(float)(-1.0 / 33554432.0)) == 0x8000);
end_suite(s);
}
#run test_f32_to_f16();

View File

@ -63,6 +63,16 @@ RDM_Bake_State :: struct {
rdm_bake : RDM_Bake_State; rdm_bake : RDM_Bake_State;
SH_Bake_State :: struct {
active : bool;
quality : s32;
include_water : bool;
chunk_keys : [..]Chunk_Key;
current_chunk : s32;
}
sh_bake : SH_Bake_State;
rdm_job_size :: (job: RDM_Bake_Job) -> s32 { rdm_job_size :: (job: RDM_Bake_Job) -> s32 {
if job.size_override > 0 then return job.size_override; if job.size_override > 0 then return job.size_override;
return g_rdm_default_sizes[job.roughness]; return g_rdm_default_sizes[job.roughness];
@ -221,20 +231,9 @@ rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys:
} }
} }
sky : Tacoma.Sky_Config; sky : Tacoma.Sky_Config = world_to_sky_config(world);
sky.skyBase = world.conf.skyBase; blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32) trile_list.count};
sky.skyTop = world.conf.skyTop; tlas : Tacoma.World = .{world_triles.data, cast(s32) world_triles.count};
sky.sunDisk = world.conf.sunDisk;
sky.horizonHalo = world.conf.horizonHalo;
sky.sunHalo = world.conf.sunHalo;
sky.sunLightColor = world.conf.sunLightColor;
sky.sunPosition = world.conf.sunPosition;
sky.sunIntensity = world.conf.sunIntensity;
sky.skyIntensity = world.conf.skyIntensity;
blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32)trile_list.count};
tlas : Tacoma.World = .{world_triles.data, cast(s32)world_triles.count};
ctx = Tacoma.tacoma_init("./modules/Tacoma/"); ctx = Tacoma.tacoma_init("./modules/Tacoma/");
Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water); Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water);
@ -493,72 +492,64 @@ rdm_cleanup_chunk_bakes :: () {
rdm_chunk_bakes = .{}; rdm_chunk_bakes = .{};
} }
tacoma_start :: (world: World, include_water: bool) { bake_sky_scale : float = 1.0;
// Trile BLASes. bake_sky_desaturation : float = 0.0;
trile_list : [..]Tacoma.Trile_Data;
trile_list.allocator = temp;
// BLAS instances to create TLAS.
world_triles : [..]Tacoma.World_Trile;
world_triles.allocator = temp;
// Build trile type list and gather world positions from chunks. world_to_sky_config :: (world: World) -> Tacoma.Sky_Config {
trile_name_to_index: Table(string, s32); sky : Tacoma.Sky_Config;
trile_name_to_index.allocator = temp; sky.skyBase = world.conf.skyBase;
sky.skyTop = world.conf.skyTop;
sky.sunDisk = world.conf.sunDisk;
sky.horizonHalo = world.conf.horizonHalo;
sky.sunHalo = world.conf.sunHalo;
sky.sunLightColor = world.conf.sunLightColor;
sky.sunPosition = world.conf.sunPosition;
sky.sunIntensity = world.conf.sunIntensity;
sky.skyIntensity = world.conf.skyIntensity * bake_sky_scale;
sky.skyDesaturation = bake_sky_desaturation;
return sky;
}
// Build trile BLASes + world TLAS from a world, then initialize ctx.
tacoma_init_scene :: (world: World, include_water: bool) {
trile_list : [..]Tacoma.Trile_Data; trile_list.allocator = temp;
world_triles : [..]Tacoma.World_Trile; world_triles.allocator = temp;
trile_name_to_index : Table(string, s32); trile_name_to_index.allocator = temp;
for chunk: world.chunks { for chunk: world.chunks {
for group: chunk.groups { for group: chunk.groups {
// Ensure this trile type is in the list.
success, idx := table_find(*trile_name_to_index, group.trile_name); success, idx := table_find(*trile_name_to_index, group.trile_name);
if !success { if !success {
trile := get_trile(group.trile_name); trile, trile_ok := get_trile(group.trile_name);
if !trile_ok continue;
ttrile : Tacoma.Trile_Data; ttrile : Tacoma.Trile_Data;
for x: 0..15 { for x: 0..15 for y: 0..15 for z: 0..15 {
for y: 0..15 { ttrile.trixels[x][y][z] = .{
for z: 0..15 { trile.trixels[x][y][z].empty,
ttrile.trixels[x][y][z] = .{ trile.trixels[x][y][z].material.color,
trile.trixels[x][y][z].empty, material_encode_to_float(trile.trixels[x][y][z].material)
trile.trixels[x][y][z].material.color, };
material_encode_to_float(trile.trixels[x][y][z].material)
};
}
}
} }
gfx := get_trile_gfx(group.trile_name); gfx := get_trile_gfx(group.trile_name);
ttrile.vertices = gfx.vertices.data; ttrile.vertices = gfx.vertices.data;
ttrile.vertexCount = cast(s32) (gfx.vertices.count / 3); ttrile.vertexCount = cast(s32)(gfx.vertices.count / 3);
idx = cast(s32) trile_list.count; idx = cast(s32) trile_list.count;
array_add(*trile_list, ttrile); array_add(*trile_list, ttrile);
table_set(*trile_name_to_index, group.trile_name, idx); table_set(*trile_name_to_index, group.trile_name, idx);
} }
for inst: group.instances { for inst: group.instances {
wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z); wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z);
array_add(*world_triles, Tacoma.World_Trile.{idx, Vector3.{cast(float) wx, cast(float) wy, cast(float) wz}, cast(s32) inst.orientation}); array_add(*world_triles, Tacoma.World_Trile.{idx,
Vector3.{cast(float) wx, cast(float) wy, cast(float) wz},
cast(s32) inst.orientation});
} }
} }
} }
sky : Tacoma.Sky_Config; sky : Tacoma.Sky_Config = world_to_sky_config(world);
blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32) trile_list.count};
sky.skyBase = world.conf.skyBase; tlas : Tacoma.World = .{world_triles.data, cast(s32) world_triles.count};
sky.skyTop = world.conf.skyTop;
sky.sunDisk = world.conf.sunDisk;
sky.horizonHalo = world.conf.horizonHalo;
sky.sunHalo = world.conf.sunHalo;
sky.sunLightColor = world.conf.sunLightColor;
sky.sunPosition = world.conf.sunPosition;
sky.sunIntensity = world.conf.sunIntensity;
sky.skyIntensity = world.conf.skyIntensity;
blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32)trile_list.count};
for world_triles {
log_debug("World trile %", it);
}
tlas : Tacoma.World = .{world_triles.data, cast(s32)world_triles.count};
ctx = Tacoma.tacoma_init("./modules/Tacoma/"); ctx = Tacoma.tacoma_init("./modules/Tacoma/");
log_debug("CTX: %", ctx);
Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water); Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water);
} }
@ -607,20 +598,126 @@ tacoma_handle_result :: (ptr: *float, w: s32, h: s32) {
} }
gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, include_water: bool, world: World) { gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, include_water: bool, world: World) {
tacoma_start(world, include_water); tacoma_init_scene(world, include_water);
ptr := Tacoma.tacoma_render_reference(ctx, w, h, eye, target, 0.01, quality); ptr := Tacoma.tacoma_render_reference(ctx, w, h, eye, target, 0.01, quality);
tacoma_handle_result(ptr, w, h); tacoma_handle_result(ptr, w, h);
tacoma_stop(); tacoma_stop();
} }
gen_rdm :: (quality: s32, include_water: bool, world: World) { gen_rdm :: (quality: s32, include_water: bool, world: World) {
tacoma_start(world, include_water); tacoma_init_scene(world, include_water);
size := g_rdm_default_sizes[0]; size := g_rdm_default_sizes[0];
ptr := Tacoma.tacoma_render_rdm(ctx, 0, 0, quality, size); ptr := Tacoma.tacoma_render_rdm(ctx, 0, 0, quality, size);
tacoma_handle_result(ptr, 2 * size, 3 * size); tacoma_handle_result(ptr, 2 * size, 3 * size);
tacoma_stop(); tacoma_stop();
} }
sh_bake_start :: (quality: s32 = 50, include_water: bool = false) {
if sh_bake.active then return;
curworld := get_current_world();
if !curworld.valid { log_warn("sh_bake_start: no world loaded"); return; }
world := *curworld.world;
for _, key: world.chunks array_add(*sh_bake.chunk_keys, key);
if sh_bake.chunk_keys.count == 0 then return;
tacoma_init_scene(world.*, include_water);
sh_bake.active = true;
sh_bake.quality = quality;
sh_bake.include_water = include_water;
sh_bake.current_chunk = 0;
} @Command
sh_bake_tick :: () {
if !sh_bake.active then return;
if sh_bake.current_chunk >= cast(s32) sh_bake.chunk_keys.count {
sh_bake_finish();
return;
}
curworld := get_current_world();
if !curworld.valid { sh_bake_finish(); return; }
n :: SH_PROBE_N;
tex_w :: SH_TEX_W;
chunk_key := sh_bake.chunk_keys[sh_bake.current_chunk];
chunk := table_find_pointer(*curworld.world.chunks, chunk_key);
sh_bake.current_chunk += 1;
if chunk == null then return;
ox, oy, oz := chunk_local_to_world(chunk_key, 0, 0, 0);
ptr := Tacoma.tacoma_render_sh_chunk(ctx,
cast(float) ox, cast(float) oy, cast(float) oz,
n, 0.5, sh_bake.quality);
tex_halfs :: tex_w * n * n * 4;
packed : *u16 = cast(*u16) alloc(tex_halfs * size_of(u16));
defer free(packed);
src := ptr;
for pz: 0..n-1 for py: 0..n-1 for px: 0..n-1 {
probe_idx := px + py * n + pz * n * n;
s := src + probe_idx * 12;
for k: 0..2 {
tex_idx := (pz * n + py) * tex_w + (px * 3 + k);
d := packed + tex_idx * 4;
for ch: 0..3 {
(d + ch).* = f32_to_f16((s + k * 4 + ch).*);
}
}
}
tex_byte_size := cast(u64)(tex_halfs * size_of(u16));
imgdata : sg_image_data;
imgdata.subimage[0][0] = .{packed, tex_byte_size};
sh_desc : sg_image_desc = .{
width = tex_w,
height = n * n,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = imgdata,
};
if chunk.sh_valid sg_destroy_image(chunk.sh_probe_grid);
chunk.sh_probe_grid = sg_make_image(*sh_desc);
chunk.sh_valid = true;
chunk.sh_dirty = false;
#if OS != .WASM {
shgrid_save_to_disk(curworld.world.name, chunk_key, ptr, n);
}
Tacoma.tacoma_free_result(ptr);
log_info("SH baked chunk % (%/%)", chunk_key, sh_bake.current_chunk, sh_bake.chunk_keys.count);
}
sh_bake_finish :: () {
tacoma_stop();
array_free(sh_bake.chunk_keys);
sh_bake = .{};
log_info("SH bake complete.");
}
shgrid_save_to_disk :: (world_name: string, chunk_key: Chunk_Key, data: *float, probe_n: s32) {
#if OS != .WASM {
file :: #import "File";
path := shgrid_filename(world_name, chunk_key);
builder : String_Builder;
header := SH_Grid_File_Header.{
magic = SH_FILE_MAGIC,
version = 1,
probe_n = probe_n,
};
write_bytes(*builder, *header, size_of(SH_Grid_File_Header));
total_floats := cast(s64) probe_n * probe_n * probe_n * 12;
write_bytes(*builder, data, total_floats * size_of(float));
file.write_entire_file(path, builder_to_string(*builder));
}
}
// --- RDM disk persistence --- // --- RDM disk persistence ---
// (RDM_File_Header, RDM_FILE_MAGIC, rdm_chunk_filename, rdm_load_from_disk // (RDM_File_Header, RDM_FILE_MAGIC, rdm_chunk_filename, rdm_load_from_disk
// are defined in rdm_disk.jai which is always loaded on non-WASM builds.) // are defined in rdm_disk.jai which is always loaded on non-WASM builds.)

View File

@ -221,15 +221,20 @@ backend_draw_trile_positions_main :: (trile : string, amount : s32, worldConf: *
// The shader gates all RDM sampling on atlas_rect.z > 0, which the // The shader gates all RDM sampling on atlas_rect.z > 0, which the
// fallback texture returns as 0, so the ambient fallback path is taken. // fallback texture returns as 0, so the ambient fallback path is taken.
curworld := get_current_world(); curworld := get_current_world();
rdm_chunk := table_find_pointer(*curworld.world.chunks, chunk_key); chunk := table_find_pointer(*curworld.world.chunks, chunk_key);
if rdm_chunk != null && rdm_chunk.rdm_valid { if chunk != null && chunk.rdm_valid {
bindings.images[3] = rdm_chunk.rdm_lookup; bindings.images[3] = chunk.rdm_lookup;
bindings.images[4] = rdm_chunk.rdm_atlas; bindings.images[4] = chunk.rdm_atlas;
} else { } else {
bindings.images[3] = g_rdm_fallback; bindings.images[3] = g_rdm_fallback;
bindings.images[4] = g_rdm_fallback; bindings.images[4] = g_rdm_fallback;
} }
bindings.images[5] = g_brdf_lut; bindings.images[5] = g_brdf_lut;
if chunk != null && chunk.sh_valid {
bindings.images[6] = chunk.sh_probe_grid;
} else {
bindings.images[6] = g_sh_fallback;
}
bindings.samplers[3] = gPipelines.trile.bind.samplers[3]; bindings.samplers[3] = gPipelines.trile.bind.samplers[3];
fs_params : Trile_Fs_Params; fs_params : Trile_Fs_Params;
@ -248,6 +253,7 @@ backend_draw_trile_positions_main :: (trile : string, amount : s32, worldConf: *
fs_params.is_preview = preview_mode; fs_params.is_preview = preview_mode;
fs_params.rdm_tint = lc.rdm_tint.component; fs_params.rdm_tint = lc.rdm_tint.component;
fs_params.rdm_diff_saturation = worldConf.rdmDiffSaturation; fs_params.rdm_diff_saturation = worldConf.rdmDiffSaturation;
fs_params.sh_enabled = ifx (chunk != null && chunk.sh_valid && !in_reflection_pass) then cast(s32)1 else cast(s32)0;
sg_apply_bindings(*bindings); sg_apply_bindings(*bindings);
sg_apply_uniforms(UB_trile_fs_params, *(sg_range.{ ptr = *fs_params, size = size_of(type_of(fs_params)) })); sg_apply_uniforms(UB_trile_fs_params, *(sg_range.{ ptr = *fs_params, size = size_of(type_of(fs_params)) }));

View File

@ -10,6 +10,7 @@ Pipeline_Binding :: struct {
g_specular_lut : sg_image; g_specular_lut : sg_image;
g_brdf_lut : sg_image; g_brdf_lut : sg_image;
g_rdm_fallback : sg_image; // 1x1 black image used when a chunk has no baked RDM data g_rdm_fallback : sg_image; // 1x1 black image used when a chunk has no baked RDM data
g_sh_fallback : sg_image; // 1x1 black 2D image used when a chunk has no SH probe grid
g_shadowmap : sg_image; g_shadowmap : sg_image;
g_shadowmap_img : sg_image; g_shadowmap_img : sg_image;
@ -1286,6 +1287,18 @@ init_brdf_lut :: () {
data = imgdata, data = imgdata,
}; };
g_rdm_fallback = sg_make_image(*desc); g_rdm_fallback = sg_make_image(*desc);
// 1x1 RGBA16F 2D texture — fallback when a chunk has no SH probe grid.
zero_sh : [4]u16 = .[0, 0, 0, 0];
sh_imgdata : sg_image_data;
sh_imgdata.subimage[0][0] = .{ zero_sh.data, size_of(type_of(zero_sh)) };
sh_desc := sg_image_desc.{
width = 1,
height = 1,
pixel_format = .RGBA16F,
data = sh_imgdata,
};
g_sh_fallback = sg_make_image(*sh_desc);
} }
} }

View File

@ -58,7 +58,7 @@ create_shadow_viewproj :: (cam: *Camera, conf: *World_Config) -> Matrix4 {
B.z, C.z, A.z, 0, B.z, C.z, A.z, 0,
-dot(B, sunCameraPosition), -dot(C, sunCameraPosition), -dot(A, sunCameraPosition), 1 -dot(B, sunCameraPosition), -dot(C, sunCameraPosition), -dot(A, sunCameraPosition), 1
}; };
proj := matrix_ortho(-15, 15, -15, 15, 0, 100); proj := matrix_ortho(-60, 60, -60, 60, 0, 100);
return view*proj; return view*proj;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -124,15 +124,16 @@ void main() {
} }
} }
float r = texture(sampler2D(pptex, ppsmp), distorted_texcoord + vec2(chromatic_aberration_intensity, 0.0)).r; vec3 sampled_color_hdr;
float g = texture(sampler2D(pptex, ppsmp), distorted_texcoord).g; if(chromatic_aberration_intensity > 0.01) {
float b = texture(sampler2D(pptex, ppsmp), distorted_texcoord - vec2(chromatic_aberration_intensity, 0.0)).b; float r = texture(sampler2D(pptex, ppsmp), distorted_texcoord + vec2(chromatic_aberration_intensity, 0.0)).r;
float g = texture(sampler2D(pptex, ppsmp), distorted_texcoord).g;
float b = texture(sampler2D(pptex, ppsmp), distorted_texcoord - vec2(chromatic_aberration_intensity, 0.0)).b;
sampled_color_hdr = vec3(r,g,b + dof_min * 0.00000000000001);
} else {
sampled_color_hdr = texture(sampler2D(pptex, ppsmp), distorted_texcoord).rgb;
}
vec3 out_focus = texture_bicubic(dof_tex, dof_smp, distorted_texcoord, vec2(dof_tex_width, dof_tex_height)).xyz;
vec3 in_focus = vec3(r, g, b);
vec4 position = texture(sampler2D(pos_buf, dof_smp), distorted_texcoord);
float blur = smoothstep(dof_min, dof_max, abs(position.z + dof_point));
vec3 sampled_color_hdr = mix(in_focus, out_focus, blur);
vec3 bloom_color = texture(sampler2D(bloom_tex, bloom_smp), distorted_texcoord).rgb; vec3 bloom_color = texture(sampler2D(bloom_tex, bloom_smp), distorted_texcoord).rgb;
vec3 color_hdr = (sampled_color_hdr + bloom_color * bloom_amount) * exposure; vec3 color_hdr = (sampled_color_hdr + bloom_color * bloom_amount) * exposure;

View File

@ -104,6 +104,7 @@ layout(binding=3) uniform trile_fs_params {
int is_preview; int is_preview;
vec3 rdm_tint; vec3 rdm_tint;
float rdm_diff_saturation; float rdm_diff_saturation;
int sh_enabled;
}; };
layout(binding = 0) uniform texture2D triletex; layout(binding = 0) uniform texture2D triletex;
@ -115,6 +116,7 @@ layout(binding = 2) uniform sampler shadowsmp;
layout(binding = 3) uniform texture2D rdm_lookup; layout(binding = 3) uniform texture2D rdm_lookup;
layout(binding = 4) uniform texture2D rdm_atlas; layout(binding = 4) uniform texture2D rdm_atlas;
layout(binding = 5) uniform texture2D brdf_lut; layout(binding = 5) uniform texture2D brdf_lut;
layout(binding = 6) uniform texture2D sh_chunk;
layout(binding = 3) uniform sampler rdmsmp; layout(binding = 3) uniform sampler rdmsmp;
const float PI = 3.1415927; const float PI = 3.1415927;
@ -308,16 +310,81 @@ vec3 rdm_indirect_diffuse(vec3 N, vec3 diff, ivec3 local_pos) {
s3 = ivec3(0, isign(delta.y), isign(delta.x)); s3 = ivec3(0, isign(delta.y), isign(delta.x));
} }
vec3 p0 = rdm_sample_diff_probe(N, ivec3(mod(vec3(local_pos), 32.0)), ambient); vec3 p0 = rdm_sample_diff_probe(N, clamp(local_pos, ivec3(0), ivec3(31)), ambient);
vec3 p1 = rdm_sample_diff_probe(N, ivec3(mod(vec3(local_pos + s1), 32.0)), ambient); vec3 p1 = rdm_sample_diff_probe(N, clamp(local_pos + s1, ivec3(0), ivec3(31)), ambient);
vec3 p2 = rdm_sample_diff_probe(N, ivec3(mod(vec3(local_pos + s2), 32.0)), ambient); vec3 p2 = rdm_sample_diff_probe(N, clamp(local_pos + s2, ivec3(0), ivec3(31)), ambient);
vec3 p3 = rdm_sample_diff_probe(N, ivec3(mod(vec3(local_pos + s1 + s2),32.0)), ambient); vec3 p3 = rdm_sample_diff_probe(N, clamp(local_pos + s1 + s2,ivec3(0), ivec3(31)), ambient);
return smix(smix(p0, p1, abs(delta.x)), return smix(smix(p0, p1, abs(delta.x)),
smix(p2, p3, abs(delta.x)), smix(p2, p3, abs(delta.x)),
abs(delta.y)); abs(delta.y));
} }
// ---- SH PROBE GRID ----
// Each probe stores 27 L2 SH coefficients (9 per RGB channel), packed into
// 3 RGBA16F texels per probe along the X axis of a 192x4096 2D texture.
// Row = probe.z * 64 + probe.y, col = probe.x * 3 + k.
// Texel layout per probe (px,py,pz):
// t0: R.c0-3 t1: G.c0-3 t2: B.c0-3
// Probe index from chunk-local world position p (0..32 range):
// ivec3(floor(p * 2.0)) clamped to [0,63]
// SH evaluation: Lambertian irradiance (L1 only), A0=PI, A1=2PI/3.
vec3 sh_eval(ivec3 probe, vec3 N) {
int base = probe.x * 3;
int row = probe.z * 64 + probe.y;
vec4 t0 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base, row), 0);
vec4 t1 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+1, row), 0);
vec4 t2 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+2, row), 0);
float x = N.x, y = N.y, z = N.z;
float r = 0.886227*t0.x + 1.023327*(t0.w*x + t0.y*y + t0.z*z);
float g = 0.886227*t1.x + 1.023327*(t1.w*x + t1.y*y + t1.z*z);
float b = 0.886227*t2.x + 1.023327*(t2.w*x + t2.y*y + t2.z*z);
return max(vec3(r, g, b) / PI, vec3(0.0));
}
// Sum of L0 irradiance across RGB — proxy for total incoming energy.
// Near-zero means the probe is buried inside solid geometry.
float sh_probe_energy(ivec3 probe) {
int base = probe.x * 3;
int row = probe.z * 64 + probe.y;
vec4 t0 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base, row), 0);
vec4 t1 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+1, row), 0);
vec4 t2 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+2, row), 0);
return max(0.886227 * (t0.x + t1.x + t2.x), 0.0);
}
// Trilinear SH evaluation with confidence weighting.
// Probes with near-zero energy (buried in geometry) are downweighted
// so they don't pull the result toward black.
vec3 sh_eval_trilinear(ivec3 p0, ivec3 p1, vec3 t, vec3 N) {
float wx[2] = float[2](1.0 - t.x, t.x);
float wy[2] = float[2](1.0 - t.y, t.y);
float wz[2] = float[2](1.0 - t.z, t.z);
vec3 result = vec3(0.0);
float total_w = 0.0;
for (int iz = 0; iz < 2; iz++) {
for (int iy = 0; iy < 2; iy++) {
for (int ix = 0; ix < 2; ix++) {
ivec3 probe = ivec3(
ix == 0 ? p0.x : p1.x,
iy == 0 ? p0.y : p1.y,
iz == 0 ? p0.z : p1.z
);
float w = wx[ix] * wy[iy] * wz[iz] * sh_probe_energy(probe);
result += sh_eval(probe, N) * w;
total_w += w;
}
}
}
return total_w > 0.001 ? result / total_w : vec3(0.0);
}
// ---- HSV ---- // ---- HSV ----
vec3 rgb2hsv(vec3 c) { vec3 rgb2hsv(vec3 c) {
@ -386,7 +453,7 @@ void main() {
return; return;
} }
// Evaluate direct light. // ---- 1. VIEW / LIGHT VECTORS ----
vec3 V = normalize(cam - vpos); vec3 V = normalize(cam - vpos);
vec3 L = normalize(sunPosition); vec3 L = normalize(sunPosition);
vec3 H = normalize(V + L); vec3 H = normalize(V + L);
@ -394,82 +461,98 @@ void main() {
float NdotV = max(dot(N, V), 0.0); float NdotV = max(dot(N, V), 0.0);
float HdotV = max(dot(H, V), 0.0); float HdotV = max(dot(H, V), 0.0);
// ---- 2. PBR TERMS ----
vec3 F0 = mix(vec3(0.04), albedo, metallic); vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 F = fresnelSchlick(HdotV, F0); vec3 F = fresnelSchlick(HdotV, F0);
float NDF = DistributionGGX(N, H, roughness); float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness); float G = GeometrySmith(N, V, L, roughness);
vec3 kD = (1.0 - F) * (1.0 - metallic);
vec3 specular = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.0001); // ---- 3. DIRECT LIGHT (sun + shadow) ----
vec3 kD = (1.0 - F) * (1.0 - metallic);
// Shadow lookup.
vec4 light_proj = mvp_shadow * vec4(floor(vpos * 16.0) / 16.0, 1.0); vec4 light_proj = mvp_shadow * vec4(floor(vpos * 16.0) / 16.0, 1.0);
vec3 light_ndc = light_proj.xyz / light_proj.w * 0.5 + 0.5; vec3 light_ndc = light_proj.xyz / light_proj.w * 0.5 + 0.5;
light_ndc.z -= 0.001; light_ndc.z -= 0.001;
float shadow = texture(sampler2DShadow(shadowtex, shadowsmp), light_ndc); float shadow = texture(sampler2DShadow(shadowtex, shadowsmp), light_ndc);
vec3 light = shadow * (kD * albedo / PI + specular) * NdotL * sunLightColor * sunIntensity; vec3 direct_specular = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.0001);
vec3 light = shadow * (kD * albedo / PI + direct_specular) * NdotL * sunLightColor * sunIntensity;
// --- Indirect lighting --- // ---- 4. INDIRECT LIGHT (RDM / SH / ambient fallback) ----
ivec3 local = ivec3(mod(floor(trileCenter), 32.0)); ivec3 local = ivec3(mod(floor(trileCenter), 32.0));
vec4 atlas_rect = rdm_atlas_rect(local, roughnessInt); vec4 atlas_rect = rdm_atlas_rect(local, roughnessInt);
float ssao = texture(sampler2D(ssaotex, rdmsmp), float ssao = texture(sampler2D(ssaotex, rdmsmp),
gl_FragCoord.xy / vec2(float(screen_w), float(screen_h))).r; gl_FragCoord.xy / vec2(float(screen_w), float(screen_h))).r;
vec3 emissive = albedo * emittance * emissive_scale;
vec3 emissive = albedo * emittance * emissive_scale; if (rdm_enabled == 1) {
vec3 Frough = FresnelSchlickRoughness(NdotV, F0, roughness);
if (rdm_enabled == 1 && atlas_rect.z > 0.0) {
vec3 Frough = FresnelSchlickRoughness(NdotV, F0, roughness);
vec3 hemispherePos = trileCenter + N * 0.49; vec3 hemispherePos = trileCenter + N * 0.49;
vec3 diff = vpos - hemispherePos; vec3 diff = vpos - hemispherePos;
// Indirect specular // 4a. Indirect specular.
if (roughness < ROUGHNESS_SPEC_CUTOFF) { // roughnessInt 0-1 with a baked RDM: ray-march or single-sample into the atlas.
// roughnessInt 2+ without RDM data: sky reflection.
// roughnessInt > ROUGHNESS_SPEC_CUTOFF: skip specular entirely.
if (roughnessInt <= 1 && atlas_rect.z > 0.0) {
int face = rdm_face_from_normal(N); int face = rdm_face_from_normal(N);
ivec2 atlasSize = textureSize(sampler2D(rdm_atlas, rdmsmp), 0); ivec2 atlasSize = textureSize(sampler2D(rdm_atlas, rdmsmp), 0);
vec2 atlasInvSize = 1.0 / vec2(atlasSize); vec2 atlasInvSz = 1.0 / vec2(atlasSize);
int rdmSize = int(atlas_rect.z * float(atlasSize.x)) / 2; int rdmSize = int(atlas_rect.z * float(atlasSize.x)) / 2;
ivec2 fOff = rdm_face_offset(atlas_rect, face, rdmSize, atlasSize); ivec2 fOff = rdm_face_offset(atlas_rect, face, rdmSize, atlasSize);
vec3 indirectSpec; vec3 indirectSpec = roughness < ROUGHNESS_RAYMARCH_MAX
if (roughness < ROUGHNESS_RAYMARCH_MAX) { ? rdm_spec_raymarch(N, -cv, diff, face, fOff, rdmSize, atlasInvSz)
indirectSpec = rdm_spec_raymarch(N, -cv, diff, face, fOff, rdmSize, atlasInvSize); : rdm_spec_single (N, -cv, diff, face, fOff, rdmSize, atlasInvSz);
} else {
indirectSpec = rdm_spec_single(N, -cv, diff, face, fOff, rdmSize, atlasInvSize);
}
indirectSpec *= rdm_tint; indirectSpec *= rdm_tint;
// Desaturate for metals to avoid double-tinting
float specLum = dot(indirectSpec, vec3(0.2126, 0.7152, 0.0722)); float specLum = dot(indirectSpec, vec3(0.2126, 0.7152, 0.0722));
indirectSpec = mix(indirectSpec, vec3(specLum), metallic); indirectSpec = mix(indirectSpec, vec3(specLum), metallic);
vec2 envBRDF = texture(sampler2D(brdf_lut, rdmsmp), vec2(NdotV, roughness)).rg; vec2 envBRDF = texture(sampler2D(brdf_lut, rdmsmp), vec2(NdotV, roughness)).rg;
float roughnessBell = 1.0 - 0.7 * sin(roughness * PI); float roughnessBell = 1.0 - 0.7 * sin(roughness * PI);
float grazingSuppress = 1.0 - 0.9 * roughness * sin(roughness * PI) * pow(1.0 - NdotV, 2.0); float grazingSuppr = 1.0 - 0.9 * roughness * sin(roughness * PI) * pow(1.0 - NdotV, 2.0);
float specRoughFade = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0); float specRoughFade = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0);
light += indirectSpec * (Frough * envBRDF.x + envBRDF.y) light += indirectSpec * (Frough * envBRDF.x + envBRDF.y)
* rdm_spec_scale * roughnessBell * grazingSuppress * specRoughFade; * rdm_spec_scale * roughnessBell * grazingSuppr * specRoughFade;
} else if (roughness < ROUGHNESS_SPEC_CUTOFF) {
vec3 R = reflect(-V, N);
vec2 envBRDF = texture(sampler2D(brdf_lut, rdmsmp), vec2(NdotV, roughness)).rg;
float specRoughFd = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0);
light += sky_reflect(R, sunPosition) * (Frough * envBRDF.x + envBRDF.y)
* rdm_spec_scale * specRoughFd;
} }
// Indirect diffuse // 4b. Indirect diffuse.
vec3 indirectDiff = rdm_indirect_diffuse(N, diff, local) * rdm_tint; // SH probe grid when available (trilinear, confidence-weighted).
// Falls back to RDM level-7 diffuse probes.
vec3 indirectDiff;
if (sh_enabled == 1) {
vec3 trile_origin = floor(trileCenter);
vec3 local_frag = vec3(local) + (vpos - trile_origin);
vec3 probe_f = clamp(local_frag * 2.0, vec3(0.0), vec3(63.0));
ivec3 p0 = ivec3(floor(probe_f));
ivec3 p1 = min(p0 + ivec3(1), ivec3(63));
indirectDiff = sh_eval_trilinear(p0, p1, fract(probe_f), N) * rdm_tint;
} else {
indirectDiff = rdm_indirect_diffuse(N, diff, local) * rdm_tint;
}
float diffLuma = dot(indirectDiff, vec3(0.2126, 0.7152, 0.0722)); float diffLuma = dot(indirectDiff, vec3(0.2126, 0.7152, 0.0722));
indirectDiff = mix(vec3(diffLuma), indirectDiff, rdm_diff_saturation); indirectDiff = mix(vec3(diffLuma), indirectDiff, rdm_diff_saturation);
vec3 kDiff = (1.0 - Frough) * (1.0 - metallic);
light += kDiff * indirectDiff / PI * albedo * ssao * rdm_diff_scale; light += (1.0 - Frough) * (1.0 - metallic) * indirectDiff / PI * albedo * ssao * rdm_diff_scale;
// Ambient floor // 4c. Ambient floor — kicks in when indirect light is below the configured minimum.
if (rdm_diff_scale < 0.001 || length(light) < ambient_intensity) if (rdm_diff_scale < 0.001 || length(light) < ambient_intensity)
light += ambient_color * max(ambient_intensity - length(light), 0.0) * albedo * ssao; light += ambient_color * max(ambient_intensity - length(light), 0.0) * albedo * ssao;
} else { } else {
// No baked data: flat ambient + sky specular.
light += ambient_color * ambient_intensity * albedo * ssao; light += ambient_color * ambient_intensity * albedo * ssao;
vec3 R = reflect(-V, N); light += F * sky_reflect(reflect(-V, N), sunPosition) * 0.1;
light += F * sky_reflect(R, sunPosition) * 0.1;
} }
// ---- 5. FINAL COMPOSITE ----
vec3 final_color = light + emissive; vec3 final_color = light + emissive;
frag_color = vec4(mix(deepColor, final_color, smoothstep(0.0, planeHeight, vpos.y)), 1.0); frag_color = vec4(mix(deepColor, final_color, smoothstep(0.0, planeHeight, vpos.y)), 1.0);

View File

@ -1,5 +1,6 @@
#load "utils.jai"; #load "utils.jai";
#load "world_test.jai"; #load "world_test.jai";
#load "../editor/rdm_disk_test.jai";
#load "engine_exe_tests/index.jai"; #load "engine_exe_tests/index.jai";
#load "exe_tests/index.jai"; #load "exe_tests/index.jai";

View File

@ -58,6 +58,10 @@ Chunk :: struct {
rdm_dirty: bool; rdm_dirty: bool;
rdm_atlas_path: string; rdm_atlas_path: string;
rdm_lookup_path: string; rdm_lookup_path: string;
sh_probe_grid: sg_image; // 192x4096 RGBA16F 2D texture (2 probes/trile/axis)
sh_valid: bool;
sh_dirty: bool;
#if !FLAG_RELEASE_BUILD { #if !FLAG_RELEASE_BUILD {
rdm_lookup_cpu: []float; rdm_lookup_cpu: []float;
rdm_lookup_w: s32; rdm_lookup_w: s32;
@ -707,8 +711,8 @@ World_Config :: struct {
sunHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color sunHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color
sunLightColor : Vector3 = .{1.0, 1.0, 1.0}; @Color sunLightColor : Vector3 = .{1.0, 1.0, 1.0}; @Color
sunPosition : Vector3 = #run normalize(Vector3.{0.2, 0.3, 0.4}); sunPosition : Vector3 = #run normalize(Vector3.{0.2, 0.3, 0.4});
sunIntensity : float = 2.0; @Slider,0,100,0.5 sunIntensity : float = 1.0; @Slider,0,4,0.1
skyIntensity : float = 1.0; @Slider,0,10,0.5 skyIntensity : float = 0.3; @Slider,0,5,0.1
hasClouds : s32 = 1; @Slider,0,1,1 hasClouds : s32 = 1; @Slider,0,1,1