diff --git a/.prettierignore b/.prettierignore
index 421d2333b..babb9a44b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,3 @@
*.mjs
build/
-csm/
vendor/
diff --git a/src/viser/client/.eslintrc.js b/src/viser/client/.eslintrc.js
index 96d596efe..082279d6a 100644
--- a/src/viser/client/.eslintrc.js
+++ b/src/viser/client/.eslintrc.js
@@ -22,7 +22,7 @@ module.exports = {
sourceType: "module",
},
plugins: ["react", "react-hooks", "@typescript-eslint", "react-refresh"],
- ignorePatterns: ["build/", ".eslintrc.js", "src/csm", "src/vendor"],
+ ignorePatterns: ["build/", ".eslintrc.js", "src/vendor"],
rules: {
// https://github.com/jsx-eslint/eslint-plugin-react/issues/3423
"react/no-unknown-property": "off",
diff --git a/src/viser/client/src/App.tsx b/src/viser/client/src/App.tsx
index 464a09518..b34e9ec05 100644
--- a/src/viser/client/src/App.tsx
+++ b/src/viser/client/src/App.tsx
@@ -59,7 +59,7 @@ import { PlaybackFromFile, PlaybackFromEmbedData } from "./FilePlayback";
import { SplatRenderContext } from "./Splatting/GaussianSplats";
import { BrowserWarning } from "./BrowserWarning";
import { MacWindowWrapper } from "./MacWindowWrapper";
-import { CsmDirectionalLight } from "./CsmDirectionalLight";
+import { CascadedDirectionalLight } from "./CascadedDirectionalLight";
import { VISER_VERSION, GITHUB_CONTRIBUTORS, Contributor } from "./VersionInfo";
import { BatchedLabelManager } from "./BatchedLabelManager";
@@ -941,13 +941,13 @@ function DefaultLights() {
// Return lights and environment map.
return (
<>
-
- {
+ position?: Vector3Tuple; // Position of the light
+ color?: number;
+ castShadow?: boolean;
+ debug?: boolean; // Show cascade visualization
+}
+
+export function CascadedDirectionalLight({
+ maxFar = 20,
+ shadowMapSize = 1024,
+ lightIntensity = 0.25,
+ cascades = 3,
+ position = [0, 0, 0],
+ shadowBias = -0.00001,
+ lightFar = 2000,
+ lightMargin = 200,
+ lightNear = 0.0001,
+ mode = "practical",
+ color = 0xffffff,
+ castShadow = true,
+ debug = false,
+}: CascadedDirectionalLightProps) {
+ // Standard directional light for the non-shadow case.
+ if (!castShadow) {
+ return (
+
+ );
+ }
+
+ // Shadow-casting implementation with approximate cascaded shadows; see
+ // the note in shadows/ShadowCascades.js for what this is and isn't.
+ return (
+
+ );
+}
+
+// Separate component for the shadow-casting implementation to avoid hook conditionals.
+function CascadedShadowLight({
+ maxFar,
+ shadowMapSize,
+ lightIntensity,
+ cascades,
+ position = [0, -1, 0],
+ shadowBias,
+ lightFar,
+ lightMargin,
+ lightNear,
+ mode,
+ color,
+ debug = false,
+}: Omit) {
+ const camera = useThree((three) => three.camera);
+ const gl = useThree((three) => three.gl);
+ const reversedDepth = gl.capabilities.reversedDepthBuffer;
+
+ // Get the scene object from the three fiber context.
+ // This is a hack, see: https://github.com/pmndrs/react-three-fiber/issues/2725
+ const { scene: scene_ } = useThree();
+ const scene = useMemo(() => {
+ let object: THREE.Object3D | null = scene_;
+ while (object) {
+ if (object instanceof THREE.Scene) return object;
+ object = object.parent;
+ }
+ throw new Error("Could not find scene object in r3f context!");
+ }, [scene_]);
+
+ const shadowCascadesRef = useRef(null);
+ const dummyGroupRef = useRef(null);
+ const helperRef = useRef(null);
+
+ // Pre-create reusable instances to avoid creating new ones in useFrame.
+ const worldPosition = useMemo(() => new Vector3(), []);
+ const origin = useMemo(() => new Vector3(0, 0, 0), []);
+ const direction = useMemo(() => new Vector3(), []);
+ const prevProjection = useMemo(() => new Matrix4(), []);
+
+ const threeColor = useMemo(() => new Color(color), [color]);
+ // Track the latest color in a ref so the CSM creation effect can apply it
+ // without taking threeColor as a dependency (a color change alone shouldn't
+ // recreate the shadow maps).
+ const colorRef = useRef(threeColor);
+ colorRef.current = threeColor;
+
+ // Depend on the position components rather than the array, which is
+ // typically a new identity on every render.
+ const [positionX, positionY, positionZ] = position;
+
+ // Create the ShadowCascades instance.
+ useEffect(() => {
+ const lightDirection = new Vector3(-positionX, -positionY, -positionZ);
+ // A light at the world origin has no defined "toward origin" direction;
+ // fall back to pointing straight down.
+ if (lightDirection.lengthSq() === 0.0) lightDirection.y = -1;
+ lightDirection.normalize();
+
+ const shadowCascades = new ShadowCascades({
+ camera,
+ cascades,
+ lightDirection,
+ lightFar,
+ lightIntensity,
+ lightMargin,
+ lightNear,
+ maxFar,
+ mode,
+ parent: scene,
+ shadowBias,
+ shadowMapSize,
+ reversedDepth,
+ });
+ shadowCascades.lights.forEach((light) => {
+ light.color = colorRef.current;
+ });
+ prevProjection.copy(camera.projectionMatrix);
+ shadowCascadesRef.current = shadowCascades;
+
+ // Create debug helper if debug mode is enabled.
+ if (debug) {
+ const helper = new CascadeHelper(shadowCascades);
+ helper.displayFrustum = true;
+ helper.displayPlanes = true;
+ helper.displayShadowBounds = true;
+ helper.updateVisibility();
+ scene.add(helper);
+ helperRef.current = helper;
+ }
+
+ return () => {
+ if (helperRef.current) {
+ scene.remove(helperRef.current);
+ helperRef.current.dispose();
+ helperRef.current = null;
+ }
+ shadowCascades.remove();
+ shadowCascades.dispose();
+ shadowCascadesRef.current = null;
+ };
+ }, [
+ camera,
+ scene,
+ cascades,
+ positionX,
+ positionY,
+ positionZ,
+ lightFar,
+ lightIntensity,
+ lightMargin,
+ lightNear,
+ maxFar,
+ mode,
+ shadowBias,
+ shadowMapSize,
+ reversedDepth,
+ debug,
+ prevProjection,
+ ]);
+
+ // Update light color when the color changes, without recreating the
+ // ShadowCascades instance. Runs after the creation effect above, so the
+ // instance exists.
+ useEffect(() => {
+ if (shadowCascadesRef.current) {
+ shadowCascadesRef.current.lights.forEach((light) => {
+ light.color = threeColor;
+ });
+ }
+ }, [threeColor]);
+
+ // Update the cascades on each frame and handle light direction changes.
+ useFrame(() => {
+ const shadowCascades = shadowCascadesRef.current;
+ if (shadowCascades === null || dummyGroupRef.current === null) return;
+
+ // Get the world position of the dummy group; the light points from there
+ // toward the origin.
+ dummyGroupRef.current.getWorldPosition(worldPosition);
+ direction.subVectors(origin, worldPosition);
+ if (direction.lengthSq() === 0.0) direction.y = -1;
+ direction.normalize();
+ shadowCascades.lightDirection.copy(direction);
+
+ // Cascade splits and shadow bounds are derived from the camera's
+ // projection; refresh them when it changes (window resize, fov/near/far
+ // updates).
+ if (!prevProjection.equals(camera.projectionMatrix)) {
+ prevProjection.copy(camera.projectionMatrix);
+ shadowCascades.updateFrustums();
+ }
+
+ shadowCascades.update();
+
+ // Update helper visualization if it exists.
+ if (helperRef.current) {
+ helperRef.current.update();
+ }
+ });
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/viser/client/src/CsmDirectionalLight.tsx b/src/viser/client/src/CsmDirectionalLight.tsx
deleted file mode 100644
index 7c12cbb67..000000000
--- a/src/viser/client/src/CsmDirectionalLight.tsx
+++ /dev/null
@@ -1,325 +0,0 @@
-import { useFrame, useThree } from "@react-three/fiber";
-import { useEffect, useMemo, useRef } from "react";
-import * as THREE from "three";
-import {
- Color,
- Material,
- Mesh,
- Object3D,
- ShaderChunk,
- Vector3,
- Vector3Tuple,
-} from "three";
-import { CSM, CSMParameters } from "./csm/CSM";
-// @ts-ignore
-import { CSMHelper } from "./csm/CSMHelper";
-
-interface CsmDirectionalLightProps extends Omit<
- CSMParameters,
- "lightDirection" | "camera" | "parent"
-> {
- fade?: boolean;
- position?: Vector3Tuple; // Position of the light
- color?: number;
- castShadow?: boolean;
- debug?: boolean; // Show CSM cascade visualization
-}
-
-// Store original shader chunks to restore them later.
-let originalLightsFragmentBegin = "";
-let originalLightsParsBegin = "";
-let activeCSMInstances = 0;
-
-// This is loosely adapted from @itsdouges in https://github.com/StrandedKitty/three-csm/issues/22.
-class CSMProxy {
- instance: CSM | undefined;
- args: CSMParameters;
-
- constructor(args: CSMParameters) {
- this.args = args;
-
- // Save original shader chunks on first creation if they haven't been saved yet.
- if (activeCSMInstances === 0) {
- originalLightsFragmentBegin = ShaderChunk.lights_fragment_begin;
- originalLightsParsBegin = ShaderChunk.lights_pars_begin;
- }
- }
-
- attach() {
- if (!this.instance) {
- this.instance = new CSM(this.args);
- activeCSMInstances++;
- }
- }
-
- dispose() {
- if (this.instance) {
- // Make sure to call remove() to clean up all lights from the scene.
- this.instance.remove();
- // CSM.dispose() only strips its shader injections; it never frees the
- // cascade lights' shadow-map render targets. Without this, every
- // shadow toggle (which remounts CsmDirectionalLight via its key)
- // leaks one WebGLRenderTarget per cascade.
- for (const light of this.instance.lights) {
- light.dispose();
- }
- this.instance.dispose();
- this.instance = undefined;
-
- // Decrement the active instances counter.
- activeCSMInstances--;
-
- // Only restore original shader chunks when the last instance is disposed.
- if (activeCSMInstances === 0) {
- ShaderChunk.lights_fragment_begin = originalLightsFragmentBegin;
- ShaderChunk.lights_pars_begin = originalLightsParsBegin;
- }
- }
- }
-}
-
-// Utility function to update materials.
-function updateMaterialsInScene(scene: Object3D): void {
- scene.traverse((object: Object3D) => {
- const mesh = object as Mesh;
- if (mesh.isMesh && mesh.material) {
- if (Array.isArray(mesh.material)) {
- mesh.material.forEach((mat: Material) => {
- mat.needsUpdate = true;
- });
- } else {
- mesh.material.needsUpdate = true;
- }
- }
- });
-}
-
-// Modified approach that uses conditional rendering with proper mount/unmount.
-export function CsmDirectionalLight({
- maxFar = 20,
- shadowMapSize = 1024,
- lightIntensity = 0.25,
- cascades = 3,
- fade = true,
- position = [0, 0, 0],
- shadowBias = -0.00001,
- lightFar = 2000,
- lightMargin = 200,
- lightNear = 0.0001,
- mode = "practical",
- color = 0xffffff,
- castShadow = true,
- debug = false,
-}: CsmDirectionalLightProps) {
- // Standard directional light for the non-shadow case.
- if (!castShadow) {
- return (
-
- );
- }
-
- // Shadow-casting implementation with CSM.
- return (
-
- );
-}
-
-// Separate component for the shadow-casting implementation to avoid hook conditionals.
-function ShadowCsmLight({
- maxFar,
- shadowMapSize,
- lightIntensity,
- cascades,
- fade,
- position = [0, -1, 0],
- shadowBias,
- lightFar,
- lightMargin,
- lightNear,
- mode,
- color,
- debug = false,
-}: Omit) {
- const camera = useThree((three) => three.camera);
- const gl = useThree((three) => three.gl);
- const reversedDepth = gl.capabilities.reversedDepthBuffer;
-
- // Get the scene object from the three fiber context.
- // This is a hack, see: https://github.com/pmndrs/react-three-fiber/issues/2725
- const { scene: scene_ } = useThree();
- const scene = useMemo(() => {
- let object: THREE.Object3D | null = scene_;
- while (object) {
- if (object instanceof THREE.Scene) return object;
- object = object.parent;
- }
- throw new Error("Could not find scene object in r3f context!");
- }, [scene_]);
-
- // Calculate light direction from position (pointing toward origin)
- const lightDirection = useMemo(() => {
- return new Vector3(-position[0], -position[1], -position[2]).normalize();
- }, [position]);
-
- const dummyGroupRef = useRef(null);
- const helperRef = useRef(null);
-
- // Pre-create reusable Vector3 instances to avoid creating new ones in useFrame.
- const worldPosition = useMemo(() => new Vector3(), []);
- const origin = useMemo(() => new Vector3(0, 0, 0), []);
- const direction = useMemo(() => new Vector3(), []);
-
- // Create the CSM proxy with initial light direction.
- const proxyInstance = useMemo(() => {
- return new CSMProxy({
- camera,
- cascades,
- lightDirection: lightDirection.clone(), // Clone to avoid mutation issues.
- lightFar,
- lightIntensity,
- lightMargin,
- lightNear,
- maxFar,
- mode,
- parent: scene,
- shadowBias,
- shadowMapSize,
- reversedDepth,
- });
- }, [
- camera,
- scene,
- cascades,
- lightDirection,
- lightFar,
- lightIntensity,
- lightMargin,
- lightNear,
- maxFar,
- mode,
- shadowBias,
- shadowMapSize,
- reversedDepth,
- ]);
-
- // Create a memoized color to avoid unnecessary recreations.
- const threeColor = useMemo(() => new Color(color), [color]);
-
- // Update light color when the color changes.
- useEffect(() => {
- if (proxyInstance.instance) {
- proxyInstance.instance.lights.forEach((light) => {
- light.color = threeColor;
- });
- proxyInstance.instance.fade = fade ?? false;
- }
- }, [proxyInstance, threeColor, fade]);
-
- // Update CSM on each frame and handle light direction changes.
- useFrame(() => {
- if (!proxyInstance.instance || !dummyGroupRef.current) return;
-
- // Get the world position of the dummy group.
- dummyGroupRef.current.getWorldPosition(worldPosition);
-
- // Calculate direction from world position to origin.
- direction.subVectors(origin, worldPosition).normalize();
-
- // Update the CSM light direction.
- proxyInstance.instance.lightDirection.copy(direction);
-
- // Update CSM.
- proxyInstance.instance.update();
-
- // Update helper visualization if it exists.
- if (helperRef.current) {
- helperRef.current.update();
- }
- });
-
- // Force a scene material update to ensure shadow changes take effect immediately.
- useEffect(() => {
- // Mark all materials to be updated when the component mounts.
- updateMaterialsInScene(scene);
-
- return () => {
- // Force renderer state to reset on unmount.
- requestAnimationFrame(() => {
- // Mark all materials as needing update when unmounting.
- updateMaterialsInScene(scene);
- });
- };
- }, [scene]);
-
- // Create/attach the CSM instance.
- useEffect(() => {
- proxyInstance.attach();
-
- // Set colors on mount.
- if (proxyInstance.instance) {
- proxyInstance.instance.lights.forEach((light) => {
- light.color = threeColor;
- });
- }
-
- // Create debug helper if debug mode is enabled.
- if (debug && proxyInstance.instance) {
- const helper = new CSMHelper(proxyInstance.instance);
- helper.displayFrustum = true;
- helper.displayPlanes = true;
- helper.displayShadowBounds = true;
- helper.updateVisibility();
- scene.add(helper);
- helperRef.current = helper;
- }
-
- return () => {
- // Clean up helper if it exists.
- if (helperRef.current) {
- scene.remove(helperRef.current);
- // Dispose of helper geometries and materials.
- helperRef.current.traverse((child: THREE.Object3D) => {
- if ((child as THREE.Mesh).geometry) {
- (child as THREE.Mesh).geometry.dispose();
- }
- if ((child as THREE.Mesh).material) {
- const material = (child as THREE.Mesh).material;
- if (Array.isArray(material)) {
- material.forEach((m) => m.dispose());
- } else {
- material.dispose();
- }
- }
- });
- helperRef.current = null;
- }
- proxyInstance.dispose();
- };
- }, [proxyInstance, threeColor, debug, scene]);
-
- return (
- <>
-
- >
- );
-}
diff --git a/src/viser/client/src/SceneTree.tsx b/src/viser/client/src/SceneTree.tsx
index 1e9cd29fa..10505f745 100644
--- a/src/viser/client/src/SceneTree.tsx
+++ b/src/viser/client/src/SceneTree.tsx
@@ -42,7 +42,7 @@ import GeneratedGuiContainer from "./ControlPanel/Generated";
import { LineSegments } from "./Line";
import { Arrows } from "./Arrows";
import { shadowArgs } from "./ShadowArgs";
-import { CsmDirectionalLight } from "./CsmDirectionalLight";
+import { CascadedDirectionalLight } from "./CascadedDirectionalLight";
import { BasicMesh } from "./mesh/BasicMesh";
import { BoxMesh } from "./mesh/BoxMesh";
import { IcosphereMesh } from "./mesh/IcosphereMesh";
@@ -608,7 +608,7 @@ function createObjectFactory(
return {
makeObject: (ref, children) => (
-
),
- // CsmDirectionalLight is not influenced by visibility, since the
+ // CascadedDirectionalLight is not influenced by visibility, since the
// lights it adds are portaled to the scene root.
unmountWhenInvisible: true,
};
diff --git a/src/viser/client/src/csm/CSMShader.js b/src/viser/client/src/csm/CSMShader.js
deleted file mode 100644
index 4b5c04270..000000000
--- a/src/viser/client/src/csm/CSMShader.js
+++ /dev/null
@@ -1,308 +0,0 @@
-import { ShaderChunk } from "three";
-
-/**
- * @module CSMShader
- * @three_import import { CSMShader } from 'three/addons/csm/CSMShader.js';
- */
-
-/**
- * The object that holds the GLSL enhancements to enable CSM. This
- * code is injected into the built-in material shaders by {@link CSM}.
- *
- * @type {Object}
- */
-const CSMShader = {
- lights_fragment_begin: /* glsl */ `
-vec3 geometryPosition = - vViewPosition;
-vec3 geometryNormal = normal;
-vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );
-
-vec3 geometryClearcoatNormal = vec3( 0.0 );
-
-#ifdef USE_CLEARCOAT
-
- geometryClearcoatNormal = clearcoatNormal;
-
-#endif
-
-#ifdef USE_IRIDESCENCE
- float dotNVi = saturate( dot( normal, geometryViewDir ) );
- if ( material.iridescenceThickness == 0.0 ) {
- material.iridescence = 0.0;
- } else {
- material.iridescence = saturate( material.iridescence );
- }
- if ( material.iridescence > 0.0 ) {
- material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );
- // Iridescence F0 approximation
- material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );
- }
-#endif
-
-IncidentLight directLight;
-
-#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )
-
- PointLight pointLight;
- #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0
- PointLightShadow pointLightShadow;
- #endif
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
-
- pointLight = pointLights[ i ];
-
- getPointLightInfo( pointLight, geometryPosition, directLight );
-
- #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )
- pointLightShadow = pointLightShadows[ i ];
- directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;
-
- #endif
-
- RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- }
- #pragma unroll_loop_end
-
-#endif
-
-#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )
-
- SpotLight spotLight;
- vec4 spotColor;
- vec3 spotLightCoord;
- bool inSpotLightMap;
-
- #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0
- SpotLightShadow spotLightShadow;
- #endif
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {
-
- spotLight = spotLights[ i ];
-
- getSpotLightInfo( spotLight, geometryPosition, directLight );
-
- // spot lights are ordered [shadows with maps, shadows without maps, maps without shadows, none]
- #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )
- #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX
- #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )
- #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS
- #else
- #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )
- #endif
- #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS )
- spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w;
- inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) );
- spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy );
- directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color;
- #endif
- #undef SPOT_LIGHT_MAP_INDEX
-
- #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )
- spotLightShadow = spotLightShadows[ i ];
- directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;
-
- #endif
-
- RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- }
- #pragma unroll_loop_end
-
-#endif
-
-#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) && defined( USE_CSM ) && defined( CSM_CASCADES )
-
- DirectionalLight directionalLight;
- float linearDepth = (vViewPosition.z) / (shadowFar - cameraNear);
- #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0
- DirectionalLightShadow directionalLightShadow;
- #endif
-
- #if defined( USE_SHADOWMAP ) && defined( CSM_FADE )
- vec2 cascade;
- float cascadeCenter;
- float closestEdge;
- float margin;
- float csmx;
- float csmy;
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
-
- directionalLight = directionalLights[ i ];
- getDirectionalLightInfo( directionalLight, directLight );
-
- #if ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
- // NOTE: Depth gets larger away from the camera.
- // cascade.x is closer, cascade.y is further
- cascade = CSM_cascades[ i ];
- cascadeCenter = ( cascade.x + cascade.y ) / 2.0;
- closestEdge = linearDepth < cascadeCenter ? cascade.x : cascade.y;
- margin = 0.25 * pow( closestEdge, 2.0 );
- csmx = cascade.x - margin / 2.0;
- csmy = cascade.y + margin / 2.0;
- if( linearDepth >= csmx && ( linearDepth < csmy || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 ) ) {
-
- float dist = min( linearDepth - csmx, csmy - linearDepth );
- float ratio = clamp( dist / margin, 0.0, 1.0 );
-
- vec3 prevColor = directLight.color;
- directionalLightShadow = directionalLightShadows[ i ];
- directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
-
- bool shouldFadeLastCascade = UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 && linearDepth > cascadeCenter;
- directLight.color = mix( prevColor, directLight.color, shouldFadeLastCascade ? ratio : 1.0 );
-
- ReflectedLight prevLight = reflectedLight;
- RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- bool shouldBlend = UNROLLED_LOOP_INDEX != CSM_CASCADES - 1 || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 && linearDepth < cascadeCenter;
- float blendRatio = shouldBlend ? ratio : 1.0;
-
- reflectedLight.directDiffuse = mix( prevLight.directDiffuse, reflectedLight.directDiffuse, blendRatio );
- reflectedLight.directSpecular = mix( prevLight.directSpecular, reflectedLight.directSpecular, blendRatio );
- reflectedLight.indirectDiffuse = mix( prevLight.indirectDiffuse, reflectedLight.indirectDiffuse, blendRatio );
- reflectedLight.indirectSpecular = mix( prevLight.indirectSpecular, reflectedLight.indirectSpecular, blendRatio );
-
- }
- #endif
-
- }
- #pragma unroll_loop_end
- #elif defined (USE_SHADOWMAP)
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
-
- directionalLight = directionalLights[ i ];
- getDirectionalLightInfo( directionalLight, directLight );
-
- #if ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
-
- directionalLightShadow = directionalLightShadows[ i ];
- if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y) directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
-
- if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && (linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1)) RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- #endif
-
- }
- #pragma unroll_loop_end
-
- #elif ( NUM_DIR_LIGHT_SHADOWS > 0 )
- // note: no loop here - all CSM lights are in fact one light only
- getDirectionalLightInfo( directionalLights[0], directLight );
- RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- #endif
-
- #if ( NUM_DIR_LIGHTS > NUM_DIR_LIGHT_SHADOWS)
- // compute the lights not casting shadows (if any)
-
- #pragma unroll_loop_start
- for ( int i = NUM_DIR_LIGHT_SHADOWS; i < NUM_DIR_LIGHTS; i ++ ) {
-
- directionalLight = directionalLights[ i ];
-
- getDirectionalLightInfo( directionalLight, directLight );
-
- RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- }
- #pragma unroll_loop_end
-
- #endif
-
-#endif
-
-
-#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) && !defined( USE_CSM ) && !defined( CSM_CASCADES )
-
- DirectionalLight directionalLight;
- #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0
- DirectionalLightShadow directionalLightShadow;
- #endif
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
-
- directionalLight = directionalLights[ i ];
-
- getDirectionalLightInfo( directionalLight, directLight );
-
- #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
- directionalLightShadow = directionalLightShadows[ i ];
- directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
- #endif
-
- RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- }
- #pragma unroll_loop_end
-
-#endif
-
-#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )
-
- RectAreaLight rectAreaLight;
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {
-
- rectAreaLight = rectAreaLights[ i ];
- RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
-
- }
- #pragma unroll_loop_end
-
-#endif
-
-#if defined( RE_IndirectDiffuse )
-
- vec3 iblIrradiance = vec3( 0.0 );
-
- vec3 irradiance = getAmbientLightIrradiance( ambientLightColor );
-
- #if defined( USE_LIGHT_PROBES )
-
- irradiance += getLightProbeIrradiance( lightProbe, geometryNormal );
-
- #endif
-
- #if ( NUM_HEMI_LIGHTS > 0 )
-
- #pragma unroll_loop_start
- for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {
-
- irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal );
-
- }
- #pragma unroll_loop_end
-
- #endif
-
-#endif
-
-#if defined( RE_IndirectSpecular )
-
- vec3 radiance = vec3( 0.0 );
- vec3 clearcoatRadiance = vec3( 0.0 );
-
-#endif
-`,
- lights_pars_begin:
- /* glsl */ `
-#if defined( USE_CSM ) && defined( CSM_CASCADES )
-uniform vec2 CSM_cascades[CSM_CASCADES];
-uniform float cameraNear;
-uniform float shadowFar;
-#endif
- ` + ShaderChunk.lights_pars_begin,
-};
-
-export { CSMShader };
diff --git a/src/viser/client/src/csm/CSMShadowNode.js b/src/viser/client/src/csm/CSMShadowNode.js
deleted file mode 100644
index 2e66816e4..000000000
--- a/src/viser/client/src/csm/CSMShadowNode.js
+++ /dev/null
@@ -1,573 +0,0 @@
-import {
- Vector2,
- Vector3,
- MathUtils,
- Matrix4,
- Box3,
- Object3D,
- WebGLCoordinateSystem,
- ShadowBaseNode,
-} from "three/webgpu";
-
-import { CSMFrustum } from "./CSMFrustum.js";
-import {
- viewZToOrthographicDepth,
- reference,
- uniform,
- float,
- vec4,
- vec2,
- If,
- Fn,
- min,
- renderGroup,
- positionView,
- shadow,
-} from "three/tsl";
-
-const _cameraToLightMatrix = new Matrix4();
-const _lightSpaceFrustum = new CSMFrustum();
-const _center = new Vector3();
-const _bbox = new Box3();
-const _uniformArray = [];
-const _logArray = [];
-const _lightDirection = new Vector3();
-const _lightOrientationMatrix = new Matrix4();
-const _lightOrientationMatrixInverse = new Matrix4();
-const _up = new Vector3(0, 1, 0);
-
-class LwLight extends Object3D {
- constructor() {
- super();
-
- this.target = new Object3D();
- }
-}
-
-/**
- * An implementation of Cascade Shadow Maps (CSM).
- *
- * This module can only be used with {@link WebGPURenderer}. When using {@link WebGLRenderer},
- * use {@link CSM} instead.
- *
- * @augments ShadowBaseNode
- * @three_import import { CSMShadowNode } from 'three/addons/csm/CSMShadowNode.js';
- */
-class CSMShadowNode extends ShadowBaseNode {
- /**
- * Constructs a new CSM shadow node.
- *
- * @param {DirectionalLight} light - The CSM light.
- * @param {CSMShadowNode~Data} [data={}] - The CSM data.
- */
- constructor(light, data = {}) {
- super(light);
-
- /**
- * The scene's camera.
- *
- * @type {?Camera}
- * @default null
- */
- this.camera = null;
-
- /**
- * The number of cascades.
- *
- * @type {number}
- * @default 3
- */
- this.cascades = data.cascades || 3;
-
- /**
- * The maximum far value.
- *
- * @type {number}
- * @default 100000
- */
- this.maxFar = data.maxFar || 100000;
-
- /**
- * The frustum split mode.
- *
- * @type {('practical'|'uniform'|'logarithmic'|'custom')}
- * @default 'practical'
- */
- this.mode = data.mode || "practical";
-
- /**
- * The light margin.
- *
- * @type {number}
- * @default 200
- */
- this.lightMargin = data.lightMargin || 200;
-
- /**
- * Custom split callback when using `mode='custom'`.
- *
- * @type {Function}
- */
- this.customSplitsCallback = data.customSplitsCallback;
-
- /**
- * Whether to fade between cascades or not.
- *
- * @type {boolean}
- * @default false
- */
- this.fade = false;
-
- /**
- * An array of numbers in the range `[0,1]` the defines how the
- * mainCSM frustum should be split up.
- *
- * @type {Array}
- */
- this.breaks = [];
-
- this._cascades = [];
-
- /**
- * The main frustum.
- *
- * @type {?CSMFrustum}
- * @default null
- */
- this.mainFrustum = null;
-
- /**
- * An array of frustums representing the cascades.
- *
- * @type {Array}
- */
- this.frustums = [];
-
- /**
- * An array of directional lights which cast the shadows for
- * the different cascades. There is one directional light for each
- * cascade.
- *
- * @type {Array}
- */
- this.lights = [];
-
- this._shadowNodes = [];
- }
-
- /**
- * Inits the CSM shadow node.
- *
- * @private
- * @param {NodeBuilder} builder - The node builder.
- */
- _init({ camera, renderer }) {
- this.camera = camera;
-
- const data = { webGL: renderer.coordinateSystem === WebGLCoordinateSystem };
- this.mainFrustum = new CSMFrustum(data);
-
- const light = this.light;
-
- for (let i = 0; i < this.cascades; i++) {
- const lwLight = new LwLight();
- lwLight.castShadow = true;
-
- const lShadow = light.shadow.clone();
- lShadow.bias = lShadow.bias * (i + 1);
-
- this.lights.push(lwLight);
-
- lwLight.shadow = lShadow;
-
- this._shadowNodes.push(shadow(lwLight, lShadow));
-
- this._cascades.push(new Vector2());
- }
-
- this.updateFrustums();
- }
-
- /**
- * Inits the cascades according to the scene's camera and breaks configuration.
- *
- * @private
- */
- _initCascades() {
- const camera = this.camera;
- camera.updateProjectionMatrix();
-
- this.mainFrustum.setFromProjectionMatrix(
- camera.projectionMatrix,
- this.maxFar,
- );
- this.mainFrustum.split(this.breaks, this.frustums);
- }
-
- /**
- * Computes the breaks of this CSM instance based on the scene's camera, number of cascades
- * and the selected split mode.
- *
- * @private
- */
- _getBreaks() {
- const camera = this.camera;
- const far = Math.min(camera.far, this.maxFar);
-
- this.breaks.length = 0;
-
- switch (this.mode) {
- case "uniform":
- uniformSplit(this.cascades, camera.near, far, this.breaks);
- break;
-
- case "logarithmic":
- logarithmicSplit(this.cascades, camera.near, far, this.breaks);
- break;
-
- case "practical":
- practicalSplit(this.cascades, camera.near, far, 0.5, this.breaks);
- break;
-
- case "custom":
- if (this.customSplitsCallback === undefined)
- console.error("CSM: Custom split scheme callback not defined.");
- this.customSplitsCallback(this.cascades, camera.near, far, this.breaks);
- break;
- }
-
- function uniformSplit(amount, near, far, target) {
- for (let i = 1; i < amount; i++) {
- target.push((near + ((far - near) * i) / amount) / far);
- }
-
- target.push(1);
- }
-
- function logarithmicSplit(amount, near, far, target) {
- for (let i = 1; i < amount; i++) {
- target.push((near * (far / near) ** (i / amount)) / far);
- }
-
- target.push(1);
- }
-
- function practicalSplit(amount, near, far, lambda, target) {
- _uniformArray.length = 0;
- _logArray.length = 0;
- logarithmicSplit(amount, near, far, _logArray);
- uniformSplit(amount, near, far, _uniformArray);
-
- for (let i = 1; i < amount; i++) {
- target.push(
- MathUtils.lerp(_uniformArray[i - 1], _logArray[i - 1], lambda),
- );
- }
-
- target.push(1);
- }
- }
-
- /**
- * Sets the light breaks.
- *
- * @private
- */
- _setLightBreaks() {
- for (let i = 0, l = this.cascades; i < l; i++) {
- const amount = this.breaks[i];
- const prev = this.breaks[i - 1] || 0;
-
- this._cascades[i].set(prev, amount);
- }
- }
-
- /**
- * Updates the shadow bounds of this CSM instance.
- *
- * @private
- */
- _updateShadowBounds() {
- const frustums = this.frustums;
-
- for (let i = 0; i < frustums.length; i++) {
- const shadowCam = this.lights[i].shadow.camera;
- const frustum = this.frustums[i];
-
- // Get the two points that represent that furthest points on the frustum assuming
- // that's either the diagonal across the far plane or the diagonal across the whole
- // frustum itself.
- const nearVerts = frustum.vertices.near;
- const farVerts = frustum.vertices.far;
- const point1 = farVerts[0];
-
- let point2;
-
- if (point1.distanceTo(farVerts[2]) > point1.distanceTo(nearVerts[2])) {
- point2 = farVerts[2];
- } else {
- point2 = nearVerts[2];
- }
-
- let squaredBBWidth = point1.distanceTo(point2);
-
- if (this.fade) {
- // expand the shadow extents by the fade margin if fade is enabled.
- const camera = this.camera;
- const far = Math.max(camera.far, this.maxFar);
- const linearDepth = frustum.vertices.far[0].z / (far - camera.near);
- const margin = 0.25 * Math.pow(linearDepth, 2.0) * (far - camera.near);
-
- squaredBBWidth += margin;
- }
-
- shadowCam.left = -squaredBBWidth / 2;
- shadowCam.right = squaredBBWidth / 2;
- shadowCam.top = squaredBBWidth / 2;
- shadowCam.bottom = -squaredBBWidth / 2;
- shadowCam.updateProjectionMatrix();
- }
- }
-
- /**
- * Applications must call this method every time they change camera or CSM settings.
- */
- updateFrustums() {
- this._getBreaks();
- this._initCascades();
- this._updateShadowBounds();
- this._setLightBreaks();
- }
-
- /**
- * Setups the TSL when using fading.
- *
- * @private
- * @return {ShaderCallNodeInternal}
- */
- _setupFade() {
- const cameraNear = reference("camera.near", "float", this).setGroup(
- renderGroup,
- );
- const cascades = reference("_cascades", "vec2", this)
- .setGroup(renderGroup)
- .setName("cascades");
-
- const shadowFar = uniform("float")
- .setGroup(renderGroup)
- .setName("shadowFar")
- .onRenderUpdate(() => Math.min(this.maxFar, this.camera.far));
-
- const linearDepth = viewZToOrthographicDepth(
- positionView.z,
- cameraNear,
- shadowFar,
- ).toVar("linearDepth");
- const lastCascade = this.cascades - 1;
-
- return Fn((builder) => {
- this.setupShadowPosition(builder);
-
- const ret = vec4(1, 1, 1, 1).toVar("shadowValue");
-
- const cascade = vec2().toVar("cascade");
- const cascadeCenter = float().toVar("cascadeCenter");
-
- const margin = float().toVar("margin");
-
- const csmX = float().toVar("csmX");
- const csmY = float().toVar("csmY");
-
- for (let i = 0; i < this.cascades; i++) {
- const isLastCascade = i === lastCascade;
-
- cascade.assign(cascades.element(i));
-
- cascadeCenter.assign(cascade.x.add(cascade.y).div(2.0));
-
- const closestEdge = linearDepth
- .lessThan(cascadeCenter)
- .select(cascade.x, cascade.y);
-
- margin.assign(float(0.25).mul(closestEdge.pow(2.0)));
-
- csmX.assign(cascade.x.sub(margin.div(2.0)));
-
- if (isLastCascade) {
- csmY.assign(cascade.y);
- } else {
- csmY.assign(cascade.y.add(margin.div(2.0)));
- }
-
- const inRange = linearDepth
- .greaterThanEqual(csmX)
- .and(linearDepth.lessThanEqual(csmY));
-
- If(inRange, () => {
- const dist = min(
- linearDepth.sub(csmX),
- csmY.sub(linearDepth),
- ).toVar();
-
- let ratio = dist.div(margin).clamp(0.0, 1.0);
-
- if (i === 0) {
- // don't fade at nearest edge
- ratio = linearDepth.greaterThan(cascadeCenter).select(ratio, 1);
- }
-
- ret.subAssign(this._shadowNodes[i].oneMinus().mul(ratio));
- });
- }
-
- return ret;
- })();
- }
-
- /**
- * Setups the TSL when no fading (default).
- *
- * @private
- * @return {ShaderCallNodeInternal}
- */
- _setupStandard() {
- const cameraNear = reference("camera.near", "float", this).setGroup(
- renderGroup,
- );
- const cascades = reference("_cascades", "vec2", this)
- .setGroup(renderGroup)
- .setName("cascades");
-
- const shadowFar = uniform("float")
- .setGroup(renderGroup)
- .setName("shadowFar")
- .onRenderUpdate(() => Math.min(this.maxFar, this.camera.far));
-
- const linearDepth = viewZToOrthographicDepth(
- positionView.z,
- cameraNear,
- shadowFar,
- ).toVar("linearDepth");
-
- return Fn((builder) => {
- this.setupShadowPosition(builder);
-
- const ret = vec4(1, 1, 1, 1).toVar("shadowValue");
- const cascade = vec2().toVar("cascade");
-
- for (let i = 0; i < this.cascades; i++) {
- cascade.assign(cascades.element(i));
-
- If(
- linearDepth
- .greaterThanEqual(cascade.x)
- .and(linearDepth.lessThanEqual(cascade.y)),
- () => {
- ret.assign(this._shadowNodes[i]);
- },
- );
- }
-
- return ret;
- })();
- }
-
- setup(builder) {
- if (this.camera === null) this._init(builder);
-
- return this.fade === true ? this._setupFade() : this._setupStandard();
- }
-
- updateBefore(/*builder*/) {
- const light = this.light;
- const parent = light.parent;
- const camera = this.camera;
- const frustums = this.frustums;
-
- // make sure the placeholder light objects which represent the
- // multiple cascade shadow casters are part of the scene graph
-
- for (let i = 0; i < this.lights.length; i++) {
- const lwLight = this.lights[i];
-
- if (lwLight.parent === null) {
- parent.add(lwLight.target);
- parent.add(lwLight);
- }
- }
-
- _lightDirection
- .subVectors(light.target.position, light.position)
- .normalize();
-
- // for each frustum we need to find its min-max box aligned with the light orientation
- // the position in _lightOrientationMatrix does not matter, as we transform there and back
- _lightOrientationMatrix.lookAt(light.position, light.target.position, _up);
- _lightOrientationMatrixInverse.copy(_lightOrientationMatrix).invert();
-
- for (let i = 0; i < frustums.length; i++) {
- const lwLight = this.lights[i];
- const shadow = lwLight.shadow;
- const shadowCam = shadow.camera;
- const texelWidth =
- (shadowCam.right - shadowCam.left) / shadow.mapSize.width;
- const texelHeight =
- (shadowCam.top - shadowCam.bottom) / shadow.mapSize.height;
-
- _cameraToLightMatrix.multiplyMatrices(
- _lightOrientationMatrixInverse,
- camera.matrixWorld,
- );
- frustums[i].toSpace(_cameraToLightMatrix, _lightSpaceFrustum);
-
- const nearVerts = _lightSpaceFrustum.vertices.near;
- const farVerts = _lightSpaceFrustum.vertices.far;
-
- _bbox.makeEmpty();
-
- for (let j = 0; j < 4; j++) {
- _bbox.expandByPoint(nearVerts[j]);
- _bbox.expandByPoint(farVerts[j]);
- }
-
- _bbox.getCenter(_center);
- _center.z = _bbox.max.z + this.lightMargin;
- _center.x = Math.floor(_center.x / texelWidth) * texelWidth;
- _center.y = Math.floor(_center.y / texelHeight) * texelHeight;
- _center.applyMatrix4(_lightOrientationMatrix);
-
- lwLight.position.copy(_center);
- lwLight.target.position.copy(_center);
- lwLight.target.position.add(_lightDirection);
- }
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
- for (let i = 0; i < this.lights.length; i++) {
- const light = this.lights[i];
- const parent = light.parent;
-
- parent.remove(light.target);
- parent.remove(light);
- }
-
- super.dispose();
- }
-}
-
-/**
- * Constructor data of `CSMShadowNode`.
- *
- * @typedef {Object} CSMShadowNode~Data
- * @property {number} [cascades=3] - The number of cascades.
- * @property {number} [maxFar=100000] - The maximum far value.
- * @property {('practical'|'uniform'|'logarithmic'|'custom')} [mode='practical'] - The frustum split mode.
- * @property {Function} [customSplitsCallback] - Custom split callback when using `mode='custom'`.
- * @property {number} [lightMargin=200] - The light margin.
- **/
-
-export { CSMShadowNode };
diff --git a/src/viser/client/src/csm/CSMFrustum.js b/src/viser/client/src/shadows/CascadeFrustum.js
similarity index 79%
rename from src/viser/client/src/csm/CSMFrustum.js
rename to src/viser/client/src/shadows/CascadeFrustum.js
index 936144e87..064012df8 100644
--- a/src/viser/client/src/csm/CSMFrustum.js
+++ b/src/viser/client/src/shadows/CascadeFrustum.js
@@ -3,21 +3,20 @@ import { Vector3, Matrix4 } from "three";
const inverseProjectionMatrix = new Matrix4();
/**
- * Represents the frustum of a CSM instance.
- *
- * @three_import import { CSMFrustum } from 'three/addons/csm/CSMFrustum.js';
+ * A view-frustum slice used to fit cascade shadow cameras. Derived from
+ * three.js's CSM addon (three/addons/csm/CSMFrustum.js).
*/
-class CSMFrustum {
+class CascadeFrustum {
/**
- * Constructs a new CSM frustum.
+ * Constructs a new cascade frustum.
*
- * @param {CSMFrustum~Data} [data] - The CSM data.
+ * @param {CascadeFrustum~Data} [data] - The frustum data.
*/
constructor(data) {
data = data || {};
/**
- * The zNear value. This value depends on whether the CSM
+ * The zNear value. This value depends on whether the frustum
* is used with WebGL or WebGPU. Both API use different
* conventions for their projection matrices.
*
@@ -50,7 +49,7 @@ class CSMFrustum {
}
/**
- * Setups this CSM frustum from the given projection matrix and max far value.
+ * Setups this cascade frustum from the given projection matrix and max far value.
*
* @param {Matrix4} projectionMatrix - The projection matrix, usually of the scene's camera.
* @param {number} maxFar - The maximum far value.
@@ -96,16 +95,16 @@ class CSMFrustum {
}
/**
- * Splits the CSM frustum by the given array. The new CSM frustum are pushed into the given
+ * Splits the cascade frustum by the given array. The new cascade frustums are pushed into the given
* target array.
*
* @param {Array} breaks - An array of numbers in the range `[0,1]` the defines how the
- * CSM frustum should be split up.
- * @param {Array} target - The target array that holds the new CSM frustums.
+ * cascade frustum should be split up.
+ * @param {Array} target - The target array that holds the new cascade frustums.
*/
split(breaks, target) {
while (breaks.length > target.length) {
- target.push(new CSMFrustum());
+ target.push(new CascadeFrustum());
}
target.length = breaks.length;
@@ -144,11 +143,11 @@ class CSMFrustum {
}
/**
- * Transforms the given target CSM frustum into the different coordinate system defined by the
+ * Transforms the given target cascade frustum into the different coordinate system defined by the
* given camera matrix.
*
* @param {Matrix4} cameraMatrix - The matrix that defines the new coordinate system.
- * @param {CSMFrustum} target - The CSM to convert.
+ * @param {CascadeFrustum} target - The frustum to convert.
*/
toSpace(cameraMatrix, target) {
for (let i = 0; i < 4; i++) {
@@ -164,12 +163,12 @@ class CSMFrustum {
}
/**
- * Constructor data of `CSMFrustum`.
+ * Constructor data of `CascadeFrustum`.
*
- * @typedef {Object} CSMFrustum~Data
- * @property {boolean} [webGL] - Whether this CSM frustum is used with WebGL or WebGPU.
+ * @typedef {Object} CascadeFrustum~Data
+ * @property {boolean} [webGL] - Whether this cascade frustum is used with WebGL or WebGPU.
* @property {Matrix4} [projectionMatrix] - A projection matrix usually of the scene's camera.
* @property {number} [maxFar] - The maximum far value.
**/
-export { CSMFrustum };
+export { CascadeFrustum };
diff --git a/src/viser/client/src/csm/CSMHelper.js b/src/viser/client/src/shadows/CascadeHelper.js
similarity index 88%
rename from src/viser/client/src/csm/CSMHelper.js
rename to src/viser/client/src/shadows/CascadeHelper.js
index e4bdc5fc4..999df11f8 100644
--- a/src/viser/client/src/csm/CSMHelper.js
+++ b/src/viser/client/src/shadows/CascadeHelper.js
@@ -13,29 +13,29 @@ import {
} from "three";
/**
- * A helper for visualizing the cascades of a CSM instance.
+ * A helper for visualizing the cascades of a ShadowCascades instance.
+ * Derived from three.js's CSM addon (three/addons/csm/CSMHelper.js).
*
* @augments Group
- * @three_import import { CSMHelper } from 'three/addons/csm/CSMHelper.js';
*/
-class CSMHelper extends Group {
+class CascadeHelper extends Group {
/**
- * Constructs a new CSM helper.
+ * Constructs a new cascade helper.
*
- * @param {CSM|CSMShadowNode} csm - The CSM instance to visualize.
+ * @param {ShadowCascades} shadowCascades - The instance to visualize.
*/
- constructor(csm) {
+ constructor(shadowCascades) {
super();
/**
- * The CSM instance to visualize.
+ * The ShadowCascades instance to visualize.
*
- * @type {CSM|CSMShadowNode}
+ * @type {ShadowCascades}
*/
- this.csm = csm;
+ this.shadowCascades = shadowCascades;
/**
- * Whether to display the CSM frustum or not.
+ * Whether to display the camera frustum or not.
*
* @type {boolean}
* @default true
@@ -109,12 +109,12 @@ class CSMHelper extends Group {
* Updates the helper. This method should be called in the app's animation loop.
*/
update() {
- const csm = this.csm;
- const camera = csm.camera;
- const cascades = csm.cascades;
- const mainFrustum = csm.mainFrustum;
- const frustums = csm.frustums;
- const lights = csm.lights;
+ const shadowCascades = this.shadowCascades;
+ const camera = shadowCascades.camera;
+ const cascades = shadowCascades.cascades;
+ const mainFrustum = shadowCascades.mainFrustum;
+ const frustums = shadowCascades.frustums;
+ const lights = shadowCascades.lights;
const frustumLines = this.frustumLines;
const frustumLinePositions = frustumLines.geometry.getAttribute("position");
@@ -235,7 +235,7 @@ class CSMHelper extends Group {
frustumLines.geometry.dispose();
frustumLines.material.dispose();
- const cascades = this.csm.cascades;
+ const cascades = this.shadowCascades.cascades;
for (let i = 0; i < cascades; i++) {
const cascadeLine = cascadeLines[i];
@@ -253,4 +253,4 @@ class CSMHelper extends Group {
}
}
-export { CSMHelper };
+export { CascadeHelper };
diff --git a/src/viser/client/src/csm/CSM.d.ts b/src/viser/client/src/shadows/ShadowCascades.d.ts
similarity index 81%
rename from src/viser/client/src/csm/CSM.d.ts
rename to src/viser/client/src/shadows/ShadowCascades.d.ts
index 3c65149f9..4f0c3c880 100644
--- a/src/viser/client/src/csm/CSM.d.ts
+++ b/src/viser/client/src/shadows/ShadowCascades.d.ts
@@ -1,6 +1,6 @@
-import { Camera, DirectionalLight, Material, Object3D, Vector3 } from "three";
+import { Camera, DirectionalLight, Object3D, Vector3 } from "three";
-export interface CSMParameters {
+export interface ShadowCascadesParams {
camera: Camera;
parent: Object3D;
cascades?: number;
@@ -22,7 +22,7 @@ export interface CSMParameters {
reversedDepth?: boolean;
}
-export class CSM {
+export class ShadowCascades {
camera: Camera;
parent: Object3D;
cascades: number;
@@ -41,14 +41,12 @@ export class CSM {
far: number,
breaks: number[],
) => void;
- fade: boolean;
lights: DirectionalLight[];
- constructor(data: CSMParameters);
+ constructor(data: ShadowCascadesParams);
update(): void;
updateFrustums(): void;
remove(): void;
dispose(): void;
- setupMaterial(material: Material): void;
}
diff --git a/src/viser/client/src/csm/CSM.js b/src/viser/client/src/shadows/ShadowCascades.js
similarity index 62%
rename from src/viser/client/src/csm/CSM.js
rename to src/viser/client/src/shadows/ShadowCascades.js
index 123444acf..f08a086ef 100644
--- a/src/viser/client/src/csm/CSM.js
+++ b/src/viser/client/src/shadows/ShadowCascades.js
@@ -1,17 +1,8 @@
-import {
- Vector2,
- Vector3,
- DirectionalLight,
- MathUtils,
- ShaderChunk,
- Matrix4,
- Box3,
-} from "three";
-import { CSMFrustum } from "./CSMFrustum.js";
-import { CSMShader } from "./CSMShader.js";
+import { Vector3, DirectionalLight, MathUtils, Matrix4, Box3 } from "three";
+import { CascadeFrustum } from "./CascadeFrustum.js";
const _cameraToLightMatrix = new Matrix4();
-const _lightSpaceFrustum = new CSMFrustum({ webGL: true });
+const _lightSpaceFrustum = new CascadeFrustum({ webGL: true });
const _center = new Vector3();
const _origin = new Vector3();
const _bbox = new Box3();
@@ -22,18 +13,27 @@ const _lightOrientationMatrixInverse = new Matrix4();
const _up = new Vector3(0, 1, 0);
/**
- * An implementation of Cascade Shadow Maps (CSM).
+ * Approximate cascaded shadows for a directional light. Derived from
+ * three.js's CSM addon (three/addons/csm/CSM.js), keeping only the
+ * cascade-fitting math; the shader-injection half (setupMaterial/CSMShader)
+ * is removed.
*
- * This module can only be used with {@link WebGLRenderer}. When using {@link WebGPURenderer},
- * use {@link CSMShadowNode} instead.
- *
- * @three_import import { CSM } from 'three/addons/csm/CSM.js';
+ * This is NOT cascaded shadow mapping in the standard sense: nothing selects
+ * a cascade per fragment. Each cascade is an ordinary shadow-casting
+ * DirectionalLight (at intensity/cascades) whose shadow camera is fitted to
+ * a slice of the view frustum. Every fragment is lit by all cascade lights,
+ * and a fragment only receives a cascade's shadow when it lands inside that
+ * cascade's shadow-camera bounds. The upside is that this composes with any
+ * material (streamed meshes, GLB imports, instanced meshes) with no
+ * per-material setup; the cost is that fully occluded points outside the
+ * nearer cascades' bounds keep part of their direct light, so distant
+ * shadows render lighter than true CSM would.
*/
-export class CSM {
+export class ShadowCascades {
/**
- * Constructs a new CSM instance.
+ * Constructs a new ShadowCascades instance.
*
- * @param {CSM~Data} data - The CSM data.
+ * @param {ShadowCascades~Data} data - The ShadowCascades data.
*/
constructor(data) {
/**
@@ -56,7 +56,7 @@ export class CSM {
* @type {number}
* @default 3
*/
- this.cascades = data.cascades || 3;
+ this.cascades = data.cascades ?? 3;
/**
* The maximum far value.
@@ -64,7 +64,7 @@ export class CSM {
* @type {number}
* @default 100000
*/
- this.maxFar = data.maxFar || 100000;
+ this.maxFar = data.maxFar ?? 100000;
/**
* The frustum split mode.
@@ -72,7 +72,7 @@ export class CSM {
* @type {('practical'|'uniform'|'logarithmic'|'custom')}
* @default 'practical'
*/
- this.mode = data.mode || "practical";
+ this.mode = data.mode ?? "practical";
/**
* The shadow map size.
@@ -80,7 +80,7 @@ export class CSM {
* @type {number}
* @default 2048
*/
- this.shadowMapSize = data.shadowMapSize || 2048;
+ this.shadowMapSize = data.shadowMapSize ?? 2048;
/**
* The shadow bias.
@@ -88,7 +88,7 @@ export class CSM {
* @type {number}
* @default 0.000001
*/
- this.shadowBias = data.shadowBias || 0.000001;
+ this.shadowBias = data.shadowBias ?? 0.000001;
/**
* The light direction.
@@ -104,7 +104,7 @@ export class CSM {
* @type {number}
* @default 3
*/
- this.lightIntensity = data.lightIntensity || 3;
+ this.lightIntensity = data.lightIntensity ?? 3;
/**
* The light near value.
@@ -112,7 +112,7 @@ export class CSM {
* @type {number}
* @default 1
*/
- this.lightNear = data.lightNear || 1;
+ this.lightNear = data.lightNear ?? 1;
/**
* The light far value.
@@ -120,15 +120,17 @@ export class CSM {
* @type {number}
* @default 2000
*/
- this.lightFar = data.lightFar || 2000;
+ this.lightFar = data.lightFar ?? 2000;
/**
- * The light margin.
+ * The light margin: how far the shadow camera is pulled back toward the
+ * light beyond the closest point of each cascade's bounding box, so
+ * off-screen geometry between the light and the view frustum still casts.
*
* @type {number}
* @default 200
*/
- this.lightMargin = data.lightMargin || 200;
+ this.lightMargin = data.lightMargin ?? 200;
/**
* Custom split callback when using `mode='custom'`.
@@ -137,28 +139,20 @@ export class CSM {
*/
this.customSplitsCallback = data.customSplitsCallback;
- /**
- * Whether to fade between cascades or not.
- *
- * @type {boolean}
- * @default false
- */
- this.fade = false;
-
/**
* Whether a reversed depth buffer is in use.
*
* @type {boolean}
* @default false
*/
- this.reversedDepth = data.reversedDepth || false;
+ this.reversedDepth = data.reversedDepth ?? false;
/**
* The main frustum.
*
- * @type {CSMFrustum}
+ * @type {CascadeFrustum}
*/
- this.mainFrustum = new CSMFrustum({
+ this.mainFrustum = new CascadeFrustum({
webGL: true,
reversedDepth: this.reversedDepth,
});
@@ -166,13 +160,13 @@ export class CSM {
/**
* An array of frustums representing the cascades.
*
- * @type {Array}
+ * @type {Array}
*/
this.frustums = [];
/**
* An array of numbers in the range `[0,1]` the defines how the
- * mainCSM frustum should be split up.
+ * main frustum should be split up.
*
* @type {Array}
*/
@@ -187,20 +181,12 @@ export class CSM {
*/
this.lights = [];
- /**
- * A Map holding enhanced material shaders.
- *
- * @type {Map}
- */
- this.shaders = new Map();
-
this._createLights();
this.updateFrustums();
- this._injectInclude();
}
/**
- * Creates the directional lights of this CSM instance.
+ * Creates the directional lights of this ShadowCascades instance.
*
* @private
*/
@@ -237,7 +223,7 @@ export class CSM {
}
/**
- * Updates the shadow bounds of this CSM instance.
+ * Updates the shadow bounds of this ShadowCascades instance.
*
* @private
*/
@@ -261,16 +247,7 @@ export class CSM {
point2 = nearVerts[2];
}
- let squaredBBWidth = point1.distanceTo(point2);
- if (this.fade) {
- // expand the shadow extents by the fade margin if fade is enabled.
- const camera = this.camera;
- const far = Math.max(camera.far, this.maxFar);
- const linearDepth = frustum.vertices.far[0].z / (far - camera.near);
- const margin = 0.25 * Math.pow(linearDepth, 2.0) * (far - camera.near);
-
- squaredBBWidth += margin;
- }
+ const squaredBBWidth = point1.distanceTo(point2);
shadowCam.left = -squaredBBWidth / 2;
shadowCam.right = squaredBBWidth / 2;
@@ -281,7 +258,7 @@ export class CSM {
}
/**
- * Computes the breaks of this CSM instance based on the scene's camera, number of cascades
+ * Computes the breaks of this ShadowCascades instance based on the scene's camera, number of cascades
* and the selected split mode.
*
* @private
@@ -303,7 +280,9 @@ export class CSM {
break;
case "custom":
if (this.customSplitsCallback === undefined)
- console.error("CSM: Custom split scheme callback not defined.");
+ console.error(
+ "ShadowCascades: Custom split scheme callback not defined.",
+ );
this.customSplitsCallback(this.cascades, camera.near, far, this.breaks);
break;
}
@@ -341,7 +320,7 @@ export class CSM {
}
/**
- * Updates the CSM. This method must be called in your animation loop before
+ * Updates the ShadowCascades. This method must be called in your animation loop before
* calling `renderer.render()`.
*/
update() {
@@ -390,107 +369,16 @@ export class CSM {
}
/**
- * Injects the CSM shader enhancements into the built-in materials.
- *
- * @private
- */
- _injectInclude() {
- ShaderChunk.lights_fragment_begin = CSMShader.lights_fragment_begin;
- ShaderChunk.lights_pars_begin = CSMShader.lights_pars_begin;
- }
-
- /**
- * Applications must call this method for all materials that should be affected by CSM.
- *
- * @param {Material} material - The material to setup for CSM support.
- */
- setupMaterial(material) {
- material.defines = material.defines || {};
- material.defines.USE_CSM = 1;
- material.defines.CSM_CASCADES = this.cascades;
-
- if (this.fade) {
- material.defines.CSM_FADE = "";
- }
-
- const breaksVec2 = [];
- const scope = this;
- const shaders = this.shaders;
-
- material.onBeforeCompile = function (shader) {
- const far = Math.min(scope.camera.far, scope.maxFar);
- scope._getExtendedBreaks(breaksVec2);
-
- shader.uniforms.CSM_cascades = { value: breaksVec2 };
- shader.uniforms.cameraNear = { value: scope.camera.near };
- shader.uniforms.shadowFar = { value: far };
-
- shaders.set(material, shader);
- };
-
- shaders.set(material, null);
- }
-
- /**
- * Updates the CSM uniforms.
- *
- * @private
- */
- _updateUniforms() {
- const far = Math.min(this.camera.far, this.maxFar);
- const shaders = this.shaders;
-
- shaders.forEach(function (shader, material) {
- if (shader !== null) {
- const uniforms = shader.uniforms;
- this._getExtendedBreaks(uniforms.CSM_cascades.value);
- uniforms.cameraNear.value = this.camera.near;
- uniforms.shadowFar.value = far;
- }
-
- if (!this.fade && "CSM_FADE" in material.defines) {
- delete material.defines.CSM_FADE;
- material.needsUpdate = true;
- } else if (this.fade && !("CSM_FADE" in material.defines)) {
- material.defines.CSM_FADE = "";
- material.needsUpdate = true;
- }
- }, this);
- }
-
- /**
- * Computes the extended breaks for the CSM uniforms.
- *
- * @private
- * @param {Array} target - The target array that holds the extended breaks.
- */
- _getExtendedBreaks(target) {
- while (target.length < this.breaks.length) {
- target.push(new Vector2());
- }
-
- target.length = this.breaks.length;
-
- for (let i = 0; i < this.cascades; i++) {
- const amount = this.breaks[i];
- const prev = this.breaks[i - 1] || 0;
- target[i].x = prev;
- target[i].y = amount;
- }
- }
-
- /**
- * Applications must call this method every time they change camera or CSM settings.
+ * Applications must call this method every time they change camera or ShadowCascades settings.
*/
updateFrustums() {
this._getBreaks();
this._initCascades();
this._updateShadowBounds();
- this._updateUniforms();
}
/**
- * Applications must call this method when they remove the CSM usage from their scene.
+ * Applications must call this method when they remove the ShadowCascades usage from their scene.
*/
remove() {
for (let i = 0; i < this.lights.length; i++) {
@@ -504,29 +392,17 @@ export class CSM {
* method whenever this instance is no longer used in your app.
*/
dispose() {
- const shaders = this.shaders;
- shaders.forEach(function (shader, material) {
- delete material.onBeforeCompile;
- delete material.defines.USE_CSM;
- delete material.defines.CSM_CASCADES;
- delete material.defines.CSM_FADE;
-
- if (shader !== null) {
- delete shader.uniforms.CSM_cascades;
- delete shader.uniforms.cameraNear;
- delete shader.uniforms.shadowFar;
- }
-
- material.needsUpdate = true;
- });
- shaders.clear();
+ for (const light of this.lights) {
+ // Frees the cascade's shadow-map render target.
+ light.dispose();
+ }
}
}
/**
- * Constructor data of `CSM`.
+ * Constructor data of `ShadowCascades`.
*
- * @typedef {Object} CSM~Data
+ * @typedef {Object} ShadowCascades~Data
* @property {Camera} camera - The scene's camera.
* @property {Object3D} parent - The parent object, usually the scene.
* @property {number} [cascades=3] - The number of cascades.
@@ -538,6 +414,6 @@ export class CSM {
* @property {Vector3} [lightDirection] - The light direction.
* @property {number} [lightIntensity=3] - The light intensity.
* @property {number} [lightNear=1] - The light near value.
- * @property {number} [lightNear=2000] - The light far value.
+ * @property {number} [lightFar=2000] - The light far value.
* @property {number} [lightMargin=200] - The light margin.
**/