@navaramap/three-csm 0.0.5 → 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 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 directLight.color *=\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 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 directLight.color *=\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 #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 directLight.color *=\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 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 directLight.color *=\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 #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";
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 N = class {
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 r = t.cascades, i = t.mainCamera.near, a = Math.min(t.mainCamera.far, t.far), o = e.uniforms;
151
- o.csmCascades = { value: r }, o.csmNear = { value: i }, o.csmFar = { value: a }, e.fragmentShader = M(e.fragmentShader);
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 P(e, t, n) {
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 F = {
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] = P((t + (n - t) * (a + 1) / e) / n, t * (n / t) ** ((a + 1) / e) / n, r);
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
- }, I = F;
206
- function L(e, t, n, r, i, a = []) {
207
- return F[e](t, n, r, i, a);
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 R = /*#__PURE__*/ new _(), z = /*#__PURE__*/ new _(), B = /*#__PURE__*/ new u(), V = /*#__PURE__*/ new u(), H = /*#__PURE__*/ new S(), U = /*#__PURE__*/ new e(), W = {
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
- }, G = class {
237
+ }, Y = class {
221
238
  mainCamera;
222
239
  needsUpdateFrusta = !0;
223
240
  directionalLights = new y();
224
- materialStates = new N();
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 { cascadeCount: n, mapSize: r, far: i, mode: a, lambda: o, margin: s, fade: c, disableLastCascadeCutoff: l } = {
238
- ...W,
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 = i, this._mode = a, this._lambda = o, this._margin = s, this._fade = c, this._disableLastCascadeCutoff = l, this.cascadeCount = n, this.mapSize = r;
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
- L(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);
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 = R.copy(e.direction).normalize(), n = B.lookAt(new _(), t, p.DEFAULT_UP), r = V.multiplyMatrices(V.copy(n).invert(), this.mainCamera.matrixWorld), i = this.margin, a = this.mapSize, o = this.cascadedFrusta, s = e.cascadedLights;
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 } = H.copy(o[e]).applyMatrix4(r), u = U.makeEmpty();
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(z);
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
- }, K = class extends s {
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 { K as CSMHelper, y as CascadedDirectionalLights, G as CascadedShadowMaps, W as cascadedShadowMapsOptionsDefaults, I as frustumSplitFunctions, M as overrideFragmentShaderForCSM, L as splitFrustum };
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 };
@@ -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
- directLight.color *=
78
- directLight.visible && receiveShadow
79
- ? getShadow(
80
- directionalShadowMap[i],
81
- directionalLightShadow.shadowMapSize,
82
- directionalLightShadow.shadowIntensity,
83
- directionalLightShadow.shadowBias,
84
- directionalLightShadow.shadowRadius,
85
- vDirectionalShadowCoord[i]
86
- )
87
- : 1.0;
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
- directLight.color *=
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
- directLight.color *=
159
- directLight.visible && receiveShadow
160
- ? getShadow(
161
- directionalShadowMap[i],
162
- directionalLightShadow.shadowMapSize,
163
- directionalLightShadow.shadowIntensity,
164
- directionalLightShadow.shadowBias,
165
- directionalLightShadow.shadowRadius,
166
- vDirectionalShadowCoord[i]
167
- )
168
- : 1.0;
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
- directLight.color *=
231
- directLight.visible && receiveShadow
232
- ? getShadow(
233
- directionalShadowMap[i],
234
- directionalLightShadow.shadowMapSize,
235
- directionalLightShadow.shadowIntensity,
236
- directionalLightShadow.shadowBias,
237
- directionalLightShadow.shadowRadius,
238
- vDirectionalShadowCoord[i]
239
- )
240
- : 1.0;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@navaramap/three-csm",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "main": "dist/index.umd.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -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
  }
@@ -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
@@ -3,3 +3,4 @@ export * from "./CascadedShadowMaps";
3
3
  export * from "./CSMHelper";
4
4
  export * from "./splitFrustum";
5
5
  export { createFragmentShader as overrideFragmentShaderForCSM } from "./createFragmentShader";
6
+ export * from "./viewSpaceShadowReceive";
@@ -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
- directLight.color *=
78
- directLight.visible && receiveShadow
79
- ? getShadow(
80
- directionalShadowMap[i],
81
- directionalLightShadow.shadowMapSize,
82
- directionalLightShadow.shadowIntensity,
83
- directionalLightShadow.shadowBias,
84
- directionalLightShadow.shadowRadius,
85
- vDirectionalShadowCoord[i]
86
- )
87
- : 1.0;
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
- directLight.color *=
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
- directLight.color *=
57
- directLight.visible && receiveShadow
58
- ? getShadow(
59
- directionalShadowMap[i],
60
- directionalLightShadow.shadowMapSize,
61
- directionalLightShadow.shadowIntensity,
62
- directionalLightShadow.shadowBias,
63
- directionalLightShadow.shadowRadius,
64
- vDirectionalShadowCoord[i]
65
- )
66
- : 1.0;
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
- directLight.color *=
13
- directLight.visible && receiveShadow
14
- ? getShadow(
15
- directionalShadowMap[i],
16
- directionalLightShadow.shadowMapSize,
17
- directionalLightShadow.shadowIntensity,
18
- directionalLightShadow.shadowBias,
19
- directionalLightShadow.shadowRadius,
20
- vDirectionalShadowCoord[i]
21
- )
22
- : 1.0;
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
+ }