three
Version:
JavaScript 3D library
375 lines (272 loc) • 13.8 kB
JavaScript
import ShadowNode from './ShadowNode.js';
import { Loop } from '../utils/LoopNode.js';
import { uniform } from '../core/UniformNode.js';
import { float, vec3, If, Fn } from '../tsl/TSLBase.js';
import { reference } from '../accessors/ReferenceNode.js';
import { cubeTexture } from '../accessors/CubeTextureNode.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { Matrix4 } from '../../math/Matrix4.js';
import { Vector3 } from '../../math/Vector3.js';
import { Color } from '../../math/Color.js';
import { BasicShadowMap, GreaterEqualCompare, LessEqualCompare, WebGPUCoordinateSystem } from '../../constants.js';
import { CubeDepthTexture } from '../../textures/CubeDepthTexture.js';
import { screenCoordinate } from '../display/ScreenNode.js';
import { interleavedGradientNoise, vogelDiskSample } from '../utils/PostProcessingUtils.js';
import { abs, normalize, cross, max } from '../math/MathNode.js';
import { viewZToLogarithmicDepth } from '../display/ViewportDepthNode.js';
const _clearColor = /*@__PURE__*/ new Color();
const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
const _lightPositionWorld = /*@__PURE__*/ new Vector3();
const _lookTarget = /*@__PURE__*/ new Vector3();
// Cube map face directions and up vectors for point light shadows
// Face order: +X, -X, +Y, -Y, +Z, -Z
// WebGPU coordinate system - same face orientations as CubeCamera
const _cubeDirectionsWebGPU = [
/*@__PURE__*/ new Vector3( - 1, 0, 0 ), /*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),
/*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 )
];
const _cubeUpsWebGPU = [
/*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 ),
/*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 )
];
// WebGL coordinate system - standard OpenGL convention
const _cubeDirectionsWebGL = [
/*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( - 1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),
/*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, - 1 )
];
const _cubeUpsWebGL = [
/*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ),
/*@__PURE__*/ new Vector3( 0, 0, - 1 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 ), /*@__PURE__*/ new Vector3( 0, - 1, 0 )
];
/**
* Returns a function that gives the depth the cube shadow map stores for the point at
* `direction * distance`, from the largest absolute component of the direction. Each face
* stores the depth along its own axis, and perspective depth is `depthA + depthB / max( |x|, |y|, |z| )`,
* so everything but the division is computed once.
*
* @private
* @param {Node<float>} distance - The distance from the light to the fragment.
* @param {Node<float>} near - The near plane of the shadow camera.
* @param {Node<float>} far - The far plane of the shadow camera.
* @param {Node<float>} bias - The shadow bias.
* @param {Renderer} renderer - The renderer.
* @return {function(Node<float>):Node<float>} The function, which takes the largest absolute component of the direction.
*/
const getPointShadowDepthFn = ( distance, near, far, bias, renderer ) => {
const { reversedDepthBuffer, logarithmicDepthBuffer } = renderer;
// logarithmic depth gets a separate depth function
if ( logarithmicDepthBuffer === true ) {
return ( maxAbs ) => viewZToLogarithmicDepth( maxAbs.mul( distance ).negate(), near, far ).add( bias );
}
// default and reverse depth share one
const depthRange = far.sub( near );
const nearFar = near.mul( far ).div( depthRange.mul( distance ) );
let depthA, depthB;
if ( reversedDepthBuffer === true ) {
depthA = near.div( depthRange ).add( bias ).negate().toConst();
depthB = nearFar.toConst();
} else {
depthA = far.div( depthRange ).add( bias ).toConst();
depthB = nearFar.negate().toConst();
}
return ( maxAbs ) => depthA.add( depthB.div( maxAbs ) );
};
export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp } ) => {
return cubeTexture( depthTexture, bd3D ).compare( dp );
} );
/**
* A shadow filtering function for point lights using Vogel disk sampling and IGN.
*
* Uses 5 samples distributed via Vogel disk pattern in tangent space around the
* sample direction, rotated per-pixel using Interleaved Gradient Noise (IGN).
*
* @method
* @param {Object} inputs - The input parameter object.
* @param {CubeDepthTexture} inputs.depthTexture - A reference to the shadow cube map.
* @param {Node<vec3>} inputs.bd3D - The normalized direction from light to fragment.
* @param {Node<float>} inputs.distance - The distance from the light to the fragment.
* @param {LightShadow} inputs.shadow - The light shadow.
* @return {Node<float>} The filtering result.
*/
export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, distance, shadow }, builder ) => {
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
const near = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup );
const far = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup );
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
const texelSize = radius.div( mapSize.x ).toConst();
// Build a tangent-space coordinate system for applying offsets
const absDir = abs( bd3D );
const tangent = normalize( cross( bd3D, absDir.x.greaterThan( absDir.z ).select( vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ) ) ).toConst();
const bitangent = cross( bd3D, tangent ).toConst();
// Use IGN to rotate sampling pattern per pixel (phi = IGN * 2π)
const phi = interleavedGradientNoise( screenCoordinate.xy ).mul( 6.28318530718 ).toConst();
// Each sample is compared against the depth of the point next to the fragment in its direction,
// on whichever face of the cube it falls, so the comparison is the same on every face.
const getDepth = getPointShadowDepthFn( distance, near, far, bias, builder.renderer );
// Accumulate five samples using Vogel disk distribution in tangent space.
const result = float( 0.0 ).toVar();
Loop( 5, ( { i } ) => {
const sample = vogelDiskSample( i, 5, phi );
const direction = bd3D.add( tangent.mul( sample.x ).add( bitangent.mul( sample.y ) ).mul( texelSize ) ).toConst();
const absDirection = abs( direction ).toConst();
const maxAbs = max( absDirection.x, absDirection.y, absDirection.z ).toConst();
result.addAssign( cubeTexture( depthTexture, direction ).compare( getDepth( maxAbs ) ) );
} );
return result.mul( 1.0 / 5.0 );
} );
const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCoord, shadow }, builder ) => {
// for point lights, the uniform @vShadowCoord is re-purposed to hold
// the vector from the light to the world-space position of the fragment.
const shadowPosition = shadowCoord.xyz.toConst();
const shadowPositionAbs = shadowPosition.abs().toConst();
const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z );
const shadowCameraNear = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near );
const shadowCameraFar = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far );
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
const result = float( 1.0 ).toVar();
If( viewZ.sub( shadowCameraFar ).lessThanEqual( 0.0 ).and( viewZ.sub( shadowCameraNear ).greaterThanEqual( 0.0 ) ), () => {
// bd3D = base direction 3D (direction from light to fragment)
const bd3D = shadowPosition.normalize();
const distance = shadowPosition.length();
const getDepth = getPointShadowDepthFn( distance, shadowCameraNear, shadowCameraFar, bias, builder.renderer );
const dp = getDepth( viewZ.div( distance ) );
// percentage-closer filtering using cube texture sampling
result.assign( filterFn( { depthTexture, bd3D, dp, distance, shadow } ) );
} );
return result;
} );
/**
* Represents the shadow implementation for point light nodes.
*
* @augments ShadowNode
*/
class PointShadowNode extends ShadowNode {
static get type() {
return 'PointShadowNode';
}
/**
* Constructs a new point shadow node.
*
* @param {PointLight} light - The shadow casting point light.
* @param {?PointLightShadow} [shadow=null] - An optional point light shadow.
*/
constructor( light, shadow = null ) {
super( light, shadow );
}
/**
* Overwrites the default implementation to return point light shadow specific
* filtering functions.
*
* @param {number} type - The shadow type.
* @return {Function} The filtering function.
*/
getShadowFilterFn( type ) {
return type === BasicShadowMap ? BasicPointShadowFilter : PointShadowFilter;
}
/**
* Overwrites the default implementation so the unaltered shadow position is used.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Node<vec3>} shadowPosition - A node representing the shadow position.
* @return {Node<vec3>} The shadow coordinates.
*/
setupShadowCoord( builder, shadowPosition ) {
return shadowPosition;
}
/**
* Overwrites the default implementation to only use point light specific
* shadow filter functions.
*
* @param {NodeBuilder} builder - A reference to the current node builder.
* @param {Object} inputs - A configuration object that defines the shadow filtering.
* @param {Function} inputs.filterFn - This function defines the filtering type of the shadow map e.g. PCF.
* @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's depth texture.
* @param {Node<vec3>} inputs.shadowCoord - Shadow coordinates which are used to sample from the shadow map.
* @param {LightShadow} inputs.shadow - The light shadow.
* @return {Node<float>} The result node of the shadow filtering.
*/
setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
return pointShadowFilter( { filterFn, depthTexture, shadowCoord, shadow } );
}
/**
* Overwrites the default implementation to create a CubeRenderTarget with CubeDepthTexture.
*
* @param {LightShadow} shadow - The light shadow object.
* @param {NodeBuilder} builder - A reference to the current node builder.
* @return {Object} An object containing the shadow map and depth texture.
*/
setupRenderTarget( shadow, builder ) {
const depthTexture = new CubeDepthTexture( shadow.mapSize.width );
depthTexture.name = 'PointShadowDepthTexture';
depthTexture.compareFunction = builder.renderer.reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare;
const shadowMap = builder.createCubeRenderTarget( shadow.mapSize.width );
shadowMap.texture.name = 'PointShadowMap';
shadowMap.depthTexture = depthTexture;
return { shadowMap, depthTexture };
}
/**
* Overwrites the default implementation with point light specific
* rendering code.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
renderShadow( frame ) {
const { shadow, shadowMap, light } = this;
const { renderer, scene } = frame;
const camera = shadow.camera;
const shadowMatrix = shadow.matrix;
// Select cube directions/ups based on coordinate system
const isWebGPU = renderer.coordinateSystem === WebGPUCoordinateSystem;
const cubeDirections = isWebGPU ? _cubeDirectionsWebGPU : _cubeDirectionsWebGL;
const cubeUps = isWebGPU ? _cubeUpsWebGPU : _cubeUpsWebGL;
shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.width );
//
const previousAutoClear = renderer.autoClear;
const previousClearColor = renderer.getClearColor( _clearColor );
const previousClearAlpha = renderer.getClearAlpha();
renderer.autoClear = false;
renderer.setClearColor( shadow.clearColor, shadow.clearAlpha );
// Render each cube face
for ( let face = 0; face < 6; face ++ ) {
// Set render target to the specific cube face
renderer.setRenderTarget( shadowMap, face );
renderer.clear();
// Update shadow camera matrices for this face
const far = light.distance || camera.far;
if ( far !== camera.far ) {
camera.far = far;
camera.updateProjectionMatrix();
}
_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
camera.position.copy( _lightPositionWorld );
_lookTarget.copy( camera.position );
_lookTarget.add( cubeDirections[ face ] );
camera.up.copy( cubeUps[ face ] );
camera.lookAt( _lookTarget );
camera.updateMatrixWorld();
shadowMatrix.makeTranslation( - _lightPositionWorld.x, - _lightPositionWorld.y, - _lightPositionWorld.z );
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
shadow._frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
//
const currentSceneName = scene.name;
scene.name = `Point Light Shadow [ ${ light.name || 'ID: ' + light.id } ] - Face ${ face + 1 }`;
renderer.render( scene, camera );
scene.name = currentSceneName;
}
//
renderer.autoClear = previousAutoClear;
renderer.setClearColor( previousClearColor, previousClearAlpha );
}
}
export default PointShadowNode;
/**
* TSL function for creating an instance of `PointShadowNode`.
*
* @tsl
* @function
* @param {PointLight} light - The shadow casting point light.
* @param {?PointLightShadow} [shadow=null] - An optional point light shadow.
* @return {PointShadowNode} The created point shadow node.
*/
export const pointShadow = ( light, shadow ) => new PointShadowNode( light, shadow );