import { Vector3, ShaderChunk, DirectionalLight, Vector2, LineBasicMaterial, Object3D, Geometry, Line } from '../../../build/three.module.js'; class FrustumVertex { constructor(x, y, z) { this.x = x || 0; this.y = y || 0; this.z = z || 0; } fromLerp(v1, v2, amount) { this.x = (1 - amount) * v1.x + amount * v2.x; this.y = (1 - amount) * v1.y + amount * v2.y; this.z = (1 - amount) * v1.z + amount * v2.z; return this; } } function toRad(degrees) { return degrees * Math.PI / 180; } class Frustum { constructor(data) { data = data || {}; this.fov = data.fov || 70; this.near = data.near || 0.1; this.far = data.far || 1000; this.aspect = data.aspect || 1; this.vertices = { near: [], far: [] }; } getViewSpaceVertices() { this.nearPlaneY = this.near * Math.tan(toRad(this.fov / 2)); this.nearPlaneX = this.aspect * this.nearPlaneY; this.farPlaneY = this.far * Math.tan(toRad(this.fov / 2)); this.farPlaneX = this.aspect * this.farPlaneY; // 3 --- 0 vertices.near/far order // | | // 2 --- 1 this.vertices.near.push( new FrustumVertex(this.nearPlaneX, this.nearPlaneY, -this.near), new FrustumVertex(this.nearPlaneX, -this.nearPlaneY, -this.near), new FrustumVertex(-this.nearPlaneX, -this.nearPlaneY, -this.near), new FrustumVertex(-this.nearPlaneX, this.nearPlaneY, -this.near) ); this.vertices.far.push( new FrustumVertex(this.farPlaneX, this.farPlaneY, -this.far), new FrustumVertex(this.farPlaneX, -this.farPlaneY, -this.far), new FrustumVertex(-this.farPlaneX, -this.farPlaneY, -this.far), new FrustumVertex(-this.farPlaneX, this.farPlaneY, -this.far) ); return this.vertices; } split(breaks) { const result = []; for(let i = 0; i < breaks.length; i++) { const cascade = new Frustum(); if(i === 0) { cascade.vertices.near = this.vertices.near; } else { for(let j = 0; j < 4; j++) { cascade.vertices.near.push(new FrustumVertex().fromLerp(this.vertices.near[j], this.vertices.far[j], breaks[i - 1])); } } if(i === breaks - 1) { cascade.vertices.far = this.vertices.far; } else { for(let j = 0; j < 4; j++) { cascade.vertices.far.push(new FrustumVertex().fromLerp(this.vertices.near[j], this.vertices.far[j], breaks[i])); } } result.push(cascade); } return result; } toSpace(cameraMatrix) { const result = new Frustum(); const point = new Vector3(); for(var i = 0; i < 4; i++) { point.set(this.vertices.near[i].x, this.vertices.near[i].y, this.vertices.near[i].z); point.applyMatrix4(cameraMatrix); result.vertices.near.push(new FrustumVertex(point.x, point.y, point.z)); point.set(this.vertices.far[i].x, this.vertices.far[i].y, this.vertices.far[i].z); point.applyMatrix4(cameraMatrix); result.vertices.far.push(new FrustumVertex(point.x, point.y, point.z)); } return result; } } class FrustumBoundingBox { constructor() { this.min = { x: 0, y: 0, z: 0 }; this.max = { x: 0, y: 0, z: 0 }; } fromFrustum(frustum) { const vertices = []; for(let i = 0; i < 4; i++) { vertices.push(frustum.vertices.near[i]); vertices.push(frustum.vertices.far[i]); } this.min = { x: vertices[0].x, y: vertices[0].y, z: vertices[0].z }; this.max = { x: vertices[0].x, y: vertices[0].y, z: vertices[0].z }; for(let i = 1; i < 8; i++) { this.min.x = Math.min(this.min.x, vertices[i].x); this.min.y = Math.min(this.min.y, vertices[i].y); this.min.z = Math.min(this.min.z, vertices[i].z); this.max.x = Math.max(this.max.x, vertices[i].x); this.max.y = Math.max(this.max.y, vertices[i].y); this.max.z = Math.max(this.max.z, vertices[i].z); } return this; } getSize() { this.size = { x: this.max.x - this.min.x, y: this.max.y - this.min.y, z: this.max.z - this.min.z }; return this.size; } getCenter(margin) { this.center = { x: (this.max.x + this.min.x) / 2, y: (this.max.y + this.min.y) / 2, z: this.max.z + margin }; return this.center; } } var Shader = { lights_fragment_begin: ` GeometricContext geometry; geometry.position = - vViewPosition; geometry.normal = normal; geometry.viewDir = normalize( vViewPosition ); #ifdef CLEARCOAT geometry.clearcoatNormal = clearcoatNormal; #endif IncidentLight directLight; #if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) PointLight pointLight; #pragma unroll_loop for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { pointLight = pointLights[ i ]; getPointDirectLightIrradiance( pointLight, geometry, directLight ); #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) directLight.color *= all( bvec3( pointLight.shadow, directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; #endif RE_Direct( directLight, geometry, material, reflectedLight ); } #endif #if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) SpotLight spotLight; #pragma unroll_loop for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { spotLight = spotLights[ i ]; getSpotDirectLightIrradiance( spotLight, geometry, directLight ); #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) directLight.color *= all( bvec3( spotLight.shadow, directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0; #endif RE_Direct( directLight, geometry, material, reflectedLight ); } #endif #if ( NUM_DIR_LIGHTS > 0) && defined( RE_Direct ) && defined( USE_CSM ) && defined( CSM_CASCADES ) DirectionalLight directionalLight; float linearDepth = (vViewPosition.z) / (shadowFar - cameraNear); #pragma unroll_loop for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { directionalLight = directionalLights[ i ]; getDirectionalDirectLightIrradiance( directionalLight, geometry, directLight ); #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y) directLight.color *= all( bvec3( directionalLight.shadow, directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; #endif if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && (linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1)) RE_Direct( directLight, geometry, material, reflectedLight ); } #endif #if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) && !defined( USE_CSM ) && !defined( CSM_CASCADES ) DirectionalLight directionalLight; #pragma unroll_loop for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { directionalLight = directionalLights[ i ]; getDirectionalDirectLightIrradiance( directionalLight, geometry, directLight ); #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) directLight.color *= all( bvec2( directionalLight.shadow, directLight.visible ) ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; #endif RE_Direct( directLight, geometry, material, reflectedLight ); } #endif #if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) RectAreaLight rectAreaLight; #pragma unroll_loop for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { rectAreaLight = rectAreaLights[ i ]; RE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight ); } #endif #if defined( RE_IndirectDiffuse ) vec3 iblIrradiance = vec3( 0.0 ); vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); irradiance += getLightProbeIrradiance( lightProbe, geometry ); #if ( NUM_HEMI_LIGHTS > 0 ) #pragma unroll_loop for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry ); } #endif #endif #if defined( RE_IndirectSpecular ) vec3 radiance = vec3( 0.0 ); vec3 clearcoatRadiance = vec3( 0.0 ); #endif `, lights_pars_begin: ` #if defined( USE_CSM ) && defined( CSM_CASCADES ) uniform vec2 CSM_cascades[CSM_CASCADES]; uniform float cameraNear; uniform float shadowFar; #endif ` + ShaderChunk.lights_pars_begin }; class CSM { constructor(data) { data = data || {}; this.camera = data.camera; this.parent = data.parent; this.fov = data.fov || this.camera.fov; this.near = this.camera.near; this.far = data.far || this.camera.far; this.aspect = data.aspect || this.camera.aspect; this.cascades = data.cascades || 3; this.mode = data.mode || 'practical'; this.shadowMapSize = data.shadowMapSize || 2048; this.shadowBias = data.shadowBias || 0.000001; this.lightDirection = data.lightDirection || new Vector3(1, -1, 1).normalize(); this.lightIntensity = data.lightIntensity || 1; this.lightNear = data.lightNear || 1; this.lightFar = data.lightFar || 2000; this.lightMargin = data.lightMargin || 200; this.customSplitsCallback = data.customSplitsCallback; this.lights = []; this.materials = []; this.createLights(); this.getBreaks(); this.initCascades(); this.injectInclude(); } createLights() { for(let i = 0; i < this.cascades; i++) { const light = new DirectionalLight(0xffffff, this.lightIntensity); light.castShadow = true; light.shadow.mapSize.width = this.shadowMapSize; light.shadow.mapSize.height = this.shadowMapSize; light.shadow.camera.near = this.lightNear; light.shadow.camera.far = this.lightFar; light.shadow.bias = this.shadowBias; this.parent.add(light); this.parent.add(light.target); this.lights.push(light); } } initCascades() { this.mainFrustum = new Frustum({ fov: this.fov, near: this.near, far: this.far, aspect: this.aspect }); this.mainFrustum.getViewSpaceVertices(); this.frustums = this.mainFrustum.split(this.breaks); } getBreaks() { this.breaks = []; switch (this.mode) { case 'uniform': this.breaks = uniformSplit(this.cascades, this.near, this.far); break; case 'logarithmic': this.breaks = logarithmicSplit(this.cascades, this.near, this.far); break; case 'practical': this.breaks = practicalSplit(this.cascades, this.near, this.far, 0.5); break; case 'custom': if(this.customSplitsCallback === undefined) console.error('CSM: Custom split scheme callback not defined.'); this.breaks = this.customSplitsCallback(this.cascades, this.near, this.far); break; } function uniformSplit(amount, near, far) { const r = []; for(let i = 1; i < amount; i++) { r.push((near + (far - near) * i / amount) / far); } r.push(1); return r; } function logarithmicSplit(amount, near, far) { const r = []; for(let i = 1; i < amount; i++) { r.push((near * (far / near) ** (i / amount)) / far); } r.push(1); return r; } function practicalSplit(amount, near, far, lambda) { const log = logarithmicSplit(amount, near, far); const uni = uniformSplit(amount, near, far); const r = []; for(let i = 1; i < amount; i++) { r.push(lambda * log[i - 1] + (1 - lambda) * uni[i - 1]); } r.push(1); return r; } } update(cameraMatrix) { for(let i = 0; i < this.frustums.length; i++) { const worldSpaceFrustum = this.frustums[i].toSpace(cameraMatrix); const light = this.lights[i]; const lightSpaceFrustum = worldSpaceFrustum.toSpace(light.shadow.camera.matrixWorldInverse); light.shadow.camera.updateMatrixWorld(true); const bbox = new FrustumBoundingBox().fromFrustum(lightSpaceFrustum); bbox.getSize(); bbox.getCenter(this.lightMargin); const squaredBBWidth = Math.max(bbox.size.x, bbox.size.y); let center = new Vector3(bbox.center.x, bbox.center.y, bbox.center.z); center.applyMatrix4(light.shadow.camera.matrixWorld); light.shadow.camera.left = -squaredBBWidth / 2; light.shadow.camera.right = squaredBBWidth / 2; light.shadow.camera.top = squaredBBWidth / 2; light.shadow.camera.bottom = -squaredBBWidth / 2; light.position.copy(center); light.target.position.copy(center); light.target.position.x += this.lightDirection.x; light.target.position.y += this.lightDirection.y; light.target.position.z += this.lightDirection.z; light.shadow.camera.updateProjectionMatrix(); light.shadow.camera.updateMatrixWorld(); } } injectInclude() { ShaderChunk.lights_fragment_begin = Shader.lights_fragment_begin; ShaderChunk.lights_pars_begin = Shader.lights_pars_begin; } setupMaterial(material) { material.defines = material.defines || {}; material.defines.USE_CSM = 1; material.defines.CSM_CASCADES = this.cascades; const breaksVec2 = []; for(let i = 0; i < this.cascades; i++) { let amount = this.breaks[i]; let prev = this.breaks[i - 1] || 0; breaksVec2.push(new Vector2(prev, amount)); } const self = this; material.onBeforeCompile = function (shader) { shader.uniforms.CSM_cascades = {value: breaksVec2}; shader.uniforms.cameraNear = {value: self.camera.near}; shader.uniforms.shadowFar = {value: self.far}; self.materials.push(shader); }; } updateUniforms() { for(let i = 0; i < this.materials.length; i++) { this.materials[i].uniforms.CSM_cascades.value = this.getExtendedBreaks(); this.materials[i].uniforms.cameraNear.value = this.camera.near; this.materials[i].uniforms.shadowFar.value = this.far; } } getExtendedBreaks() { let breaksVec2 = []; for(let i = 0; i < this.cascades; i++) { let amount = this.breaks[i]; let prev = this.breaks[i - 1] || 0; breaksVec2.push(new Vector2(prev, amount)); } return breaksVec2; } setAspect(aspect) { this.aspect = aspect; this.initCascades(); } updateFrustums() { this.getBreaks(); this.initCascades(); this.updateUniforms(); } helper(cameraMatrix) { let frustum; let geometry; const material = new LineBasicMaterial({color: 0xffffff}); const object = new Object3D(); for(let i = 0; i < this.frustums.length; i++) { frustum = this.frustums[i].toSpace(cameraMatrix); geometry = new Geometry(); for(let i = 0; i < 5; i++) { const point = frustum.vertices.near[i === 4 ? 0 : i]; geometry.vertices.push(new Vector3(point.x, point.y, point.z)); } object.add(new Line(geometry, material)); geometry = new Geometry(); for(let i = 0; i < 5; i++) { const point = frustum.vertices.far[i === 4 ? 0 : i]; geometry.vertices.push(new Vector3(point.x, point.y, point.z)); } object.add(new Line(geometry, material)); for(let i = 0; i < 4; i++) { geometry = new Geometry(); const near = frustum.vertices.near[i]; const far = frustum.vertices.far[i]; geometry.vertices.push(new Vector3(near.x, near.y, near.z)); geometry.vertices.push(new Vector3(far.x, far.y, far.z)); object.add(new Line(geometry, material)); } } return object; } remove() { for(let i = 0; i < this.lights.length; i++) { this.parent.remove(this.lights[i]); } } } export default CSM;