diff --git a/src/nodes/lighting/PointShadowNode.js b/src/nodes/lighting/PointShadowNode.js index 7da1d82754b190..38f253d44cb8a1 100644 --- a/src/nodes/lighting/PointShadowNode.js +++ b/src/nodes/lighting/PointShadowNode.js @@ -8,18 +8,55 @@ import { sub, div, mul } from '../math/OperatorNode.js'; import { renderGroup } from '../core/UniformGroupNode.js'; import { Vector2 } from '../../math/Vector2.js'; import { Vector4 } from '../../math/Vector4.js'; +import { glslFn } from '../code/FunctionNode.js'; +import { perspectiveDepthToViewZ } from '../TSL.js'; -export const cubeToUV = /*@__PURE__*/ Fn( ( [ v_immutable, texelSizeY_immutable ] ) => { +// cubeToUV() maps a 3D direction vector suitable for cube texture mapping to a 2D +// vector suitable for 2D texture mapping. This code uses the following layout for the +// 2D texture: +// +// xzXZ +// y Y +// +// Y - Positive y direction +// y - Negative y direction +// X - Positive x direction +// x - Negative x direction +// Z - Positive z direction +// z - Negative z direction +// +// Source and test bed: +// https://gist.github.com/tschw/da10c43c467ce8afd0c4 + +export const cubeToUV = /*@__PURE__*/ Fn( ( [ pos, texelSizeY ] ) => { + + const v = pos.toVar(); + + // Number of texels to avoid at the edge of each square + + const absV = abs( v ); + + // Intersect unit cube - const texelSizeY = float( texelSizeY_immutable ).toVar(); - const v = vec3( v_immutable ).toVar(); - const absV = vec3( abs( v ) ).toVar(); - const scaleToCube = float( div( 1.0, max( absV.x, max( absV.y, absV.z ) ) ) ).toVar(); + const scaleToCube = div( 1.0, max( absV.x, max( absV.y, absV.z ) ) ); absV.mulAssign( scaleToCube ); - v.mulAssign( scaleToCube.mul( sub( 1.0, mul( 2.0, texelSizeY ) ) ) ); + + // Apply scale to avoid seams + + // two texels less per square (one texel will do for NEAREST) + v.mulAssign( scaleToCube.mul( texelSizeY.mul( 2 ).oneMinus() ) ); + + // Unwrap + + // space: -1 ... 1 range for each square + // + // #X## dim := ( 4 , 2 ) + // # # center := ( 1 , 1 ) + const planar = vec2( v.xy ).toVar(); - const almostATexel = float( mul( 1.5, texelSizeY ) ).toVar(); - const almostOne = float( sub( 1.0, almostATexel ) ).toVar(); + + const almostATexel = texelSizeY.mul( 1.5 ); + const almostOne = almostATexel.oneMinus(); If( absV.z.greaterThanEqual( almostOne ), () => { @@ -31,17 +68,21 @@ export const cubeToUV = /*@__PURE__*/ Fn( ( [ v_immutable, texelSizeY_immutable } ).ElseIf( absV.x.greaterThanEqual( almostOne ), () => { - const signX = float( sign( v.x ) ).toVar(); - planar.x.assign( v.z.mul( signX ).add( mul( 2.0, signX ) ) ); + const signX = sign( v.x ); + planar.x.assign( v.z.mul( signX ).add( signX.mul( 2.0 ) ) ); } ).ElseIf( absV.y.greaterThanEqual( almostOne ), () => { - const signY = float( sign( v.y ) ).toVar(); - planar.x.assign( v.x.add( mul( 2.0, signY ) ).add( 2.0 ) ); + const signY = sign( v.y ); + planar.x.assign( v.x.add( signY.mul( 2.0 ) ).add( 2.0 ) ); planar.y.assign( v.z.mul( signY ).sub( 2.0 ) ); } ); + // Transform to UV space + + // scale := 0.5 / dim + // translate := ( center + 0.5 ) / dim return vec2( 0.125, 0.25 ).mul( planar ).add( vec2( 0.375, 0.75 ) ); } ).setLayout( { @@ -55,6 +96,8 @@ export const cubeToUV = /*@__PURE__*/ Fn( ( [ v_immutable, texelSizeY_immutable const BasicShadowMap = Fn( ( { depthTexture, shadowCoord, shadow } ) => { + // 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 lightToPosition = shadowCoord.xyz.toVar(); const lightToPositionLength = lightToPosition.length(); @@ -65,23 +108,24 @@ const BasicShadowMap = Fn( ( { depthTexture, shadowCoord, shadow } ) => { const result = float( 1.0 ).toVar(); - If( lightToPositionLength.sub( cameraFarLocal ).lessThanEqual( 0.0 ).and( lightToPositionLength.sub( cameraNearLocal ) ), () => { + If( lightToPositionLength.sub( cameraFarLocal ).lessThanEqual( 0.0 ).and( lightToPositionLength.sub( cameraNearLocal ).greaterThanEqual( 0.0 ) ), () => { - const dp = lightToPositionLength.sub( cameraNearLocal ).div( cameraFarLocal.sub( cameraNearLocal ) ).toVar(); + // dp = normalized distance from light to fragment position + const dp = lightToPositionLength.sub( cameraNearLocal ).div( cameraFarLocal.sub( cameraNearLocal ) ).toVar(); // need to clamp? dp.addAssign( bias ); + // bd3D = base direction 3D const bd3D = lightToPosition.normalize(); const texelSize = vec2( 1.0 ).div( mapSize.mul( vec2( 4.0, 2.0 ) ) ); - const uv = cubeToUV( bd3D, texelSize.y ).flipY(); - - const dpRemap = dp.add( 1 ).div( 2 ); // unchecked + const uv = cubeToUV( bd3D, texelSize.y ); - result.assign( texture( depthTexture, uv ).compare( dpRemap ).select( 1, 0 ) ); + // no percentage-closer filtering + result.assign( texture( depthTexture, uv.flipY() ).compare( dp ).select( 1, 0 ) ); } ); - return mix( 1.0, result, 1 ); + return result; } ); @@ -111,9 +155,9 @@ class PointShadowNode extends ShadowNode { } - setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) { + setupShadowFilter( builder, { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } ) { - return BasicShadowMap( { depthTexture, shadowCoord, shadow } ); + return BasicShadowMap( { shadowTexture, depthTexture, shadowCoord, shadow } ); } diff --git a/src/nodes/lighting/ShadowNode.js b/src/nodes/lighting/ShadowNode.js index 2c92a2a40b5305..010351f95422eb 100644 --- a/src/nodes/lighting/ShadowNode.js +++ b/src/nodes/lighting/ShadowNode.js @@ -4,7 +4,7 @@ import { uniform } from '../core/UniformNode.js'; import { float, vec2, vec3, vec4, If, int, Fn, nodeObject } from '../tsl/TSLBase.js'; import { reference } from '../accessors/ReferenceNode.js'; import { texture } from '../accessors/TextureNode.js'; -import { positionWorld } from '../accessors/Position.js'; +import { positionView, positionWorld } from '../accessors/Position.js'; import { transformedNormalWorld } from '../accessors/Normal.js'; import { mix, fract, step, max, clamp, sqrt } from '../math/MathNode.js'; import { add, sub } from '../math/OperatorNode.js'; @@ -15,7 +15,8 @@ import { Loop } from '../utils/LoopNode.js'; import { screenCoordinate } from '../display/ScreenNode.js'; import { HalfFloatType, LessCompare, RGFormat, VSMShadowMap, WebGPUCoordinateSystem } from '../../constants.js'; import { renderGroup } from '../core/UniformGroupNode.js'; -import { viewZToLogarithmicDepth } from '../display/ViewportDepthNode.js'; +import { depth, linearDepth, viewZToLogarithmicDepth, viewZToPerspectiveDepth } from '../display/ViewportDepthNode.js'; +import { modelViewProjection, objectPosition } from '../TSL.js'; const shadowWorldPosition = vec3().toVar( 'shadowWorldPosition' ); @@ -298,18 +299,28 @@ class ShadowNode extends Node { const { renderer } = builder; + const shadow = this.shadow; + const shadowMapType = renderer.shadowMap.type; + if ( _overrideMaterial === null ) { + const nearDistance = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup ); + const farDistance = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup ); + + const referencePosition = objectPosition( shadow.camera ); + + let dist = positionWorld.sub( referencePosition ).length(); + dist = dist.sub( nearDistance ).div( farDistance.sub( nearDistance ) ); + dist = dist.saturate(); // clamp to [ 0, 1 ] + _overrideMaterial = new NodeMaterial(); _overrideMaterial.fragmentNode = vec4( 0, 0, 0, 1 ); + _overrideMaterial.depthNode = dist; _overrideMaterial.isShadowNodeMaterial = true; // Use to avoid other overrideMaterial override material.fragmentNode unintentionally when using material.shadowNode _overrideMaterial.name = 'ShadowMaterial'; } - const shadow = this.shadow; - const shadowMapType = renderer.shadowMap.type; - const depthTexture = new DepthTexture( shadow.mapSize.width, shadow.mapSize.height ); depthTexture.compareFunction = LessCompare; @@ -364,7 +375,7 @@ class ShadowNode extends Node { const shadowDepthTexture = ( shadowMapType === VSMShadowMap ) ? this.vsmShadowMapHorizontal.texture : depthTexture; - const shadowNode = this.setupShadowFilter( builder, { filterFn, depthTexture: shadowDepthTexture, shadowCoord, shadow } ); + const shadowNode = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture: shadowDepthTexture, shadowCoord, shadow } ); const shadowColor = texture( shadowMap.texture, shadowCoord ); const shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar();