Skip to content

Commit a01cf8a

Browse files
committed
Add bounded local virtual shadows
1 parent 20ac21e commit a01cf8a

21 files changed

Lines changed: 1427 additions & 105 deletions

examples/quality-stress/main.ts

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
genMeshCube, setAmbientLight, setDirectionalLight,
1313
} from "bloom/models";
1414
import {
15-
enableShadows, addPointLight, createSceneNode, attachModelToNode,
15+
enableShadows, addPointLight, addShadowedPointLight, createSceneNode, attachModelToNode,
1616
setSceneNodeTransform, setSceneNodeColor, setSceneNodePbr,
1717
setSceneNodeCastShadow, setSceneNodeReceiveShadow,
1818
} from "bloom/scene";
@@ -21,14 +21,19 @@ import { mat4Identity, mat4Translate, mat4Scale } from "bloom/math";
2121
const argv: string[] = getCommandLineArgs();
2222
const config = parseQualityRun(argv);
2323
let vsmGpuCasterFixture = false;
24+
let vsmLocalLightsFixture = false;
25+
let vsmLocalReferenceFixture = false;
2426
for (let i = 0; i < argv.length; i = i + 1) {
2527
if (argv[i] === "--vsm-gpu-casters") vsmGpuCasterFixture = true;
28+
if (argv[i] === "--vsm-local-lights") vsmLocalLightsFixture = true;
29+
if (argv[i] === "--vsm-local-reference") vsmLocalReferenceFixture = true;
2630
}
2731

28-
const GRID_X = vsmGpuCasterFixture ? 32 : 128;
29-
const GRID_Z = vsmGpuCasterFixture ? 16 : 80;
32+
const localLightLayout = vsmLocalLightsFixture || vsmLocalReferenceFixture;
33+
const GRID_X = localLightLayout ? 16 : (vsmGpuCasterFixture ? 32 : 128);
34+
const GRID_Z = localLightLayout ? 16 : (vsmGpuCasterFixture ? 16 : 80);
3035
const DRAW_COUNT = GRID_X * GRID_Z;
31-
const LIGHT_COUNT = 192;
36+
const LIGHT_COUNT = localLightLayout ? 128 : 192;
3237

3338
initWindow(1280, 720, "Bloom Quality: 10k Draws + Many Lights", 0);
3439
if (config !== null) resize(1280, 720, 1280, 720);
@@ -42,6 +47,18 @@ enableShadows();
4247
const quality: QualityRun | null = config !== null ? new QualityRun(config) : null;
4348

4449
const cube = genMeshCube(1, 1, 1);
50+
if (localLightLayout) {
51+
const floor = createSceneNode();
52+
attachModelToNode(floor, cube.handle, 0);
53+
let floorTransform = mat4Identity();
54+
floorTransform = mat4Translate(floorTransform, { x: 0, y: -0.15, z: 0 });
55+
floorTransform = mat4Scale(floorTransform, { x: 14, y: 0.2, z: 10 });
56+
setSceneNodeTransform(floor, floorTransform);
57+
setSceneNodeColor(floor, 125, 130, 138);
58+
setSceneNodePbr(floor, 0.72, 0.0);
59+
setSceneNodeCastShadow(floor, false);
60+
setSceneNodeReceiveShadow(floor, true);
61+
}
4562
for (let i = 0; i < DRAW_COUNT; i = i + 1) {
4663
const xIndex = i % GRID_X;
4764
const zIndex = Math.floor(i / GRID_X);
@@ -63,7 +80,7 @@ for (let i = 0; i < DRAW_COUNT; i = i + 1) {
6380
setSceneNodePbr(node, 0.22 + (xIndex % 6) * 0.12, (zIndex % 9 === 0) ? 0.8 : 0.05);
6481
// The ordinary 10k case stays focused on main-view submission. The opt-in
6582
// 512-node VSM oracle stays below the per-page compatibility cap.
66-
setSceneNodeCastShadow(node, vsmGpuCasterFixture);
83+
setSceneNodeCastShadow(node, vsmGpuCasterFixture || localLightLayout);
6784
setSceneNodeReceiveShadow(node, true);
6885
}
6986

@@ -72,31 +89,50 @@ while (!windowShouldClose()) {
7289
const capture = quality !== null ? quality.beginFrame() : false;
7390
fixtureFrame = fixtureFrame + 1;
7491
beginDrawing();
75-
setAmbientLight({ r: 105, g: 115, b: 135, a: 255 }, 0.25);
92+
setAmbientLight(
93+
{ r: 105, g: 115, b: 135, a: 255 },
94+
localLightLayout ? 0.08 : 0.25,
95+
);
7696
setDirectionalLight(
7797
vsmGpuCasterFixture && Math.floor(fixtureFrame / 30) % 2 !== 0
7898
? { x: -0.28, y: 0.90, z: 0.34 }
7999
: { x: -0.4, y: 0.85, z: 0.3 },
80100
{ r: 255, g: 244, b: 228, a: 255 },
81-
1.4,
101+
localLightLayout ? 0.15 : 1.4,
82102
);
83103
for (let i = 0; i < LIGHT_COUNT; i = i + 1) {
84-
const col = i % 24;
85-
const row = Math.floor(i / 24);
104+
const columns = localLightLayout ? 16 : 24;
105+
const col = i % columns;
106+
const row = Math.floor(i / columns);
86107
const hue = i % 3;
87-
addPointLight(
88-
(col - 11.5) * 3.3,
89-
2.0 + (i % 4) * 0.6,
90-
(row - 3.5) * 6.0,
91-
5.5,
108+
const priorityLocalLight = localLightLayout && i < 5;
109+
const args = [
110+
priorityLocalLight ? 0.0
111+
: (localLightLayout ? (col - 7.5) * 1.0 : (col - 11.5) * 3.3),
112+
priorityLocalLight ? 6.0 : 2.0 + (i % 4) * 0.6,
113+
priorityLocalLight ? 4.0
114+
: (localLightLayout ? (row - 3.5) * 1.8 : (row - 3.5) * 6.0),
115+
priorityLocalLight ? 15.0 : (localLightLayout ? 4.5 : 5.5),
92116
hue === 0 ? 1.0 : 0.2,
93117
hue === 1 ? 1.0 : 0.25,
94118
hue === 2 ? 1.0 : 0.2,
95-
3.0,
96-
);
119+
priorityLocalLight ? 2.0 : (localLightLayout ? 8.0 : 3.0),
120+
] as const;
121+
if (vsmLocalLightsFixture) {
122+
if (!addShadowedPointLight(...args) && fixtureFrame === 1) {
123+
throw new Error(`shadowed point-light request ${i} was rejected`);
124+
}
125+
} else if (
126+
!vsmLocalReferenceFixture
127+
|| i < 5
128+
) {
129+
addPointLight(...args);
130+
}
97131
}
98132
beginMode3D({
99-
position: { x: 0, y: 38, z: 52 },
133+
position: localLightLayout
134+
? { x: 0, y: 9, z: 17 }
135+
: { x: 0, y: 38, z: 52 },
100136
target: { x: 0, y: 0, z: 0 },
101137
up: { x: 0, y: 1, z: 0 },
102138
fovy: 58,

native/shared/shaders/virtual_shadows/material_bindings.wgsl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
struct LocalVsmSamplingSlot {
2+
face_vps: array<mat4x4<f32>, 6>,
3+
face_pages_0_3: vec4<u32>,
4+
face_pages_4_5: vec4<u32>,
5+
};
16
struct DirectionalVsmParams {
27
level_vps: array<mat4x4<f32>, 3>,
3-
enabled: u32,
4-
virtual_pages_per_axis: u32,
5-
page_interior: u32,
6-
page_border: u32,
8+
words: vec4<u32>,
9+
local_light_meta: array<vec4<u32>, 256>,
10+
local_slots: array<LocalVsmSamplingSlot, 5>,
711
};
812
@group(1) @binding(10) var vsm_page_table: texture_2d_array<u32>;
913
@group(1) @binding(11) var vsm_physical_pages: texture_depth_2d_array;

native/shared/shaders/virtual_shadows/material_helper.wgsl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ fn sample_shadow_cascade(
22
cascade_idx: u32,
33
world_pos: vec3<f32>,
44
) -> f32 {
5-
if (vsm_params.enabled == 0u) {
5+
if (vsm_params.words.x == 0u) {
66
return sample_shadow_cascade_csm(cascade_idx, world_pos);
77
}
88
let light_clip = vsm_params.level_vps[cascade_idx]
@@ -17,7 +17,7 @@ fn sample_shadow_cascade(
1717
1.0 - (light_ndc.y * 0.5 + 0.5),
1818
);
1919
let depth_ref = light_ndc.z - 0.001;
20-
let axis = vsm_params.virtual_pages_per_axis;
20+
let axis = vsm_params.words.y;
2121
let scaled_uv = shadow_uv * f32(axis);
2222
let page_xy = min(vec2<u32>(scaled_uv), vec2<u32>(axis - 1u));
2323
let encoded = textureLoad(
@@ -30,8 +30,8 @@ fn sample_shadow_cascade(
3030
return sample_shadow_cascade_csm(cascade_idx, world_pos);
3131
}
3232
let physical_layer = i32((encoded & 0xffffu) - 1u);
33-
let interior = f32(vsm_params.page_interior);
34-
let border = f32(vsm_params.page_border);
33+
let interior = f32(vsm_params.words.z);
34+
let border = f32(vsm_params.words.w);
3535
let physical_size = interior + 2.0 * border;
3636
let local_uv = clamp(
3737
scaled_uv - vec2<f32>(page_xy),

native/shared/shaders/virtual_shadows/scene_bindings.wgsl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
struct LocalVsmSamplingSlot {
2+
face_vps: array<mat4x4<f32>, 6>,
3+
face_pages_0_3: vec4<u32>,
4+
face_pages_4_5: vec4<u32>,
5+
};
16
struct DirectionalVsmParams {
27
level_vps: array<mat4x4<f32>, 3>,
3-
enabled: u32,
4-
virtual_pages_per_axis: u32,
5-
page_interior: u32,
6-
page_border: u32,
8+
words: vec4<u32>,
9+
local_light_meta: array<vec4<u32>, 256>,
10+
local_slots: array<LocalVsmSamplingSlot, 5>,
711
};
812
@group(1) @binding(13) var vsm_page_table: texture_2d_array<u32>;
913
@group(1) @binding(14) var vsm_physical_pages: texture_depth_2d_array;

native/shared/shaders/virtual_shadows/scene_helper.wgsl

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ fn sample_virtual_shadow(
44
csm_uv: vec2<f32>,
55
csm_depth_ref: f32,
66
) -> f32 {
7-
if (vsm_params.enabled == 0u) {
7+
if (vsm_params.words.x == 0u) {
88
return sample_cascade(cascade, csm_uv, csm_depth_ref);
99
}
1010
let light_clip = vsm_params.level_vps[cascade] * vec4<f32>(world_pos, 1.0);
@@ -18,7 +18,7 @@ fn sample_virtual_shadow(
1818
return sample_cascade(cascade, csm_uv, csm_depth_ref);
1919
}
2020
let depth_ref = light_ndc.z - 0.001;
21-
let axis = vsm_params.virtual_pages_per_axis;
21+
let axis = vsm_params.words.y;
2222
let scaled_uv = shadow_uv * f32(axis);
2323
let page_xy = min(vec2<u32>(scaled_uv), vec2<u32>(axis - 1u));
2424
let encoded = textureLoad(
@@ -32,8 +32,8 @@ fn sample_virtual_shadow(
3232
}
3333

3434
let physical_layer = i32((encoded & 0xffffu) - 1u);
35-
let interior = f32(vsm_params.page_interior);
36-
let border = f32(vsm_params.page_border);
35+
let interior = f32(vsm_params.words.z);
36+
let border = f32(vsm_params.words.w);
3737
let physical_size = interior + 2.0 * border;
3838
let local_uv = clamp(
3939
scaled_uv - vec2<f32>(page_xy),
@@ -69,3 +69,79 @@ fn sample_virtual_shadow(
6969
}
7070
return virtual_value;
7171
}
72+
73+
fn local_shadow_face(direction: vec3<f32>) -> u32 {
74+
let absolute = abs(direction);
75+
if (absolute.x >= absolute.y && absolute.x >= absolute.z) {
76+
return select(1u, 0u, direction.x >= 0.0);
77+
}
78+
if (absolute.y >= absolute.z) {
79+
return select(3u, 2u, direction.y >= 0.0);
80+
}
81+
return select(5u, 4u, direction.z >= 0.0);
82+
}
83+
84+
fn local_shadow_page(slot: LocalVsmSamplingSlot, face: u32) -> u32 {
85+
if (face < 4u) {
86+
return slot.face_pages_0_3[face];
87+
}
88+
return slot.face_pages_4_5[face - 4u];
89+
}
90+
91+
// Metadata states are deliberately fail-closed:
92+
// 0 = ordinary point light (unshadowed),
93+
// 1 = shadow requested but not fully resident (suppress contribution),
94+
// 2..6 = fully resident local slot + 2.
95+
fn sample_local_shadow(light_index: u32, world_pos: vec3<f32>) -> f32 {
96+
let state = vsm_params.local_light_meta[light_index].x;
97+
if (state == 0u) {
98+
return 1.0;
99+
}
100+
if (state == 1u) {
101+
return 0.0;
102+
}
103+
let slot = vsm_params.local_slots[state - 2u];
104+
let direction = world_pos - lighting.point_lights[light_index].position.xyz;
105+
let face = local_shadow_face(direction);
106+
let encoded = local_shadow_page(slot, face);
107+
if (encoded == 0u) {
108+
return 0.0;
109+
}
110+
let light_clip = slot.face_vps[face] * vec4<f32>(world_pos, 1.0);
111+
let light_ndc = light_clip.xyz / light_clip.w;
112+
if (abs(light_ndc.x) > 1.0 || abs(light_ndc.y) > 1.0
113+
|| light_ndc.z < 0.0 || light_ndc.z > 1.0) {
114+
return 0.0;
115+
}
116+
let shadow_uv = vec2<f32>(
117+
light_ndc.x * 0.5 + 0.5,
118+
1.0 - (light_ndc.y * 0.5 + 0.5),
119+
);
120+
let interior = f32(vsm_params.words.z);
121+
let border = f32(vsm_params.words.w);
122+
let physical_size = interior + 2.0 * border;
123+
let page_uv = (vec2<f32>(border) + shadow_uv * interior) / physical_size;
124+
let texel = vec2<f32>(1.0 / physical_size);
125+
let offsets = array<vec2<f32>, 4>(
126+
vec2<f32>(-0.5, -0.5),
127+
vec2<f32>( 0.5, -0.5),
128+
vec2<f32>(-0.5, 0.5),
129+
vec2<f32>( 0.5, 0.5),
130+
);
131+
let physical_layer = i32((encoded & 0xffffu) - 1u);
132+
let depth_bias = max(0.0005, 0.002 * (1.0 - light_ndc.z));
133+
var shadow_value = 0.0;
134+
for (var tap = 0; tap < 4; tap = tap + 1) {
135+
shadow_value += textureSampleCompareLevel(
136+
vsm_physical_pages,
137+
shadow_samp,
138+
page_uv + offsets[tap] * texel,
139+
physical_layer,
140+
light_ndc.z - depth_bias,
141+
);
142+
}
143+
shadow_value *= 0.25;
144+
// New pages fade in from the fail-closed state, never from an
145+
// unshadowed local light.
146+
return shadow_value * min(f32(encoded >> 16u) / 8.0, 1.0);
147+
}

native/shared/src/ffi_core/visual.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,32 @@ macro_rules! __bloom_ffi_visual {
820820
})
821821
}
822822

823+
// bloom_add_shadowed_point_light [issue #132 local VSM]
824+
#[no_mangle]
825+
pub extern "C" fn bloom_add_shadowed_point_light(
826+
x: f64,
827+
y: f64,
828+
z: f64,
829+
range: f64,
830+
r: f64,
831+
g: f64,
832+
b: f64,
833+
intensity: f64,
834+
) -> f64 {
835+
$crate::ffi::guard("bloom_add_shadowed_point_light", move || {
836+
f64::from(engine().renderer.add_shadowed_point_light(
837+
x as f32,
838+
y as f32,
839+
z as f32,
840+
range as f32,
841+
r as f32,
842+
g as f32,
843+
b as f32,
844+
intensity as f32,
845+
))
846+
})
847+
}
848+
823849
// bloom_set_cursor_shape [source: macos]
824850
#[no_mangle]
825851
pub extern "C" fn bloom_set_cursor_shape(shape: f64) {

native/shared/src/renderer/froxel.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ const CLUSTERED_LOOP: &str = "
157157
let cl_count = cluster_counts[cluster];
158158
let cl_base = cluster * 256u;
159159
for (var ci = 0u; ci < cl_count; ci++) {
160-
let pl = lighting.point_lights[cluster_indices[cl_base + ci]];
160+
let light_index = cluster_indices[cl_base + ci];
161+
let pl = lighting.point_lights[light_index];
161162
let to_light = pl.position.xyz - in.world_pos;
162163
let dist = length(to_light);
163164
let range = pl.position.w;
@@ -413,6 +414,21 @@ impl FroxelPass {
413414
}
414415

415416
impl super::Renderer {
417+
/// Replace the already-queued froxel light snapshot after local-shadow
418+
/// admission compacts suppressed requests out of the live lighting list.
419+
pub(super) fn refresh_froxel_lights(&self) {
420+
let Some(froxel) = &self.froxel else { return };
421+
let n = (self.lighting_uniforms.point_light_count[0] as u32).min(MAX_LIGHTS_PER_CLUSTER);
422+
let count = [n as f32, 0.0, 0.0, 0.0_f32];
423+
self.queue
424+
.write_buffer(&froxel.lights_buffer, 0, bytemuck::bytes_of(&count));
425+
self.queue.write_buffer(
426+
&froxel.lights_buffer,
427+
16,
428+
bytemuck::cast_slice(&self.lighting_uniforms.point_lights),
429+
);
430+
}
431+
416432
/// Upload froxel params + the compact light list and dispatch the
417433
/// assignment pass. Runs every 3D frame on supported devices —
418434
/// even with zero lights, so `cluster_counts` never carries stale
@@ -451,14 +467,7 @@ impl super::Renderer {
451467
};
452468
self.queue
453469
.write_buffer(&froxel.params_buffer, 0, bytemuck::bytes_of(&params));
454-
let count = [n as f32, 0.0, 0.0, 0.0_f32];
455-
self.queue
456-
.write_buffer(&froxel.lights_buffer, 0, bytemuck::bytes_of(&count));
457-
self.queue.write_buffer(
458-
&froxel.lights_buffer,
459-
16,
460-
bytemuck::cast_slice(&self.lighting_uniforms.point_lights),
461-
);
470+
self.refresh_froxel_lights();
462471
froxel.record(encoder);
463472
}
464473
}

0 commit comments

Comments
 (0)