UNPKG

three

Version:

JavaScript 3D library

320 lines (225 loc) 9.33 kB
import { Frustum, LightShadow, Matrix4, OrthographicCamera, Vector3, Vector4, WebGPUCoordinateSystem } from 'three'; const _lightOrientationMatrix = /*@__PURE__*/ new Matrix4(); const _viewToLightMatrix = /*@__PURE__*/ new Matrix4(); const _lightDirection = /*@__PURE__*/ new Vector3(); const _up = /*@__PURE__*/ new Vector3(); const _center = /*@__PURE__*/ new Vector3(); const _nearCorners = [ /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3() ]; const _farCorners = [ /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3() ]; const _cascadeCorners = [ /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3(), /*@__PURE__*/ new Vector3() ]; // must match the cascade count in the sun shadow shader chunks const _cascadeCount = 2; // fraction of each cascade's depth range that blends into the next cascade const _cascadeFade = 0.1; /** * Represents the shadow configuration of {@link SunLight}, using two * cascaded shadow maps (CSM). * * The shadow camera projection is fitted automatically to slices of the view * frustum, up to a distance of `camera.far` (or the view camera's far plane, * whichever is smaller), and adjacent cascades blend into each other over a * small depth range. `camera.left/right/top/bottom` are ignored. * * The default `mapSize` is `1024x1024` per cascade. * * @augments LightShadow * @three_import import { SunLightShadow } from 'three/addons/lights/SunLightShadow.js'; */ class SunLightShadow extends LightShadow { /** * Constructs a new sun light shadow. */ constructor() { super( new OrthographicCamera( - 5, 5, 5, - 5, 0.5, 500 ) ); /** * This flag can be used for type testing. * * @type {boolean} * @readonly * @default true */ this.isSunLightShadow = true; this.mapSize.set( 1024, 1024 ); this._cameras = []; this._matrices = []; this._frustums = []; this._cascadeSplits = new Array( _cascadeCount + 1 ).fill( 0 ); // per cascade ( begin, end, fade start ) view depths, consumed by the renderer this._cascadeData = []; this._viewportCount = _cascadeCount; this._frameExtents.set( 2, 1 ); for ( let i = 0; i < _cascadeCount; i ++ ) { this._cameras.push( new OrthographicCamera() ); this._matrices.push( new Matrix4() ); this._frustums.push( new Frustum() ); this._cascadeData.push( new Vector4() ); } while ( this._viewports.length < _cascadeCount ) this._viewports.push( new Vector4() ); } /** * Returns the shadow camera of the given cascade. * * @param {number} [cascadeIndex=0] - The cascade index. * @return {OrthographicCamera} The shadow camera. */ getCamera( cascadeIndex = 0 ) { return this._cameras[ cascadeIndex ]; } /** * Returns the shadow matrix of the given cascade. * * @param {number} [cascadeIndex=0] - The cascade index. * @return {Matrix4} The shadow matrix. */ getMatrix( cascadeIndex = 0 ) { return this._matrices[ cascadeIndex ]; } /** * Returns the shadow camera frustum of the given cascade. Used internally by * the renderer to cull objects. * * @param {number} [cascadeIndex=0] - The cascade index. * @return {Frustum} The shadow camera frustum. */ getFrustum( cascadeIndex = 0 ) { return this._frustums[ cascadeIndex ]; } /** * Update the matrices for the cascade cameras and shadows, used internally * by the renderer. * * @param {Light} light - The light for which the shadow is being rendered. * @param {Camera} viewCamera - The camera the scene is rendered with. */ updateMatrices( light, viewCamera ) { if ( viewCamera === undefined ) return; // inset the cascade viewports so shadow filtering cannot read across atlas tiles const insetX = Math.min( 0.25, ( Math.ceil( this.radius ) + 1 ) / this.mapSize.x ); const insetY = Math.min( 0.25, ( Math.ceil( this.radius ) + 1 ) / this.mapSize.y ); for ( let i = 0; i < _cascadeCount; i ++ ) { this._viewports[ i ].set( i + insetX, insetY, 1 - 2 * insetX, 1 - 2 * insetY ); } const resolutionX = this.mapSize.x * ( 1 - 2 * insetX ); const resolutionY = this.mapSize.y * ( 1 - 2 * insetY ); const resolution = Math.min( resolutionX, resolutionY ); const camera = this.camera; const cameraNear = viewCamera.near; const cameraFar = Math.max( cameraNear + 1e-6, Math.min( camera.far, viewCamera.far ) ); // practical split scheme: the average of uniform and logarithmic splits const splits = this._cascadeSplits; splits[ 0 ] = cameraNear; for ( let i = 1; i < _cascadeCount; i ++ ) { const amount = i / _cascadeCount; const uniform = cameraNear + ( cameraFar - cameraNear ) * amount; const logarithmic = cameraNear > 0 ? cameraNear * Math.pow( cameraFar / cameraNear, amount ) : uniform; splits[ i ] = ( uniform + logarithmic ) * 0.5; } splits[ _cascadeCount ] = cameraFar; _lightDirection.setFromMatrixPosition( light.matrixWorld ).negate().normalize(); _up.set( 0, 1, 0 ); if ( Math.abs( _up.dot( _lightDirection ) ) > 0.99 ) _up.set( 0, 0, 1 ); _lightOrientationMatrix.lookAt( _center.set( 0, 0, 0 ), _lightDirection, _up ); _viewToLightMatrix.copy( _lightOrientationMatrix ).transpose().multiply( viewCamera.matrixWorld ); // view frustum corners in light space; the rotation preserves distances, // so the cascades can be fitted and snapped directly in this space const zNear = viewCamera.reversedDepth ? 1 : ( viewCamera.coordinateSystem === WebGPUCoordinateSystem ? 0 : - 1 ); const inverseProjectionMatrix = viewCamera.projectionMatrixInverse; let globalMaxZ = - Infinity; for ( let i = 0; i < 4; i ++ ) { const x = i === 0 || i === 1 ? 1 : - 1; const y = i === 0 || i === 3 ? 1 : - 1; const nearCorner = _nearCorners[ i ].set( x, y, zNear ).applyMatrix4( inverseProjectionMatrix ); const farCorner = _farCorners[ i ]; if ( viewCamera.isPerspectiveCamera === true ) { farCorner.copy( nearCorner ).multiplyScalar( cameraFar / cameraNear ); } else { farCorner.set( nearCorner.x, nearCorner.y, - cameraFar ); } nearCorner.applyMatrix4( _viewToLightMatrix ); farCorner.applyMatrix4( _viewToLightMatrix ); globalMaxZ = Math.max( globalMaxZ, nearCorner.z, farCorner.z ); } // raise the ceiling one shadow range towards the light so casters outside // the view frustum still cast into it globalMaxZ += cameraFar; const shadowNear = camera.near; for ( let i = 0; i < _cascadeCount; i ++ ) { // each cascade covers the fade band of the previous one so both can be sampled while blending const cascadeNear = i === 0 ? splits[ 0 ] : this._cascadeData[ i - 1 ].z; const cascadeFar = splits[ i + 1 ]; const fadeStart = cascadeFar - _cascadeFade * ( cascadeFar - splits[ i ] ); this._cascadeData[ i ].set( i === 0 ? - 1e10 : cascadeNear, cascadeFar, fadeStart, 0 ); // bounding sphere of the cascade slice for a rotation-stable projection const nearAlpha = ( cascadeNear - cameraNear ) / ( cameraFar - cameraNear ); const farAlpha = ( cascadeFar - cameraNear ) / ( cameraFar - cameraNear ); _center.set( 0, 0, 0 ); for ( let j = 0; j < 4; j ++ ) { _cascadeCorners[ j * 2 ].lerpVectors( _nearCorners[ j ], _farCorners[ j ], nearAlpha ); _cascadeCorners[ j * 2 + 1 ].lerpVectors( _nearCorners[ j ], _farCorners[ j ], farAlpha ); _center.add( _cascadeCorners[ j * 2 ] ).add( _cascadeCorners[ j * 2 + 1 ] ); } _center.multiplyScalar( 1 / 8 ); let radiusSq = 0; let minZ = Infinity; for ( let j = 0; j < 8; j ++ ) { radiusSq = Math.max( radiusSq, _cascadeCorners[ j ].distanceToSquared( _center ) ); minZ = Math.min( minZ, _cascadeCorners[ j ].z ); } let radius = Math.sqrt( radiusSq ); // snap to the texel grid to avoid shimmering when the view camera moves if ( resolution > 1 ) { // pad by half a texel so snapping cannot clip a frustum corner radius /= 1 - 1 / resolution; const texelSizeX = 2 * radius / resolutionX; const texelSizeY = 2 * radius / resolutionY; _center.x = Math.round( _center.x / texelSizeX ) * texelSizeX; _center.y = Math.round( _center.y / texelSizeY ) * texelSizeY; } // place the near plane at the caster ceiling _center.z = globalMaxZ + shadowNear; _center.applyMatrix4( _lightOrientationMatrix ); const cascadeCamera = this._cameras[ i ]; cascadeCamera.position.copy( _center ); cascadeCamera.quaternion.setFromRotationMatrix( _lightOrientationMatrix ); cascadeCamera.left = - radius; cascadeCamera.right = radius; cascadeCamera.top = radius; cascadeCamera.bottom = - radius; cascadeCamera.near = shadowNear; cascadeCamera.far = globalMaxZ - minZ + 2 * shadowNear; cascadeCamera.coordinateSystem = camera.coordinateSystem; cascadeCamera._reversedDepth = camera.reversedDepth; cascadeCamera.updateProjectionMatrix(); cascadeCamera.updateMatrixWorld(); this._updateMatrix( cascadeCamera, this._matrices[ i ], this._frustums[ i ], this._viewports[ i ] ); } } } export { SunLightShadow };