@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,1036 @@
|
|
|
1
|
+
//! Cascaded shadow mapping (CSM) for Bloom Engine.
|
|
2
|
+
//!
|
|
3
|
+
//! Implements 3-cascade directional light shadow mapping with PCF
|
|
4
|
+
//! (Percentage-Closer Filtering). The camera frustum is split into
|
|
5
|
+
//! near/mid/far slices, each rendered from the light's perspective into
|
|
6
|
+
//! its own depth texture. The scene shader selects the tightest cascade
|
|
7
|
+
//! for each fragment, giving high shadow resolution near the camera and
|
|
8
|
+
//! coverage out to the far plane.
|
|
9
|
+
|
|
10
|
+
use crate::renderer::IDENTITY_MAT4;
|
|
11
|
+
|
|
12
|
+
/// Number of shadow cascades.
|
|
13
|
+
pub const NUM_CASCADES: usize = 3;
|
|
14
|
+
/// Per-cascade shadow map resolution. Back to 2048 for desktop targets:
|
|
15
|
+
/// the 1024 cut was made chasing 60 fps on the Sponza benchmark machine
|
|
16
|
+
/// (integrated GPU); discrete desktop GPUs have shadow-pass headroom, and
|
|
17
|
+
/// at fullscreen native resolutions 1024 maps read visibly soft on
|
|
18
|
+
/// near-field edges. The normal-offset receiver bias and PCF radius are
|
|
19
|
+
/// texel-proportional, so both adapt to the size automatically.
|
|
20
|
+
pub const CASCADE_MAP_SIZE: u32 = 2048;
|
|
21
|
+
pub const SHADOW_NEAR: f32 = 0.1;
|
|
22
|
+
pub const SHADOW_FAR: f32 = 100.0;
|
|
23
|
+
/// Dynamic-uniform buffer stride for per-node shadow uniforms. Must
|
|
24
|
+
/// be >= sizeof(ShadowUniforms) (144B) and a multiple of the device's
|
|
25
|
+
/// min_uniform_buffer_offset_alignment. 256 is safe on every platform.
|
|
26
|
+
pub const SHADOW_UNIFORM_STRIDE: u32 = 256;
|
|
27
|
+
pub const SHADOW_MAX_NODES: u32 = 1024;
|
|
28
|
+
/// Slots at the TAIL of each cascade's uniform region reserved for
|
|
29
|
+
/// dynamic casters, which re-render every frame while static casters keep their
|
|
30
|
+
/// cached depth. Disjoint slot ranges keep the every-frame dynamic writes from
|
|
31
|
+
/// clobbering the uniforms the static render was encoded against (all
|
|
32
|
+
/// `write_buffer`s land at submit, before any pass executes).
|
|
33
|
+
///
|
|
34
|
+
/// EN-042 — raised from 64. Sixty-four was fine while "dynamic" meant a handful of
|
|
35
|
+
/// characters, and became a trap the moment a *forest* could go dynamic: 88 trees x
|
|
36
|
+
/// 4 primitives is 352 casters, the overflow was dropped in queue order, and what
|
|
37
|
+
/// disappeared was whatever happened to be last — twice this session, that was the
|
|
38
|
+
/// player's own shadow from under their feet. 256 covers a moving crowd; the drop is
|
|
39
|
+
/// also RANKED now (see shadow_pass.rs) so an overflow costs a canopy shadow rather
|
|
40
|
+
/// than a character's.
|
|
41
|
+
pub const SHADOW_MAX_DYNAMIC: u32 = 256;
|
|
42
|
+
|
|
43
|
+
/// Depth-only shader for shadow pass.
|
|
44
|
+
pub const SHADOW_SHADER: &str = concat!(
|
|
45
|
+
include_str!("../shaders/common/foliage_wind.wgsl"),
|
|
46
|
+
r#"
|
|
47
|
+
struct ShadowUniforms {
|
|
48
|
+
light_vp: mat4x4<f32>,
|
|
49
|
+
model: mat4x4<f32>,
|
|
50
|
+
misc: vec4<f32>, // x = joint offset (skinned variant), z = foliage wind amount
|
|
51
|
+
wind: vec4<f32>, // xy = dir, z = amplitude, w = time
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
@group(0) @binding(0) var<uniform> shadow_u: ShadowUniforms;
|
|
55
|
+
|
|
56
|
+
struct ShadowVertexInput {
|
|
57
|
+
@location(0) position: vec3<f32>,
|
|
58
|
+
@location(1) normal: vec3<f32>,
|
|
59
|
+
@location(2) color: vec4<f32>,
|
|
60
|
+
@location(3) uv: vec2<f32>,
|
|
61
|
+
@location(4) joints: vec4<f32>,
|
|
62
|
+
@location(5) weights: vec4<f32>,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
@vertex
|
|
66
|
+
fn vs_shadow(in: ShadowVertexInput) -> @builtin(position) vec4<f32> {
|
|
67
|
+
// is_leaf = 0: this pipeline draws the opaque casters (trunks, branches).
|
|
68
|
+
let p = foliage_wind_local(in.position, shadow_u.model, shadow_u.wind, shadow_u.misc.z, 0.0);
|
|
69
|
+
let world_pos = shadow_u.model * vec4<f32>(p, 1.0);
|
|
70
|
+
return shadow_u.light_vp * world_pos;
|
|
71
|
+
}
|
|
72
|
+
"#);
|
|
73
|
+
|
|
74
|
+
/// Alpha-tested shadow shader for cutout foliage (trees, grass, leaves). Same
|
|
75
|
+
/// depth-only output as SHADOW_SHADER but samples the caster's base-colour
|
|
76
|
+
/// alpha and discards below the material cutoff, so cutout cards cast their
|
|
77
|
+
/// real shape (dappled light) instead of an opaque billboard blob. Used by a
|
|
78
|
+
/// dedicated pipeline; the opaque shadow path stays untouched.
|
|
79
|
+
pub const SHADOW_SHADER_CUTOUT: &str = concat!(
|
|
80
|
+
include_str!("../shaders/common/foliage_wind.wgsl"),
|
|
81
|
+
r#"
|
|
82
|
+
struct ShadowUniforms {
|
|
83
|
+
light_vp: mat4x4<f32>,
|
|
84
|
+
model: mat4x4<f32>,
|
|
85
|
+
misc: vec4<f32>, // x = joint offset (skinned variant), z = foliage wind amount
|
|
86
|
+
wind: vec4<f32>, // xy = dir, z = amplitude, w = time
|
|
87
|
+
};
|
|
88
|
+
@group(0) @binding(0) var<uniform> shadow_u: ShadowUniforms;
|
|
89
|
+
|
|
90
|
+
struct CutoutUniforms { cutoff: vec4<f32> }; // x = alpha cutoff
|
|
91
|
+
@group(1) @binding(0) var base_tex: texture_2d<f32>;
|
|
92
|
+
@group(1) @binding(1) var base_samp: sampler;
|
|
93
|
+
@group(1) @binding(2) var<uniform> cut: CutoutUniforms;
|
|
94
|
+
|
|
95
|
+
struct ShadowVertexInput {
|
|
96
|
+
@location(0) position: vec3<f32>,
|
|
97
|
+
@location(1) normal: vec3<f32>,
|
|
98
|
+
@location(2) color: vec4<f32>,
|
|
99
|
+
@location(3) uv: vec2<f32>,
|
|
100
|
+
@location(4) joints: vec4<f32>,
|
|
101
|
+
@location(5) weights: vec4<f32>,
|
|
102
|
+
};
|
|
103
|
+
struct VsOut {
|
|
104
|
+
@builtin(position) pos: vec4<f32>,
|
|
105
|
+
@location(0) uv: vec2<f32>,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@vertex
|
|
110
|
+
fn vs_shadow_cutout(in: ShadowVertexInput) -> VsOut {
|
|
111
|
+
var o: VsOut;
|
|
112
|
+
// is_leaf = 1: this pipeline draws the cutout cards, so they get the fast
|
|
113
|
+
// flutter layer -- and their shadows now flutter with them.
|
|
114
|
+
let p = foliage_wind_local(in.position, shadow_u.model, shadow_u.wind, shadow_u.misc.z, 1.0);
|
|
115
|
+
let world_pos = shadow_u.model * vec4<f32>(p, 1.0);
|
|
116
|
+
o.pos = shadow_u.light_vp * world_pos;
|
|
117
|
+
o.uv = in.uv;
|
|
118
|
+
return o;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@fragment
|
|
122
|
+
fn fs_shadow_cutout(in: VsOut) {
|
|
123
|
+
let a = textureSample(base_tex, base_samp, in.uv).a;
|
|
124
|
+
if (a < cut.cutoff.x) { discard; }
|
|
125
|
+
}
|
|
126
|
+
"#);
|
|
127
|
+
|
|
128
|
+
/// Skinned shadow shader for animated characters (player, enemies). Their
|
|
129
|
+
/// vertices are *rest-pose* (cached model VBs with raw joint indices, or the
|
|
130
|
+
/// immediate-mode batch with pre-offset ones), with world placement living
|
|
131
|
+
/// entirely in the per-frame joint matrices. The plain `vs_shadow` doesn't
|
|
132
|
+
/// skin, so it would render those characters as a rest pose at the world
|
|
133
|
+
/// origin — i.e. no shadow under their feet. This variant skins per-vertex
|
|
134
|
+
/// (same math as the main scene shader) so characters cast a real, posed
|
|
135
|
+
/// shadow. The branch on total weight means the non-skinned verts sharing a
|
|
136
|
+
/// batch (ground cube, rigid accessories) still transform by the model
|
|
137
|
+
/// matrix as before.
|
|
138
|
+
pub const SHADOW_SHADER_SKINNED: &str = "
|
|
139
|
+
struct ShadowUniforms {
|
|
140
|
+
light_vp: mat4x4<f32>,
|
|
141
|
+
model: mat4x4<f32>,
|
|
142
|
+
misc: vec4<f32>, // x = joint offset for cached skinned casters
|
|
143
|
+
};
|
|
144
|
+
@group(0) @binding(0) var<uniform> shadow_u: ShadowUniforms;
|
|
145
|
+
|
|
146
|
+
struct JointMatrices { matrices: array<mat4x4<f32>, 1024> };
|
|
147
|
+
@group(1) @binding(0) var<uniform> joints: JointMatrices;
|
|
148
|
+
|
|
149
|
+
struct ShadowVertexInput {
|
|
150
|
+
@location(0) position: vec3<f32>,
|
|
151
|
+
@location(1) normal: vec3<f32>,
|
|
152
|
+
@location(2) color: vec4<f32>,
|
|
153
|
+
@location(3) uv: vec2<f32>,
|
|
154
|
+
@location(4) joints: vec4<f32>,
|
|
155
|
+
@location(5) weights: vec4<f32>,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
@vertex
|
|
159
|
+
fn vs_shadow_skinned(in: ShadowVertexInput) -> @builtin(position) vec4<f32> {
|
|
160
|
+
let total_weight = in.weights.x + in.weights.y + in.weights.z + in.weights.w;
|
|
161
|
+
var pos = vec4<f32>(in.position, 1.0);
|
|
162
|
+
if (total_weight > 0.01) {
|
|
163
|
+
// misc.x = joint-buffer base offset for cached skinned casters
|
|
164
|
+
// (raw VB indices); 0 for the immediate batch (pre-offset).
|
|
165
|
+
let j0 = u32(in.joints.x + shadow_u.misc.x); let j1 = u32(in.joints.y + shadow_u.misc.x);
|
|
166
|
+
let j2 = u32(in.joints.z + shadow_u.misc.x); let j3 = u32(in.joints.w + shadow_u.misc.x);
|
|
167
|
+
// Joint matrices already bake scale + world position + rotation, so the
|
|
168
|
+
// skinned result is world-space; the model matrix is identity for the
|
|
169
|
+
// immediate-mode batch and is intentionally not re-applied here.
|
|
170
|
+
pos = joints.matrices[j0] * pos * in.weights.x
|
|
171
|
+
+ joints.matrices[j1] * pos * in.weights.y
|
|
172
|
+
+ joints.matrices[j2] * pos * in.weights.z
|
|
173
|
+
+ joints.matrices[j3] * pos * in.weights.w;
|
|
174
|
+
return shadow_u.light_vp * pos;
|
|
175
|
+
}
|
|
176
|
+
let world_pos = shadow_u.model * pos;
|
|
177
|
+
return shadow_u.light_vp * world_pos;
|
|
178
|
+
}
|
|
179
|
+
";
|
|
180
|
+
|
|
181
|
+
/// Uniform data for the shadow pass.
|
|
182
|
+
#[repr(C)]
|
|
183
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
184
|
+
pub struct ShadowUniforms {
|
|
185
|
+
pub light_vp: [[f32; 4]; 4],
|
|
186
|
+
pub model: [[f32; 4]; 4],
|
|
187
|
+
/// x = joint-buffer base offset for skinned CACHED casters (their
|
|
188
|
+
/// VBs keep raw joint indices; `vs_shadow_skinned` adds this before
|
|
189
|
+
/// indexing). 0 for everything else, including the immediate batch
|
|
190
|
+
/// whose vertex joints are pre-offset CPU-side.
|
|
191
|
+
/// z = foliage wind amount for this caster (0 = rigid). A tree that bends in
|
|
192
|
+
/// the scene pass but not in the shadow pass detaches from its own shadow.
|
|
193
|
+
/// yw unused.
|
|
194
|
+
pub misc: [f32; 4],
|
|
195
|
+
/// Global wind: xy = direction in XZ, z = amplitude, w = elapsed seconds.
|
|
196
|
+
/// The shadow pass needs it for the same reason the scene pass does.
|
|
197
|
+
pub wind: [f32; 4],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Shadow map resources for cascaded shadow mapping.
|
|
201
|
+
pub struct ShadowMap {
|
|
202
|
+
pub depth_textures: [wgpu::Texture; NUM_CASCADES],
|
|
203
|
+
pub depth_views: [wgpu::TextureView; NUM_CASCADES],
|
|
204
|
+
/// Cached static-caster depth per cascade ("cached whole-scene
|
|
205
|
+
/// shadows"): scene nodes / cached models / material draws render in
|
|
206
|
+
/// here only when the cascade's VP or static content changes. Every
|
|
207
|
+
/// frame the live cascade texture starts as a copy of this and only
|
|
208
|
+
/// dynamic casters (the immediate batch: animated characters,
|
|
209
|
+
/// moving primitives) are drawn on top — so one animated model no
|
|
210
|
+
/// longer forces the whole forest to re-render three times.
|
|
211
|
+
pub static_depth_textures: [wgpu::Texture; NUM_CASCADES],
|
|
212
|
+
pub static_depth_views: [wgpu::TextureView; NUM_CASCADES],
|
|
213
|
+
pub sampler: wgpu::Sampler,
|
|
214
|
+
pub bind_group_layout: wgpu::BindGroupLayout,
|
|
215
|
+
pub bind_group: wgpu::BindGroup,
|
|
216
|
+
pub pipeline: wgpu::RenderPipeline,
|
|
217
|
+
/// Alpha-tested variant for cutout foliage casters. Opaque casters keep
|
|
218
|
+
/// using `pipeline` (byte-identical to before this was added).
|
|
219
|
+
pub pipeline_cutout: wgpu::RenderPipeline,
|
|
220
|
+
/// Skinning-aware variant for the immediate-mode batch (animated
|
|
221
|
+
/// characters). Binds the joint-matrix buffer at group 1 and skins
|
|
222
|
+
/// per-vertex so player/enemies cast a real posed shadow.
|
|
223
|
+
pub pipeline_skinned: wgpu::RenderPipeline,
|
|
224
|
+
/// Group-1 layout for `pipeline_cutout`: base-colour tex + sampler + a
|
|
225
|
+
/// cutoff uniform. Per-mesh bind groups are built against this in
|
|
226
|
+
/// `cache_model_if_static`.
|
|
227
|
+
pub cutout_tex_layout: wgpu::BindGroupLayout,
|
|
228
|
+
pub uniform_buffer: wgpu::Buffer,
|
|
229
|
+
pub uniform_bind_group: wgpu::BindGroup,
|
|
230
|
+
pub uniform_layout: wgpu::BindGroupLayout,
|
|
231
|
+
pub light_vps: [[[f32; 4]; 4]; NUM_CASCADES],
|
|
232
|
+
/// View-space Z split distances for each cascade. Cascade i covers
|
|
233
|
+
/// [cascade_splits[i-1], cascade_splits[i]]; cascade 0 starts at near.
|
|
234
|
+
pub cascade_splits: [f32; NUM_CASCADES],
|
|
235
|
+
pub enabled: bool,
|
|
236
|
+
/// Forces a shadow re-render next frame. Set by `invalidate()`
|
|
237
|
+
/// (on light direction change, `setShadowsEnabled(true)`, resize,
|
|
238
|
+
/// shadow-texture aliasing, etc.). Cleared after a render.
|
|
239
|
+
pub dirty: bool,
|
|
240
|
+
/// Escape hatch for games with continuously-changing light state
|
|
241
|
+
/// where the cache hit rate would be ~zero anyway. When true, every
|
|
242
|
+
/// frame renders shadows; the cache is bypassed.
|
|
243
|
+
pub always_fresh: bool,
|
|
244
|
+
/// Cascade VPs that correspond to the contents currently stored in
|
|
245
|
+
/// the depth textures. `None` before the first render. When the
|
|
246
|
+
/// freshly-computed VPs match this byte-for-byte AND nothing else
|
|
247
|
+
/// has invalidated, we can skip the render entirely and sample the
|
|
248
|
+
/// retained depth textures. Texel-snapping + radius quantization in
|
|
249
|
+
/// `compute_cascade_vps` means identical camera poses produce
|
|
250
|
+
/// identical VPs, so this check is robust.
|
|
251
|
+
pub rendered_light_vps: Option<[[[f32; 4]; 4]; NUM_CASCADES]>,
|
|
252
|
+
/// Light direction used for the current depth-texture contents.
|
|
253
|
+
/// Checked at the cache gate rather than on the setter because
|
|
254
|
+
/// `begin_frame` resets `lighting_uniforms` to defaults every
|
|
255
|
+
/// frame — comparing a setter's old-vs-new would always see the
|
|
256
|
+
/// default as the "old" value and invalidate every frame.
|
|
257
|
+
pub rendered_light_dir: Option<[f32; 3]>,
|
|
258
|
+
/// Scene-graph version counter sampled at the last shadow render.
|
|
259
|
+
/// `SceneGraph::shadow_version` increments whenever a shadow-casting
|
|
260
|
+
/// node's transform / cast_shadow / visibility / geometry changes;
|
|
261
|
+
/// a mismatch here forces a re-render.
|
|
262
|
+
pub rendered_scene_version: u64,
|
|
263
|
+
/// Shadow-flicker fix — per-cascade accepted pancake extents
|
|
264
|
+
/// (back, far). Animated caster bounds drift a few centimetres per
|
|
265
|
+
/// frame, and the raw 1/16 m ceil() quantization made the fitted VP
|
|
266
|
+
/// toggle between two matrices whenever that drift straddled a
|
|
267
|
+
/// step. The accepted extent grows immediately (casters must stay
|
|
268
|
+
/// inside the volume) but shrinks only when the raw need has
|
|
269
|
+
/// dropped well below it — so idle animations stop re-fitting the
|
|
270
|
+
/// cascades every few frames.
|
|
271
|
+
pancake_hysteresis: [[f32; 2]; NUM_CASCADES],
|
|
272
|
+
/// Per-cascade STATIC caster-content signature at the last render
|
|
273
|
+
/// of that cascade's static depth texture (hash of every
|
|
274
|
+
/// non-immediate caster that passed its frustum filter: identity +
|
|
275
|
+
/// transform). 0 = never rendered. Together with a per-cascade VP
|
|
276
|
+
/// compare this decides when the static cache must re-render — the
|
|
277
|
+
/// whole-pass cache above only ever hit for fully-retained scenes.
|
|
278
|
+
pub rendered_cascade_sig: [u64; NUM_CASCADES],
|
|
279
|
+
/// Whether the live cascade texture currently contains dynamic
|
|
280
|
+
/// casters. A cascade whose dynamics all left still needs one
|
|
281
|
+
/// refresh copy to clear their stale shadows.
|
|
282
|
+
pub had_dynamic: [bool; NUM_CASCADES],
|
|
283
|
+
/// Monotonic counter folded into animated casters' signatures so
|
|
284
|
+
/// any cascade containing one re-renders every frame.
|
|
285
|
+
pub frame_nonce: u64,
|
|
286
|
+
/// Cascade re-fit slack — the accepted ortho fit per cascade
|
|
287
|
+
/// (cascades ≥ 1). While the freshly-required bounding sphere and
|
|
288
|
+
/// pancake extents still fit inside the accepted volume, the
|
|
289
|
+
/// cascade keeps its previous VP byte-for-byte, so camera travel
|
|
290
|
+
/// of a few metres no longer invalidates the far cascades' cached
|
|
291
|
+
/// depth. Accepted fits are inflated by `REFIT_SLACK` (the
|
|
292
|
+
/// resolution cost of that inflation is bounded and uniform; the
|
|
293
|
+
/// near cascade is exempt and stays exact-fit).
|
|
294
|
+
accepted_fit: [Option<AcceptedFit>; NUM_CASCADES],
|
|
295
|
+
accepted_light_dir: Option<[f32; 3]>,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Accepted (slack-inflated) ortho fit for one cascade. `ls_x`/`ls_y`
|
|
299
|
+
/// are the snapped center in the light-plane basis; `radius` is the
|
|
300
|
+
/// final ortho half-extent; `back`/`far` the accepted pancake extents
|
|
301
|
+
/// along the light axis relative to `center`.
|
|
302
|
+
#[derive(Copy, Clone)]
|
|
303
|
+
struct AcceptedFit {
|
|
304
|
+
ls_x: f32,
|
|
305
|
+
ls_y: f32,
|
|
306
|
+
center: [f32; 3],
|
|
307
|
+
radius: f32,
|
|
308
|
+
back: f32,
|
|
309
|
+
far: f32,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// Re-fit slack factor for cascades ≥ 1. 15% larger ortho extent buys
|
|
313
|
+
/// ~0.15 × radius of camera travel between re-fits (≈ 4-5 m on a far
|
|
314
|
+
/// cascade) at the cost of ~15% coarser texels on the mid/far cascades
|
|
315
|
+
/// only. PCF radius and the normal-offset receiver bias are
|
|
316
|
+
/// texel-proportional, so the softening stays coherent (no acne).
|
|
317
|
+
const REFIT_SLACK: f32 = 1.15;
|
|
318
|
+
|
|
319
|
+
impl ShadowMap {
|
|
320
|
+
pub fn new(
|
|
321
|
+
device: &wgpu::Device,
|
|
322
|
+
vertex_layout: wgpu::VertexBufferLayout<'static>,
|
|
323
|
+
joint_layout: &wgpu::BindGroupLayout,
|
|
324
|
+
) -> Self {
|
|
325
|
+
// Create NUM_CASCADES depth textures (live + static cache).
|
|
326
|
+
let mut depth_textures_vec: Vec<wgpu::Texture> = Vec::new();
|
|
327
|
+
let mut depth_views_vec: Vec<wgpu::TextureView> = Vec::new();
|
|
328
|
+
let mut static_textures_vec: Vec<wgpu::Texture> = Vec::new();
|
|
329
|
+
let mut static_views_vec: Vec<wgpu::TextureView> = Vec::new();
|
|
330
|
+
for i in 0..NUM_CASCADES {
|
|
331
|
+
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
|
332
|
+
label: Some(&format!("shadow_depth_cascade_{}", i)),
|
|
333
|
+
size: wgpu::Extent3d {
|
|
334
|
+
width: CASCADE_MAP_SIZE,
|
|
335
|
+
height: CASCADE_MAP_SIZE,
|
|
336
|
+
depth_or_array_layers: 1,
|
|
337
|
+
},
|
|
338
|
+
mip_level_count: 1,
|
|
339
|
+
sample_count: 1,
|
|
340
|
+
dimension: wgpu::TextureDimension::D2,
|
|
341
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
342
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
343
|
+
| wgpu::TextureUsages::TEXTURE_BINDING
|
|
344
|
+
| wgpu::TextureUsages::COPY_SRC
|
|
345
|
+
// Refreshed from the static cache before dynamic
|
|
346
|
+
// casters draw on top.
|
|
347
|
+
| wgpu::TextureUsages::COPY_DST,
|
|
348
|
+
view_formats: &[],
|
|
349
|
+
});
|
|
350
|
+
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
|
|
351
|
+
depth_textures_vec.push(tex);
|
|
352
|
+
depth_views_vec.push(view);
|
|
353
|
+
let stex = device.create_texture(&wgpu::TextureDescriptor {
|
|
354
|
+
label: Some(&format!("shadow_static_depth_cascade_{}", i)),
|
|
355
|
+
size: wgpu::Extent3d {
|
|
356
|
+
width: CASCADE_MAP_SIZE,
|
|
357
|
+
height: CASCADE_MAP_SIZE,
|
|
358
|
+
depth_or_array_layers: 1,
|
|
359
|
+
},
|
|
360
|
+
mip_level_count: 1,
|
|
361
|
+
sample_count: 1,
|
|
362
|
+
dimension: wgpu::TextureDimension::D2,
|
|
363
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
364
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
365
|
+
| wgpu::TextureUsages::COPY_SRC,
|
|
366
|
+
view_formats: &[],
|
|
367
|
+
});
|
|
368
|
+
let sview = stex.create_view(&wgpu::TextureViewDescriptor::default());
|
|
369
|
+
static_textures_vec.push(stex);
|
|
370
|
+
static_views_vec.push(sview);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Convert Vecs to fixed-size arrays
|
|
374
|
+
let depth_textures: [wgpu::Texture; NUM_CASCADES] =
|
|
375
|
+
depth_textures_vec.try_into().unwrap_or_else(|_| panic!("cascade texture count mismatch"));
|
|
376
|
+
let depth_views: [wgpu::TextureView; NUM_CASCADES] =
|
|
377
|
+
depth_views_vec.try_into().unwrap_or_else(|_| panic!("cascade view count mismatch"));
|
|
378
|
+
let static_depth_textures: [wgpu::Texture; NUM_CASCADES] =
|
|
379
|
+
static_textures_vec.try_into().unwrap_or_else(|_| panic!("static cascade texture count mismatch"));
|
|
380
|
+
let static_depth_views: [wgpu::TextureView; NUM_CASCADES] =
|
|
381
|
+
static_views_vec.try_into().unwrap_or_else(|_| panic!("static cascade view count mismatch"));
|
|
382
|
+
|
|
383
|
+
// Comparison sampler for PCF
|
|
384
|
+
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
385
|
+
label: Some("shadow_sampler"),
|
|
386
|
+
compare: Some(wgpu::CompareFunction::LessEqual),
|
|
387
|
+
mag_filter: wgpu::FilterMode::Linear,
|
|
388
|
+
min_filter: wgpu::FilterMode::Linear,
|
|
389
|
+
..Default::default()
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Bind group layout for sampling shadow maps in the main pass:
|
|
393
|
+
// 3 depth textures (bindings 0,1,2) + 1 comparison sampler (binding 3)
|
|
394
|
+
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
395
|
+
label: Some("shadow_sample_layout"),
|
|
396
|
+
entries: &[
|
|
397
|
+
wgpu::BindGroupLayoutEntry {
|
|
398
|
+
binding: 0,
|
|
399
|
+
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
400
|
+
ty: wgpu::BindingType::Texture {
|
|
401
|
+
sample_type: wgpu::TextureSampleType::Depth,
|
|
402
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
403
|
+
multisampled: false,
|
|
404
|
+
},
|
|
405
|
+
count: None,
|
|
406
|
+
},
|
|
407
|
+
wgpu::BindGroupLayoutEntry {
|
|
408
|
+
binding: 1,
|
|
409
|
+
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
410
|
+
ty: wgpu::BindingType::Texture {
|
|
411
|
+
sample_type: wgpu::TextureSampleType::Depth,
|
|
412
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
413
|
+
multisampled: false,
|
|
414
|
+
},
|
|
415
|
+
count: None,
|
|
416
|
+
},
|
|
417
|
+
wgpu::BindGroupLayoutEntry {
|
|
418
|
+
binding: 2,
|
|
419
|
+
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
420
|
+
ty: wgpu::BindingType::Texture {
|
|
421
|
+
sample_type: wgpu::TextureSampleType::Depth,
|
|
422
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
423
|
+
multisampled: false,
|
|
424
|
+
},
|
|
425
|
+
count: None,
|
|
426
|
+
},
|
|
427
|
+
wgpu::BindGroupLayoutEntry {
|
|
428
|
+
binding: 3,
|
|
429
|
+
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
430
|
+
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
|
431
|
+
count: None,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
437
|
+
label: Some("shadow_sample_bg"),
|
|
438
|
+
layout: &bind_group_layout,
|
|
439
|
+
entries: &[
|
|
440
|
+
wgpu::BindGroupEntry {
|
|
441
|
+
binding: 0,
|
|
442
|
+
resource: wgpu::BindingResource::TextureView(&depth_views[0]),
|
|
443
|
+
},
|
|
444
|
+
wgpu::BindGroupEntry {
|
|
445
|
+
binding: 1,
|
|
446
|
+
resource: wgpu::BindingResource::TextureView(&depth_views[1]),
|
|
447
|
+
},
|
|
448
|
+
wgpu::BindGroupEntry {
|
|
449
|
+
binding: 2,
|
|
450
|
+
resource: wgpu::BindingResource::TextureView(&depth_views[2]),
|
|
451
|
+
},
|
|
452
|
+
wgpu::BindGroupEntry {
|
|
453
|
+
binding: 3,
|
|
454
|
+
resource: wgpu::BindingResource::Sampler(&sampler),
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Shadow pass uniform layout (dynamic offset for per-node)
|
|
460
|
+
let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
461
|
+
label: Some("shadow_uniform_layout"),
|
|
462
|
+
entries: &[wgpu::BindGroupLayoutEntry {
|
|
463
|
+
binding: 0,
|
|
464
|
+
visibility: wgpu::ShaderStages::VERTEX,
|
|
465
|
+
ty: wgpu::BindingType::Buffer {
|
|
466
|
+
ty: wgpu::BufferBindingType::Uniform,
|
|
467
|
+
has_dynamic_offset: true,
|
|
468
|
+
min_binding_size: std::num::NonZeroU64::new(
|
|
469
|
+
std::mem::size_of::<ShadowUniforms>() as u64,
|
|
470
|
+
),
|
|
471
|
+
},
|
|
472
|
+
count: None,
|
|
473
|
+
}],
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// One region PER CASCADE. The pass encodes all three cascades before
|
|
477
|
+
// the queue submits, and `queue.write_buffer` executes at submit —
|
|
478
|
+
// sharing one region meant every cascade rendered with the LAST
|
|
479
|
+
// cascade's light_vp + models, killing shadows for everything that
|
|
480
|
+
// sampled cascades 0/1 (i.e. all near-camera receivers: the player
|
|
481
|
+
// and enemies never had shadows; distant trees kept theirs because
|
|
482
|
+
// cascade 2's write happened to be the surviving one).
|
|
483
|
+
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
484
|
+
label: Some("shadow_uniform_buf"),
|
|
485
|
+
size: (SHADOW_UNIFORM_STRIDE * SHADOW_MAX_NODES * NUM_CASCADES as u32) as u64,
|
|
486
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
487
|
+
mapped_at_creation: false,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
491
|
+
label: Some("shadow_uniform_bg"),
|
|
492
|
+
layout: &uniform_layout,
|
|
493
|
+
entries: &[wgpu::BindGroupEntry {
|
|
494
|
+
binding: 0,
|
|
495
|
+
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
|
496
|
+
buffer: &uniform_buffer,
|
|
497
|
+
offset: 0,
|
|
498
|
+
size: std::num::NonZeroU64::new(
|
|
499
|
+
std::mem::size_of::<ShadowUniforms>() as u64,
|
|
500
|
+
),
|
|
501
|
+
}),
|
|
502
|
+
}],
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Shadow depth-only pipeline
|
|
506
|
+
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
507
|
+
label: Some("shadow_shader"),
|
|
508
|
+
source: wgpu::ShaderSource::Wgsl(SHADOW_SHADER.into()),
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
512
|
+
label: Some("shadow_pipeline_layout"),
|
|
513
|
+
bind_group_layouts: &[Some(&uniform_layout)],
|
|
514
|
+
immediate_size: 0,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
518
|
+
label: Some("shadow_pipeline"),
|
|
519
|
+
layout: Some(&pipeline_layout),
|
|
520
|
+
vertex: wgpu::VertexState {
|
|
521
|
+
module: &shader,
|
|
522
|
+
entry_point: Some("vs_shadow"),
|
|
523
|
+
buffers: &[vertex_layout.clone()],
|
|
524
|
+
compilation_options: Default::default(),
|
|
525
|
+
},
|
|
526
|
+
fragment: None, // depth only
|
|
527
|
+
primitive: wgpu::PrimitiveState {
|
|
528
|
+
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
529
|
+
front_face: wgpu::FrontFace::Ccw,
|
|
530
|
+
cull_mode: None,
|
|
531
|
+
..Default::default()
|
|
532
|
+
},
|
|
533
|
+
depth_stencil: Some(wgpu::DepthStencilState {
|
|
534
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
535
|
+
depth_write_enabled: Some(true),
|
|
536
|
+
depth_compare: Some(wgpu::CompareFunction::Less),
|
|
537
|
+
stencil: Default::default(),
|
|
538
|
+
bias: wgpu::DepthBiasState {
|
|
539
|
+
constant: 1,
|
|
540
|
+
slope_scale: 1.0,
|
|
541
|
+
clamp: 0.0,
|
|
542
|
+
},
|
|
543
|
+
}),
|
|
544
|
+
multisample: Default::default(),
|
|
545
|
+
multiview_mask: None,
|
|
546
|
+
cache: None,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Cutout (alpha-tested) shadow pipeline. Separate so the opaque path
|
|
550
|
+
// above is untouched. Adds a group-1 texture/sampler/cutoff layout and
|
|
551
|
+
// a fragment stage that discards below the alpha cutoff. Still depth-
|
|
552
|
+
// only (no colour targets).
|
|
553
|
+
let cutout_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
554
|
+
label: Some("shadow_shader_cutout"),
|
|
555
|
+
source: wgpu::ShaderSource::Wgsl(SHADOW_SHADER_CUTOUT.into()),
|
|
556
|
+
});
|
|
557
|
+
let cutout_tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
558
|
+
label: Some("shadow_cutout_tex_layout"),
|
|
559
|
+
entries: &[
|
|
560
|
+
wgpu::BindGroupLayoutEntry {
|
|
561
|
+
binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
|
|
562
|
+
ty: wgpu::BindingType::Texture {
|
|
563
|
+
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
564
|
+
view_dimension: wgpu::TextureViewDimension::D2, multisampled: false,
|
|
565
|
+
},
|
|
566
|
+
count: None,
|
|
567
|
+
},
|
|
568
|
+
wgpu::BindGroupLayoutEntry {
|
|
569
|
+
binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
|
|
570
|
+
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
571
|
+
count: None,
|
|
572
|
+
},
|
|
573
|
+
wgpu::BindGroupLayoutEntry {
|
|
574
|
+
binding: 2, visibility: wgpu::ShaderStages::FRAGMENT,
|
|
575
|
+
ty: wgpu::BindingType::Buffer {
|
|
576
|
+
ty: wgpu::BufferBindingType::Uniform,
|
|
577
|
+
has_dynamic_offset: false, min_binding_size: None,
|
|
578
|
+
},
|
|
579
|
+
count: None,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
});
|
|
583
|
+
let cutout_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
584
|
+
label: Some("shadow_cutout_pipeline_layout"),
|
|
585
|
+
bind_group_layouts: &[Some(&uniform_layout), Some(&cutout_tex_layout)],
|
|
586
|
+
immediate_size: 0,
|
|
587
|
+
});
|
|
588
|
+
let pipeline_cutout = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
589
|
+
label: Some("shadow_pipeline_cutout"),
|
|
590
|
+
layout: Some(&cutout_pl_layout),
|
|
591
|
+
vertex: wgpu::VertexState {
|
|
592
|
+
module: &cutout_shader,
|
|
593
|
+
entry_point: Some("vs_shadow_cutout"),
|
|
594
|
+
buffers: &[vertex_layout.clone()],
|
|
595
|
+
compilation_options: Default::default(),
|
|
596
|
+
},
|
|
597
|
+
fragment: Some(wgpu::FragmentState {
|
|
598
|
+
module: &cutout_shader,
|
|
599
|
+
entry_point: Some("fs_shadow_cutout"),
|
|
600
|
+
targets: &[], // depth only
|
|
601
|
+
compilation_options: Default::default(),
|
|
602
|
+
}),
|
|
603
|
+
primitive: wgpu::PrimitiveState {
|
|
604
|
+
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
605
|
+
front_face: wgpu::FrontFace::Ccw,
|
|
606
|
+
cull_mode: None,
|
|
607
|
+
..Default::default()
|
|
608
|
+
},
|
|
609
|
+
depth_stencil: Some(wgpu::DepthStencilState {
|
|
610
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
611
|
+
depth_write_enabled: Some(true),
|
|
612
|
+
depth_compare: Some(wgpu::CompareFunction::Less),
|
|
613
|
+
stencil: Default::default(),
|
|
614
|
+
bias: wgpu::DepthBiasState { constant: 1, slope_scale: 1.0, clamp: 0.0 },
|
|
615
|
+
}),
|
|
616
|
+
multisample: Default::default(),
|
|
617
|
+
multiview_mask: None,
|
|
618
|
+
cache: None,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Skinned (animated-character) shadow pipeline. Group 0 = the shared
|
|
622
|
+
// shadow uniforms (light_vp + model, dynamic offset); group 1 = the
|
|
623
|
+
// joint-matrix buffer. Depth-only, same bias as the opaque path.
|
|
624
|
+
let skinned_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
625
|
+
label: Some("shadow_shader_skinned"),
|
|
626
|
+
source: wgpu::ShaderSource::Wgsl(SHADOW_SHADER_SKINNED.into()),
|
|
627
|
+
});
|
|
628
|
+
let skinned_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
629
|
+
label: Some("shadow_skinned_pipeline_layout"),
|
|
630
|
+
bind_group_layouts: &[Some(&uniform_layout), Some(joint_layout)],
|
|
631
|
+
immediate_size: 0,
|
|
632
|
+
});
|
|
633
|
+
let pipeline_skinned = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
634
|
+
label: Some("shadow_pipeline_skinned"),
|
|
635
|
+
layout: Some(&skinned_pl_layout),
|
|
636
|
+
vertex: wgpu::VertexState {
|
|
637
|
+
module: &skinned_shader,
|
|
638
|
+
entry_point: Some("vs_shadow_skinned"),
|
|
639
|
+
buffers: &[vertex_layout],
|
|
640
|
+
compilation_options: Default::default(),
|
|
641
|
+
},
|
|
642
|
+
fragment: None, // depth only
|
|
643
|
+
primitive: wgpu::PrimitiveState {
|
|
644
|
+
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
645
|
+
front_face: wgpu::FrontFace::Ccw,
|
|
646
|
+
cull_mode: None,
|
|
647
|
+
..Default::default()
|
|
648
|
+
},
|
|
649
|
+
depth_stencil: Some(wgpu::DepthStencilState {
|
|
650
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
651
|
+
depth_write_enabled: Some(true),
|
|
652
|
+
depth_compare: Some(wgpu::CompareFunction::Less),
|
|
653
|
+
stencil: Default::default(),
|
|
654
|
+
bias: wgpu::DepthBiasState { constant: 1, slope_scale: 1.0, clamp: 0.0 },
|
|
655
|
+
}),
|
|
656
|
+
multisample: Default::default(),
|
|
657
|
+
multiview_mask: None,
|
|
658
|
+
cache: None,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
Self {
|
|
662
|
+
depth_textures,
|
|
663
|
+
depth_views,
|
|
664
|
+
static_depth_textures,
|
|
665
|
+
static_depth_views,
|
|
666
|
+
sampler,
|
|
667
|
+
bind_group_layout,
|
|
668
|
+
bind_group,
|
|
669
|
+
pipeline,
|
|
670
|
+
pipeline_cutout,
|
|
671
|
+
pipeline_skinned,
|
|
672
|
+
cutout_tex_layout,
|
|
673
|
+
uniform_buffer,
|
|
674
|
+
uniform_bind_group,
|
|
675
|
+
uniform_layout,
|
|
676
|
+
light_vps: [IDENTITY_MAT4; NUM_CASCADES],
|
|
677
|
+
cascade_splits: [8.0, 25.0, 80.0],
|
|
678
|
+
enabled: false,
|
|
679
|
+
dirty: true,
|
|
680
|
+
always_fresh: false,
|
|
681
|
+
rendered_light_vps: None,
|
|
682
|
+
rendered_light_dir: None,
|
|
683
|
+
rendered_scene_version: 0,
|
|
684
|
+
pancake_hysteresis: [[0.0; 2]; NUM_CASCADES],
|
|
685
|
+
rendered_cascade_sig: [0; NUM_CASCADES],
|
|
686
|
+
had_dynamic: [false; NUM_CASCADES],
|
|
687
|
+
frame_nonce: 0,
|
|
688
|
+
accepted_fit: [None; NUM_CASCADES],
|
|
689
|
+
accepted_light_dir: None,
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/// Force the next shadow pass to re-render the depth textures.
|
|
694
|
+
/// Called on `setShadowsEnabled(true)`, swap-chain resize, or any
|
|
695
|
+
/// other event that invalidates the cached cascade contents.
|
|
696
|
+
pub fn invalidate(&mut self) {
|
|
697
|
+
self.dirty = true;
|
|
698
|
+
self.rendered_light_vps = None;
|
|
699
|
+
self.rendered_light_dir = None;
|
|
700
|
+
self.rendered_cascade_sig = [0; NUM_CASCADES];
|
|
701
|
+
self.had_dynamic = [false; NUM_CASCADES];
|
|
702
|
+
self.accepted_fit = [None; NUM_CASCADES];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/// Compute cascade view-projection matrices by splitting the camera
|
|
706
|
+
/// frustum into NUM_CASCADES slices and fitting a tight ortho projection
|
|
707
|
+
/// around each slice from the light's perspective.
|
|
708
|
+
///
|
|
709
|
+
/// `light_dir` points from the surface toward the light (the same
|
|
710
|
+
/// convention as the rest of the engine).
|
|
711
|
+
pub fn compute_cascade_vps(
|
|
712
|
+
&mut self,
|
|
713
|
+
light_dir: [f32; 3],
|
|
714
|
+
_camera_pos: [f32; 3],
|
|
715
|
+
camera_view: [[f32; 4]; 4],
|
|
716
|
+
camera_proj: [[f32; 4]; 4],
|
|
717
|
+
near: f32,
|
|
718
|
+
far: f32,
|
|
719
|
+
scene_bounds: Option<([f32; 3], [f32; 3])>,
|
|
720
|
+
) {
|
|
721
|
+
let len = (light_dir[0] * light_dir[0]
|
|
722
|
+
+ light_dir[1] * light_dir[1]
|
|
723
|
+
+ light_dir[2] * light_dir[2])
|
|
724
|
+
.sqrt();
|
|
725
|
+
let d = if len > 1e-6 {
|
|
726
|
+
[light_dir[0] / len, light_dir[1] / len, light_dir[2] / len]
|
|
727
|
+
} else {
|
|
728
|
+
[0.0, 1.0, 0.0]
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Compute frustum split distances using practical split scheme
|
|
732
|
+
// (Nvidia GPU Gems 3, Chapter 10): blend of logarithmic and
|
|
733
|
+
// uniform split for stability.
|
|
734
|
+
let lambda = 0.5f32; // blend factor (0 = uniform, 1 = logarithmic)
|
|
735
|
+
let ratio = far / near;
|
|
736
|
+
let mut splits = [0.0f32; NUM_CASCADES + 1];
|
|
737
|
+
splits[0] = near;
|
|
738
|
+
for i in 1..NUM_CASCADES {
|
|
739
|
+
let p = i as f32 / NUM_CASCADES as f32;
|
|
740
|
+
let log_split = near * ratio.powf(p);
|
|
741
|
+
let uniform_split = near + (far - near) * p;
|
|
742
|
+
splits[i] = lambda * log_split + (1.0 - lambda) * uniform_split;
|
|
743
|
+
}
|
|
744
|
+
splits[NUM_CASCADES] = far;
|
|
745
|
+
|
|
746
|
+
// Store view-space Z split distances for shader cascade selection.
|
|
747
|
+
// cascade_splits[i] = far edge of cascade i.
|
|
748
|
+
for i in 0..NUM_CASCADES {
|
|
749
|
+
self.cascade_splits[i] = splits[i + 1];
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// A light-direction change invalidates every accepted fit (the
|
|
753
|
+
// light-plane basis itself moves).
|
|
754
|
+
if self.accepted_light_dir != Some(d) {
|
|
755
|
+
self.accepted_fit = [None; NUM_CASCADES];
|
|
756
|
+
self.accepted_light_dir = Some(d);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Light-space basis vectors for texel snapping
|
|
760
|
+
let up_hint = if d[1].abs() > 0.99 {
|
|
761
|
+
[1.0f32, 0.0, 0.0]
|
|
762
|
+
} else {
|
|
763
|
+
[0.0f32, 1.0, 0.0]
|
|
764
|
+
};
|
|
765
|
+
let right = normalize3([
|
|
766
|
+
up_hint[1] * d[2] - up_hint[2] * d[1],
|
|
767
|
+
up_hint[2] * d[0] - up_hint[0] * d[2],
|
|
768
|
+
up_hint[0] * d[1] - up_hint[1] * d[0],
|
|
769
|
+
]);
|
|
770
|
+
let ortho_up = [
|
|
771
|
+
d[1] * right[2] - d[2] * right[1],
|
|
772
|
+
d[2] * right[0] - d[0] * right[2],
|
|
773
|
+
d[0] * right[1] - d[1] * right[0],
|
|
774
|
+
];
|
|
775
|
+
|
|
776
|
+
for c in 0..NUM_CASCADES {
|
|
777
|
+
let c_near = splits[c];
|
|
778
|
+
let c_far = splits[c + 1];
|
|
779
|
+
|
|
780
|
+
// Frustum-slice corners for [c_near, c_far], computed DIRECTLY in
|
|
781
|
+
// view space from the camera FOV, then transformed to world by the
|
|
782
|
+
// (affine, well-conditioned) view inverse.
|
|
783
|
+
//
|
|
784
|
+
// We deliberately do NOT invert the perspective projection here.
|
|
785
|
+
// `mat4_perspective` uses the OpenGL [-1,1] NDC-z convention, so
|
|
786
|
+
// unprojecting its NDC clip corners handed the near plane a NEGATIVE
|
|
787
|
+
// homogeneous w — every corner then divided down onto the z=-1 plane
|
|
788
|
+
// and the frustum bounding sphere collapsed to ~0.12 m. The cascade
|
|
789
|
+
// ortho covered almost nothing, so terrain a few metres from the
|
|
790
|
+
// camera projected outside the shadow frustum and self-shadowed:
|
|
791
|
+
// the reported "moving dark patch" that scaled disproportionately
|
|
792
|
+
// with the camera. Half-extents per unit view depth come straight
|
|
793
|
+
// from the projection (no inversion, no convention hazard):
|
|
794
|
+
// tan(fovy/2) = 1 / proj[1][1]
|
|
795
|
+
// tan(fovy/2) * aspect = 1 / proj[0][0]
|
|
796
|
+
let inv_view = crate::renderer::mat4_invert(camera_view);
|
|
797
|
+
let half_w_per_d = 1.0 / camera_proj[0][0];
|
|
798
|
+
let half_h_per_d = 1.0 / camera_proj[1][1];
|
|
799
|
+
let mut world_corners = [[0.0f32; 3]; 8];
|
|
800
|
+
let mut ci = 0usize;
|
|
801
|
+
for &d in [c_near, c_far].iter() {
|
|
802
|
+
let hw = d * half_w_per_d;
|
|
803
|
+
let hh = d * half_h_per_d;
|
|
804
|
+
for &(sx, sy) in [(-1.0f32, -1.0f32), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)].iter() {
|
|
805
|
+
// View space looks down -Z, so this slice sits at z = -d.
|
|
806
|
+
let vp = [sx * hw, sy * hh, -d, 1.0];
|
|
807
|
+
let wc = crate::renderer::mat4_mul_vec4(&inv_view, &vp);
|
|
808
|
+
world_corners[ci] = [wc[0], wc[1], wc[2]];
|
|
809
|
+
ci += 1;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Bounding sphere of this cascade's frustum slice. Sphere
|
|
814
|
+
// (not AABB) gives rotation-invariant extent so the ortho
|
|
815
|
+
// volume doesn't resize as the camera rotates.
|
|
816
|
+
let mut center = [0.0f32; 3];
|
|
817
|
+
for i in 0..8 {
|
|
818
|
+
center[0] += world_corners[i][0];
|
|
819
|
+
center[1] += world_corners[i][1];
|
|
820
|
+
center[2] += world_corners[i][2];
|
|
821
|
+
}
|
|
822
|
+
center[0] /= 8.0;
|
|
823
|
+
center[1] /= 8.0;
|
|
824
|
+
center[2] /= 8.0;
|
|
825
|
+
let mut radius: f32 = 0.0;
|
|
826
|
+
for i in 0..8 {
|
|
827
|
+
let dx = world_corners[i][0] - center[0];
|
|
828
|
+
let dy = world_corners[i][1] - center[1];
|
|
829
|
+
let dz = world_corners[i][2] - center[2];
|
|
830
|
+
let r2 = dx*dx + dy*dy + dz*dz;
|
|
831
|
+
if r2 > radius { radius = r2; }
|
|
832
|
+
}
|
|
833
|
+
radius = radius.sqrt();
|
|
834
|
+
|
|
835
|
+
// Re-fit slack (cascades ≥ 1): if the required sphere and
|
|
836
|
+
// pancake extents still fit inside the previously accepted
|
|
837
|
+
// (slack-inflated) ortho volume, keep the previous VP
|
|
838
|
+
// byte-identical. The per-cascade shadow cache compares VPs
|
|
839
|
+
// exactly, so a kept VP means the cascade's cached depth
|
|
840
|
+
// stays valid while the camera travels within the slack.
|
|
841
|
+
// EN-045 — cascade 0 gets the slack too.
|
|
842
|
+
//
|
|
843
|
+
// It was excluded, and that quietly made the whole static-shadow cache a
|
|
844
|
+
// title-screen feature. Cascade 0 is the NEAR cascade: it holds the
|
|
845
|
+
// player and everything they are standing next to. Re-fitting it every
|
|
846
|
+
// frame means its VP changes every frame the camera moves — which is all
|
|
847
|
+
// of gameplay — so its cached depth was thrown away and every static
|
|
848
|
+
// caster in it re-rendered, every frame. Measured: shadow_pass 0.12 ms on
|
|
849
|
+
// the stationary title screen, 3.2 ms in a moving fight.
|
|
850
|
+
//
|
|
851
|
+
// The slack costs ~15% of near-field shadow resolution and buys a cache
|
|
852
|
+
// that survives ~15 frames of walking instead of zero.
|
|
853
|
+
{
|
|
854
|
+
if let Some(acc) = self.accepted_fit[c] {
|
|
855
|
+
let ls_x = dot3(center, right);
|
|
856
|
+
let ls_y = dot3(center, ortho_up);
|
|
857
|
+
let fits_xy = (ls_x - acc.ls_x).abs() + radius <= acc.radius
|
|
858
|
+
&& (ls_y - acc.ls_y).abs() + radius <= acc.radius;
|
|
859
|
+
// Required extents along the light axis, relative to
|
|
860
|
+
// the ACCEPTED center: the slice sphere plus the
|
|
861
|
+
// scene AABB corners (same needs the pancake fit
|
|
862
|
+
// below covers, measured against the old volume).
|
|
863
|
+
let rel = [
|
|
864
|
+
center[0] - acc.center[0],
|
|
865
|
+
center[1] - acc.center[1],
|
|
866
|
+
center[2] - acc.center[2],
|
|
867
|
+
];
|
|
868
|
+
let along = dot3(rel, d);
|
|
869
|
+
let mut req_back = along + radius;
|
|
870
|
+
let mut req_far = radius - along;
|
|
871
|
+
if let Some((bmin, bmax)) = scene_bounds {
|
|
872
|
+
for i in 0..8 {
|
|
873
|
+
let p = [
|
|
874
|
+
if i & 1 == 0 { bmin[0] } else { bmax[0] },
|
|
875
|
+
if i & 2 == 0 { bmin[1] } else { bmax[1] },
|
|
876
|
+
if i & 4 == 0 { bmin[2] } else { bmax[2] },
|
|
877
|
+
];
|
|
878
|
+
let a = dot3(
|
|
879
|
+
[
|
|
880
|
+
p[0] - acc.center[0],
|
|
881
|
+
p[1] - acc.center[1],
|
|
882
|
+
p[2] - acc.center[2],
|
|
883
|
+
],
|
|
884
|
+
d,
|
|
885
|
+
);
|
|
886
|
+
if a > req_back { req_back = a; }
|
|
887
|
+
if -a > req_far { req_far = -a; }
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if fits_xy && req_back <= acc.back && req_far <= acc.far {
|
|
891
|
+
// Keep light_vps[c] from the accepted fit.
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
let radius = radius * REFIT_SLACK;
|
|
897
|
+
// Quantize radius so subpixel camera movement can't shift
|
|
898
|
+
// the texel grid.
|
|
899
|
+
let radius = (radius * 16.0).ceil() / 16.0;
|
|
900
|
+
|
|
901
|
+
// Texel snap: quantize the ortho center to texel boundaries
|
|
902
|
+
// in light space so camera translation doesn't crawl edges.
|
|
903
|
+
let texel_world = (2.0 * radius) / CASCADE_MAP_SIZE as f32;
|
|
904
|
+
let ls_x = dot3(center, right);
|
|
905
|
+
let ls_y = dot3(center, ortho_up);
|
|
906
|
+
let snapped_x = (ls_x / texel_world).floor() * texel_world;
|
|
907
|
+
let snapped_y = (ls_y / texel_world).floor() * texel_world;
|
|
908
|
+
let dx_snap = snapped_x - ls_x;
|
|
909
|
+
let dy_snap = snapped_y - ls_y;
|
|
910
|
+
let snapped_center = [
|
|
911
|
+
center[0] + dx_snap * right[0] + dy_snap * ortho_up[0],
|
|
912
|
+
center[1] + dx_snap * right[1] + dy_snap * ortho_up[1],
|
|
913
|
+
center[2] + dx_snap * right[2] + dy_snap * ortho_up[2],
|
|
914
|
+
];
|
|
915
|
+
|
|
916
|
+
// Extend Z-range using the scene AABB so casters behind the
|
|
917
|
+
// visible slice (from the light's view) still project shadows
|
|
918
|
+
// into it. This is "pancaking" — cascade XY is tight to the
|
|
919
|
+
// frustum sphere, but Z reaches back to the full scene.
|
|
920
|
+
let mut pancake_back: f32 = radius; // +d distance (toward light)
|
|
921
|
+
let mut pancake_far: f32 = radius; // -d distance (away from light)
|
|
922
|
+
if let Some((bmin, bmax)) = scene_bounds {
|
|
923
|
+
let corners = [
|
|
924
|
+
[bmin[0], bmin[1], bmin[2]],
|
|
925
|
+
[bmax[0], bmin[1], bmin[2]],
|
|
926
|
+
[bmin[0], bmax[1], bmin[2]],
|
|
927
|
+
[bmax[0], bmax[1], bmin[2]],
|
|
928
|
+
[bmin[0], bmin[1], bmax[2]],
|
|
929
|
+
[bmax[0], bmin[1], bmax[2]],
|
|
930
|
+
[bmin[0], bmax[1], bmax[2]],
|
|
931
|
+
[bmax[0], bmax[1], bmax[2]],
|
|
932
|
+
];
|
|
933
|
+
for p in corners.iter() {
|
|
934
|
+
let rel = [
|
|
935
|
+
p[0] - snapped_center[0],
|
|
936
|
+
p[1] - snapped_center[1],
|
|
937
|
+
p[2] - snapped_center[2],
|
|
938
|
+
];
|
|
939
|
+
let along_d = dot3(rel, d);
|
|
940
|
+
if along_d > pancake_back { pancake_back = along_d; }
|
|
941
|
+
if -along_d > pancake_far { pancake_far = -along_d; }
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Quantize Z range so scene-bounds drift doesn't shift depths.
|
|
945
|
+
// Flicker fix: animated casters (idle anims, wind-swayed
|
|
946
|
+
// proxies) drift the raw pancake need by centimetres-to-
|
|
947
|
+
// decimetres every cycle, and every resulting VP change
|
|
948
|
+
// re-rolls the acne pattern on grazing receivers — visible
|
|
949
|
+
// as periodic banding bursts. Quantize UP to whole 2 m steps
|
|
950
|
+
// and only shrink after a full 2-step (4 m) drop, so the
|
|
951
|
+
// fitted VP is byte-stable against anything short of a
|
|
952
|
+
// structural scene change. The cost is a few metres of extra
|
|
953
|
+
// ortho depth range on a Depth32Float target — irrelevant —
|
|
954
|
+
// and coverage stays correct: the accepted extent is never
|
|
955
|
+
// below the raw need.
|
|
956
|
+
const PANCAKE_STEP: f32 = 2.0;
|
|
957
|
+
let quantize = |v: f32| (v / PANCAKE_STEP).ceil() * PANCAKE_STEP;
|
|
958
|
+
let prev = self.pancake_hysteresis[c];
|
|
959
|
+
let pancake_back = if pancake_back > prev[0]
|
|
960
|
+
|| pancake_back < prev[0] - 2.0 * PANCAKE_STEP
|
|
961
|
+
{
|
|
962
|
+
quantize(pancake_back)
|
|
963
|
+
} else {
|
|
964
|
+
prev[0]
|
|
965
|
+
};
|
|
966
|
+
let pancake_far = if pancake_far > prev[1]
|
|
967
|
+
|| pancake_far < prev[1] - 2.0 * PANCAKE_STEP
|
|
968
|
+
{
|
|
969
|
+
quantize(pancake_far)
|
|
970
|
+
} else {
|
|
971
|
+
prev[1]
|
|
972
|
+
};
|
|
973
|
+
self.pancake_hysteresis[c] = [pancake_back, pancake_far];
|
|
974
|
+
|
|
975
|
+
// Place light eye at the far-back edge of the Z range so
|
|
976
|
+
// ortho near=0 exactly touches the top of the pancake volume.
|
|
977
|
+
let eye_offset = pancake_back;
|
|
978
|
+
let light_pos = [
|
|
979
|
+
snapped_center[0] + d[0] * eye_offset,
|
|
980
|
+
snapped_center[1] + d[1] * eye_offset,
|
|
981
|
+
snapped_center[2] + d[2] * eye_offset,
|
|
982
|
+
];
|
|
983
|
+
|
|
984
|
+
let snapped_view = crate::renderer::mat4_look_at(light_pos, snapped_center, up_hint);
|
|
985
|
+
let light_proj = crate::renderer::mat4_ortho(
|
|
986
|
+
-radius, radius,
|
|
987
|
+
-radius, radius,
|
|
988
|
+
0.0,
|
|
989
|
+
eye_offset + pancake_far,
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
self.light_vps[c] = crate::renderer::mat4_multiply(light_proj, snapped_view);
|
|
993
|
+
|
|
994
|
+
// Record the accepted fit so subsequent frames can keep this VP while
|
|
995
|
+
// their requirements stay inside it. EN-045 — cascade 0 included now;
|
|
996
|
+
// excluding it was what made the static-shadow cache a title-screen
|
|
997
|
+
// feature, because cascade 0's VP changed on every frame the camera moved.
|
|
998
|
+
{
|
|
999
|
+
self.accepted_fit[c] = Some(AcceptedFit {
|
|
1000
|
+
ls_x: dot3(snapped_center, right),
|
|
1001
|
+
ls_y: dot3(snapped_center, ortho_up),
|
|
1002
|
+
center: snapped_center,
|
|
1003
|
+
radius,
|
|
1004
|
+
back: pancake_back,
|
|
1005
|
+
far: pancake_far,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/// Enable shadow mapping.
|
|
1012
|
+
pub fn enable(&mut self) {
|
|
1013
|
+
if !self.enabled {
|
|
1014
|
+
self.invalidate();
|
|
1015
|
+
}
|
|
1016
|
+
self.enabled = true;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/// Disable shadow mapping.
|
|
1020
|
+
pub fn disable(&mut self) {
|
|
1021
|
+
self.enabled = false;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
fn normalize3(v: [f32; 3]) -> [f32; 3] {
|
|
1026
|
+
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
|
1027
|
+
if len > 1e-6 {
|
|
1028
|
+
[v[0] / len, v[1] / len, v[2] / len]
|
|
1029
|
+
} else {
|
|
1030
|
+
[0.0, 0.0, 1.0]
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
|
|
1035
|
+
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
1036
|
+
}
|