Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion native/shared/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ struct WsrcBakeParams {
shadow_splits: [f32; 4],
/// x = shadow bias, y = shadows_enabled (0/1), zw unused.
flags: [f32; 4],
/// EN-023 — xyz = scene-average albedo (mean of card-instance flat
/// albedos) for the SW bake's ground-bounce term; w unused. The HW
/// bake ignores it (it traces real geometry).
ground_albedo: [f32; 4],
}

/// Uniform struct for the card-lighting compute pass. Matches
Expand Down Expand Up @@ -532,10 +536,20 @@ struct InstanceGiDataCpu {
/// `card_slot.w` = flag (1.0 = card captured, 0.0 = no card → fall
/// back to `albedo` flat value).
card_slot: [f32; 4],
/// Object-space AABB min (xyz) + unused pad (w).
/// Object-space AABB min (xyz) + unused pad (w). The HW paths
/// transform hits into object space (hit.world_to_object) and
/// compare against THESE — do not world-ify them.
card_aabb_min: [f32; 4],
/// Object-space AABB max (xyz) + unused pad (w).
card_aabb_max: [f32; 4],
/// EN-023 — WORLD-space AABB min/max. The SDF trace has no
/// world_to_object (it marches a world-space clipmap), so its
/// broad-phase compares the world hit against these. With the old
/// object-space-only bounds, every transformed instance fell
/// through to the flat-gray analytic fallback — zero colored
/// bounce on non-RT adapters (round-2 audit F4).
world_aabb_min: [f32; 4],
world_aabb_max: [f32; 4],
}

#[repr(C)]
Expand Down Expand Up @@ -979,6 +993,10 @@ pub struct Renderer {
pub ssgi_radius: f32,
/// SSGI master switch.
pub ssgi_enabled: bool,
/// EN-023 — mean flat albedo over the card instances; feeds the SW
/// WSRC bake's ground-bounce term. Neutral mid-gray until the first
/// instance-data upload computes the real scene average.
pub gi_scene_avg_albedo: [f32; 3],
/// One-shot log guard: which SSGI trace backend was last reported to
/// stderr (hw-ray-query / sdf-clipmap / hiz-screen). The silent HW→SW
/// fallback made "why is bounce gray?" a debugger question during the
Expand Down Expand Up @@ -6028,6 +6046,7 @@ impl Renderer {
ssgi_radius: 20.0,
ssgi_enabled: true,
ssgi_backend_logged: None,
gi_scene_avg_albedo: [0.35, 0.35, 0.35],
probe_grid_w,
probe_grid_h,
probe_header_buffer,
Expand Down Expand Up @@ -7190,6 +7209,12 @@ impl Renderer {
c as f32,
0.0,
],
ground_albedo: [
self.gi_scene_avg_albedo[0],
self.gi_scene_avg_albedo[1],
self.gi_scene_avg_albedo[2],
0.0,
],
};
self.queue.write_buffer(
&self.wsrc_bake_uniform,
Expand Down Expand Up @@ -7527,13 +7552,28 @@ impl Renderer {

let mut instance_data: Vec<InstanceGiDataCpu> =
Vec::with_capacity(instance_count as usize);
// EN-023 — running mean of instance albedos feeds the SW WSRC
// bake's ground-bounce term.
let mut albedo_sum = [0.0f32; 3];
for &h in instance_handles {
let n = scene.nodes.get(h).unwrap();
let e = n.material.emissive;
let (first_slot, has_card) = match n.card_first_slot {
Some(s) => (s as f32, 1.0_f32),
None => (0.0, 0.0),
};
// EN-023 — world AABB for the SDF broad-phase. The scene's
// bounds pass keeps world_bounds fresh; the sentinel
// (min.x > max.x = not yet computed) falls back to the local
// box, which matches the old behaviour for identity nodes.
let (wmin, wmax) = if n.world_bounds_min[0] <= n.world_bounds_max[0] {
(n.world_bounds_min, n.world_bounds_max)
} else {
(n.bounds_min, n.bounds_max)
};
albedo_sum[0] += n.flat_albedo[0];
albedo_sum[1] += n.flat_albedo[1];
albedo_sum[2] += n.flat_albedo[2];
instance_data.push(InstanceGiDataCpu {
albedo: n.flat_albedo,
emissive_luma: (e[0] + e[1] + e[2]) * (1.0 / 3.0),
Expand All @@ -7542,8 +7582,18 @@ impl Renderer {
card_slot: [first_slot, 0.0, 0.0, has_card],
card_aabb_min: [n.bounds_min[0], n.bounds_min[1], n.bounds_min[2], 0.0],
card_aabb_max: [n.bounds_max[0], n.bounds_max[1], n.bounds_max[2], 0.0],
world_aabb_min: [wmin[0], wmin[1], wmin[2], 0.0],
world_aabb_max: [wmax[0], wmax[1], wmax[2], 0.0],
});
}
if !instance_data.is_empty() {
let inv = 1.0 / instance_data.len() as f32;
self.gi_scene_avg_albedo = [
albedo_sum[0] * inv,
albedo_sum[1] * inv,
albedo_sum[2] * inv,
];
}
if !instance_data.is_empty() {
self.queue.write_buffer(
self.tlas_instance_data_buffer.as_ref().unwrap(),
Expand Down
20 changes: 19 additions & 1 deletion native/shared/src/renderer/shaders/gi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ struct WsrcBakeParams {
// output z-slice so this pipeline can be dispatched once per
// cascade with the same layout.
flags: vec4<f32>,
// EN-023 — xyz = scene-average albedo for the ground-bounce term.
ground_albedo: vec4<f32>,
};

@group(0) @binding(0) var<uniform> u: WsrcBakeParams;
Expand Down Expand Up @@ -441,7 +443,17 @@ fn cs_main(
let up = clamp(dir.y * 0.5 + 0.5, 0.0, 1.0);
let sky = u.sky_color.xyz * up * up;

let radiance = sun + sky;
// EN-023 — ground bounce for below-horizon octels. This envelope
// was light-source-only: any miss ray pointing downward returned
// ~black, so shaded receivers lost the strongest real bounce
// source (sunlit ground). Approximate it as the scene-average
// albedo lit by the sun (shadowed at the probe) + half the sky.
let down = clamp(-dir.y, 0.0, 1.0);
let ground_irr = u.sun_color.xyz * max(u.sun_dir.y, 0.0) * shadow
+ u.sky_color.xyz * 0.5;
let ground = u.ground_albedo.xyz * ground_irr * down * down;

let radiance = sun + sky + ground;

// V13 — cascade index in flags.z offsets the output z-slice.
let cascade_idx = u32(u.flags.z);
Expand Down Expand Up @@ -485,6 +497,9 @@ struct WsrcBakeParams {
shadow_vps: array<mat4x4<f32>, 3>,
shadow_splits: vec4<f32>,
flags: vec4<f32>,
// EN-023 — layout mirror; the HW bake traces real geometry and
// ignores the scene-average ground albedo.
ground_albedo: vec4<f32>,
};

struct HwBakeInstanceGiData {
Expand All @@ -495,6 +510,9 @@ struct HwBakeInstanceGiData {
card_slot: vec4<f32>,
card_aabb_min: vec4<f32>,
card_aabb_max: vec4<f32>,
// EN-023 — world-space AABB (SDF path only; layout mirror).
world_aabb_min: vec4<f32>,
world_aabb_max: vec4<f32>,
};

const HW_BAKE_CARD_SLOTS_PER_ROW: f32 = 64.0;
Expand Down
44 changes: 33 additions & 11 deletions native/shared/src/renderer/shaders/ssgi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ struct InstanceGiData {
// Object-space AABB min (xyz) / max (xyz).
card_aabb_min: vec4<f32>,
card_aabb_max: vec4<f32>,
// EN-023 — world-space AABB (SDF path only; layout mirror).
world_aabb_min: vec4<f32>,
world_aabb_max: vec4<f32>,
};

const CARD_SLOTS_PER_ROW: f32 = 64.0;
Expand Down Expand Up @@ -715,6 +718,12 @@ struct SdfInstanceGiData {
card_slot: vec4<f32>,
card_aabb_min: vec4<f32>,
card_aabb_max: vec4<f32>,
// EN-023 — world-space AABB. This trace marches a WORLD-space
// clipmap and has no world_to_object; comparing world hits against
// the object-space box above only ever worked for identity-
// transform assets (Sponza).
world_aabb_min: vec4<f32>,
world_aabb_max: vec4<f32>,
};

const SDF_CARD_SLOTS_PER_ROW: f32 = 64.0;
Expand Down Expand Up @@ -913,16 +922,29 @@ fn cs_main(
// geometry, etc.).
let count = arrayLength(&instance_data);
var picked: i32 = -1;
var picked_vol: f32 = 1e30;
for (var i: u32 = 0u; i < count; i = i + 1u) {
let ad = instance_data[i];
if (ad.card_slot.w < 0.5) { continue; }
let bmin = ad.card_aabb_min.xyz - vec3<f32>(0.05);
let bmax = ad.card_aabb_max.xyz + vec3<f32>(0.05);
// EN-023 — compare the WORLD hit against the WORLD AABB.
// The old object-space comparison only matched assets whose
// vertices were already in world space; every transformed
// instance fell through to the gray analytic fallback.
// Pick the SMALLEST containing box, not the first: a scene-
// spanning instance (the shooter's ±140 m terrain proxy)
// otherwise swallows every hit — walls and trees included —
// and its mostly-empty side cards darken the bounce.
let bmin = ad.world_aabb_min.xyz - vec3<f32>(0.05);
let bmax = ad.world_aabb_max.xyz + vec3<f32>(0.05);
if (hit_pos.x >= bmin.x && hit_pos.x <= bmax.x &&
hit_pos.y >= bmin.y && hit_pos.y <= bmax.y &&
hit_pos.z >= bmin.z && hit_pos.z <= bmax.z) {
picked = i32(i);
break;
let ext = bmax - bmin;
let vol = ext.x * ext.y * ext.z;
if (vol < picked_vol) {
picked = i32(i);
picked_vol = vol;
}
}
}

Expand All @@ -947,13 +969,13 @@ fn cs_main(
let slot_x = slot % 64u;
let slot_y = slot / 64u;

let bmin = ad.card_aabb_min.xyz;
let bmax = ad.card_aabb_max.xyz;
// Hit is in world space and Sponza meshes are in world
// space too (no per-instance transform beyond identity on
// the Sponza asset). For now use world-space hit directly;
// instances with a non-identity transform would need the
// transform stored on `instance_data` to round-trip.
// EN-023 — project against the WORLD AABB, consistent with
// the world-space hit. Exact for the translate+scale (yaw-0)
// instances the GI proxies use; a yaw-rotated instance would
// sample its card with rotated UVs — hue still right, which
// is what the probe integral actually consumes at 64² cards.
let bmin = ad.world_aabb_min.xyz;
let bmax = ad.world_aabb_max.xyz;
var u_os: f32;
var v_os: f32;
var u_lo: f32; var u_hi: f32;
Expand Down