@navaramap/three-csm 0.0.4 → 0.0.6
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/dist/index.d.ts +79 -0
- package/dist/index.js +57 -25
- package/dist/index.umd.cjs +88 -35
- package/package.json +1 -1
- package/src/CascadedShadowMaps.ts +51 -0
- package/src/MaterialStates.ts +9 -1
- package/src/index.ts +1 -0
- package/src/shaders/cascadedFadedLights.glsl +21 -11
- package/src/shaders/cascadedLights.glsl +23 -12
- package/src/shaders/defaultLights.glsl +18 -11
- package/src/viewSpaceShadowReceive.test.ts +245 -0
- package/src/viewSpaceShadowReceive.ts +145 -0
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,33 @@ import { OrthographicCamera } from 'three';
|
|
|
10
10
|
import { PerspectiveCamera } from 'three';
|
|
11
11
|
import { Vector2 } from 'three';
|
|
12
12
|
import { Vector3 } from 'three';
|
|
13
|
+
import { WebGLRenderer } from 'three';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rewrite a material's vertex shader so directional (CSM) shadow coordinates
|
|
17
|
+
* are computed from the view-space position and the camera-composed shadow
|
|
18
|
+
* matrices instead of `directionalShadowMatrix * worldPosition`.
|
|
19
|
+
*
|
|
20
|
+
* Requires `mvPosition` to be in scope at `#include <shadowmap_vertex>`,
|
|
21
|
+
* which holds for every three.js built-in lit shader and for RTE projection
|
|
22
|
+
* chunks. The world-space normal bias is rotated into view space with
|
|
23
|
+
* `viewMatrix`, which is exactly equivalent to the stock world-space offset.
|
|
24
|
+
*
|
|
25
|
+
* Point/spot shadows — and shadow-casting directional lights beyond the CSM
|
|
26
|
+
* cascades (`CSM_CASCADE_COUNT`) — keep the stock absolute-world-position
|
|
27
|
+
* path, so extra lights coexist with the cascades exactly as before. Shaders
|
|
28
|
+
* without `#include <shadowmap_vertex>` (unlit, depth, or fully custom) are
|
|
29
|
+
* left untouched, as are shaders compiled for the shadow-map depth pass
|
|
30
|
+
* (`USE_SHADOWMAP_DEPTH` — they never sample shadows and would upload the
|
|
31
|
+
* uniform before the shadow maps for the frame are rendered).
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyViewSpaceShadowReceive(shader: {
|
|
34
|
+
vertexShader: string;
|
|
35
|
+
defines?: Record<string, unknown>;
|
|
36
|
+
uniforms: Record<string, {
|
|
37
|
+
value: unknown;
|
|
38
|
+
}>;
|
|
39
|
+
}, uniform: ShadowMatricesViewUniform): void;
|
|
13
40
|
|
|
14
41
|
export declare class CascadedDirectionalLights extends Object3D {
|
|
15
42
|
readonly mainLight: DirectionalLight;
|
|
@@ -36,9 +63,28 @@ export declare class CascadedShadowMaps {
|
|
|
36
63
|
private _margin;
|
|
37
64
|
private _fade;
|
|
38
65
|
private _disableLastCascadeCutoff;
|
|
66
|
+
private readonly _shadowMatricesView;
|
|
67
|
+
private _shadowMatricesViewFrame;
|
|
68
|
+
private _renderer;
|
|
69
|
+
/**
|
|
70
|
+
* Per-cascade `shadow.matrix * mainCamera.matrixWorld`, composed in f64 for
|
|
71
|
+
* precise shadow receiving (see `viewSpaceShadowReceive.ts`). The value is
|
|
72
|
+
* refreshed lazily on access: three.js uploads material uniforms during the
|
|
73
|
+
* main-pass draws, after it has rendered the shadow maps, so reading here
|
|
74
|
+
* always observes the current frame's `shadow.matrix`. Refreshes are
|
|
75
|
+
* deduplicated per `renderer.info.render.frame`.
|
|
76
|
+
*/
|
|
77
|
+
readonly shadowMatricesViewUniform: ShadowMatricesViewUniform;
|
|
39
78
|
constructor(mainCamera: PerspectiveCamera | OrthographicCamera, params?: CascadedShadowMapsOptions);
|
|
40
79
|
dispose(): void;
|
|
41
80
|
setupMaterial<T extends Material>(material: T): T;
|
|
81
|
+
/**
|
|
82
|
+
* Remember the renderer so lazy refreshes of `shadowMatricesViewUniform`
|
|
83
|
+
* can be deduplicated per render call. Called from the material hook, which
|
|
84
|
+
* always runs before the first uniform upload.
|
|
85
|
+
*/
|
|
86
|
+
captureRenderer(renderer: WebGLRenderer): void;
|
|
87
|
+
private refreshShadowMatricesView;
|
|
42
88
|
rollbackMaterial<T extends Material>(material: T): T;
|
|
43
89
|
setupMaterials<T extends readonly Material[]>(materials: T): () => void;
|
|
44
90
|
private updateCascades;
|
|
@@ -141,6 +187,16 @@ export declare const cascadedShadowMapsOptionsDefaults: {
|
|
|
141
187
|
disableLastCascadeCutoff: false;
|
|
142
188
|
};
|
|
143
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Compose `light.shadow.matrix * cameraMatrixWorld` for each cascade light
|
|
192
|
+
* into `matrices`, entirely in f64. The camera must be the render camera of
|
|
193
|
+
* the pass (its `viewMatrix` produced the `mvPosition` the shader multiplies
|
|
194
|
+
* with), and `shadow.matrix` must already hold the current frame's cascade
|
|
195
|
+
* transform — three.js renders the shadow maps before any main-pass material
|
|
196
|
+
* uploads its uniforms, so refreshing lazily at upload time is safe.
|
|
197
|
+
*/
|
|
198
|
+
export declare function composeViewSpaceShadowMatrices(lights: readonly DirectionalLight[], cameraMatrixWorld: Matrix4, matrices: Matrix4[]): void;
|
|
199
|
+
|
|
144
200
|
export declare class CSMHelper extends Group {
|
|
145
201
|
private readonly csm;
|
|
146
202
|
displayFrustum: boolean;
|
|
@@ -192,6 +248,29 @@ declare class MaterialStates {
|
|
|
192
248
|
|
|
193
249
|
export declare function overrideFragmentShaderForCSM(fragmentShader: string): string;
|
|
194
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Uniform holding per-cascade directional shadow matrices composed against
|
|
253
|
+
* the render camera's world matrix on the CPU: `light.shadow.matrix *
|
|
254
|
+
* camera.matrixWorld` (f64).
|
|
255
|
+
*
|
|
256
|
+
* Sampling shadow maps with `directionalShadowMatrix * worldPosition` loses
|
|
257
|
+
* precision on a globe: the absolute ECEF world position (~6.4e6 m) and the
|
|
258
|
+
* shadow matrix translation cancel in f32 on the GPU, and because the CSM
|
|
259
|
+
* cascade matrices follow the camera the rounding error changes every frame —
|
|
260
|
+
* received shadows jitter while the geometry itself stays put.
|
|
261
|
+
*
|
|
262
|
+
* Folding the camera's world matrix in instead lets the shader use the
|
|
263
|
+
* view-space position `mvPosition`, which is precise by construction for
|
|
264
|
+
* every positioning mode: RTE meshes compute it from camera-relative
|
|
265
|
+
* coordinates, RTC meshes get it from the CPU-composed f64 modelViewMatrix,
|
|
266
|
+
* and instancing folds instanceMatrix into it. Shadow sampling then reuses
|
|
267
|
+
* the exact quantity that renders stably, so it can never jitter relative to
|
|
268
|
+
* the geometry — and a single patch covers every mesh type.
|
|
269
|
+
*/
|
|
270
|
+
export declare type ShadowMatricesViewUniform = {
|
|
271
|
+
value: Matrix4[];
|
|
272
|
+
};
|
|
273
|
+
|
|
195
274
|
export declare function splitFrustum(mode: FrustumSplitMode, count: number, near: number, far: number, lambda?: number, result?: number[]): number[];
|
|
196
275
|
|
|
197
276
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -92,7 +92,7 @@ var S = class e {
|
|
|
92
92
|
for (let t = 0; t < 4; ++t) this.near[t].applyMatrix4(e), this.far[t].applyMatrix4(e);
|
|
93
93
|
return this;
|
|
94
94
|
}
|
|
95
|
-
}, C = "// Based on the following work:\n// https://github.com/StrandedKitty/three-csm/tree/master/src\n// https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm\n\nvoid cascadedFadedLights() {\n directionalLight = directionalLights[0];\n getDirectionalLightInfo(directionalLight, directLight);\n\n float clipDepth = vViewPosition.z / (csmFar - csmNear);\n bool lastCascade;\n vec2 cascade;\n float cascadeCenter;\n float closestEdge;\n float margin;\n float shadow = 1.0;\n float nextShadow = 1.0;\n\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n #if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n\n lastCascade = UNROLLED_LOOP_INDEX == CSM_CASCADE_COUNT - 1;\n cascade = csmCascades[i];\n cascadeCenter = (cascade.x + cascade.y) * 0.5;\n closestEdge = clipDepth < cascadeCenter ? cascade.x : cascade.y;\n margin = 0.25 * pow(closestEdge, 2.0);\n cascade += margin * vec2(-0.5, 0.5);\n\n if (clipDepth >= cascade.x && (clipDepth < cascade.y || lastCascade)) {\n directionalLightShadow = directionalLightShadows[i];\n float prevShadow = nextShadow;\n nextShadow =\n directLight.visible && receiveShadow\n ? getShadow(\n directionalShadowMap[i],\n directionalLightShadow.shadowMapSize,\n directionalLightShadow.shadowIntensity,\n directionalLightShadow.shadowBias,\n directionalLightShadow.shadowRadius,\n vDirectionalShadowCoord[i]\n )\n : 1.0;\n\n shadow = mix(\n prevShadow,\n nextShadow,\n saturate(min(clipDepth - cascade.x, cascade.y - clipDepth) / margin)\n );\n }\n #endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n }\n #pragma unroll_loop_end\n\n // No loop here: all cascaded lights are in fact one light only.\n directLight.color *= shadow;\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n\n // The rest of directional lights.\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n #if UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT\n directionalLight = directionalLights[i];\n getDirectionalLightInfo(directionalLight, directLight);\n\n directionalLightShadow = directionalLightShadows[i];\n
|
|
95
|
+
}, C = "// Based on the following work:\n// https://github.com/StrandedKitty/three-csm/tree/master/src\n// https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm\n\nvoid cascadedFadedLights() {\n directionalLight = directionalLights[0];\n getDirectionalLightInfo(directionalLight, directLight);\n\n float clipDepth = vViewPosition.z / (csmFar - csmNear);\n bool lastCascade;\n vec2 cascade;\n float cascadeCenter;\n float closestEdge;\n float margin;\n float shadow = 1.0;\n float nextShadow = 1.0;\n\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n #if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n\n lastCascade = UNROLLED_LOOP_INDEX == CSM_CASCADE_COUNT - 1;\n cascade = csmCascades[i];\n cascadeCenter = (cascade.x + cascade.y) * 0.5;\n closestEdge = clipDepth < cascadeCenter ? cascade.x : cascade.y;\n margin = 0.25 * pow(closestEdge, 2.0);\n cascade += margin * vec2(-0.5, 0.5);\n\n if (clipDepth >= cascade.x && (clipDepth < cascade.y || lastCascade)) {\n directionalLightShadow = directionalLightShadows[i];\n float prevShadow = nextShadow;\n nextShadow =\n directLight.visible && receiveShadow\n ? getShadow(\n directionalShadowMap[i],\n directionalLightShadow.shadowMapSize,\n directionalLightShadow.shadowIntensity,\n directionalLightShadow.shadowBias,\n directionalLightShadow.shadowRadius,\n vDirectionalShadowCoord[i]\n )\n : 1.0;\n\n shadow = mix(\n prevShadow,\n nextShadow,\n saturate(min(clipDepth - cascade.x, cascade.y - clipDepth) / margin)\n );\n }\n #endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n }\n #pragma unroll_loop_end\n\n // No loop here: all cascaded lights are in fact one light only.\n directLight.color *= shadow;\n #if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)\n // Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).\n nvr_shadowMask *= shadow;\n #endif\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n\n // The rest of directional lights.\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n #if UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT\n directionalLight = directionalLights[i];\n getDirectionalLightInfo(directionalLight, directLight);\n\n directionalLightShadow = directionalLightShadows[i];\n {\n float nvrShadowFactor =\n directLight.visible && receiveShadow\n ? getShadow(\n directionalShadowMap[i],\n directionalLightShadow.shadowMapSize,\n directionalLightShadow.shadowIntensity,\n directionalLightShadow.shadowBias,\n directionalLightShadow.shadowRadius,\n vDirectionalShadowCoord[i]\n )\n : 1.0;\n directLight.color *= nvrShadowFactor;\n #if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)\n nvr_shadowMask *= nvrShadowFactor;\n #endif\n }\n\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n #endif // UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT\n #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n }\n #pragma unroll_loop_end\n}\n", w = "// Based on the following work:\n// https://github.com/StrandedKitty/three-csm/tree/master/src\n// https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm\n\nvoid cascadedLights() {\n directionalLight = directionalLights[0];\n getDirectionalLightInfo(directionalLight, directLight);\n\n float clipDepth = vViewPosition.z / (csmFar - csmNear);\n vec2 cascade;\n\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n #if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n cascade = csmCascades[i];\n directionalLightShadow = directionalLightShadows[i];\n if (clipDepth >= cascade.x && clipDepth < cascade.y) {\n float nvrShadowFactor =\n directLight.visible && receiveShadow\n ? getShadow(\n directionalShadowMap[i],\n directionalLightShadow.shadowMapSize,\n directionalLightShadow.shadowIntensity,\n directionalLightShadow.shadowBias,\n directionalLightShadow.shadowRadius,\n vDirectionalShadowCoord[i]\n )\n : 1.0;\n directLight.color *= nvrShadowFactor;\n #if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)\n // Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).\n nvr_shadowMask *= nvrShadowFactor;\n #endif\n }\n #endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n }\n #pragma unroll_loop_end\n\n // No loop here: all cascaded lights are in fact one light only.\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n\n // The rest of directional lights.\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n #if UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT\n directionalLight = directionalLights[i];\n getDirectionalLightInfo(directionalLight, directLight);\n\n directionalLightShadow = directionalLightShadows[i];\n {\n float nvrShadowFactor =\n directLight.visible && receiveShadow\n ? getShadow(\n directionalShadowMap[i],\n directionalLightShadow.shadowMapSize,\n directionalLightShadow.shadowIntensity,\n directionalLightShadow.shadowBias,\n directionalLightShadow.shadowRadius,\n vDirectionalShadowCoord[i]\n )\n : 1.0;\n directLight.color *= nvrShadowFactor;\n #if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)\n nvr_shadowMask *= nvrShadowFactor;\n #endif\n }\n\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n #endif // UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT\n #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n }\n #pragma unroll_loop_end\n}\n", T = "// Based on the following work:\n// https://github.com/StrandedKitty/three-csm/tree/master/src\n// https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm\n\nvoid cascadedLightsWithoutShadows() {\n // No loop here: all cascaded lights are in fact one light only.\n getDirectionalLightInfo(directionalLights[0], directLight);\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n\n // The rest of directional lights.\n #pragma unroll_loop_start\n for (int i = CSM_CASCADE_COUNT; i < NUM_DIR_LIGHTS; i++) {\n directionalLight = directionalLights[i];\n getDirectionalLightInfo(directionalLight, directLight);\n\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n }\n #pragma unroll_loop_end\n}\n", E = "// Default shader.\n// See: https://github.com/mrdoob/three.js/blob/r169/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js#L136\n\nvoid defaultLights() {\n #pragma unroll_loop_start\n for (int i = 0; i < NUM_DIR_LIGHTS; i++) {\n directionalLight = directionalLights[i];\n getDirectionalLightInfo(directionalLight, directLight);\n\n #if defined(USE_SHADOWMAP) && UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS\n directionalLightShadow = directionalLightShadows[i];\n {\n float nvrShadowFactor =\n directLight.visible && receiveShadow\n ? getShadow(\n directionalShadowMap[i],\n directionalLightShadow.shadowMapSize,\n directionalLightShadow.shadowIntensity,\n directionalLightShadow.shadowBias,\n directionalLightShadow.shadowRadius,\n vDirectionalShadowCoord[i]\n )\n : 1.0;\n directLight.color *= nvrShadowFactor;\n #if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)\n // Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).\n nvr_shadowMask *= nvrShadowFactor;\n #endif\n }\n #endif\n\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n }\n #pragma unroll_loop_end\n}\n", D = "void nonShadowCastingLights() {\n #pragma unroll_loop_start\n for (int i = NUM_DIR_LIGHT_SHADOWS; i < NUM_DIR_LIGHTS; i++) {\n directionalLight = directionalLights[i];\n getDirectionalLightInfo(directionalLight, directLight);\n\n RE_Direct(\n directLight,\n geometryPosition,\n geometryNormal,\n geometryViewDir,\n geometryClearcoatNormal,\n material,\n reflectedLight\n );\n }\n #pragma unroll_loop_end\n}\n", O = "\n #include <lights_pars_begin>\n\n #ifdef CSM\n uniform vec2 csmCascades[CSM_CASCADE_COUNT];\n uniform float csmNear;\n uniform float csmFar;\n #endif // CSM\n";
|
|
96
96
|
function k(e) {
|
|
97
97
|
return e.replace(/^\s*void\s+\w+\s*\(\s*\)\s*/m, "");
|
|
98
98
|
}
|
|
@@ -137,18 +137,35 @@ function M(e) {
|
|
|
137
137
|
}));
|
|
138
138
|
}
|
|
139
139
|
//#endregion
|
|
140
|
+
//#region src/viewSpaceShadowReceive.ts
|
|
141
|
+
var N = "\n#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n// Per-cascade shadow matrices composed with the render camera's world matrix\n// on the CPU in f64 (shadow.matrix * camera.matrixWorld). Multiplied with the\n// view-space position instead of the absolute world position to avoid f32\n// cancellation at ECEF magnitudes.\n//\n// Only the CSM cascade lights (the first CSM_CASCADE_COUNT shadow indices,\n// matching the ordering assumption of the CSM fragment shader) get these\n// matrices; additional shadow-casting directional lights keep the stock\n// directionalShadowMatrix path. Outside a CSM material the define is absent —\n// treat every directional shadow as camera-composed then.\n#ifndef CSM_CASCADE_COUNT\n#define CSM_CASCADE_COUNT NUM_DIR_LIGHT_SHADOWS\n#endif\nuniform mat4 nvrCsmShadowMatrixView[ CSM_CASCADE_COUNT ];\n#endif\n";
|
|
142
|
+
function P(e, t, n) {
|
|
143
|
+
if (!e.includes(t)) throw Error(`Failed to replace "${t}" in shader code. The pattern was not found.`);
|
|
144
|
+
return e.replace(t, n);
|
|
145
|
+
}
|
|
146
|
+
function F(e, t) {
|
|
147
|
+
if (e.defines?.USE_SHADOWMAP_DEPTH || !e.vertexShader.includes("#include <shadowmap_vertex>")) return;
|
|
148
|
+
e.uniforms.nvrCsmShadowMatrixView = t;
|
|
149
|
+
let n = P(h.shadowmap_vertex, "shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );", "#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n shadowWorldPosition = vec4( mvPosition.xyz + ( viewMatrix * vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0.0 ) ).xyz, 1.0 );\n #else\n shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n #endif");
|
|
150
|
+
n = P(n, "vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;", "#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT\n vDirectionalShadowCoord[ i ] = nvrCsmShadowMatrixView[ i ] * shadowWorldPosition;\n #else\n vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n #endif"), e.vertexShader = P(P(e.vertexShader, "#include <shadowmap_pars_vertex>", `#include <shadowmap_pars_vertex>\n${N}`), "#include <shadowmap_vertex>", n);
|
|
151
|
+
}
|
|
152
|
+
function I(e, t, n) {
|
|
153
|
+
n.length = e.length;
|
|
154
|
+
for (let r = 0; r < e.length; r++) (n[r] ?? (n[r] = new u())).multiplyMatrices(e[r].shadow.matrix, t);
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
140
157
|
//#region src/MaterialStates.ts
|
|
141
|
-
var
|
|
158
|
+
var L = class {
|
|
142
159
|
map = /* @__PURE__ */ new Map();
|
|
143
160
|
setup(e, t) {
|
|
144
161
|
if (this.map.has(e)) return e;
|
|
145
162
|
e.userData.defines ??= {}, e.userData.defines.CSM = 1, e.userData.defines.CSM_CASCADE_COUNT = t.cascadeCount, t.fade && (e.userData.defines.CSM_FADE = 1);
|
|
146
163
|
let n = {};
|
|
147
164
|
this.map.set(e, n);
|
|
148
|
-
let r = (e) => {
|
|
149
|
-
n.parameters = e;
|
|
150
|
-
let
|
|
151
|
-
|
|
165
|
+
let r = (e, r) => {
|
|
166
|
+
n.parameters = e, t.captureRenderer(r);
|
|
167
|
+
let i = t.cascades, a = t.mainCamera.near, o = Math.min(t.mainCamera.far, t.far), s = e.uniforms;
|
|
168
|
+
s.csmCascades = { value: i }, s.csmNear = { value: a }, s.csmFar = { value: o }, e.fragmentShader = M(e.fragmentShader), F(e, t.shadowMatricesViewUniform);
|
|
152
169
|
}, i = e.onBeforeCompile.bind(e), a = e.customProgramCacheKey.bind(e);
|
|
153
170
|
Object.prototype.hasOwnProperty.call(e, "hasOwnProperty") && (n.originalHandler = i), e.customProgramCacheKey = () => i.toString() + a(), e.onBeforeCompile = (...t) => {
|
|
154
171
|
let [n] = t;
|
|
@@ -176,7 +193,7 @@ var N = class {
|
|
|
176
193
|
return t == null ? e : this.rollbackToState(e, t);
|
|
177
194
|
}
|
|
178
195
|
rollbackToState(e, { parameters: t, originalHandler: n }) {
|
|
179
|
-
return t != null && (delete t.uniforms.csmCascades, delete t.uniforms.csmNear, delete t.uniforms.csmFar), n == null ? delete e.onBeforeCompile : e.onBeforeCompile = n, e.userData.defines != null && (delete e.userData.defines.CSM, delete e.userData.defines.CSM_CASCADE_COUNT, delete e.userData.defines.CSM_FADE), e.needsUpdate = !0, this.map.delete(e), e;
|
|
196
|
+
return t != null && (delete t.uniforms.csmCascades, delete t.uniforms.csmNear, delete t.uniforms.csmFar, delete t.uniforms.nvrCsmShadowMatrixView), n == null ? delete e.onBeforeCompile : e.onBeforeCompile = n, e.userData.defines != null && (delete e.userData.defines.CSM, delete e.userData.defines.CSM_CASCADE_COUNT, delete e.userData.defines.CSM_FADE), e.needsUpdate = !0, this.map.delete(e), e;
|
|
180
197
|
}
|
|
181
198
|
dispose() {
|
|
182
199
|
for (let [e, t] of this.map) this.rollbackToState(e, t);
|
|
@@ -184,12 +201,12 @@ var N = class {
|
|
|
184
201
|
}
|
|
185
202
|
};
|
|
186
203
|
Math.PI / 180, 180 / Math.PI;
|
|
187
|
-
function
|
|
204
|
+
function R(e, t, n) {
|
|
188
205
|
return (1 - n) * e + n * t;
|
|
189
206
|
}
|
|
190
207
|
//#endregion
|
|
191
208
|
//#region src/splitFrustum.ts
|
|
192
|
-
var
|
|
209
|
+
var z = {
|
|
193
210
|
uniform: (e, t, n, r, i = []) => {
|
|
194
211
|
for (let r = 0; r < e; ++r) i[r] = (t + (n - t) * (r + 1) / e) / n;
|
|
195
212
|
return i.length = e, i;
|
|
@@ -199,16 +216,16 @@ var F = {
|
|
|
199
216
|
return i.length = e, i;
|
|
200
217
|
},
|
|
201
218
|
practical: (e, t, n, r = .5, i = []) => {
|
|
202
|
-
for (let a = 0; a < e; ++a) i[a] =
|
|
219
|
+
for (let a = 0; a < e; ++a) i[a] = R((t + (n - t) * (a + 1) / e) / n, t * (n / t) ** ((a + 1) / e) / n, r);
|
|
203
220
|
return i.length = e, i;
|
|
204
221
|
}
|
|
205
|
-
},
|
|
206
|
-
function
|
|
207
|
-
return
|
|
222
|
+
}, B = z;
|
|
223
|
+
function V(e, t, n, r, i, a = []) {
|
|
224
|
+
return z[e](t, n, r, i, a);
|
|
208
225
|
}
|
|
209
226
|
//#endregion
|
|
210
227
|
//#region src/CascadedShadowMaps.ts
|
|
211
|
-
var
|
|
228
|
+
var H = /*#__PURE__*/ new _(), U = /*#__PURE__*/ new _(), W = /*#__PURE__*/ new u(), G = /*#__PURE__*/ new u(), K = /*#__PURE__*/ new S(), q = /*#__PURE__*/ new e(), J = {
|
|
212
229
|
cascadeCount: 4,
|
|
213
230
|
mapSize: 2048,
|
|
214
231
|
far: 5e3,
|
|
@@ -217,11 +234,11 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
217
234
|
margin: 200,
|
|
218
235
|
fade: !0,
|
|
219
236
|
disableLastCascadeCutoff: !1
|
|
220
|
-
},
|
|
237
|
+
}, Y = class {
|
|
221
238
|
mainCamera;
|
|
222
239
|
needsUpdateFrusta = !0;
|
|
223
240
|
directionalLights = new y();
|
|
224
|
-
materialStates = new
|
|
241
|
+
materialStates = new L();
|
|
225
242
|
mainFrustum = new S();
|
|
226
243
|
cascadedFrusta = [];
|
|
227
244
|
splits = [];
|
|
@@ -232,13 +249,21 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
232
249
|
_margin;
|
|
233
250
|
_fade;
|
|
234
251
|
_disableLastCascadeCutoff;
|
|
252
|
+
_shadowMatricesView = [];
|
|
253
|
+
_shadowMatricesViewFrame = -1;
|
|
254
|
+
_renderer = null;
|
|
255
|
+
shadowMatricesViewUniform;
|
|
235
256
|
constructor(e, t) {
|
|
236
257
|
this.mainCamera = e;
|
|
237
|
-
let
|
|
238
|
-
|
|
258
|
+
let n = () => (this.refreshShadowMatricesView(), this._shadowMatricesView);
|
|
259
|
+
this.shadowMatricesViewUniform = { get value() {
|
|
260
|
+
return n();
|
|
261
|
+
} };
|
|
262
|
+
let { cascadeCount: r, mapSize: i, far: a, mode: o, lambda: s, margin: c, fade: l, disableLastCascadeCutoff: u } = {
|
|
263
|
+
...J,
|
|
239
264
|
...t
|
|
240
265
|
};
|
|
241
|
-
this._far =
|
|
266
|
+
this._far = a, this._mode = o, this._lambda = s, this._margin = c, this._fade = l, this._disableLastCascadeCutoff = u, this.cascadeCount = r, this.mapSize = i;
|
|
242
267
|
}
|
|
243
268
|
dispose() {
|
|
244
269
|
this.materialStates.dispose(), this.directionalLights.dispose();
|
|
@@ -246,6 +271,13 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
246
271
|
setupMaterial(e) {
|
|
247
272
|
return this.materialStates.setup(e, this);
|
|
248
273
|
}
|
|
274
|
+
captureRenderer(e) {
|
|
275
|
+
this._renderer ??= e;
|
|
276
|
+
}
|
|
277
|
+
refreshShadowMatricesView() {
|
|
278
|
+
let e = this._renderer?.info.render.frame ?? -1;
|
|
279
|
+
e !== -1 && e === this._shadowMatricesViewFrame || (this._shadowMatricesViewFrame = e, I(this.directionalLights.cascadedLights, this.mainCamera.matrixWorld, this._shadowMatricesView));
|
|
280
|
+
}
|
|
249
281
|
rollbackMaterial(e) {
|
|
250
282
|
return this.materialStates.rollback(e);
|
|
251
283
|
}
|
|
@@ -257,7 +289,7 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
257
289
|
}
|
|
258
290
|
updateCascades() {
|
|
259
291
|
let e = this.cascadeCount, t = this.splits;
|
|
260
|
-
|
|
292
|
+
V(this.mode, e, this.mainCamera.near, Math.min(this.mainCamera.far, this.far), this.lambda, t), this.mainFrustum.setFromCamera(this.mainCamera, this.far), this.mainFrustum.split(t, this.cascadedFrusta);
|
|
261
293
|
let n = this.cascades;
|
|
262
294
|
for (let r = 0; r < e; ++r) (n[r] ?? (n[r] = new g())).set(t[r - 1] ?? 0, t[r] ?? 0);
|
|
263
295
|
this.disableLastCascadeCutoff && (n[e - 1].y = Infinity), n.length = e;
|
|
@@ -283,11 +315,11 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
283
315
|
}
|
|
284
316
|
update() {
|
|
285
317
|
this.needsUpdateFrusta && (this.needsUpdateFrusta = !1, this.updateFrusta());
|
|
286
|
-
let e = this.directionalLights, t =
|
|
318
|
+
let e = this.directionalLights, t = H.copy(e.direction).normalize(), n = W.lookAt(new _(), t, p.DEFAULT_UP), r = G.multiplyMatrices(G.copy(n).invert(), this.mainCamera.matrixWorld), i = this.margin, a = this.mapSize, o = this.cascadedFrusta, s = e.cascadedLights;
|
|
287
319
|
for (let e = 0; e < o.length; ++e) {
|
|
288
|
-
let { near: c, far: l } =
|
|
320
|
+
let { near: c, far: l } = K.copy(o[e]).applyMatrix4(r), u = q.makeEmpty();
|
|
289
321
|
for (let e = 0; e < 4; e++) u.expandByPoint(c[e]), u.expandByPoint(l[e]);
|
|
290
|
-
let d = u.getCenter(
|
|
322
|
+
let d = u.getCenter(U);
|
|
291
323
|
d.z = u.max.z + i;
|
|
292
324
|
let f = s[e], { left: p, right: m, top: h, bottom: g } = f.shadow.camera, _ = (m - p) / a, v = (h - g) / a;
|
|
293
325
|
d.x = Math.round(d.x / _) * _, d.y = Math.round(d.y / v) * v, d.applyMatrix4(n), f.position.copy(d), f.target.position.copy(d), f.target.position.add(t);
|
|
@@ -407,7 +439,7 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
407
439
|
for (let n = 0; n < t.length; ++n) t[n].shadow.blurSamples = e;
|
|
408
440
|
}
|
|
409
441
|
}
|
|
410
|
-
},
|
|
442
|
+
}, X = class extends s {
|
|
411
443
|
csm;
|
|
412
444
|
displayFrustum = !0;
|
|
413
445
|
displayPlanes = !0;
|
|
@@ -480,4 +512,4 @@ var R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new
|
|
|
480
512
|
}
|
|
481
513
|
};
|
|
482
514
|
//#endregion
|
|
483
|
-
export {
|
|
515
|
+
export { X as CSMHelper, y as CascadedDirectionalLights, Y as CascadedShadowMaps, F as applyViewSpaceShadowReceive, J as cascadedShadowMapsOptionsDefaults, I as composeViewSpaceShadowMatrices, B as frustumSplitFunctions, M as overrideFragmentShaderForCSM, V as splitFrustum };
|
package/dist/index.umd.cjs
CHANGED
|
@@ -55,6 +55,10 @@ void cascadedFadedLights() {
|
|
|
55
55
|
|
|
56
56
|
// No loop here: all cascaded lights are in fact one light only.
|
|
57
57
|
directLight.color *= shadow;
|
|
58
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
59
|
+
// Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).
|
|
60
|
+
nvr_shadowMask *= shadow;
|
|
61
|
+
#endif
|
|
58
62
|
RE_Direct(
|
|
59
63
|
directLight,
|
|
60
64
|
geometryPosition,
|
|
@@ -74,17 +78,23 @@ void cascadedFadedLights() {
|
|
|
74
78
|
getDirectionalLightInfo(directionalLight, directLight);
|
|
75
79
|
|
|
76
80
|
directionalLightShadow = directionalLightShadows[i];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
{
|
|
82
|
+
float nvrShadowFactor =
|
|
83
|
+
directLight.visible && receiveShadow
|
|
84
|
+
? getShadow(
|
|
85
|
+
directionalShadowMap[i],
|
|
86
|
+
directionalLightShadow.shadowMapSize,
|
|
87
|
+
directionalLightShadow.shadowIntensity,
|
|
88
|
+
directionalLightShadow.shadowBias,
|
|
89
|
+
directionalLightShadow.shadowRadius,
|
|
90
|
+
vDirectionalShadowCoord[i]
|
|
91
|
+
)
|
|
92
|
+
: 1.0;
|
|
93
|
+
directLight.color *= nvrShadowFactor;
|
|
94
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
95
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
96
|
+
#endif
|
|
97
|
+
}
|
|
88
98
|
|
|
89
99
|
RE_Direct(
|
|
90
100
|
directLight,
|
|
@@ -118,7 +128,7 @@ void cascadedLights() {
|
|
|
118
128
|
cascade = csmCascades[i];
|
|
119
129
|
directionalLightShadow = directionalLightShadows[i];
|
|
120
130
|
if (clipDepth >= cascade.x && clipDepth < cascade.y) {
|
|
121
|
-
|
|
131
|
+
float nvrShadowFactor =
|
|
122
132
|
directLight.visible && receiveShadow
|
|
123
133
|
? getShadow(
|
|
124
134
|
directionalShadowMap[i],
|
|
@@ -129,6 +139,11 @@ void cascadedLights() {
|
|
|
129
139
|
vDirectionalShadowCoord[i]
|
|
130
140
|
)
|
|
131
141
|
: 1.0;
|
|
142
|
+
directLight.color *= nvrShadowFactor;
|
|
143
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
144
|
+
// Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).
|
|
145
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
146
|
+
#endif
|
|
132
147
|
}
|
|
133
148
|
#endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
|
|
134
149
|
#endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
|
|
@@ -155,17 +170,23 @@ void cascadedLights() {
|
|
|
155
170
|
getDirectionalLightInfo(directionalLight, directLight);
|
|
156
171
|
|
|
157
172
|
directionalLightShadow = directionalLightShadows[i];
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
{
|
|
174
|
+
float nvrShadowFactor =
|
|
175
|
+
directLight.visible && receiveShadow
|
|
176
|
+
? getShadow(
|
|
177
|
+
directionalShadowMap[i],
|
|
178
|
+
directionalLightShadow.shadowMapSize,
|
|
179
|
+
directionalLightShadow.shadowIntensity,
|
|
180
|
+
directionalLightShadow.shadowBias,
|
|
181
|
+
directionalLightShadow.shadowRadius,
|
|
182
|
+
vDirectionalShadowCoord[i]
|
|
183
|
+
)
|
|
184
|
+
: 1.0;
|
|
185
|
+
directLight.color *= nvrShadowFactor;
|
|
186
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
187
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
188
|
+
#endif
|
|
189
|
+
}
|
|
169
190
|
|
|
170
191
|
RE_Direct(
|
|
171
192
|
directLight,
|
|
@@ -227,17 +248,24 @@ void defaultLights() {
|
|
|
227
248
|
|
|
228
249
|
#if defined(USE_SHADOWMAP) && UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
|
|
229
250
|
directionalLightShadow = directionalLightShadows[i];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
251
|
+
{
|
|
252
|
+
float nvrShadowFactor =
|
|
253
|
+
directLight.visible && receiveShadow
|
|
254
|
+
? getShadow(
|
|
255
|
+
directionalShadowMap[i],
|
|
256
|
+
directionalLightShadow.shadowMapSize,
|
|
257
|
+
directionalLightShadow.shadowIntensity,
|
|
258
|
+
directionalLightShadow.shadowBias,
|
|
259
|
+
directionalLightShadow.shadowRadius,
|
|
260
|
+
vDirectionalShadowCoord[i]
|
|
261
|
+
)
|
|
262
|
+
: 1.0;
|
|
263
|
+
directLight.color *= nvrShadowFactor;
|
|
264
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
265
|
+
// Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).
|
|
266
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
267
|
+
#endif
|
|
268
|
+
}
|
|
241
269
|
#endif
|
|
242
270
|
|
|
243
271
|
RE_Direct(
|
|
@@ -303,4 +331,29 @@ void defaultLights() {
|
|
|
303
331
|
${p(u)}
|
|
304
332
|
#endif // CSM
|
|
305
333
|
#endif // NUM_DIR_LIGHTS > 0 && defined(RE_Direct)
|
|
306
|
-
`;function h({source:e,startLine:t,endLine:n,replacement:r}){let i=`\n${t}\n`,a=e.indexOf(i);if(a<0)return e;let o=`\n${n}\n`,s=e.indexOf(o,a+i.length);return s<0?e:e.slice(0,a+1)+r+e.slice(s+o.length-1)}function g(e){return e.replace(`#include <lights_pars_begin>`,f).replace(`#include <lights_fragment_begin>`,h({source:t.ShaderChunk.lights_fragment_begin,startLine:`#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )`,endLine:`#endif`,replacement:m}))}var _=class{map=new Map;setup(e,t){if(this.map.has(e))return e;e.userData.defines??={},e.userData.defines.CSM=1,e.userData.defines.CSM_CASCADE_COUNT=t.cascadeCount,t.fade&&(e.userData.defines.CSM_FADE=1);let n={};this.map.set(e,n);let r=e=>{n.parameters=e;let r=t.cascades,i=t.mainCamera.near,a=Math.min(t.mainCamera.far,t.far),o=e.uniforms;o.csmCascades={value:r},o.csmNear={value:i},o.csmFar={value:a},e.fragmentShader=g(e.fragmentShader)},i=e.onBeforeCompile.bind(e),a=e.customProgramCacheKey.bind(e);Object.prototype.hasOwnProperty.call(e,`hasOwnProperty`)&&(n.originalHandler=i),e.customProgramCacheKey=()=>i.toString()+a(),e.onBeforeCompile=(...t)=>{let[n]=t;n.defines??={},Object.assign(n.defines,e.userData.defines),i.apply(e,t),r(...t)};let o=()=>{this.map.delete(e),e.removeEventListener(`dispose`,o)};return e.addEventListener(`dispose`,o),e}update(e){let t=e.cascadeCount,n=e.fade,r=e.cascades,i=e.mainCamera.near,a=Math.min(e.mainCamera.far,e.far);for(let[e,o]of this.map.entries()){let s=e.userData.defines;if(s!=null){let r=!1;s.CSM_FADE!=null!==n&&(n?s.CSM_FADE=1:delete s.CSM_FADE,r=!0),s.CSM_CASCADE_COUNT!==t&&(s.CSM_CASCADE_COUNT=t,r=!0),r&&(e.needsUpdate=!0)}let c=o.parameters?.uniforms;c!=null&&(c.csmCascades.value=r,c.csmNear.value=i,c.csmFar.value=a)}}rollback(e){let t=this.map.get(e);return t==null?e:this.rollbackToState(e,t)}rollbackToState(e,{parameters:t,originalHandler:n}){return t!=null&&(delete t.uniforms.csmCascades,delete t.uniforms.csmNear,delete t.uniforms.csmFar),n==null?delete e.onBeforeCompile:e.onBeforeCompile=n,e.userData.defines!=null&&(delete e.userData.defines.CSM,delete e.userData.defines.CSM_CASCADE_COUNT,delete e.userData.defines.CSM_FADE),e.needsUpdate=!0,this.map.delete(e),e}dispose(){for(let[e,t]of this.map)this.rollbackToState(e,t);this.map.clear()}};Math.PI/180,180/Math.PI;function v(e,t,n){return(1-n)*e+n*t}var y={uniform:(e,t,n,r,i=[])=>{for(let r=0;r<e;++r)i[r]=(t+(n-t)*(r+1)/e)/n;return i.length=e,i},logarithmic:(e,t,n,r,i=[])=>{for(let r=0;r<e;++r)i[r]=t*(n/t)**((r+1)/e)/n;return i.length=e,i},practical:(e,t,n,r=.5,i=[])=>{for(let a=0;a<e;++a)i[a]=v((t+(n-t)*(a+1)/e)/n,t*(n/t)**((a+1)/e)/n,r);return i.length=e,i}},b=y;function x(e,t,n,r,i,a=[]){return y[e](t,n,r,i,a)}var S=new t.Vector3,C=new t.Vector3,w=new t.Matrix4,T=new t.Matrix4,E=new o,D=new t.Box3,O={cascadeCount:4,mapSize:2048,far:5e3,mode:`practical`,lambda:.5,margin:200,fade:!0,disableLastCascadeCutoff:!1},k=class{mainCamera;needsUpdateFrusta=!0;directionalLights=new r;materialStates=new _;mainFrustum=new o;cascadedFrusta=[];splits=[];cascades=[];_far;_mode;_lambda;_margin;_fade;_disableLastCascadeCutoff;constructor(e,t){this.mainCamera=e;let{cascadeCount:n,mapSize:r,far:i,mode:a,lambda:o,margin:s,fade:c,disableLastCascadeCutoff:l}={...O,...t};this._far=i,this._mode=a,this._lambda=o,this._margin=s,this._fade=c,this._disableLastCascadeCutoff=l,this.cascadeCount=n,this.mapSize=r}dispose(){this.materialStates.dispose(),this.directionalLights.dispose()}setupMaterial(e){return this.materialStates.setup(e,this)}rollbackMaterial(e){return this.materialStates.rollback(e)}setupMaterials(e){for(let t=0;t<e.length;++t)this.materialStates.setup(e[t],this);return()=>{for(let t=0;t<e.length;++t)this.materialStates.rollback(e[t])}}updateCascades(){let e=this.cascadeCount,n=this.splits;x(this.mode,e,this.mainCamera.near,Math.min(this.mainCamera.far,this.far),this.lambda,n),this.mainFrustum.setFromCamera(this.mainCamera,this.far),this.mainFrustum.split(n,this.cascadedFrusta);let r=this.cascades;for(let i=0;i<e;++i)(r[i]??(r[i]=new t.Vector2)).set(n[i-1]??0,n[i]??0);this.disableLastCascadeCutoff&&(r[e-1].y=1/0),r.length=e}getFrustumRadius(e){let t=e.near,n=e.far,r=Math.max(n[0].distanceTo(n[2]),n[0].distanceTo(t[2]));if(this.fade){let e=this.mainCamera,t=e.near,i=Math.min(e.far,this.far),a=n[0].z/(i-t);r+=.25*a**2*(i-t)}return r*.5}updateShadowBounds(){let e=this.cascadedFrusta,t=this.directionalLights.cascadedLights;a(e.length===t.length);for(let n=0;n<e.length;++n){let e=this.getFrustumRadius(this.cascadedFrusta[n]),r=t[n].shadow.camera;r.left=-e,r.right=e,r.top=e,r.bottom=-e,r.near=0,r.far=e*2+this.margin,r.updateProjectionMatrix()}}updateFrusta(){this.updateCascades(),this.updateShadowBounds(),this.materialStates.update(this)}update(){this.needsUpdateFrusta&&(this.needsUpdateFrusta=!1,this.updateFrusta());let e=this.directionalLights,n=S.copy(e.direction).normalize(),r=w.lookAt(new t.Vector3,n,t.Object3D.DEFAULT_UP),i=T.multiplyMatrices(T.copy(r).invert(),this.mainCamera.matrixWorld),a=this.margin,o=this.mapSize,s=this.cascadedFrusta,c=e.cascadedLights;for(let e=0;e<s.length;++e){let{near:t,far:l}=E.copy(s[e]).applyMatrix4(i),u=D.makeEmpty();for(let e=0;e<4;e++)u.expandByPoint(t[e]),u.expandByPoint(l[e]);let d=u.getCenter(C);d.z=u.max.z+a;let f=c[e],{left:p,right:m,top:h,bottom:g}=f.shadow.camera,_=(m-p)/o,v=(h-g)/o;d.x=Math.round(d.x/_)*_,d.y=Math.round(d.y/v)*v,d.applyMatrix4(r),f.position.copy(d),f.target.position.copy(d),f.target.position.add(n)}}get cascadeCount(){return this.directionalLights.cascadedLights.length}set cascadeCount(e){e!==this.cascadeCount&&(this.directionalLights.setCount(e),this.needsUpdateFrusta=!0)}get mapSize(){return this.directionalLights.mainLight.shadow.mapSize.width}set mapSize(e){if(e!==this.mapSize){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n){let r=t[n].shadow;r.mapSize.width=e,r.mapSize.height=e,r.map!=null&&(r.map.dispose(),r.map=null)}}}get far(){return this._far}set far(e){e!==this._far&&(this._far=e,this.needsUpdateFrusta=!0)}get mode(){return this._mode}set mode(e){e!==this._mode&&(this._mode=e,this.needsUpdateFrusta=!0)}get lambda(){return this._lambda}set lambda(e){e!==this._lambda&&(this._lambda=e,this.needsUpdateFrusta=!0)}get margin(){return this._margin}set margin(e){e!==this._margin&&(this._margin=e,this.needsUpdateFrusta=!0)}get fade(){return this._fade}set fade(e){e!==this._fade&&(this._fade=e,this.needsUpdateFrusta=!0)}get disableLastCascadeCutoff(){return this._disableLastCascadeCutoff}set disableLastCascadeCutoff(e){e!==this._disableLastCascadeCutoff&&(this._disableLastCascadeCutoff=e,this.needsUpdateFrusta=!0)}get intensity(){return this.directionalLights.mainLight.intensity}set intensity(e){if(e!==this.intensity){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].intensity=e}}get shadowIntensity(){return this.directionalLights.mainLight.shadow.intensity}set shadowIntensity(e){if(e!==this.shadowIntensity){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.intensity=e}}get color(){return this.directionalLights.mainLight.color}set color(e){e!==this.color&&this.directionalLights.mainLight.color.copy(e)}get bias(){return this.directionalLights.mainLight.shadow.bias}set bias(e){if(e!==this.bias){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.bias=e}}get normalBias(){return this.directionalLights.mainLight.shadow.normalBias}set normalBias(e){if(e!==this.normalBias){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.normalBias=e}}get radius(){return this.directionalLights.mainLight.shadow.radius}set radius(e){if(e!==this.radius){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.radius=e}}get blurSamples(){return this.directionalLights.mainLight.shadow.blurSamples}set blurSamples(e){if(e!==this.blurSamples){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.blurSamples=e}}};e.CSMHelper=class extends t.Group{csm;displayFrustum=!0;displayPlanes=!0;displayShadowBounds=!0;frustumLines;cascadeLines=[];cascadePlanes=[];shadowLines=[];constructor(e){super(),this.csm=e;let n=new Uint16Array([0,1,1,2,2,3,3,0,4,5,5,6,6,7,7,4,0,4,1,5,2,6,3,7]),r=new Float32Array(24),i=new t.BufferGeometry;i.setIndex(new t.BufferAttribute(n,1)),i.setAttribute(`position`,new t.BufferAttribute(r,3,!1));let a=new t.LineSegments(i,new t.LineBasicMaterial);this.add(a),this.frustumLines=a}updateVisibility(){let e=this.displayFrustum,t=this.displayPlanes,n=this.displayShadowBounds,r=this.frustumLines,i=this.cascadeLines,a=this.cascadePlanes,o=this.shadowLines;for(let r=0,s=i.length;r<s;r++){let s=i[r],c=a[r],l=o[r];s.visible=e,c.visible=e&&t,l.visible=n}r.visible=e}update(){let e=this.csm,n=e.mainCamera,r=e.cascadeCount,i=e.mainFrustum,a=e.cascadedFrusta,o=e.directionalLights.cascadedLights,s=this.frustumLines.geometry.getAttribute(`position`),c=this.cascadeLines,l=this.cascadePlanes,u=this.shadowLines;for(this.position.copy(n.position),this.quaternion.copy(n.quaternion),this.scale.copy(n.scale),this.updateMatrixWorld(!0);c.length>r;){let e=c.pop(),t=l.pop(),n=u.pop();e&&this.remove(e),t&&this.remove(t),n&&this.remove(n)}for(;c.length<r;){let e=new t.Box3Helper(new t.Box3,new t.Color(16777215)),n=new t.MeshBasicMaterial({transparent:!0,opacity:.1,depthWrite:!1,side:t.DoubleSide}),r=new t.Mesh(new t.PlaneGeometry,n),i=new t.Group,a=new t.Box3Helper(new t.Box3,new t.Color(16776960));i.add(a),this.add(e),this.add(r),this.add(i),c.push(e),l.push(r),u.push(i)}for(let e=0;e<r;e++){let t=a[e],n=o[e].shadow.camera,r=t.far,i=c[e],s=l[e],d=u[e],f=d.children[0];i.box.min.copy(r[2]),i.box.max.copy(r[0]),i.box.max.z+=1e-4,s.position.addVectors(r[0],r[2]),s.position.multiplyScalar(.5),s.scale.subVectors(r[0],r[2]),s.scale.z=1e-4,this.remove(d),d.position.copy(n.position),d.quaternion.copy(n.quaternion),d.scale.copy(n.scale),d.updateMatrixWorld(!0),this.attach(d),f.box.min.set(n.bottom,n.left,-n.far),f.box.max.set(n.top,n.right,-n.near)}let d=i.near,f=i.far;s.setXYZ(0,f[0].x,f[0].y,f[0].z),s.setXYZ(1,f[3].x,f[3].y,f[3].z),s.setXYZ(2,f[2].x,f[2].y,f[2].z),s.setXYZ(3,f[1].x,f[1].y,f[1].z),s.setXYZ(4,d[0].x,d[0].y,d[0].z),s.setXYZ(5,d[3].x,d[3].y,d[3].z),s.setXYZ(6,d[2].x,d[2].y,d[2].z),s.setXYZ(7,d[1].x,d[1].y,d[1].z),s.needsUpdate=!0}},e.CascadedDirectionalLights=r,e.CascadedShadowMaps=k,e.cascadedShadowMapsOptionsDefaults=O,e.frustumSplitFunctions=b,e.overrideFragmentShaderForCSM=g,e.splitFrustum=x});
|
|
334
|
+
`;function h({source:e,startLine:t,endLine:n,replacement:r}){let i=`\n${t}\n`,a=e.indexOf(i);if(a<0)return e;let o=`\n${n}\n`,s=e.indexOf(o,a+i.length);return s<0?e:e.slice(0,a+1)+r+e.slice(s+o.length-1)}function g(e){return e.replace(`#include <lights_pars_begin>`,f).replace(`#include <lights_fragment_begin>`,h({source:t.ShaderChunk.lights_fragment_begin,startLine:`#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )`,endLine:`#endif`,replacement:m}))}var _=`
|
|
335
|
+
#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0
|
|
336
|
+
// Per-cascade shadow matrices composed with the render camera's world matrix
|
|
337
|
+
// on the CPU in f64 (shadow.matrix * camera.matrixWorld). Multiplied with the
|
|
338
|
+
// view-space position instead of the absolute world position to avoid f32
|
|
339
|
+
// cancellation at ECEF magnitudes.
|
|
340
|
+
//
|
|
341
|
+
// Only the CSM cascade lights (the first CSM_CASCADE_COUNT shadow indices,
|
|
342
|
+
// matching the ordering assumption of the CSM fragment shader) get these
|
|
343
|
+
// matrices; additional shadow-casting directional lights keep the stock
|
|
344
|
+
// directionalShadowMatrix path. Outside a CSM material the define is absent —
|
|
345
|
+
// treat every directional shadow as camera-composed then.
|
|
346
|
+
#ifndef CSM_CASCADE_COUNT
|
|
347
|
+
#define CSM_CASCADE_COUNT NUM_DIR_LIGHT_SHADOWS
|
|
348
|
+
#endif
|
|
349
|
+
uniform mat4 nvrCsmShadowMatrixView[ CSM_CASCADE_COUNT ];
|
|
350
|
+
#endif
|
|
351
|
+
`;function v(e,t,n){if(!e.includes(t))throw Error(`Failed to replace "${t}" in shader code. The pattern was not found.`);return e.replace(t,n)}function y(e,n){if(e.defines?.USE_SHADOWMAP_DEPTH||!e.vertexShader.includes(`#include <shadowmap_vertex>`))return;e.uniforms.nvrCsmShadowMatrixView=n;let r=v(t.ShaderChunk.shadowmap_vertex,`shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );`,`#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
|
|
352
|
+
shadowWorldPosition = vec4( mvPosition.xyz + ( viewMatrix * vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0.0 ) ).xyz, 1.0 );
|
|
353
|
+
#else
|
|
354
|
+
shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );
|
|
355
|
+
#endif`);r=v(r,`vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;`,`#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
|
|
356
|
+
vDirectionalShadowCoord[ i ] = nvrCsmShadowMatrixView[ i ] * shadowWorldPosition;
|
|
357
|
+
#else
|
|
358
|
+
vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;
|
|
359
|
+
#endif`),e.vertexShader=v(v(e.vertexShader,`#include <shadowmap_pars_vertex>`,`#include <shadowmap_pars_vertex>\n${_}`),`#include <shadowmap_vertex>`,r)}function b(e,n,r){r.length=e.length;for(let i=0;i<e.length;i++)(r[i]??(r[i]=new t.Matrix4)).multiplyMatrices(e[i].shadow.matrix,n)}var x=class{map=new Map;setup(e,t){if(this.map.has(e))return e;e.userData.defines??={},e.userData.defines.CSM=1,e.userData.defines.CSM_CASCADE_COUNT=t.cascadeCount,t.fade&&(e.userData.defines.CSM_FADE=1);let n={};this.map.set(e,n);let r=(e,r)=>{n.parameters=e,t.captureRenderer(r);let i=t.cascades,a=t.mainCamera.near,o=Math.min(t.mainCamera.far,t.far),s=e.uniforms;s.csmCascades={value:i},s.csmNear={value:a},s.csmFar={value:o},e.fragmentShader=g(e.fragmentShader),y(e,t.shadowMatricesViewUniform)},i=e.onBeforeCompile.bind(e),a=e.customProgramCacheKey.bind(e);Object.prototype.hasOwnProperty.call(e,`hasOwnProperty`)&&(n.originalHandler=i),e.customProgramCacheKey=()=>i.toString()+a(),e.onBeforeCompile=(...t)=>{let[n]=t;n.defines??={},Object.assign(n.defines,e.userData.defines),i.apply(e,t),r(...t)};let o=()=>{this.map.delete(e),e.removeEventListener(`dispose`,o)};return e.addEventListener(`dispose`,o),e}update(e){let t=e.cascadeCount,n=e.fade,r=e.cascades,i=e.mainCamera.near,a=Math.min(e.mainCamera.far,e.far);for(let[e,o]of this.map.entries()){let s=e.userData.defines;if(s!=null){let r=!1;s.CSM_FADE!=null!==n&&(n?s.CSM_FADE=1:delete s.CSM_FADE,r=!0),s.CSM_CASCADE_COUNT!==t&&(s.CSM_CASCADE_COUNT=t,r=!0),r&&(e.needsUpdate=!0)}let c=o.parameters?.uniforms;c!=null&&(c.csmCascades.value=r,c.csmNear.value=i,c.csmFar.value=a)}}rollback(e){let t=this.map.get(e);return t==null?e:this.rollbackToState(e,t)}rollbackToState(e,{parameters:t,originalHandler:n}){return t!=null&&(delete t.uniforms.csmCascades,delete t.uniforms.csmNear,delete t.uniforms.csmFar,delete t.uniforms.nvrCsmShadowMatrixView),n==null?delete e.onBeforeCompile:e.onBeforeCompile=n,e.userData.defines!=null&&(delete e.userData.defines.CSM,delete e.userData.defines.CSM_CASCADE_COUNT,delete e.userData.defines.CSM_FADE),e.needsUpdate=!0,this.map.delete(e),e}dispose(){for(let[e,t]of this.map)this.rollbackToState(e,t);this.map.clear()}};Math.PI/180,180/Math.PI;function S(e,t,n){return(1-n)*e+n*t}var C={uniform:(e,t,n,r,i=[])=>{for(let r=0;r<e;++r)i[r]=(t+(n-t)*(r+1)/e)/n;return i.length=e,i},logarithmic:(e,t,n,r,i=[])=>{for(let r=0;r<e;++r)i[r]=t*(n/t)**((r+1)/e)/n;return i.length=e,i},practical:(e,t,n,r=.5,i=[])=>{for(let a=0;a<e;++a)i[a]=S((t+(n-t)*(a+1)/e)/n,t*(n/t)**((a+1)/e)/n,r);return i.length=e,i}},w=C;function T(e,t,n,r,i,a=[]){return C[e](t,n,r,i,a)}var E=new t.Vector3,D=new t.Vector3,O=new t.Matrix4,k=new t.Matrix4,A=new o,j=new t.Box3,M={cascadeCount:4,mapSize:2048,far:5e3,mode:`practical`,lambda:.5,margin:200,fade:!0,disableLastCascadeCutoff:!1},N=class{mainCamera;needsUpdateFrusta=!0;directionalLights=new r;materialStates=new x;mainFrustum=new o;cascadedFrusta=[];splits=[];cascades=[];_far;_mode;_lambda;_margin;_fade;_disableLastCascadeCutoff;_shadowMatricesView=[];_shadowMatricesViewFrame=-1;_renderer=null;shadowMatricesViewUniform;constructor(e,t){this.mainCamera=e;let n=()=>(this.refreshShadowMatricesView(),this._shadowMatricesView);this.shadowMatricesViewUniform={get value(){return n()}};let{cascadeCount:r,mapSize:i,far:a,mode:o,lambda:s,margin:c,fade:l,disableLastCascadeCutoff:u}={...M,...t};this._far=a,this._mode=o,this._lambda=s,this._margin=c,this._fade=l,this._disableLastCascadeCutoff=u,this.cascadeCount=r,this.mapSize=i}dispose(){this.materialStates.dispose(),this.directionalLights.dispose()}setupMaterial(e){return this.materialStates.setup(e,this)}captureRenderer(e){this._renderer??=e}refreshShadowMatricesView(){let e=this._renderer?.info.render.frame??-1;e!==-1&&e===this._shadowMatricesViewFrame||(this._shadowMatricesViewFrame=e,b(this.directionalLights.cascadedLights,this.mainCamera.matrixWorld,this._shadowMatricesView))}rollbackMaterial(e){return this.materialStates.rollback(e)}setupMaterials(e){for(let t=0;t<e.length;++t)this.materialStates.setup(e[t],this);return()=>{for(let t=0;t<e.length;++t)this.materialStates.rollback(e[t])}}updateCascades(){let e=this.cascadeCount,n=this.splits;T(this.mode,e,this.mainCamera.near,Math.min(this.mainCamera.far,this.far),this.lambda,n),this.mainFrustum.setFromCamera(this.mainCamera,this.far),this.mainFrustum.split(n,this.cascadedFrusta);let r=this.cascades;for(let i=0;i<e;++i)(r[i]??(r[i]=new t.Vector2)).set(n[i-1]??0,n[i]??0);this.disableLastCascadeCutoff&&(r[e-1].y=1/0),r.length=e}getFrustumRadius(e){let t=e.near,n=e.far,r=Math.max(n[0].distanceTo(n[2]),n[0].distanceTo(t[2]));if(this.fade){let e=this.mainCamera,t=e.near,i=Math.min(e.far,this.far),a=n[0].z/(i-t);r+=.25*a**2*(i-t)}return r*.5}updateShadowBounds(){let e=this.cascadedFrusta,t=this.directionalLights.cascadedLights;a(e.length===t.length);for(let n=0;n<e.length;++n){let e=this.getFrustumRadius(this.cascadedFrusta[n]),r=t[n].shadow.camera;r.left=-e,r.right=e,r.top=e,r.bottom=-e,r.near=0,r.far=e*2+this.margin,r.updateProjectionMatrix()}}updateFrusta(){this.updateCascades(),this.updateShadowBounds(),this.materialStates.update(this)}update(){this.needsUpdateFrusta&&(this.needsUpdateFrusta=!1,this.updateFrusta());let e=this.directionalLights,n=E.copy(e.direction).normalize(),r=O.lookAt(new t.Vector3,n,t.Object3D.DEFAULT_UP),i=k.multiplyMatrices(k.copy(r).invert(),this.mainCamera.matrixWorld),a=this.margin,o=this.mapSize,s=this.cascadedFrusta,c=e.cascadedLights;for(let e=0;e<s.length;++e){let{near:t,far:l}=A.copy(s[e]).applyMatrix4(i),u=j.makeEmpty();for(let e=0;e<4;e++)u.expandByPoint(t[e]),u.expandByPoint(l[e]);let d=u.getCenter(D);d.z=u.max.z+a;let f=c[e],{left:p,right:m,top:h,bottom:g}=f.shadow.camera,_=(m-p)/o,v=(h-g)/o;d.x=Math.round(d.x/_)*_,d.y=Math.round(d.y/v)*v,d.applyMatrix4(r),f.position.copy(d),f.target.position.copy(d),f.target.position.add(n)}}get cascadeCount(){return this.directionalLights.cascadedLights.length}set cascadeCount(e){e!==this.cascadeCount&&(this.directionalLights.setCount(e),this.needsUpdateFrusta=!0)}get mapSize(){return this.directionalLights.mainLight.shadow.mapSize.width}set mapSize(e){if(e!==this.mapSize){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n){let r=t[n].shadow;r.mapSize.width=e,r.mapSize.height=e,r.map!=null&&(r.map.dispose(),r.map=null)}}}get far(){return this._far}set far(e){e!==this._far&&(this._far=e,this.needsUpdateFrusta=!0)}get mode(){return this._mode}set mode(e){e!==this._mode&&(this._mode=e,this.needsUpdateFrusta=!0)}get lambda(){return this._lambda}set lambda(e){e!==this._lambda&&(this._lambda=e,this.needsUpdateFrusta=!0)}get margin(){return this._margin}set margin(e){e!==this._margin&&(this._margin=e,this.needsUpdateFrusta=!0)}get fade(){return this._fade}set fade(e){e!==this._fade&&(this._fade=e,this.needsUpdateFrusta=!0)}get disableLastCascadeCutoff(){return this._disableLastCascadeCutoff}set disableLastCascadeCutoff(e){e!==this._disableLastCascadeCutoff&&(this._disableLastCascadeCutoff=e,this.needsUpdateFrusta=!0)}get intensity(){return this.directionalLights.mainLight.intensity}set intensity(e){if(e!==this.intensity){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].intensity=e}}get shadowIntensity(){return this.directionalLights.mainLight.shadow.intensity}set shadowIntensity(e){if(e!==this.shadowIntensity){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.intensity=e}}get color(){return this.directionalLights.mainLight.color}set color(e){e!==this.color&&this.directionalLights.mainLight.color.copy(e)}get bias(){return this.directionalLights.mainLight.shadow.bias}set bias(e){if(e!==this.bias){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.bias=e}}get normalBias(){return this.directionalLights.mainLight.shadow.normalBias}set normalBias(e){if(e!==this.normalBias){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.normalBias=e}}get radius(){return this.directionalLights.mainLight.shadow.radius}set radius(e){if(e!==this.radius){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.radius=e}}get blurSamples(){return this.directionalLights.mainLight.shadow.blurSamples}set blurSamples(e){if(e!==this.blurSamples){let t=this.directionalLights.cascadedLights;for(let n=0;n<t.length;++n)t[n].shadow.blurSamples=e}}};e.CSMHelper=class extends t.Group{csm;displayFrustum=!0;displayPlanes=!0;displayShadowBounds=!0;frustumLines;cascadeLines=[];cascadePlanes=[];shadowLines=[];constructor(e){super(),this.csm=e;let n=new Uint16Array([0,1,1,2,2,3,3,0,4,5,5,6,6,7,7,4,0,4,1,5,2,6,3,7]),r=new Float32Array(24),i=new t.BufferGeometry;i.setIndex(new t.BufferAttribute(n,1)),i.setAttribute(`position`,new t.BufferAttribute(r,3,!1));let a=new t.LineSegments(i,new t.LineBasicMaterial);this.add(a),this.frustumLines=a}updateVisibility(){let e=this.displayFrustum,t=this.displayPlanes,n=this.displayShadowBounds,r=this.frustumLines,i=this.cascadeLines,a=this.cascadePlanes,o=this.shadowLines;for(let r=0,s=i.length;r<s;r++){let s=i[r],c=a[r],l=o[r];s.visible=e,c.visible=e&&t,l.visible=n}r.visible=e}update(){let e=this.csm,n=e.mainCamera,r=e.cascadeCount,i=e.mainFrustum,a=e.cascadedFrusta,o=e.directionalLights.cascadedLights,s=this.frustumLines.geometry.getAttribute(`position`),c=this.cascadeLines,l=this.cascadePlanes,u=this.shadowLines;for(this.position.copy(n.position),this.quaternion.copy(n.quaternion),this.scale.copy(n.scale),this.updateMatrixWorld(!0);c.length>r;){let e=c.pop(),t=l.pop(),n=u.pop();e&&this.remove(e),t&&this.remove(t),n&&this.remove(n)}for(;c.length<r;){let e=new t.Box3Helper(new t.Box3,new t.Color(16777215)),n=new t.MeshBasicMaterial({transparent:!0,opacity:.1,depthWrite:!1,side:t.DoubleSide}),r=new t.Mesh(new t.PlaneGeometry,n),i=new t.Group,a=new t.Box3Helper(new t.Box3,new t.Color(16776960));i.add(a),this.add(e),this.add(r),this.add(i),c.push(e),l.push(r),u.push(i)}for(let e=0;e<r;e++){let t=a[e],n=o[e].shadow.camera,r=t.far,i=c[e],s=l[e],d=u[e],f=d.children[0];i.box.min.copy(r[2]),i.box.max.copy(r[0]),i.box.max.z+=1e-4,s.position.addVectors(r[0],r[2]),s.position.multiplyScalar(.5),s.scale.subVectors(r[0],r[2]),s.scale.z=1e-4,this.remove(d),d.position.copy(n.position),d.quaternion.copy(n.quaternion),d.scale.copy(n.scale),d.updateMatrixWorld(!0),this.attach(d),f.box.min.set(n.bottom,n.left,-n.far),f.box.max.set(n.top,n.right,-n.near)}let d=i.near,f=i.far;s.setXYZ(0,f[0].x,f[0].y,f[0].z),s.setXYZ(1,f[3].x,f[3].y,f[3].z),s.setXYZ(2,f[2].x,f[2].y,f[2].z),s.setXYZ(3,f[1].x,f[1].y,f[1].z),s.setXYZ(4,d[0].x,d[0].y,d[0].z),s.setXYZ(5,d[3].x,d[3].y,d[3].z),s.setXYZ(6,d[2].x,d[2].y,d[2].z),s.setXYZ(7,d[1].x,d[1].y,d[1].z),s.needsUpdate=!0}},e.CascadedDirectionalLights=r,e.CascadedShadowMaps=N,e.applyViewSpaceShadowReceive=y,e.cascadedShadowMapsOptionsDefaults=M,e.composeViewSpaceShadowMatrices=b,e.frustumSplitFunctions=w,e.overrideFragmentShaderForCSM=g,e.splitFrustum=T});
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Material,
|
|
13
13
|
type OrthographicCamera,
|
|
14
14
|
type PerspectiveCamera,
|
|
15
|
+
type WebGLRenderer,
|
|
15
16
|
} from "three";
|
|
16
17
|
import invariant from "tiny-invariant";
|
|
17
18
|
|
|
@@ -19,6 +20,10 @@ import { CascadedDirectionalLights } from "./CascadedDirectionalLights";
|
|
|
19
20
|
import { FrustumCorners } from "./FrustumCorners";
|
|
20
21
|
import { MaterialStates } from "./MaterialStates";
|
|
21
22
|
import { splitFrustum, type FrustumSplitMode } from "./splitFrustum";
|
|
23
|
+
import {
|
|
24
|
+
composeViewSpaceShadowMatrices,
|
|
25
|
+
type ShadowMatricesViewUniform,
|
|
26
|
+
} from "./viewSpaceShadowReceive";
|
|
22
27
|
|
|
23
28
|
const vectorScratch1 = /*#__PURE__*/ new Vector3();
|
|
24
29
|
const vectorScratch2 = /*#__PURE__*/ new Vector3();
|
|
@@ -107,10 +112,33 @@ export class CascadedShadowMaps {
|
|
|
107
112
|
private _fade: boolean;
|
|
108
113
|
private _disableLastCascadeCutoff: boolean;
|
|
109
114
|
|
|
115
|
+
private readonly _shadowMatricesView: Matrix4[] = [];
|
|
116
|
+
private _shadowMatricesViewFrame = -1;
|
|
117
|
+
private _renderer: WebGLRenderer | null = null;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Per-cascade `shadow.matrix * mainCamera.matrixWorld`, composed in f64 for
|
|
121
|
+
* precise shadow receiving (see `viewSpaceShadowReceive.ts`). The value is
|
|
122
|
+
* refreshed lazily on access: three.js uploads material uniforms during the
|
|
123
|
+
* main-pass draws, after it has rendered the shadow maps, so reading here
|
|
124
|
+
* always observes the current frame's `shadow.matrix`. Refreshes are
|
|
125
|
+
* deduplicated per `renderer.info.render.frame`.
|
|
126
|
+
*/
|
|
127
|
+
readonly shadowMatricesViewUniform: ShadowMatricesViewUniform;
|
|
128
|
+
|
|
110
129
|
constructor(
|
|
111
130
|
readonly mainCamera: PerspectiveCamera | OrthographicCamera,
|
|
112
131
|
params?: CascadedShadowMapsOptions,
|
|
113
132
|
) {
|
|
133
|
+
const refreshAndGetShadowMatricesView = (): Matrix4[] => {
|
|
134
|
+
this.refreshShadowMatricesView();
|
|
135
|
+
return this._shadowMatricesView;
|
|
136
|
+
};
|
|
137
|
+
this.shadowMatricesViewUniform = {
|
|
138
|
+
get value(): Matrix4[] {
|
|
139
|
+
return refreshAndGetShadowMatricesView();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
114
142
|
const {
|
|
115
143
|
cascadeCount,
|
|
116
144
|
mapSize,
|
|
@@ -140,6 +168,29 @@ export class CascadedShadowMaps {
|
|
|
140
168
|
return this.materialStates.setup(material, this);
|
|
141
169
|
}
|
|
142
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Remember the renderer so lazy refreshes of `shadowMatricesViewUniform`
|
|
173
|
+
* can be deduplicated per render call. Called from the material hook, which
|
|
174
|
+
* always runs before the first uniform upload.
|
|
175
|
+
*/
|
|
176
|
+
captureRenderer(renderer: WebGLRenderer): void {
|
|
177
|
+
this._renderer ??= renderer;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private refreshShadowMatricesView(): void {
|
|
181
|
+
// Until a renderer is known there is no frame counter to dedupe on;
|
|
182
|
+
// recompute on every access (only possible before the first compile).
|
|
183
|
+
const frame = this._renderer?.info.render.frame ?? -1;
|
|
184
|
+
if (frame !== -1 && frame === this._shadowMatricesViewFrame) return;
|
|
185
|
+
this._shadowMatricesViewFrame = frame;
|
|
186
|
+
|
|
187
|
+
composeViewSpaceShadowMatrices(
|
|
188
|
+
this.directionalLights.cascadedLights,
|
|
189
|
+
this.mainCamera.matrixWorld,
|
|
190
|
+
this._shadowMatricesView,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
143
194
|
rollbackMaterial<T extends Material>(material: T): T {
|
|
144
195
|
return this.materialStates.rollback(material);
|
|
145
196
|
}
|
package/src/MaterialStates.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Material, type WebGLProgramParametersWithUniforms } from "three";
|
|
|
6
6
|
|
|
7
7
|
import { type CascadedShadowMaps } from "./CascadedShadowMaps";
|
|
8
8
|
import { createFragmentShader } from "./createFragmentShader";
|
|
9
|
+
import { applyViewSpaceShadowReceive } from "./viewSpaceShadowReceive";
|
|
9
10
|
|
|
10
11
|
type OnBeforeCompile = Material["onBeforeCompile"];
|
|
11
12
|
|
|
@@ -32,8 +33,9 @@ export class MaterialStates {
|
|
|
32
33
|
const stateRef: MaterialState = {};
|
|
33
34
|
this.map.set(material, stateRef);
|
|
34
35
|
|
|
35
|
-
const handleBeforeCompile: OnBeforeCompile = (parameters) => {
|
|
36
|
+
const handleBeforeCompile: OnBeforeCompile = (parameters, renderer) => {
|
|
36
37
|
stateRef.parameters = parameters;
|
|
38
|
+
csm.captureRenderer(renderer);
|
|
37
39
|
|
|
38
40
|
const cascades = csm.cascades;
|
|
39
41
|
const near = csm.mainCamera.near;
|
|
@@ -46,6 +48,11 @@ export class MaterialStates {
|
|
|
46
48
|
parameters.fragmentShader = createFragmentShader(
|
|
47
49
|
parameters.fragmentShader,
|
|
48
50
|
);
|
|
51
|
+
|
|
52
|
+
// Precision-safe shadow receiving: sample directional shadow maps from
|
|
53
|
+
// the view-space position via camera-composed matrices. Skips depth
|
|
54
|
+
// variants (USE_SHADOWMAP_DEPTH) and shaders without shadowmap chunks.
|
|
55
|
+
applyViewSpaceShadowReceive(parameters, csm.shadowMatricesViewUniform);
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
const originalHandler = material.onBeforeCompile.bind(material);
|
|
@@ -126,6 +133,7 @@ export class MaterialStates {
|
|
|
126
133
|
delete parameters.uniforms.csmCascades;
|
|
127
134
|
delete parameters.uniforms.csmNear;
|
|
128
135
|
delete parameters.uniforms.csmFar;
|
|
136
|
+
delete parameters.uniforms.nvrCsmShadowMatrixView;
|
|
129
137
|
}
|
|
130
138
|
if (originalHandler != null) {
|
|
131
139
|
material.onBeforeCompile = originalHandler;
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,10 @@ void cascadedFadedLights() {
|
|
|
55
55
|
|
|
56
56
|
// No loop here: all cascaded lights are in fact one light only.
|
|
57
57
|
directLight.color *= shadow;
|
|
58
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
59
|
+
// Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).
|
|
60
|
+
nvr_shadowMask *= shadow;
|
|
61
|
+
#endif
|
|
58
62
|
RE_Direct(
|
|
59
63
|
directLight,
|
|
60
64
|
geometryPosition,
|
|
@@ -74,17 +78,23 @@ void cascadedFadedLights() {
|
|
|
74
78
|
getDirectionalLightInfo(directionalLight, directLight);
|
|
75
79
|
|
|
76
80
|
directionalLightShadow = directionalLightShadows[i];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
{
|
|
82
|
+
float nvrShadowFactor =
|
|
83
|
+
directLight.visible && receiveShadow
|
|
84
|
+
? getShadow(
|
|
85
|
+
directionalShadowMap[i],
|
|
86
|
+
directionalLightShadow.shadowMapSize,
|
|
87
|
+
directionalLightShadow.shadowIntensity,
|
|
88
|
+
directionalLightShadow.shadowBias,
|
|
89
|
+
directionalLightShadow.shadowRadius,
|
|
90
|
+
vDirectionalShadowCoord[i]
|
|
91
|
+
)
|
|
92
|
+
: 1.0;
|
|
93
|
+
directLight.color *= nvrShadowFactor;
|
|
94
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
95
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
96
|
+
#endif
|
|
97
|
+
}
|
|
88
98
|
|
|
89
99
|
RE_Direct(
|
|
90
100
|
directLight,
|
|
@@ -16,7 +16,7 @@ void cascadedLights() {
|
|
|
16
16
|
cascade = csmCascades[i];
|
|
17
17
|
directionalLightShadow = directionalLightShadows[i];
|
|
18
18
|
if (clipDepth >= cascade.x && clipDepth < cascade.y) {
|
|
19
|
-
|
|
19
|
+
float nvrShadowFactor =
|
|
20
20
|
directLight.visible && receiveShadow
|
|
21
21
|
? getShadow(
|
|
22
22
|
directionalShadowMap[i],
|
|
@@ -27,6 +27,11 @@ void cascadedLights() {
|
|
|
27
27
|
vDirectionalShadowCoord[i]
|
|
28
28
|
)
|
|
29
29
|
: 1.0;
|
|
30
|
+
directLight.color *= nvrShadowFactor;
|
|
31
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
32
|
+
// Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).
|
|
33
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
34
|
+
#endif
|
|
30
35
|
}
|
|
31
36
|
#endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
|
|
32
37
|
#endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
|
|
@@ -53,17 +58,23 @@ void cascadedLights() {
|
|
|
53
58
|
getDirectionalLightInfo(directionalLight, directLight);
|
|
54
59
|
|
|
55
60
|
directionalLightShadow = directionalLightShadows[i];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
{
|
|
62
|
+
float nvrShadowFactor =
|
|
63
|
+
directLight.visible && receiveShadow
|
|
64
|
+
? getShadow(
|
|
65
|
+
directionalShadowMap[i],
|
|
66
|
+
directionalLightShadow.shadowMapSize,
|
|
67
|
+
directionalLightShadow.shadowIntensity,
|
|
68
|
+
directionalLightShadow.shadowBias,
|
|
69
|
+
directionalLightShadow.shadowRadius,
|
|
70
|
+
vDirectionalShadowCoord[i]
|
|
71
|
+
)
|
|
72
|
+
: 1.0;
|
|
73
|
+
directLight.color *= nvrShadowFactor;
|
|
74
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
75
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
76
|
+
#endif
|
|
77
|
+
}
|
|
67
78
|
|
|
68
79
|
RE_Direct(
|
|
69
80
|
directLight,
|
|
@@ -9,17 +9,24 @@ void defaultLights() {
|
|
|
9
9
|
|
|
10
10
|
#if defined(USE_SHADOWMAP) && UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
|
|
11
11
|
directionalLightShadow = directionalLightShadows[i];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
{
|
|
13
|
+
float nvrShadowFactor =
|
|
14
|
+
directLight.visible && receiveShadow
|
|
15
|
+
? getShadow(
|
|
16
|
+
directionalShadowMap[i],
|
|
17
|
+
directionalLightShadow.shadowMapSize,
|
|
18
|
+
directionalLightShadow.shadowIntensity,
|
|
19
|
+
directionalLightShadow.shadowBias,
|
|
20
|
+
directionalLightShadow.shadowRadius,
|
|
21
|
+
vDirectionalShadowCoord[i]
|
|
22
|
+
)
|
|
23
|
+
: 1.0;
|
|
24
|
+
directLight.color *= nvrShadowFactor;
|
|
25
|
+
#if defined(USE_GBUFFER_SHADOW) && !defined(USE_SHADOWMAP_DEPTH)
|
|
26
|
+
// Feed the G-buffer shadow output (declared by gbuffer_pars_fragment).
|
|
27
|
+
nvr_shadowMask *= nvrShadowFactor;
|
|
28
|
+
#endif
|
|
29
|
+
}
|
|
23
30
|
#endif
|
|
24
31
|
|
|
25
32
|
RE_Direct(
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DirectionalLight,
|
|
3
|
+
Euler,
|
|
4
|
+
Matrix4,
|
|
5
|
+
ShaderChunk,
|
|
6
|
+
ShaderLib,
|
|
7
|
+
Vector3,
|
|
8
|
+
Vector4,
|
|
9
|
+
} from "three";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
applyViewSpaceShadowReceive,
|
|
14
|
+
composeViewSpaceShadowMatrices,
|
|
15
|
+
} from "./viewSpaceShadowReceive";
|
|
16
|
+
|
|
17
|
+
function makeLightWithShadowMatrix(matrix: Matrix4): DirectionalLight {
|
|
18
|
+
const light = new DirectionalLight();
|
|
19
|
+
light.shadow.matrix.copy(matrix);
|
|
20
|
+
return light;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A plausible shadow matrix: bias * ortho projection * light view with a
|
|
24
|
+
// large ECEF-scale translation.
|
|
25
|
+
function makeEcefScaleShadowMatrix(): Matrix4 {
|
|
26
|
+
return new Matrix4()
|
|
27
|
+
.makeRotationY(0.7)
|
|
28
|
+
.multiply(new Matrix4().makeScale(1 / 250, 1 / 250, 1 / 5000))
|
|
29
|
+
.multiply(new Matrix4().makeTranslation(-6378137.2, -12345.6, 78901.2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("composeViewSpaceShadowMatrices", () => {
|
|
33
|
+
it("produces matrices equivalent to sampling with the absolute world position", () => {
|
|
34
|
+
const shadowMatrix = makeEcefScaleShadowMatrix();
|
|
35
|
+
// A camera at ECEF magnitude looking somewhere.
|
|
36
|
+
const cameraMatrixWorld = new Matrix4()
|
|
37
|
+
.makeRotationFromEuler(new Euler(0.3, -1.1, 0.05))
|
|
38
|
+
.setPosition(6378137.125, 12345.75, -78901.0625);
|
|
39
|
+
|
|
40
|
+
const matrices: Matrix4[] = [];
|
|
41
|
+
composeViewSpaceShadowMatrices(
|
|
42
|
+
[makeLightWithShadowMatrix(shadowMatrix)],
|
|
43
|
+
cameraMatrixWorld,
|
|
44
|
+
matrices,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// A world point ~50 m from the camera, expressed both ways.
|
|
48
|
+
const worldPosition = new Vector4(
|
|
49
|
+
6378137.125 + 12.5,
|
|
50
|
+
12345.75 - 37.25,
|
|
51
|
+
-78901.0625 + 21.0,
|
|
52
|
+
1,
|
|
53
|
+
);
|
|
54
|
+
const viewPosition = worldPosition
|
|
55
|
+
.clone()
|
|
56
|
+
.applyMatrix4(cameraMatrixWorld.clone().invert());
|
|
57
|
+
|
|
58
|
+
const viaView = viewPosition.clone().applyMatrix4(matrices[0]);
|
|
59
|
+
const viaWorld = worldPosition.clone().applyMatrix4(shadowMatrix);
|
|
60
|
+
expect(viaView.x).toBeCloseTo(viaWorld.x, 6);
|
|
61
|
+
expect(viaView.y).toBeCloseTo(viaWorld.y, 6);
|
|
62
|
+
expect(viaView.z).toBeCloseTo(viaWorld.z, 6);
|
|
63
|
+
expect(viaView.w).toBeCloseTo(viaWorld.w, 6);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reuses existing matrices and trims the array to the light count", () => {
|
|
67
|
+
const matrices: Matrix4[] = [];
|
|
68
|
+
const lights = [
|
|
69
|
+
makeLightWithShadowMatrix(new Matrix4()),
|
|
70
|
+
makeLightWithShadowMatrix(new Matrix4()),
|
|
71
|
+
];
|
|
72
|
+
const cameraMatrixWorld = new Matrix4().setPosition(new Vector3(1, 2, 3));
|
|
73
|
+
|
|
74
|
+
composeViewSpaceShadowMatrices(lights, cameraMatrixWorld, matrices);
|
|
75
|
+
expect(matrices).toHaveLength(2);
|
|
76
|
+
const first = matrices[0];
|
|
77
|
+
|
|
78
|
+
composeViewSpaceShadowMatrices(
|
|
79
|
+
lights.slice(0, 1),
|
|
80
|
+
cameraMatrixWorld,
|
|
81
|
+
matrices,
|
|
82
|
+
);
|
|
83
|
+
expect(matrices).toHaveLength(1);
|
|
84
|
+
// Same instance mutated in place, no per-frame allocation.
|
|
85
|
+
expect(matrices[0]).toBe(first);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("applyViewSpaceShadowReceive", () => {
|
|
90
|
+
const uniform = { value: [] as Matrix4[] };
|
|
91
|
+
|
|
92
|
+
const litVertexShader = [
|
|
93
|
+
"#include <common>",
|
|
94
|
+
"#include <shadowmap_pars_vertex>",
|
|
95
|
+
"void main() {",
|
|
96
|
+
"#include <shadowmap_vertex>",
|
|
97
|
+
"}",
|
|
98
|
+
].join("\n");
|
|
99
|
+
|
|
100
|
+
it("rewrites the directional shadow loop against the view-space matrices", () => {
|
|
101
|
+
const shader = {
|
|
102
|
+
vertexShader: litVertexShader,
|
|
103
|
+
uniforms: {} as Record<string, { value: unknown }>,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
applyViewSpaceShadowReceive(shader, uniform);
|
|
107
|
+
|
|
108
|
+
expect(shader.uniforms.nvrCsmShadowMatrixView).toBe(uniform);
|
|
109
|
+
// Sized to the cascade count: additional shadow-casting directional
|
|
110
|
+
// lights beyond the cascades are not part of the composed array.
|
|
111
|
+
expect(shader.vertexShader).toContain(
|
|
112
|
+
"uniform mat4 nvrCsmShadowMatrixView[ CSM_CASCADE_COUNT ];",
|
|
113
|
+
);
|
|
114
|
+
expect(shader.vertexShader).toContain(
|
|
115
|
+
"vDirectionalShadowCoord[ i ] = nvrCsmShadowMatrixView[ i ] * shadowWorldPosition;",
|
|
116
|
+
);
|
|
117
|
+
expect(shader.vertexShader).toContain(
|
|
118
|
+
"shadowWorldPosition = vec4( mvPosition.xyz + ( viewMatrix * vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0.0 ) ).xyz, 1.0 );",
|
|
119
|
+
);
|
|
120
|
+
// Guards against upstream chunk drift: the patched chunk must still be
|
|
121
|
+
// derived from the current three.js source.
|
|
122
|
+
expect(ShaderChunk.shadowmap_vertex).toContain(
|
|
123
|
+
"vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("keeps the stock path for extra directional lights and point shadows", () => {
|
|
128
|
+
const shader = {
|
|
129
|
+
vertexShader: litVertexShader,
|
|
130
|
+
uniforms: {} as Record<string, { value: unknown }>,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
applyViewSpaceShadowReceive(shader, uniform);
|
|
134
|
+
|
|
135
|
+
// Cascade indices branch on the view-space path; any shadow-casting
|
|
136
|
+
// directional light beyond CSM_CASCADE_COUNT falls back to the stock
|
|
137
|
+
// matrices managed by three.js (the composed array doesn't cover them).
|
|
138
|
+
expect(shader.vertexShader).toContain(
|
|
139
|
+
"#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT",
|
|
140
|
+
);
|
|
141
|
+
expect(shader.vertexShader).toContain(
|
|
142
|
+
"vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;",
|
|
143
|
+
);
|
|
144
|
+
expect(shader.vertexShader).toContain(
|
|
145
|
+
"shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );",
|
|
146
|
+
);
|
|
147
|
+
// Point shadows keep the stock loop untouched.
|
|
148
|
+
expect(shader.vertexShader).toContain(
|
|
149
|
+
"vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;",
|
|
150
|
+
);
|
|
151
|
+
// Standalone use (no CSM defines) treats every directional shadow as
|
|
152
|
+
// camera-composed via the fallback define.
|
|
153
|
+
expect(shader.vertexShader).toContain(
|
|
154
|
+
"#define CSM_CASCADE_COUNT NUM_DIR_LIGHT_SHADOWS",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("skips shadow-map depth pass variants", () => {
|
|
159
|
+
const shader = {
|
|
160
|
+
vertexShader: litVertexShader,
|
|
161
|
+
defines: { USE_SHADOWMAP_DEPTH: 1 } as Record<string, unknown>,
|
|
162
|
+
uniforms: {} as Record<string, { value: unknown }>,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
applyViewSpaceShadowReceive(shader, uniform);
|
|
166
|
+
|
|
167
|
+
expect(shader.vertexShader).toBe(litVertexShader);
|
|
168
|
+
expect(shader.uniforms.nvrCsmShadowMatrixView).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("leaves shaders without shadowmap_vertex untouched", () => {
|
|
172
|
+
const source = "#include <common>\nvoid main() {}";
|
|
173
|
+
const shader = {
|
|
174
|
+
vertexShader: source,
|
|
175
|
+
uniforms: {} as Record<string, { value: unknown }>,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
applyViewSpaceShadowReceive(shader, uniform);
|
|
179
|
+
|
|
180
|
+
expect(shader.vertexShader).toBe(source);
|
|
181
|
+
expect(shader.uniforms.nvrCsmShadowMatrixView).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// The patch is applied centrally to every material registered for CSM
|
|
185
|
+
// (MaterialStates.setup), so it must hold for the real vertex shader of
|
|
186
|
+
// every three.js built-in material — the same source onBeforeCompile
|
|
187
|
+
// receives. These fixtures pin that compatibility so a three.js upgrade
|
|
188
|
+
// that changes the ShaderLib surfaces here instead of at runtime.
|
|
189
|
+
|
|
190
|
+
// Materials that receive shadows (their shader has the shadowmap chunks).
|
|
191
|
+
const SHADOW_RECEIVING_SHADERS = [
|
|
192
|
+
"lambert", // MeshLambertMaterial
|
|
193
|
+
"phong", // MeshPhongMaterial
|
|
194
|
+
"standard", // MeshStandardMaterial
|
|
195
|
+
"physical", // MeshPhysicalMaterial
|
|
196
|
+
"toon", // MeshToonMaterial
|
|
197
|
+
"shadow", // ShadowMaterial
|
|
198
|
+
] as const;
|
|
199
|
+
|
|
200
|
+
// Everything else must pass through untouched (unlit, depth/distance,
|
|
201
|
+
// points/lines/sprites, background/env shaders).
|
|
202
|
+
const NON_RECEIVING_SHADERS = Object.keys(ShaderLib).filter(
|
|
203
|
+
(name) => !(SHADOW_RECEIVING_SHADERS as readonly string[]).includes(name),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
it.each(SHADOW_RECEIVING_SHADERS)(
|
|
207
|
+
"patches the built-in '%s' material shader with mvPosition in scope",
|
|
208
|
+
(name) => {
|
|
209
|
+
const shader = {
|
|
210
|
+
vertexShader: ShaderLib[name].vertexShader,
|
|
211
|
+
uniforms: {} as Record<string, { value: unknown }>,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
applyViewSpaceShadowReceive(shader, uniform);
|
|
215
|
+
|
|
216
|
+
expect(shader.uniforms.nvrCsmShadowMatrixView).toBe(uniform);
|
|
217
|
+
const samplingIndex = shader.vertexShader.indexOf(
|
|
218
|
+
"vDirectionalShadowCoord[ i ] = nvrCsmShadowMatrixView[ i ]",
|
|
219
|
+
);
|
|
220
|
+
expect(samplingIndex).toBeGreaterThan(-1);
|
|
221
|
+
// mvPosition must already be declared where the patched code reads it.
|
|
222
|
+
const projectIndex = shader.vertexShader.indexOf(
|
|
223
|
+
"#include <project_vertex>",
|
|
224
|
+
);
|
|
225
|
+
expect(projectIndex).toBeGreaterThan(-1);
|
|
226
|
+
expect(projectIndex).toBeLessThan(samplingIndex);
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
it.each(NON_RECEIVING_SHADERS)(
|
|
231
|
+
"leaves the built-in '%s' material shader untouched",
|
|
232
|
+
(name) => {
|
|
233
|
+
const source = ShaderLib[name as keyof typeof ShaderLib].vertexShader;
|
|
234
|
+
const shader = {
|
|
235
|
+
vertexShader: source,
|
|
236
|
+
uniforms: {} as Record<string, { value: unknown }>,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
applyViewSpaceShadowReceive(shader, uniform);
|
|
240
|
+
|
|
241
|
+
expect(shader.vertexShader).toBe(source);
|
|
242
|
+
expect(shader.uniforms.nvrCsmShadowMatrixView).toBeUndefined();
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Matrix4, ShaderChunk, type DirectionalLight } from "three";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Uniform holding per-cascade directional shadow matrices composed against
|
|
5
|
+
* the render camera's world matrix on the CPU: `light.shadow.matrix *
|
|
6
|
+
* camera.matrixWorld` (f64).
|
|
7
|
+
*
|
|
8
|
+
* Sampling shadow maps with `directionalShadowMatrix * worldPosition` loses
|
|
9
|
+
* precision on a globe: the absolute ECEF world position (~6.4e6 m) and the
|
|
10
|
+
* shadow matrix translation cancel in f32 on the GPU, and because the CSM
|
|
11
|
+
* cascade matrices follow the camera the rounding error changes every frame —
|
|
12
|
+
* received shadows jitter while the geometry itself stays put.
|
|
13
|
+
*
|
|
14
|
+
* Folding the camera's world matrix in instead lets the shader use the
|
|
15
|
+
* view-space position `mvPosition`, which is precise by construction for
|
|
16
|
+
* every positioning mode: RTE meshes compute it from camera-relative
|
|
17
|
+
* coordinates, RTC meshes get it from the CPU-composed f64 modelViewMatrix,
|
|
18
|
+
* and instancing folds instanceMatrix into it. Shadow sampling then reuses
|
|
19
|
+
* the exact quantity that renders stably, so it can never jitter relative to
|
|
20
|
+
* the geometry — and a single patch covers every mesh type.
|
|
21
|
+
*/
|
|
22
|
+
export type ShadowMatricesViewUniform = { value: Matrix4[] };
|
|
23
|
+
|
|
24
|
+
const SHADOW_MATRIX_VIEW_PARS_VERTEX = /* glsl */ `
|
|
25
|
+
#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0
|
|
26
|
+
// Per-cascade shadow matrices composed with the render camera's world matrix
|
|
27
|
+
// on the CPU in f64 (shadow.matrix * camera.matrixWorld). Multiplied with the
|
|
28
|
+
// view-space position instead of the absolute world position to avoid f32
|
|
29
|
+
// cancellation at ECEF magnitudes.
|
|
30
|
+
//
|
|
31
|
+
// Only the CSM cascade lights (the first CSM_CASCADE_COUNT shadow indices,
|
|
32
|
+
// matching the ordering assumption of the CSM fragment shader) get these
|
|
33
|
+
// matrices; additional shadow-casting directional lights keep the stock
|
|
34
|
+
// directionalShadowMatrix path. Outside a CSM material the define is absent —
|
|
35
|
+
// treat every directional shadow as camera-composed then.
|
|
36
|
+
#ifndef CSM_CASCADE_COUNT
|
|
37
|
+
#define CSM_CASCADE_COUNT NUM_DIR_LIGHT_SHADOWS
|
|
38
|
+
#endif
|
|
39
|
+
uniform mat4 nvrCsmShadowMatrixView[ CSM_CASCADE_COUNT ];
|
|
40
|
+
#endif
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/** Replace `search` in `source`, throwing when the pattern is missing so a
|
|
44
|
+
* three.js chunk drift surfaces as an error instead of a silent no-op. */
|
|
45
|
+
function replaceOrThrow(
|
|
46
|
+
source: string,
|
|
47
|
+
search: string,
|
|
48
|
+
replace: string,
|
|
49
|
+
): string {
|
|
50
|
+
if (!source.includes(search)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Failed to replace "${search}" in shader code. The pattern was not found.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return source.replace(search, replace);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rewrite a material's vertex shader so directional (CSM) shadow coordinates
|
|
60
|
+
* are computed from the view-space position and the camera-composed shadow
|
|
61
|
+
* matrices instead of `directionalShadowMatrix * worldPosition`.
|
|
62
|
+
*
|
|
63
|
+
* Requires `mvPosition` to be in scope at `#include <shadowmap_vertex>`,
|
|
64
|
+
* which holds for every three.js built-in lit shader and for RTE projection
|
|
65
|
+
* chunks. The world-space normal bias is rotated into view space with
|
|
66
|
+
* `viewMatrix`, which is exactly equivalent to the stock world-space offset.
|
|
67
|
+
*
|
|
68
|
+
* Point/spot shadows — and shadow-casting directional lights beyond the CSM
|
|
69
|
+
* cascades (`CSM_CASCADE_COUNT`) — keep the stock absolute-world-position
|
|
70
|
+
* path, so extra lights coexist with the cascades exactly as before. Shaders
|
|
71
|
+
* without `#include <shadowmap_vertex>` (unlit, depth, or fully custom) are
|
|
72
|
+
* left untouched, as are shaders compiled for the shadow-map depth pass
|
|
73
|
+
* (`USE_SHADOWMAP_DEPTH` — they never sample shadows and would upload the
|
|
74
|
+
* uniform before the shadow maps for the frame are rendered).
|
|
75
|
+
*/
|
|
76
|
+
export function applyViewSpaceShadowReceive(
|
|
77
|
+
shader: {
|
|
78
|
+
vertexShader: string;
|
|
79
|
+
defines?: Record<string, unknown>;
|
|
80
|
+
uniforms: Record<string, { value: unknown }>;
|
|
81
|
+
},
|
|
82
|
+
uniform: ShadowMatricesViewUniform,
|
|
83
|
+
): void {
|
|
84
|
+
if (shader.defines?.["USE_SHADOWMAP_DEPTH"]) return;
|
|
85
|
+
if (!shader.vertexShader.includes("#include <shadowmap_vertex>")) return;
|
|
86
|
+
|
|
87
|
+
shader.uniforms.nvrCsmShadowMatrixView = uniform;
|
|
88
|
+
|
|
89
|
+
// Rewrite only the directional-light loop of three's shadowmap_vertex
|
|
90
|
+
// chunk, preserving the unroll pragmas. Each line becomes a per-index
|
|
91
|
+
// preprocessor branch (the unroller substitutes UNROLLED_LOOP_INDEX with a
|
|
92
|
+
// literal, the same pattern the CSM fragment shader uses): cascade indices
|
|
93
|
+
// take the view-space path, any additional shadow-casting directional
|
|
94
|
+
// lights keep the stock path — their matrices are managed by three, not by
|
|
95
|
+
// the CSM compose, so routing them through nvrCsmShadowMatrixView would
|
|
96
|
+
// read past the uploaded array.
|
|
97
|
+
let shadowmapVertexView = replaceOrThrow(
|
|
98
|
+
ShaderChunk.shadowmap_vertex,
|
|
99
|
+
"shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );",
|
|
100
|
+
`#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
|
|
101
|
+
shadowWorldPosition = vec4( mvPosition.xyz + ( viewMatrix * vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0.0 ) ).xyz, 1.0 );
|
|
102
|
+
#else
|
|
103
|
+
shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );
|
|
104
|
+
#endif`,
|
|
105
|
+
);
|
|
106
|
+
shadowmapVertexView = replaceOrThrow(
|
|
107
|
+
shadowmapVertexView,
|
|
108
|
+
"vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;",
|
|
109
|
+
`#if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
|
|
110
|
+
vDirectionalShadowCoord[ i ] = nvrCsmShadowMatrixView[ i ] * shadowWorldPosition;
|
|
111
|
+
#else
|
|
112
|
+
vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;
|
|
113
|
+
#endif`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
shader.vertexShader = replaceOrThrow(
|
|
117
|
+
replaceOrThrow(
|
|
118
|
+
shader.vertexShader,
|
|
119
|
+
"#include <shadowmap_pars_vertex>",
|
|
120
|
+
`#include <shadowmap_pars_vertex>\n${SHADOW_MATRIX_VIEW_PARS_VERTEX}`,
|
|
121
|
+
),
|
|
122
|
+
"#include <shadowmap_vertex>",
|
|
123
|
+
shadowmapVertexView,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compose `light.shadow.matrix * cameraMatrixWorld` for each cascade light
|
|
129
|
+
* into `matrices`, entirely in f64. The camera must be the render camera of
|
|
130
|
+
* the pass (its `viewMatrix` produced the `mvPosition` the shader multiplies
|
|
131
|
+
* with), and `shadow.matrix` must already hold the current frame's cascade
|
|
132
|
+
* transform — three.js renders the shadow maps before any main-pass material
|
|
133
|
+
* uploads its uniforms, so refreshing lazily at upload time is safe.
|
|
134
|
+
*/
|
|
135
|
+
export function composeViewSpaceShadowMatrices(
|
|
136
|
+
lights: readonly DirectionalLight[],
|
|
137
|
+
cameraMatrixWorld: Matrix4,
|
|
138
|
+
matrices: Matrix4[],
|
|
139
|
+
): void {
|
|
140
|
+
matrices.length = lights.length;
|
|
141
|
+
for (let i = 0; i < lights.length; i++) {
|
|
142
|
+
const matrix = matrices[i] ?? (matrices[i] = new Matrix4());
|
|
143
|
+
matrix.multiplyMatrices(lights[i].shadow.matrix, cameraMatrixWorld);
|
|
144
|
+
}
|
|
145
|
+
}
|