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. **/