@bornengine/engine 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
//! Cascaded shadow-map pass: cascade fitting from the primary
|
|
2
|
+
//! directional light, the ticket-004 cache-hit skip, and the per-cascade
|
|
3
|
+
//! depth renders. Split from end_frame_with_scene (2000-line file policy
|
|
4
|
+
//! + render-graph migration prep).
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
|
|
8
|
+
impl Renderer {
|
|
9
|
+
pub(super) fn record_shadow_pass(
|
|
10
|
+
&mut self,
|
|
11
|
+
encoder: &mut wgpu::CommandEncoder,
|
|
12
|
+
profiler: &mut crate::profiler::Profiler,
|
|
13
|
+
scene: &mut crate::scene::SceneGraph,
|
|
14
|
+
) {
|
|
15
|
+
// Shadow pass: render scene nodes from light's perspective into
|
|
16
|
+
// cascaded shadow maps (3 cascades).
|
|
17
|
+
//
|
|
18
|
+
// Cache hit path (ticket 004): if no caster moved, the light
|
|
19
|
+
// didn't move, and the freshly-computed cascade VPs match the
|
|
20
|
+
// ones the cached depth textures were rendered with, we skip
|
|
21
|
+
// the whole pass. The depth textures retain their content and
|
|
22
|
+
// the main pass samples from them as if we had redrawn.
|
|
23
|
+
profiler.begin("shadow_pass");
|
|
24
|
+
if self.shadow_map.enabled {
|
|
25
|
+
// EN-043 — take last frame's caster transforms out of `self` up front: the
|
|
26
|
+
// caster lists below hold immutable borrows of self.model_gpu_cache for the
|
|
27
|
+
// rest of the function, so this map cannot be touched through `self` again
|
|
28
|
+
// until they are dead.
|
|
29
|
+
let prev_caster_ids = std::mem::take(&mut self.shadow_caster_tf);
|
|
30
|
+
let mut caster_ids_now: std::collections::HashSet<u64> =
|
|
31
|
+
std::collections::HashSet::with_capacity(prev_caster_ids.len() + 64);
|
|
32
|
+
// Compute cascade VPs from the primary directional light and camera.
|
|
33
|
+
let light_dir = [
|
|
34
|
+
self.lighting_uniforms.light_dir[0],
|
|
35
|
+
self.lighting_uniforms.light_dir[1],
|
|
36
|
+
self.lighting_uniforms.light_dir[2],
|
|
37
|
+
];
|
|
38
|
+
// Auto-fit: compute world-space AABB across every visible,
|
|
39
|
+
// cast-shadow node so the ortho volume always covers the
|
|
40
|
+
// scene regardless of what's loaded. No per-scene magic
|
|
41
|
+
// numbers.
|
|
42
|
+
let scene_bounds = scene.compute_shadow_bounds();
|
|
43
|
+
self.shadow_map.compute_cascade_vps(
|
|
44
|
+
light_dir,
|
|
45
|
+
self.current_camera_pos,
|
|
46
|
+
self.current_view_matrix,
|
|
47
|
+
// Use the pre-jitter projection so the cascade VPs
|
|
48
|
+
// stay byte-stable when the camera is actually
|
|
49
|
+
// stationary (the shadow cache compares them exactly).
|
|
50
|
+
self.current_proj_matrix_unjittered,
|
|
51
|
+
0.5, // near — start cascades slightly past the camera
|
|
52
|
+
80.0, // far — shadow coverage range
|
|
53
|
+
scene_bounds,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Re-upload lighting uniforms with cascade VPs and splits.
|
|
57
|
+
// Always write these — even on a cache hit the cascade
|
|
58
|
+
// split distances and view matrix track camera movement
|
|
59
|
+
// (they drive per-pixel cascade selection in the main
|
|
60
|
+
// shader), which is independent of shadow texture content.
|
|
61
|
+
self.lighting_uniforms.shadow_cascade_vps = self.shadow_map.light_vps;
|
|
62
|
+
self.lighting_uniforms.shadow_cascade_splits = [
|
|
63
|
+
self.shadow_map.cascade_splits[0],
|
|
64
|
+
self.shadow_map.cascade_splits[1],
|
|
65
|
+
self.shadow_map.cascade_splits[2],
|
|
66
|
+
// .w = mip-LOD bias for material textures. Bias finer
|
|
67
|
+
// by log2(render_scale) — recovers -1.0 at 0.5 (one
|
|
68
|
+
// mip finer to offset hardware's coarser selection
|
|
69
|
+
// at half-res), ~-0.42 at 0.75, 0 at native.
|
|
70
|
+
if self.render_scale < 0.999 { self.render_scale.log2() } else { 0.0 },
|
|
71
|
+
];
|
|
72
|
+
self.lighting_uniforms.shadow_view_matrix = self.current_view_matrix;
|
|
73
|
+
self.queue.write_buffer(
|
|
74
|
+
&self.lighting_buffer,
|
|
75
|
+
0,
|
|
76
|
+
bytemuck::bytes_of(&self.lighting_uniforms),
|
|
77
|
+
);
|
|
78
|
+
// Shadow-flicker fix: the material system's PerView buffer was
|
|
79
|
+
// uploaded before this fit ran and still carries LAST frame's
|
|
80
|
+
// cascade VPs. Patch its shadow fields so material-path
|
|
81
|
+
// receivers sample the depth maps with the same matrices the
|
|
82
|
+
// maps are rendered with this frame. Keep .w at 0.0 — that slot
|
|
83
|
+
// is the TSR mip-LOD bias, which the material path historically
|
|
84
|
+
// never received (begin_mode_3d resets it before the material
|
|
85
|
+
// PerView upload); delivering -1.0 here makes hardware mip
|
|
86
|
+
// selection flip per-panel under TAA jitter (visible texture
|
|
87
|
+
// detail popping).
|
|
88
|
+
let mut splits = self.lighting_uniforms.shadow_cascade_splits;
|
|
89
|
+
splits[3] = 0.0;
|
|
90
|
+
self.material_system.refresh_shadow_uniforms(
|
|
91
|
+
&self.queue,
|
|
92
|
+
splits,
|
|
93
|
+
self.lighting_uniforms.shadow_view_matrix,
|
|
94
|
+
self.shadow_map.light_vps,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Cache gate, stage 1 — whole-pass invalidators. Per-cascade
|
|
98
|
+
// staleness (VP compare + caster-content signature) is decided
|
|
99
|
+
// after the caster lists are built. Texel-snap + radius
|
|
100
|
+
// quantization + re-fit slack in `compute_cascade_vps` make the
|
|
101
|
+
// per-cascade VP compare exact, so a kept VP + unchanged content
|
|
102
|
+
// means the cascade's cached depth texture is still valid.
|
|
103
|
+
let scene_ver = scene.shadow_version;
|
|
104
|
+
let light_changed = self.shadow_map.rendered_light_dir
|
|
105
|
+
.map(|cached| cached != light_dir)
|
|
106
|
+
.unwrap_or(true);
|
|
107
|
+
let force_all = self.shadow_map.always_fresh
|
|
108
|
+
|| self.shadow_map.dirty
|
|
109
|
+
|| light_changed
|
|
110
|
+
|| self.shadow_map.rendered_light_vps.is_none()
|
|
111
|
+
|| self.shadow_map.rendered_scene_version != scene_ver;
|
|
112
|
+
|
|
113
|
+
// Build a shared caster list + buffer-ref vectors, then
|
|
114
|
+
// filter per cascade against that cascade's ortho frustum.
|
|
115
|
+
// A caster outside cascade N's frustum can't write pixels
|
|
116
|
+
// into cascade N; near/far pancaking already covers
|
|
117
|
+
// behind-camera casters via the cascade's own far plane.
|
|
118
|
+
struct ShadowDrawEntry {
|
|
119
|
+
vb_idx: usize,
|
|
120
|
+
ib_idx: usize,
|
|
121
|
+
index_start: u32,
|
|
122
|
+
index_count: u32,
|
|
123
|
+
transform: [[f32; 4]; 4],
|
|
124
|
+
wmin: [f32; 3],
|
|
125
|
+
wmax: [f32; 3],
|
|
126
|
+
// Index into `cutout_bgs` for an alpha-tested caster (cutout
|
|
127
|
+
// foliage), or -1 for an opaque caster (plain depth pipeline).
|
|
128
|
+
cutout_idx: i32,
|
|
129
|
+
// Immediate-mode segment containing skinned characters —
|
|
130
|
+
// rendered with the skinning-aware shadow pipeline so animated
|
|
131
|
+
// player/enemies cast a posed shadow instead of a rest pose at
|
|
132
|
+
// the origin. (Mixed segments: non-skinned verts still
|
|
133
|
+
// transform by the model matrix via the shader's weight branch.)
|
|
134
|
+
skinned: bool,
|
|
135
|
+
// Content identity for the per-cascade cache: stable across
|
|
136
|
+
// frames for static casters, salted with `frame_nonce` for
|
|
137
|
+
// animated ones so their cascades re-render every frame.
|
|
138
|
+
sig: u64,
|
|
139
|
+
// Immediate-batch content (animated characters, per-frame
|
|
140
|
+
// primitives). Dynamic casters render into the live cascade
|
|
141
|
+
// texture every frame on top of the cached static depth;
|
|
142
|
+
// they never invalidate the static cache.
|
|
143
|
+
dynamic: bool,
|
|
144
|
+
// Base slot of a skinned cached draw's pose in the shared
|
|
145
|
+
// joint buffer (the cached VB keeps RAW joint indices, so
|
|
146
|
+
// vs_shadow_skinned adds this via ShadowUniforms.misc.x).
|
|
147
|
+
// 0.0 for everything else — including immediate-batch
|
|
148
|
+
// skinned segments, whose vertex joints are pre-offset.
|
|
149
|
+
joint_offset: f32,
|
|
150
|
+
// Foliage wind amount for this caster (0 = rigid). Non-zero makes the
|
|
151
|
+
// caster MOVE, which is why it also forces `dynamic` — a swaying tree
|
|
152
|
+
// cannot reuse its cached static shadow depth.
|
|
153
|
+
foliage: f32,
|
|
154
|
+
// EN-043 — stable identity (NOT including the transform), so a caster
|
|
155
|
+
// that moved can be told apart from a caster that appeared.
|
|
156
|
+
key: u64,
|
|
157
|
+
}
|
|
158
|
+
fn entry_sig(kind: u8, id: u64, idx: u64, transform: &[[f32; 4]; 4]) -> u64 {
|
|
159
|
+
let mut h = FNV_OFFSET;
|
|
160
|
+
h = fnv1a_bytes(h, &[kind]);
|
|
161
|
+
h = fnv1a_bytes(h, &id.to_le_bytes());
|
|
162
|
+
h = fnv1a_bytes(h, &idx.to_le_bytes());
|
|
163
|
+
fnv1a_bytes(h, bytemuck::bytes_of(transform))
|
|
164
|
+
}
|
|
165
|
+
// EN-043 — "was this exact caster, at this exact transform, here last frame?"
|
|
166
|
+
//
|
|
167
|
+
// One combined hash of identity AND transform, looked up in a SET. If it is
|
|
168
|
+
// in last frame's set, the caster has not moved and stays static. If it is
|
|
169
|
+
// not, it either moved or is new — either way it goes in the dynamic set,
|
|
170
|
+
// where it draws on top of the cached static depth instead of invalidating it.
|
|
171
|
+
//
|
|
172
|
+
// ORDER-INDEPENDENT, and that is the whole point of the rewrite. The first
|
|
173
|
+
// version keyed on "the Nth draw of this model handle", which was fine until
|
|
174
|
+
// the game started drawing its forest FRONT-TO-BACK: the sort order changes
|
|
175
|
+
// as the camera moves, so occurrence N became a different tree every frame,
|
|
176
|
+
// dozens of perfectly stationary trees were misread as movers, and the
|
|
177
|
+
// dynamic set blew past 32 casters in combat. A set membership test does not
|
|
178
|
+
// care what order the draws arrive in.
|
|
179
|
+
fn caster_id(kind: u8, id: u64, idx: u64, transform: &[[f32; 4]; 4]) -> u64 {
|
|
180
|
+
let mut h = FNV_OFFSET;
|
|
181
|
+
h = fnv1a_bytes(h, &[kind]);
|
|
182
|
+
h = fnv1a_bytes(h, &id.to_le_bytes());
|
|
183
|
+
h = fnv1a_bytes(h, &idx.to_le_bytes());
|
|
184
|
+
fnv1a_bytes(h, bytemuck::bytes_of(transform))
|
|
185
|
+
}
|
|
186
|
+
// Per-frame nonce for animated casters' signatures. Bumped
|
|
187
|
+
// whenever shadows render — skinned CACHED model draws need it
|
|
188
|
+
// even when the immediate batch is empty, which is the norm now
|
|
189
|
+
// that skinned models draw through the cache.
|
|
190
|
+
self.shadow_map.frame_nonce = self.shadow_map.frame_nonce.wrapping_add(1);
|
|
191
|
+
let nonce = self.shadow_map.frame_nonce;
|
|
192
|
+
|
|
193
|
+
let mut shadow_nodes: Vec<ShadowDrawEntry> = Vec::new();
|
|
194
|
+
let mut shadow_vbs: Vec<&wgpu::Buffer> = Vec::new();
|
|
195
|
+
let mut shadow_ibs: Vec<&wgpu::Buffer> = Vec::new();
|
|
196
|
+
let mut cutout_bgs: Vec<&wgpu::BindGroup> = Vec::new();
|
|
197
|
+
for (i, (_handle, node)) in scene.nodes.iter().enumerate() {
|
|
198
|
+
// gi_only proxies duplicate geometry that already casts through
|
|
199
|
+
// the material-command path below — including them would
|
|
200
|
+
// double-render every caster.
|
|
201
|
+
if !node.visible || node.gi_only || !node.cast_shadow || node.indices.is_empty() {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let Some(vb) = &node.gpu_vb else { continue };
|
|
205
|
+
let Some(ib) = &node.gpu_ib else { continue };
|
|
206
|
+
let vb_idx = shadow_vbs.len();
|
|
207
|
+
shadow_vbs.push(vb);
|
|
208
|
+
shadow_ibs.push(ib);
|
|
209
|
+
// MASK-material nodes carry an alpha-test shadow bind group so
|
|
210
|
+
// foliage casts dappled shadows (same as the cached-model path).
|
|
211
|
+
let cutout_idx = match &node.gpu_shadow_cutout_bg {
|
|
212
|
+
Some(bg) => { let i = cutout_bgs.len(); cutout_bgs.push(bg); i as i32 }
|
|
213
|
+
None => -1,
|
|
214
|
+
};
|
|
215
|
+
shadow_nodes.push(ShadowDrawEntry {
|
|
216
|
+
vb_idx,
|
|
217
|
+
ib_idx: vb_idx,
|
|
218
|
+
index_start: 0,
|
|
219
|
+
index_count: node.gpu_index_count,
|
|
220
|
+
transform: node.transform,
|
|
221
|
+
wmin: node.world_bounds_min,
|
|
222
|
+
wmax: node.world_bounds_max,
|
|
223
|
+
cutout_idx,
|
|
224
|
+
skinned: false,
|
|
225
|
+
sig: entry_sig(0, i as u64, node.gpu_index_count as u64, &node.transform),
|
|
226
|
+
dynamic: false,
|
|
227
|
+
joint_offset: 0.0,
|
|
228
|
+
foliage: 0.0,
|
|
229
|
+
key: caster_id(0, i as u64, node.gpu_index_count as u64, &node.transform),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Immediate-mode 3D batch (drawCube/drawSphere/non-cached models),
|
|
234
|
+
// one entry per segment. These verts are already in WORLD space, so
|
|
235
|
+
// the model matrix is identity. Model draws maintain per-segment
|
|
236
|
+
// bounds inline (skinned via joint-transformed rest AABBs);
|
|
237
|
+
// primitive-only segments are scanned here. Segments with skinned
|
|
238
|
+
// content take a per-frame nonce as their signature (animation
|
|
239
|
+
// means their rendered output changes every frame); static
|
|
240
|
+
// segments hash their vertex positions, so e.g. pickups
|
|
241
|
+
// re-submitted identically each frame don't dirty their cascades.
|
|
242
|
+
if !self.indices_3d.is_empty() {
|
|
243
|
+
self.scan_unbounded_segments_3d();
|
|
244
|
+
let vb_idx = shadow_vbs.len();
|
|
245
|
+
shadow_vbs.push(&self.persistent_vb_3d);
|
|
246
|
+
shadow_ibs.push(&self.persistent_ib_3d);
|
|
247
|
+
if self.draw_calls_3d.is_empty() {
|
|
248
|
+
// Fallback: vertices without segment tracking — one
|
|
249
|
+
// unbounded, always-dirty entry (pre-segmentation shape).
|
|
250
|
+
shadow_nodes.push(ShadowDrawEntry {
|
|
251
|
+
vb_idx,
|
|
252
|
+
ib_idx: vb_idx,
|
|
253
|
+
index_start: 0,
|
|
254
|
+
index_count: self.indices_3d.len() as u32,
|
|
255
|
+
transform: IDENTITY_MAT4,
|
|
256
|
+
wmin: [1.0, 1.0, 1.0],
|
|
257
|
+
wmax: [-1.0, -1.0, -1.0],
|
|
258
|
+
cutout_idx: -1,
|
|
259
|
+
skinned: true,
|
|
260
|
+
sig: nonce,
|
|
261
|
+
dynamic: true,
|
|
262
|
+
joint_offset: 0.0,
|
|
263
|
+
foliage: 0.0,
|
|
264
|
+
key: 0,
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
let num_calls = self.draw_calls_3d.len();
|
|
268
|
+
for ci in 0..num_calls {
|
|
269
|
+
let call = &self.draw_calls_3d[ci];
|
|
270
|
+
let next_start = if ci + 1 < num_calls {
|
|
271
|
+
self.draw_calls_3d[ci + 1].index_start
|
|
272
|
+
} else {
|
|
273
|
+
self.indices_3d.len() as u32
|
|
274
|
+
};
|
|
275
|
+
let count = next_start - call.index_start;
|
|
276
|
+
if count == 0 { continue; }
|
|
277
|
+
shadow_nodes.push(ShadowDrawEntry {
|
|
278
|
+
vb_idx,
|
|
279
|
+
ib_idx: vb_idx,
|
|
280
|
+
index_start: call.index_start,
|
|
281
|
+
index_count: count,
|
|
282
|
+
transform: IDENTITY_MAT4,
|
|
283
|
+
wmin: call.wmin,
|
|
284
|
+
wmax: call.wmax,
|
|
285
|
+
cutout_idx: -1,
|
|
286
|
+
skinned: call.has_skinned,
|
|
287
|
+
sig: if call.has_skinned { nonce } else { call.content_hash },
|
|
288
|
+
dynamic: true,
|
|
289
|
+
joint_offset: 0.0,
|
|
290
|
+
foliage: 0.0,
|
|
291
|
+
key: 0,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Foliage promoted to the dynamic set this frame. Capped well below
|
|
298
|
+
// SHADOW_MAX_DYNAMIC so the characters — whose shadows are the ones a
|
|
299
|
+
// player actually looks at — always keep their slots.
|
|
300
|
+
const MAX_FOLIAGE_DYNAMIC: u32 = 24;
|
|
301
|
+
let mut foliage_dynamic: u32 = 0;
|
|
302
|
+
|
|
303
|
+
// Cached models (drawModel: trees, characters, etc.) — each is a
|
|
304
|
+
// GpuMesh plus its object→world matrix. World AABB from the
|
|
305
|
+
// cache-time local AABB so per-cascade culling rejects casters
|
|
306
|
+
// outside a cascade's ortho frustum (the forest was previously
|
|
307
|
+
// re-drawn into every cascade every frame). Skinned cached draws
|
|
308
|
+
// render through the skinning pipeline as dynamic casters (pose
|
|
309
|
+
// changes every frame → nonce signature) with the joint-union
|
|
310
|
+
// AABB computed at submit time.
|
|
311
|
+
for cmd in self.model_draw_commands.iter() {
|
|
312
|
+
if let Some(Some(meshes)) = self.model_gpu_cache.get(&cmd.cache_handle) {
|
|
313
|
+
if cmd.mesh_idx < meshes.len() {
|
|
314
|
+
let mesh = &meshes[cmd.mesh_idx];
|
|
315
|
+
let vb_idx = shadow_vbs.len();
|
|
316
|
+
shadow_vbs.push(&mesh.vb);
|
|
317
|
+
shadow_ibs.push(&mesh.ib);
|
|
318
|
+
// Cutout foliage → alpha-tested shadow pipeline.
|
|
319
|
+
let cutout_idx = match &mesh.shadow_cutout_bg {
|
|
320
|
+
Some(bg) => { let i = cutout_bgs.len(); cutout_bgs.push(bg); i as i32 }
|
|
321
|
+
None => -1,
|
|
322
|
+
};
|
|
323
|
+
if cmd.skinned {
|
|
324
|
+
// Sentinel bounds (min > max) when the submit-time
|
|
325
|
+
// AABB was empty → uncullable, never lost.
|
|
326
|
+
let (wmin, wmax) = cmd.bounds_override
|
|
327
|
+
.unwrap_or(([1.0, 1.0, 1.0], [-1.0, -1.0, -1.0]));
|
|
328
|
+
shadow_nodes.push(ShadowDrawEntry {
|
|
329
|
+
vb_idx,
|
|
330
|
+
ib_idx: vb_idx,
|
|
331
|
+
index_start: 0,
|
|
332
|
+
index_count: mesh.index_count,
|
|
333
|
+
transform: cmd.model,
|
|
334
|
+
wmin,
|
|
335
|
+
wmax,
|
|
336
|
+
cutout_idx,
|
|
337
|
+
skinned: true,
|
|
338
|
+
sig: nonce,
|
|
339
|
+
dynamic: true,
|
|
340
|
+
joint_offset: cmd.joint_offset,
|
|
341
|
+
foliage: 0.0,
|
|
342
|
+
key: 0,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
// Only sway the shadow if the game asked for it AND there is
|
|
346
|
+
// room in the dynamic-caster budget.
|
|
347
|
+
//
|
|
348
|
+
// That second condition is not paranoia. A swaying caster
|
|
349
|
+
// cannot reuse the cached static depth, so it must move to
|
|
350
|
+
// the DYNAMIC set — and that set holds SHADOW_MAX_DYNAMIC
|
|
351
|
+
// (64) entries. The shooter's forest alone is 88 trees x 4
|
|
352
|
+
// primitives = 352. Marking them all dynamic overflows the
|
|
353
|
+
// budget, and the overflow is dropped — which does not merely
|
|
354
|
+
// cost frames, it silently DELETES shadows. Measured: turning
|
|
355
|
+
// this on removed every tree shadow AND the player's own
|
|
356
|
+
// shadow from under their feet, while reporting a higher fps.
|
|
357
|
+
//
|
|
358
|
+
// So: sway as many as fit, leave the rest rigid. A slightly
|
|
359
|
+
// stale canopy shadow is invisible; a missing one is not.
|
|
360
|
+
let fol = if self.foliage_shadow_motion
|
|
361
|
+
&& foliage_dynamic < MAX_FOLIAGE_DYNAMIC
|
|
362
|
+
{
|
|
363
|
+
let f = self.foliage_wind.get(&cmd.cache_handle).copied().unwrap_or(0.0);
|
|
364
|
+
if f > 0.0 { foliage_dynamic += 1; }
|
|
365
|
+
f
|
|
366
|
+
} else { 0.0 };
|
|
367
|
+
let (wmin, wmax) =
|
|
368
|
+
transform_aabb(&cmd.model, mesh.local_min, mesh.local_max);
|
|
369
|
+
shadow_nodes.push(ShadowDrawEntry {
|
|
370
|
+
vb_idx,
|
|
371
|
+
ib_idx: vb_idx,
|
|
372
|
+
index_start: 0,
|
|
373
|
+
index_count: mesh.index_count,
|
|
374
|
+
transform: cmd.model,
|
|
375
|
+
wmin,
|
|
376
|
+
wmax,
|
|
377
|
+
cutout_idx,
|
|
378
|
+
skinned: false,
|
|
379
|
+
// A swaying caster changes shape every frame, so it
|
|
380
|
+
// cannot share the cached static depth: signature goes
|
|
381
|
+
// to the per-frame nonce and it renders as dynamic.
|
|
382
|
+
// That is exactly the cost `foliage_shadow_motion`
|
|
383
|
+
// gates, which is why it defaults off.
|
|
384
|
+
sig: if fol > 0.0 { nonce }
|
|
385
|
+
else { entry_sig(1, cmd.cache_handle, cmd.mesh_idx as u64, &cmd.model) },
|
|
386
|
+
dynamic: fol > 0.0,
|
|
387
|
+
joint_offset: 0.0,
|
|
388
|
+
foliage: fol,
|
|
389
|
+
key: caster_id(1, cmd.cache_handle, cmd.mesh_idx as u64, &cmd.model),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Material-system draws (terrain / building / trees rendered through
|
|
397
|
+
// compiled materials). Same GpuMesh cache as drawModel — the command
|
|
398
|
+
// carries a CPU-side copy of its model matrix precisely for this
|
|
399
|
+
// pass. `commands` holds only the opaque + cutout buckets, so water /
|
|
400
|
+
// glass / additive effects never cast. Instanced draws (the 20k-blade
|
|
401
|
+
// grass field) are skipped deliberately: vs_shadow has no instance
|
|
402
|
+
// stream, and per-blade grass shadows are sub-texel noise at these
|
|
403
|
+
// cascade resolutions anyway.
|
|
404
|
+
for cmd in self.material_system.commands.iter() {
|
|
405
|
+
if cmd.instance.is_some() { continue; }
|
|
406
|
+
if let Some(Some(meshes)) = self.model_gpu_cache.get(&cmd.mesh_handle) {
|
|
407
|
+
if cmd.mesh_idx < meshes.len() {
|
|
408
|
+
let mesh = &meshes[cmd.mesh_idx];
|
|
409
|
+
let vb_idx = shadow_vbs.len();
|
|
410
|
+
shadow_vbs.push(&mesh.vb);
|
|
411
|
+
shadow_ibs.push(&mesh.ib);
|
|
412
|
+
// MASK-material meshes (leaf cards) keep their dappled
|
|
413
|
+
// alpha-tested shadows, same as the other two paths.
|
|
414
|
+
let cutout_idx = match &mesh.shadow_cutout_bg {
|
|
415
|
+
Some(bg) => { let i = cutout_bgs.len(); cutout_bgs.push(bg); i as i32 }
|
|
416
|
+
None => -1,
|
|
417
|
+
};
|
|
418
|
+
let (wmin, wmax) =
|
|
419
|
+
transform_aabb(&cmd.model, mesh.local_min, mesh.local_max);
|
|
420
|
+
shadow_nodes.push(ShadowDrawEntry {
|
|
421
|
+
vb_idx,
|
|
422
|
+
ib_idx: vb_idx,
|
|
423
|
+
index_start: 0,
|
|
424
|
+
index_count: mesh.index_count,
|
|
425
|
+
transform: cmd.model,
|
|
426
|
+
wmin,
|
|
427
|
+
wmax,
|
|
428
|
+
cutout_idx,
|
|
429
|
+
skinned: false,
|
|
430
|
+
sig: entry_sig(2, cmd.mesh_handle, cmd.mesh_idx as u64, &cmd.model),
|
|
431
|
+
dynamic: false,
|
|
432
|
+
joint_offset: 0.0,
|
|
433
|
+
foliage: 0.0,
|
|
434
|
+
key: caster_id(2, cmd.mesh_handle, cmd.mesh_idx as u64, &cmd.model),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// EN-043 — promote MOVERS to the dynamic set.
|
|
441
|
+
//
|
|
442
|
+
// A non-skinned cached caster whose transform changed since last frame used
|
|
443
|
+
// to stay in the STATIC set with a different content signature. That
|
|
444
|
+
// invalidated the cascade's cached depth, so every tree, wall and terrain
|
|
445
|
+
// tile in the world re-rendered into all three cascades — every frame —
|
|
446
|
+
// because one pickup was bobbing. Measured on the shooter's title screen:
|
|
447
|
+
// shadow_pass GPU 6.0-7.0 ms against the 0.1-1.7 ms the cache was built to
|
|
448
|
+
// deliver.
|
|
449
|
+
//
|
|
450
|
+
// A caster that moves is DYNAMIC, by definition. Dynamic casters draw on
|
|
451
|
+
// top of the cached static depth every frame and never invalidate it, which
|
|
452
|
+
// is exactly what a moving object needs and costs one draw instead of a
|
|
453
|
+
// thousand.
|
|
454
|
+
for e in shadow_nodes.iter_mut() {
|
|
455
|
+
if e.dynamic { continue; }
|
|
456
|
+
caster_ids_now.insert(e.key);
|
|
457
|
+
// Not here last frame at this exact transform => it moved (or is new).
|
|
458
|
+
// Either way: dynamic. A first-frame caster costs one extra dynamic draw
|
|
459
|
+
// and settles into the static set next frame.
|
|
460
|
+
if !prev_caster_ids.is_empty() && !prev_caster_ids.contains(&e.key) {
|
|
461
|
+
e.dynamic = true;
|
|
462
|
+
e.sig = nonce;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let cascade_planes: [[[f32; 4]; 6]; crate::shadows::NUM_CASCADES] =
|
|
467
|
+
std::array::from_fn(|c| {
|
|
468
|
+
crate::scene::extract_frustum_planes(&self.shadow_map.light_vps[c])
|
|
469
|
+
});
|
|
470
|
+
let mut cascade_indices: [Vec<usize>; crate::shadows::NUM_CASCADES] =
|
|
471
|
+
std::array::from_fn(|_| Vec::with_capacity(shadow_nodes.len()));
|
|
472
|
+
for (i, entry) in shadow_nodes.iter().enumerate() {
|
|
473
|
+
let has_bounds = entry.wmin[0] <= entry.wmax[0];
|
|
474
|
+
for c in 0..crate::shadows::NUM_CASCADES {
|
|
475
|
+
if has_bounds
|
|
476
|
+
&& crate::scene::aabb_outside_frustum(&cascade_planes[c], entry.wmin, entry.wmax)
|
|
477
|
+
{
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
cascade_indices[c].push(i);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Per-cascade STATIC content signature: fold every surviving
|
|
484
|
+
// non-dynamic caster's identity, in draw order. The static depth
|
|
485
|
+
// cache re-renders only when its cascade's VP changed, this
|
|
486
|
+
// signature changed, or a whole-pass invalidator fired. Dynamic
|
|
487
|
+
// casters are excluded — they draw on top of the cached static
|
|
488
|
+
// depth every frame and never invalidate it.
|
|
489
|
+
let mut cascade_sigs = [0u64; crate::shadows::NUM_CASCADES];
|
|
490
|
+
for c in 0..crate::shadows::NUM_CASCADES {
|
|
491
|
+
let mut h = FNV_OFFSET;
|
|
492
|
+
for &ei in cascade_indices[c].iter() {
|
|
493
|
+
if shadow_nodes[ei].dynamic { continue; }
|
|
494
|
+
h = fnv1a_bytes(h, &shadow_nodes[ei].sig.to_le_bytes());
|
|
495
|
+
}
|
|
496
|
+
cascade_sigs[c] = h;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Render each cascade. Static casters live in a cached depth
|
|
500
|
+
// texture ("cached whole-scene shadows") re-rendered only when
|
|
501
|
+
// the cascade's VP or static content changes; every frame the
|
|
502
|
+
// live texture is refreshed by copy and the few dynamic casters
|
|
503
|
+
// draw on top with Load. A cascade with no change is skipped
|
|
504
|
+
// entirely. Uniform slots: static casters use the head of the
|
|
505
|
+
// cascade's region, dynamic casters the reserved tail — the
|
|
506
|
+
// ranges are disjoint because every write_buffer lands at
|
|
507
|
+
// submit, before any encoded pass executes.
|
|
508
|
+
for cascade in 0..crate::shadows::NUM_CASCADES {
|
|
509
|
+
let stride = crate::shadows::SHADOW_UNIFORM_STRIDE as usize;
|
|
510
|
+
let max = crate::shadows::SHADOW_MAX_NODES as usize;
|
|
511
|
+
let max_dynamic = crate::shadows::SHADOW_MAX_DYNAMIC as usize;
|
|
512
|
+
let max_static = max - max_dynamic;
|
|
513
|
+
let cascade_base = cascade * stride * max;
|
|
514
|
+
let cascade_vp = self.shadow_map.light_vps[cascade];
|
|
515
|
+
let entries = &cascade_indices[cascade];
|
|
516
|
+
|
|
517
|
+
let vp_changed = self.shadow_map.rendered_light_vps
|
|
518
|
+
.map(|vps| vps[cascade] != self.shadow_map.light_vps[cascade])
|
|
519
|
+
.unwrap_or(true);
|
|
520
|
+
let static_stale = force_all || vp_changed
|
|
521
|
+
|| self.shadow_map.rendered_cascade_sig[cascade] != cascade_sigs[cascade];
|
|
522
|
+
let dyn_now = entries.iter().any(|&ei| shadow_nodes[ei].dynamic);
|
|
523
|
+
if !static_stale && !dyn_now && !self.shadow_map.had_dynamic[cascade] {
|
|
524
|
+
// Live texture already holds exactly this content.
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if static_stale {
|
|
529
|
+
let static_entries: Vec<usize> = entries.iter().copied()
|
|
530
|
+
.filter(|&ei| !shadow_nodes[ei].dynamic)
|
|
531
|
+
.take(max_static)
|
|
532
|
+
.collect();
|
|
533
|
+
let mut uniform_data: Vec<u8> =
|
|
534
|
+
vec![0u8; stride * static_entries.len().max(1)];
|
|
535
|
+
for (slot, &ei) in static_entries.iter().enumerate() {
|
|
536
|
+
let uniforms = crate::shadows::ShadowUniforms {
|
|
537
|
+
light_vp: cascade_vp,
|
|
538
|
+
model: shadow_nodes[ei].transform,
|
|
539
|
+
misc: [shadow_nodes[ei].joint_offset, 0.0, shadow_nodes[ei].foliage, 0.0],
|
|
540
|
+
wind: self.lighting_uniforms.wind,
|
|
541
|
+
};
|
|
542
|
+
let off = slot * stride;
|
|
543
|
+
uniform_data[off..off + std::mem::size_of::<crate::shadows::ShadowUniforms>()]
|
|
544
|
+
.copy_from_slice(bytemuck::bytes_of(&uniforms));
|
|
545
|
+
}
|
|
546
|
+
// Each cascade owns its own slice of the uniform buffer —
|
|
547
|
+
// all write_buffer calls execute at submit, BEFORE any of
|
|
548
|
+
// the encoded passes run, so sharing one region would
|
|
549
|
+
// leave every cascade rendering with the last cascade's
|
|
550
|
+
// matrices (the no-near-shadows bug).
|
|
551
|
+
if !static_entries.is_empty() {
|
|
552
|
+
self.queue.write_buffer(
|
|
553
|
+
&self.shadow_map.uniform_buffer,
|
|
554
|
+
cascade_base as u64,
|
|
555
|
+
&uniform_data[..static_entries.len() * stride],
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
{
|
|
559
|
+
let shadow_ts = profiler.pass_timestamp_writes("shadow_pass");
|
|
560
|
+
let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
561
|
+
label: Some("shadow_pass_static"),
|
|
562
|
+
color_attachments: &[],
|
|
563
|
+
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
|
564
|
+
view: &self.shadow_map.static_depth_views[cascade],
|
|
565
|
+
depth_ops: Some(wgpu::Operations {
|
|
566
|
+
load: wgpu::LoadOp::Clear(1.0),
|
|
567
|
+
store: wgpu::StoreOp::Store,
|
|
568
|
+
}),
|
|
569
|
+
stencil_ops: None,
|
|
570
|
+
}),
|
|
571
|
+
timestamp_writes: shadow_ts,
|
|
572
|
+
occlusion_query_set: None,
|
|
573
|
+
multiview_mask: None,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Pipeline kind per caster: 0 opaque, 1 cutout, 2 skinned.
|
|
577
|
+
// Only switch when the kind changes.
|
|
578
|
+
let mut cur_kind: u8 = 0;
|
|
579
|
+
shadow_pass.set_pipeline(&self.shadow_map.pipeline);
|
|
580
|
+
for (slot, &ei) in static_entries.iter().enumerate() {
|
|
581
|
+
let entry = &shadow_nodes[ei];
|
|
582
|
+
let offset = (cascade_base + slot * stride) as u32;
|
|
583
|
+
let kind: u8 = if entry.skinned { 2 }
|
|
584
|
+
else if entry.cutout_idx >= 0 { 1 }
|
|
585
|
+
else { 0 };
|
|
586
|
+
if kind != cur_kind {
|
|
587
|
+
shadow_pass.set_pipeline(match kind {
|
|
588
|
+
1 => &self.shadow_map.pipeline_cutout,
|
|
589
|
+
2 => &self.shadow_map.pipeline_skinned,
|
|
590
|
+
_ => &self.shadow_map.pipeline,
|
|
591
|
+
});
|
|
592
|
+
cur_kind = kind;
|
|
593
|
+
}
|
|
594
|
+
shadow_pass.set_bind_group(0, &self.shadow_map.uniform_bind_group, &[offset]);
|
|
595
|
+
if kind == 1 {
|
|
596
|
+
shadow_pass.set_bind_group(1, cutout_bgs[entry.cutout_idx as usize], &[]);
|
|
597
|
+
} else if kind == 2 {
|
|
598
|
+
shadow_pass.set_bind_group(1, &self.joint_bind_group, &[]);
|
|
599
|
+
}
|
|
600
|
+
shadow_pass.set_vertex_buffer(0, shadow_vbs[entry.vb_idx].slice(..));
|
|
601
|
+
shadow_pass.set_index_buffer(shadow_ibs[entry.ib_idx].slice(..), wgpu::IndexFormat::Uint32);
|
|
602
|
+
shadow_pass.draw_indexed(
|
|
603
|
+
entry.index_start..entry.index_start + entry.index_count,
|
|
604
|
+
0,
|
|
605
|
+
0..1,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
self.shadow_map.rendered_cascade_sig[cascade] = cascade_sigs[cascade];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Refresh the live texture from the static cache, then draw
|
|
613
|
+
// dynamic casters on top.
|
|
614
|
+
encoder.copy_texture_to_texture(
|
|
615
|
+
self.shadow_map.static_depth_textures[cascade].as_image_copy(),
|
|
616
|
+
self.shadow_map.depth_textures[cascade].as_image_copy(),
|
|
617
|
+
wgpu::Extent3d {
|
|
618
|
+
width: crate::shadows::CASCADE_MAP_SIZE,
|
|
619
|
+
height: crate::shadows::CASCADE_MAP_SIZE,
|
|
620
|
+
depth_or_array_layers: 1,
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
if dyn_now {
|
|
624
|
+
let dyn_base = cascade_base + stride * max_static;
|
|
625
|
+
// EN-042 — the dynamic budget can overflow, and the overflow IS
|
|
626
|
+
// dropped. Which caster gets dropped must not be an accident of
|
|
627
|
+
// queue order. It was, and it cost this project twice: both times
|
|
628
|
+
// the thing that silently vanished was the player's own shadow, and
|
|
629
|
+
// both times the frame rate went UP and looked like a win.
|
|
630
|
+
//
|
|
631
|
+
// Rank them, so if we must lose a shadow we lose one nobody misses:
|
|
632
|
+
// characters first (the shadow a player actually looks at), then
|
|
633
|
+
// other movers, then foliage — a swaying canopy shadow is soft and
|
|
634
|
+
// dappled and the most forgiving thing in the frame.
|
|
635
|
+
let mut dyn_entries: Vec<usize> = entries.iter().copied()
|
|
636
|
+
.filter(|&ei| shadow_nodes[ei].dynamic)
|
|
637
|
+
.collect();
|
|
638
|
+
if dyn_entries.len() > max_dynamic {
|
|
639
|
+
dyn_entries.sort_by_key(|&ei| {
|
|
640
|
+
let e = &shadow_nodes[ei];
|
|
641
|
+
if e.skinned { 0u8 }
|
|
642
|
+
else if e.foliage > 0.0 { 2u8 }
|
|
643
|
+
else { 1u8 }
|
|
644
|
+
});
|
|
645
|
+
dyn_entries.truncate(max_dynamic);
|
|
646
|
+
}
|
|
647
|
+
let mut uniform_data: Vec<u8> =
|
|
648
|
+
vec![0u8; stride * dyn_entries.len().max(1)];
|
|
649
|
+
for (slot, &ei) in dyn_entries.iter().enumerate() {
|
|
650
|
+
let uniforms = crate::shadows::ShadowUniforms {
|
|
651
|
+
light_vp: cascade_vp,
|
|
652
|
+
model: shadow_nodes[ei].transform,
|
|
653
|
+
misc: [shadow_nodes[ei].joint_offset, 0.0, shadow_nodes[ei].foliage, 0.0],
|
|
654
|
+
wind: self.lighting_uniforms.wind,
|
|
655
|
+
};
|
|
656
|
+
let off = slot * stride;
|
|
657
|
+
uniform_data[off..off + std::mem::size_of::<crate::shadows::ShadowUniforms>()]
|
|
658
|
+
.copy_from_slice(bytemuck::bytes_of(&uniforms));
|
|
659
|
+
}
|
|
660
|
+
self.queue.write_buffer(
|
|
661
|
+
&self.shadow_map.uniform_buffer,
|
|
662
|
+
dyn_base as u64,
|
|
663
|
+
&uniform_data[..dyn_entries.len() * stride],
|
|
664
|
+
);
|
|
665
|
+
{
|
|
666
|
+
let shadow_ts = profiler.pass_timestamp_writes("shadow_pass");
|
|
667
|
+
let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
668
|
+
label: Some("shadow_pass_dynamic"),
|
|
669
|
+
color_attachments: &[],
|
|
670
|
+
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
|
671
|
+
view: &self.shadow_map.depth_views[cascade],
|
|
672
|
+
depth_ops: Some(wgpu::Operations {
|
|
673
|
+
// Refreshed static depth is the base.
|
|
674
|
+
load: wgpu::LoadOp::Load,
|
|
675
|
+
store: wgpu::StoreOp::Store,
|
|
676
|
+
}),
|
|
677
|
+
stencil_ops: None,
|
|
678
|
+
}),
|
|
679
|
+
timestamp_writes: shadow_ts,
|
|
680
|
+
occlusion_query_set: None,
|
|
681
|
+
multiview_mask: None,
|
|
682
|
+
});
|
|
683
|
+
let mut cur_kind: u8 = 0;
|
|
684
|
+
shadow_pass.set_pipeline(&self.shadow_map.pipeline);
|
|
685
|
+
for (slot, &ei) in dyn_entries.iter().enumerate() {
|
|
686
|
+
let entry = &shadow_nodes[ei];
|
|
687
|
+
let offset = (dyn_base + slot * stride) as u32;
|
|
688
|
+
let kind: u8 = if entry.skinned { 2 }
|
|
689
|
+
else if entry.cutout_idx >= 0 { 1 }
|
|
690
|
+
else { 0 };
|
|
691
|
+
if kind != cur_kind {
|
|
692
|
+
shadow_pass.set_pipeline(match kind {
|
|
693
|
+
1 => &self.shadow_map.pipeline_cutout,
|
|
694
|
+
2 => &self.shadow_map.pipeline_skinned,
|
|
695
|
+
_ => &self.shadow_map.pipeline,
|
|
696
|
+
});
|
|
697
|
+
cur_kind = kind;
|
|
698
|
+
}
|
|
699
|
+
shadow_pass.set_bind_group(0, &self.shadow_map.uniform_bind_group, &[offset]);
|
|
700
|
+
if kind == 1 {
|
|
701
|
+
shadow_pass.set_bind_group(1, cutout_bgs[entry.cutout_idx as usize], &[]);
|
|
702
|
+
} else if kind == 2 {
|
|
703
|
+
// Joint matrices for skinning the animated
|
|
704
|
+
// characters in the immediate-mode batch.
|
|
705
|
+
shadow_pass.set_bind_group(1, &self.joint_bind_group, &[]);
|
|
706
|
+
}
|
|
707
|
+
shadow_pass.set_vertex_buffer(0, shadow_vbs[entry.vb_idx].slice(..));
|
|
708
|
+
shadow_pass.set_index_buffer(shadow_ibs[entry.ib_idx].slice(..), wgpu::IndexFormat::Uint32);
|
|
709
|
+
shadow_pass.draw_indexed(
|
|
710
|
+
entry.index_start..entry.index_start + entry.index_count,
|
|
711
|
+
0,
|
|
712
|
+
0..1,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
self.shadow_map.had_dynamic[cascade] = dyn_now;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Cache bookkeeping — next frame skips every cascade whose VP
|
|
721
|
+
// and caster content stay put.
|
|
722
|
+
self.shadow_caster_tf = caster_ids_now;
|
|
723
|
+
self.shadow_map.rendered_light_vps = Some(self.shadow_map.light_vps);
|
|
724
|
+
self.shadow_map.rendered_light_dir = Some(light_dir);
|
|
725
|
+
self.shadow_map.rendered_scene_version = scene_ver;
|
|
726
|
+
self.shadow_map.dirty = false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
profiler.end("shadow_pass");
|
|
730
|
+
}
|
|
731
|
+
}
|