Skip to content
Merged
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
13 changes: 10 additions & 3 deletions native/shared/shaders/virtual_shadows/material_helper.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ fn sample_shadow_cascade(
if (vsm_params.words.x == 0u) {
return sample_shadow_cascade_csm(cascade_idx, world_pos, outside_value);
}
let light_clip = vsm_params.level_vps[cascade_idx]
* vec4<f32>(world_pos, 1.0);
let level_vp = vsm_params.level_vps[cascade_idx];
let light_clip = level_vp * vec4<f32>(world_pos, 1.0);
let light_ndc = light_clip.xyz / light_clip.w;
if (abs(light_ndc.x) > 1.0 || abs(light_ndc.y) > 1.0
|| light_ndc.z < 0.0 || light_ndc.z > 1.0) {
Expand All @@ -17,8 +17,15 @@ fn sample_shadow_cascade(
light_ndc.x * 0.5 + 0.5,
1.0 - (light_ndc.y * 0.5 + 0.5),
);
let depth_ref = light_ndc.z - 0.001;
let axis = vsm_params.words.y;
// Receiver bias of two virtual texels of world distance, as NDC depth
// (the x row of the ortho level VP has length 2 / width in metres, the
// z row 1 / depth range). The constant in NDC it replaces scaled with
// whatever the scene bounds pancaked that range to.
let x_row = length(vec3<f32>(level_vp[0][0], level_vp[1][0], level_vp[2][0]));
let z_row = length(vec3<f32>(level_vp[0][2], level_vp[1][2], level_vp[2][2]));
let virtual_texel_m = 2.0 / (x_row * f32(axis * vsm_params.words.z));
let depth_ref = light_ndc.z - 2.0 * virtual_texel_m * z_row;
let scaled_uv = shadow_uv * f32(axis);
let page_xy = min(vec2<u32>(scaled_uv), vec2<u32>(axis - 1u));
let encoded = textureLoad(
Expand Down
12 changes: 10 additions & 2 deletions native/shared/shaders/virtual_shadows/scene_helper.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ fn sample_virtual_shadow(
if (vsm_params.words.x == 0u) {
return sample_cascade(cascade, csm_uv, csm_depth_ref);
}
let light_clip = vsm_params.level_vps[cascade] * vec4<f32>(world_pos, 1.0);
let level_vp = vsm_params.level_vps[cascade];
let light_clip = level_vp * vec4<f32>(world_pos, 1.0);
let light_ndc = light_clip.xyz / light_clip.w;
let shadow_uv = vec2<f32>(
light_ndc.x * 0.5 + 0.5,
Expand All @@ -17,8 +18,15 @@ fn sample_virtual_shadow(
|| light_ndc.z < 0.0 || light_ndc.z > 1.0) {
return sample_cascade(cascade, csm_uv, csm_depth_ref);
}
let depth_ref = light_ndc.z - 0.001;
let axis = vsm_params.words.y;
// Receiver bias of one virtual texel of world distance, as NDC depth
// (the x row of the ortho level VP has length 2 / width in metres, the
// z row 1 / depth range). The constant in NDC it replaces scaled with
// whatever the scene bounds pancaked that range to.
let x_row = length(vec3<f32>(level_vp[0][0], level_vp[1][0], level_vp[2][0]));
let z_row = length(vec3<f32>(level_vp[0][2], level_vp[1][2], level_vp[2][2]));
let virtual_texel_m = 2.0 / (x_row * f32(axis * vsm_params.words.z));
let depth_ref = light_ndc.z - virtual_texel_m * z_row;
let scaled_uv = shadow_uv * f32(axis);
let page_xy = min(vec2<u32>(scaled_uv), vec2<u32>(axis - 1u));
let encoded = textureLoad(
Expand Down
4 changes: 3 additions & 1 deletion native/shared/src/renderer/gi_bake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,9 @@ impl Renderer {
shadow_vps,
shadow_splits,
flags: [
0.002,
// x = receiver depth bias in shadow texels. Probes sit
// in free space, so there is no normal to offset along.
2.0,
if shadows_enabled { 1.0 } else { 0.0 },
c as f32,
0.0,
Expand Down
4 changes: 3 additions & 1 deletion native/shared/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9776,7 +9776,9 @@ impl Renderer {
shadow_splits,
view_matrix: self.current_view_matrix,
flags: [
0.002,
// x = receiver depth bias in shadow texels; the shader adds
// a 1.5-texel normal offset of its own.
1.0,
if shadows_enabled { 1.0 } else { 0.0 },
if use_hw {
self.tlas_instance_data_count as f32
Expand Down
9 changes: 7 additions & 2 deletions native/shared/src/renderer/shaders/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,15 +917,20 @@ fn refl_sun_shadow(wp: vec3<f32>, n: vec3<f32>) -> f32 {
if (c == 0) { fit = light.shadow_splits.x; }
else if (c == 1) { fit = light.shadow_splits.y; }
let dim = f32(textureDimensions(refl_shadow_0).x);
let vp_c = light.shadow_cascades[c];
let p = wp + n * (2.0 * fit / dim) * 1.5;
let clip = light.shadow_cascades[c] * vec4<f32>(p, 1.0);
let clip = vp_c * vec4<f32>(p, 1.0);
let ndc = clip.xyz / clip.w;
if (ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 ||
ndc.z < 0.0 || ndc.z > 1.0) {
return 1.0;
}
let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 1.0 - (ndc.y * 0.5 + 0.5));
let ref_d = ndc.z - 0.001;
// One texel of world distance as NDC depth (the z row of the ortho VP
// is 1 / its depth range), the main pass's receiver bias. A constant
// in NDC scaled with whatever the scene bounds pancake that range to.
let ndc_per_m = length(vec3<f32>(vp_c[0][2], vp_c[1][2], vp_c[2][2]));
let ref_d = ndc.z - (2.0 * fit / dim) * ndc_per_m;
if (c == 0) { return textureSampleCompareLevel(refl_shadow_0, refl_shadow_samp, uv, ref_d); }
if (c == 1) { return textureSampleCompareLevel(refl_shadow_1, refl_shadow_samp, uv, ref_d); }
return textureSampleCompareLevel(refl_shadow_2, refl_shadow_samp, uv, ref_d);
Expand Down
75 changes: 61 additions & 14 deletions native/shared/src/renderer/shaders/gi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,33 @@ struct CardLightParams {
@group(0) @binding(8) var shadow_atlas_2: texture_depth_2d;
@group(0) @binding(9) var shadow_samp: sampler_comparison;

fn sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
fn shadow_vp(cascade: i32) -> mat4x4<f32> {
if (cascade == 0) { return u.shadow_vps[0]; }
if (cascade == 1) { return u.shadow_vps[1]; }
return u.shadow_vps[2];
}

// One shadow-map texel of world distance, read off the cascade's own
// orthographic view-projection (its x row has length 2 / width in metres).
fn shadow_texel_m(cascade: i32) -> f32 {
let vp = shadow_vp(cascade);
let x_row = length(vec3<f32>(vp[0][0], vp[1][0], vp[2][0]));
return 2.0 / (x_row * f32(textureDimensions(shadow_atlas_0).x));
}

// The same texel as NDC depth (the z row has length 1 / depth range).
// Receiver biases are measured in texels so that they keep their size in
// the world whatever the scene bounds pancake the depth range to. The
// constant 0.002 in NDC this replaces grew to a metre over a 600 m ground
// plane, at which point a deck no longer shadowed the hull beneath it in
// the GI feed.
fn shadow_texel_ndc(cascade: i32) -> f32 {
let vp = shadow_vp(cascade);
let z_row = length(vec3<f32>(vp[0][2], vp[1][2], vp[2][2]));
return shadow_texel_m(cascade) * z_row;
}

fn sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias_texels: f32) -> f32 {
var clip: vec4<f32>;
if (cascade == 0) {
clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0);
Expand All @@ -383,7 +409,7 @@ fn sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
return 0.0;
}
let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
let ref_depth = ndc.z - bias;
let ref_depth = ndc.z - bias_texels * shadow_texel_ndc(cascade);
if (cascade == 0) {
return textureSampleCompareLevel(shadow_atlas_0, shadow_samp, shadow_uv, ref_depth);
} else if (cascade == 1) {
Expand Down Expand Up @@ -459,21 +485,16 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
if (u.flags.y > 0.5) {
shadow = 0.0;
for (var cascade: i32 = 0; cascade < 3; cascade = cascade + 1) {
var clip: vec4<f32>;
if (cascade == 0) {
clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0);
} else if (cascade == 1) {
clip = u.shadow_vps[1] * vec4<f32>(pos_ws, 1.0);
} else {
clip = u.shadow_vps[2] * vec4<f32>(pos_ws, 1.0);
}
// Receiver normal offset of 1.5 texels, as in the main pass.
let p = pos_ws + n_ws * shadow_texel_m(cascade) * 1.5;
let clip = shadow_vp(cascade) * vec4<f32>(p, 1.0);
if (clip.w <= 0.0) { continue; }
let ndc = clip.xyz / clip.w;
if (ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 ||
ndc.z < 0.0 || ndc.z > 1.0) {
continue;
}
shadow = sample_cascade(cascade, pos_ws, u.flags.x);
shadow = sample_cascade(cascade, p, u.flags.x);
break;
}
}
Expand Down Expand Up @@ -727,7 +748,7 @@ struct WsrcBakeParams {
grid: vec4<f32>,
shadow_vps: array<mat4x4<f32>, 3>,
shadow_splits: vec4<f32>,
// x = shadow bias, y = shadows-enabled flag, z = cascade index
// x = receiver depth bias in shadow texels, y = shadows-enabled flag, z = cascade index
// (0..WSRC_CASCADE_COUNT), w unused. Cascade index offsets the
// output z-slice so this pipeline can be dispatched once per
// cascade with the same layout.
Expand All @@ -743,7 +764,33 @@ struct WsrcBakeParams {
@group(0) @binding(4) var shadow_samp: sampler_comparison;
@group(0) @binding(5) var wsrc_out: texture_storage_3d<rgba16float, write>;

fn wsrc_sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
fn shadow_vp(cascade: i32) -> mat4x4<f32> {
if (cascade == 0) { return u.shadow_vps[0]; }
if (cascade == 1) { return u.shadow_vps[1]; }
return u.shadow_vps[2];
}

// One shadow-map texel of world distance, read off the cascade's own
// orthographic view-projection (its x row has length 2 / width in metres).
fn shadow_texel_m(cascade: i32) -> f32 {
let vp = shadow_vp(cascade);
let x_row = length(vec3<f32>(vp[0][0], vp[1][0], vp[2][0]));
return 2.0 / (x_row * f32(textureDimensions(shadow_atlas_0).x));
}

// The same texel as NDC depth (the z row has length 1 / depth range).
// Receiver biases are measured in texels so that they keep their size in
// the world whatever the scene bounds pancake the depth range to. The
// constant 0.002 in NDC this replaces grew to a metre over a 600 m ground
// plane, at which point a deck no longer shadowed the hull beneath it in
// the GI feed.
fn shadow_texel_ndc(cascade: i32) -> f32 {
let vp = shadow_vp(cascade);
let z_row = length(vec3<f32>(vp[0][2], vp[1][2], vp[2][2]));
return shadow_texel_m(cascade) * z_row;
}

fn wsrc_sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias_texels: f32) -> f32 {
var clip: vec4<f32>;
if (cascade == 0) {
clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0);
Expand All @@ -757,7 +804,7 @@ fn wsrc_sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
return 0.0;
}
let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
let ref_depth = ndc.z - bias;
let ref_depth = ndc.z - bias_texels * shadow_texel_ndc(cascade);
if (cascade == 0) {
return textureSampleCompareLevel(shadow_atlas_0, shadow_samp, shadow_uv, ref_depth);
} else if (cascade == 1) {
Expand Down
32 changes: 19 additions & 13 deletions native/shared/src/renderer/shaders/pt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,30 +195,36 @@ fn lin_depth(d: f32) -> f32 {
// Sun visibility from the shadow cascades (near -> far fallthrough by
// coverage, same scheme as the WSRC bake). Deterministic and smooth —
// the whole reason RT mode's direct light doesn't shimmer or dither.
fn sun_vis_cascade(pos_ws: vec3<f32>) -> f32 {
fn sun_vis_cascade(pos_ws: vec3<f32>, n: vec3<f32>) -> f32 {
for (var c = 0; c < 3; c = c + 1) {
var clip: vec4<f32>;
if (c == 0) { clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0); }
else if (c == 1) { clip = u.shadow_vps[1] * vec4<f32>(pos_ws, 1.0); }
else { clip = u.shadow_vps[2] * vec4<f32>(pos_ws, 1.0); }
var vp: mat4x4<f32>;
var dims: vec2<u32>;
if (c == 0) { vp = u.shadow_vps[0]; dims = textureDimensions(shadow_atlas_0); }
else if (c == 1) { vp = u.shadow_vps[1]; dims = textureDimensions(shadow_atlas_1); }
else { vp = u.shadow_vps[2]; dims = textureDimensions(shadow_atlas_2); }
let fdims = vec2<f32>(dims);
// Receiver bias in shadow texels, the raster main pass's scheme:
// 1.5 texels of normal offset and one texel of depth, sized from
// the cascade's own ortho projection (x row = 2 / width in metres,
// z row = 1 / depth range). The constant in NDC it replaces grew
// with the pancaked depth range, to a metre over a 600 m ground.
let x_row = length(vec3<f32>(vp[0][0], vp[1][0], vp[2][0]));
let z_row = length(vec3<f32>(vp[0][2], vp[1][2], vp[2][2]));
let texel_m = 2.0 / (x_row * fdims.x);
let clip = vp * vec4<f32>(pos_ws + n * texel_m * 1.5, 1.0);
if (abs(clip.w) < 1e-6) { continue; }
let ndc = clip.xyz / clip.w;
if (ndc.x < -0.99 || ndc.x > 0.99 || ndc.y < -0.99 || ndc.y > 0.99 || ndc.z < 0.0 || ndc.z > 1.0) {
continue;
}
let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
let ref_depth = ndc.z - 0.002;
let ref_depth = ndc.z - texel_m * z_row;
// Manual load-and-compare with a 2x2 average instead of the
// comparison sampler: SampleCmp from a COMPUTE stage proved
// unreliable on this DXC path (constant 0, independent of the
// matrices — same failure shape as the ray-query saga), and the
// only other compute-stage user (WSRC bake) was never validated
// on DX12. textureLoad is proven (the PT depth reads use it).
var dims: vec2<u32>;
if (c == 0) { dims = textureDimensions(shadow_atlas_0); }
else if (c == 1) { dims = textureDimensions(shadow_atlas_1); }
else { dims = textureDimensions(shadow_atlas_2); }
let fdims = vec2<f32>(dims);
var vis = 0.0;
for (var ty = 0; ty <= 1; ty = ty + 1) {
for (var tx = 0; tx <= 1; tx = tx + 1) {
Expand Down Expand Up @@ -630,7 +636,7 @@ fn albedo_at_hit(
// ray otherwise (reference quality, soft penumbra).
fn sun_visibility(p: vec3<f32>, n: vec3<f32>, r2: vec2<f32>) -> f32 {
if (u.ext.z == 1u) {
return sun_vis_cascade(p);
return sun_vis_cascade(p, n);
}
let sd = sun_cone_sample(r2);
if (dot(n, sd) <= 0.0) {
Expand Down Expand Up @@ -1137,7 +1143,7 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
// primary surface. Must match the raster shadow shapes exactly
// (crisp tree/building shadows). Gray everywhere = the VPs are
// wrong (transposition or stale).
let vis18 = sun_vis_cascade(p0 + n0 * 0.02);
let vis18 = sun_vis_cascade(p0, n0);
textureStore(out_hdr, px_full, vec4<f32>(vec3<f32>(vis18 * 50.0), 1.0));
return;
}
Expand Down
36 changes: 21 additions & 15 deletions native/shared/src/renderer/transmitted_shadows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,13 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
vec2<i32>(blocker_dims) - vec2<i32>(1),
);
let blocker = textureLoad(opaque_depth, blocker_pixel, 0);
if (blocker + 0.00075 < in.position.z) {
// Tolerance of two opaque-map texels of world distance, as NDC depth
// (x row of the ortho light VP = 2 / width in metres, z row = 1 / depth
// range). A constant in NDC scaled with the scene bounds.
let x_row = length(vec3<f32>(u.light_vp[0][0], u.light_vp[1][0], u.light_vp[2][0]));
let z_row = length(vec3<f32>(u.light_vp[0][2], u.light_vp[1][2], u.light_vp[2][2]));
let tolerance = 2.0 * (2.0 / (x_row * f32(blocker_dims.x))) * z_row;
if (blocker + tolerance < in.position.z) {
discard;
}

Expand Down Expand Up @@ -380,12 +386,9 @@ fn load_trans_color(cascade: i32, pixel: vec2<i32>) -> vec3<f32> {
return textureLoad(trans_color_2, pixel, 0).rgb;
}

fn transmitted_visibility(
cascade: i32,
world_pos: vec3<f32>,
receiver_bias: f32,
) -> vec3<f32> {
let clip = u.cascade_vps[cascade] * vec4<f32>(world_pos, 1.0);
fn transmitted_visibility(cascade: i32, world_pos: vec3<f32>) -> vec3<f32> {
let vp = u.cascade_vps[cascade];
let clip = vp * vec4<f32>(world_pos, 1.0);
let ndc = clip.xyz / max(abs(clip.w), 0.000001);
if (
ndc.x < -1.0 || ndc.x > 1.0
Expand All @@ -397,6 +400,12 @@ fn transmitted_visibility(

let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
let dims = vec2<i32>(textureDimensions(trans_depth_0));
// Receiver bias of two transmitted-map texels of world distance, as NDC
// depth (x row of the ortho VP = 2 / width in metres, z row = 1 / depth
// range). The constant in NDC it replaces scaled with the scene bounds.
let x_row = length(vec3<f32>(vp[0][0], vp[1][0], vp[2][0]));
let z_row = length(vec3<f32>(vp[0][2], vp[1][2], vp[2][2]));
let receiver_bias = 2.0 * (2.0 / (x_row * f32(dims.x))) * z_row;
let texel = uv * vec2<f32>(dims) - vec2<f32>(0.5);
let base = vec2<i32>(floor(texel));
let fraction = fract(texel);
Expand Down Expand Up @@ -429,8 +438,7 @@ fn sample_transmitted_shadow(world_pos: vec3<f32>) -> vec3<f32> {
} else if (distance_to_camera <= u.cascade_splits.y) {
cascade = 1;
}
let receiver_bias = 0.0015;
let value = transmitted_visibility(cascade, world_pos, receiver_bias);
let value = transmitted_visibility(cascade, world_pos);

var split_near = 0.0;
var split_far = u.cascade_splits.x;
Expand All @@ -444,11 +452,7 @@ fn sample_transmitted_shadow(world_pos: vec3<f32>) -> vec3<f32> {
let blend_zone = max((split_far - split_near) * 0.1, 0.0001);
let distance_to_edge = split_far - distance_to_camera;
if (distance_to_edge < blend_zone && cascade < 2) {
let next_value = transmitted_visibility(
cascade + 1,
world_pos,
receiver_bias,
);
let next_value = transmitted_visibility(cascade + 1, world_pos);
return mix(next_value, value, clamp(distance_to_edge / blend_zone, 0.0, 1.0));
}
return value;
Expand Down Expand Up @@ -718,7 +722,9 @@ impl TransmittedShadowResources {
label: Some("transmitted_shadow_draw_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
// The fragment stage reads `light_vp` for its hidden-glass
// tolerance.
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
Expand Down
6 changes: 4 additions & 2 deletions native/shared/src/renderer/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,8 @@ pub(super) struct WsrcBakeParams {
/// xyz = view-space split distances (unused by V6 because it
/// always samples cascade 2), w = 0.
pub(super) shadow_splits: [f32; 4],
/// x = shadow bias, y = shadows_enabled (0/1), zw unused.
/// x = receiver depth bias in shadow texels (world-sized in the
/// shader), y = shadows_enabled (0/1), zw unused.
pub(super) 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
Expand All @@ -1079,7 +1080,8 @@ pub(super) struct CardLightParams {
/// Camera view matrix — needed to convert card-texel world pos
/// into view-space Z for cascade selection.
pub(super) view_matrix: [[f32; 4]; 4],
/// x = shadow bias, y = shadows_enabled (0/1), zw unused.
/// x = receiver depth bias in shadow texels (world-sized in the
/// shader), y = shadows_enabled (0/1), zw unused.
pub(super) flags: [f32; 4],
}

Expand Down
Loading