Explorar el Código

WebGPURenderer: PCF shadow filtering with Vogel disk sampling and IGN. (#32381)

mrdoob hace 7 meses
padre
commit
d5f3ee98a9

+ 1 - 0
src/Three.TSL.js

@@ -203,6 +203,7 @@ export const getDistanceAttenuation = TSL.getDistanceAttenuation;
 export const getGeometryRoughness = TSL.getGeometryRoughness;
 export const getGeometryRoughness = TSL.getGeometryRoughness;
 export const getNormalFromDepth = TSL.getNormalFromDepth;
 export const getNormalFromDepth = TSL.getNormalFromDepth;
 export const interleavedGradientNoise = TSL.interleavedGradientNoise;
 export const interleavedGradientNoise = TSL.interleavedGradientNoise;
+export const vogelDiskSample = TSL.vogelDiskSample;
 export const getParallaxCorrectNormal = TSL.getParallaxCorrectNormal;
 export const getParallaxCorrectNormal = TSL.getParallaxCorrectNormal;
 export const getRoughness = TSL.getRoughness;
 export const getRoughness = TSL.getRoughness;
 export const getScreenPosition = TSL.getScreenPosition;
 export const getScreenPosition = TSL.getScreenPosition;

+ 41 - 14
src/nodes/lighting/PointShadowNode.js

@@ -1,6 +1,6 @@
 import ShadowNode from './ShadowNode.js';
 import ShadowNode from './ShadowNode.js';
 import { uniform } from '../core/UniformNode.js';
 import { uniform } from '../core/UniformNode.js';
-import { float, vec2, If, Fn, nodeObject } from '../tsl/TSLBase.js';
+import { float, vec3, If, Fn, nodeObject } from '../tsl/TSLBase.js';
 import { reference } from '../accessors/ReferenceNode.js';
 import { reference } from '../accessors/ReferenceNode.js';
 import { cubeTexture } from '../accessors/CubeTextureNode.js';
 import { cubeTexture } from '../accessors/CubeTextureNode.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
@@ -9,6 +9,9 @@ import { Vector3 } from '../../math/Vector3.js';
 import { Color } from '../../math/Color.js';
 import { Color } from '../../math/Color.js';
 import { BasicShadowMap, LessCompare, WebGPUCoordinateSystem } from '../../constants.js';
 import { BasicShadowMap, LessCompare, WebGPUCoordinateSystem } from '../../constants.js';
 import { CubeDepthTexture } from '../../textures/CubeDepthTexture.js';
 import { CubeDepthTexture } from '../../textures/CubeDepthTexture.js';
+import { screenCoordinate } from '../display/ScreenNode.js';
+import { interleavedGradientNoise, vogelDiskSample } from '../utils/PostProcessingUtils.js';
+import { abs, normalize, cross } from '../math/MathNode.js';
 
 
 const _clearColor = /*@__PURE__*/ new Color();
 const _clearColor = /*@__PURE__*/ new Color();
 const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
 const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
@@ -45,24 +48,48 @@ export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D,
 
 
 } );
 } );
 
 
+/**
+ * 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.dp - The depth value to compare against.
+ * @param {LightShadow} inputs.shadow - The light shadow.
+ * @return {Node<float>} The filtering result.
+ */
 export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => {
 export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, shadow } ) => {
 
 
 	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
 	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
 	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
 	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
 
 
-	const texelSize = float( 1 ).div( mapSize.x );
-	const offset = vec2( - 1.0, 1.0 ).mul( radius ).mul( texelSize );
-
-	return cubeTexture( depthTexture, bd3D.add( offset.xyy ) ).compare( dp )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.yyy ) ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.xyx ) ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.yyx ) ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.xxy ) ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.yxy ) ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.xxx ) ).compare( dp ) )
-		.add( cubeTexture( depthTexture, bd3D.add( offset.yxx ) ).compare( dp ) )
-		.mul( 1.0 / 9.0 );
+	const texelSize = radius.div( mapSize.x );
+
+	// 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 ) ) ) );
+	const bitangent = cross( bd3D, tangent );
+
+	// Use IGN to rotate sampling pattern per pixel (phi = IGN * 2π)
+	const phi = interleavedGradientNoise( screenCoordinate.xy ).mul( 6.28318530718 );
+
+	// 5 samples using Vogel disk distribution in tangent space
+	const sample0 = vogelDiskSample( 0, 5, phi );
+	const sample1 = vogelDiskSample( 1, 5, phi );
+	const sample2 = vogelDiskSample( 2, 5, phi );
+	const sample3 = vogelDiskSample( 3, 5, phi );
+	const sample4 = vogelDiskSample( 4, 5, phi );
+
+	return cubeTexture( depthTexture, bd3D.add( tangent.mul( sample0.x ).add( bitangent.mul( sample0.y ) ).mul( texelSize ) ) ).compare( dp )
+		.add( cubeTexture( depthTexture, bd3D.add( tangent.mul( sample1.x ).add( bitangent.mul( sample1.y ) ).mul( texelSize ) ) ).compare( dp ) )
+		.add( cubeTexture( depthTexture, bd3D.add( tangent.mul( sample2.x ).add( bitangent.mul( sample2.y ) ).mul( texelSize ) ) ).compare( dp ) )
+		.add( cubeTexture( depthTexture, bd3D.add( tangent.mul( sample3.x ).add( bitangent.mul( sample3.y ) ).mul( texelSize ) ) ).compare( dp ) )
+		.add( cubeTexture( depthTexture, bd3D.add( tangent.mul( sample4.x ).add( bitangent.mul( sample4.y ) ).mul( texelSize ) ) ).compare( dp ) )
+		.mul( 1.0 / 5.0 );
 
 
 } );
 } );
 
 

+ 18 - 27
src/nodes/lighting/ShadowFilterNode.js

@@ -7,6 +7,8 @@ import { renderGroup } from '../core/UniformGroupNode.js';
 import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
 import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
 import { objectPosition } from '../accessors/Object3DNode.js';
 import { objectPosition } from '../accessors/Object3DNode.js';
 import { positionWorld } from '../accessors/Position.js';
 import { positionWorld } from '../accessors/Position.js';
+import { screenCoordinate } from '../display/ScreenNode.js';
+import { interleavedGradientNoise, vogelDiskSample } from '../utils/PostProcessingUtils.js';
 
 
 const shadowMaterialLib = /*@__PURE__*/ new WeakMap();
 const shadowMaterialLib = /*@__PURE__*/ new WeakMap();
 
 
@@ -35,7 +37,11 @@ export const BasicShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord
 } );
 } );
 
 
 /**
 /**
- * A shadow filtering function performing PCF filtering.
+ * A shadow filtering function performing PCF filtering with Vogel disk sampling and IGN.
+ *
+ * Uses 5 samples distributed via Vogel disk pattern, rotated per-pixel using Interleaved
+ * Gradient Noise (IGN) to break up banding artifacts. Combined with hardware PCF (4-tap
+ * filtering per sample), this effectively provides 20 filtered taps with better distribution.
  *
  *
  * @method
  * @method
  * @param {Object} inputs - The input parameter object.
  * @param {Object} inputs - The input parameter object.
@@ -64,34 +70,19 @@ export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord,
 	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
 	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
 
 
 	const texelSize = vec2( 1 ).div( mapSize );
 	const texelSize = vec2( 1 ).div( mapSize );
-	const dx0 = texelSize.x.negate().mul( radius );
-	const dy0 = texelSize.y.negate().mul( radius );
-	const dx1 = texelSize.x.mul( radius );
-	const dy1 = texelSize.y.mul( radius );
-	const dx2 = dx0.div( 2 );
-	const dy2 = dy0.div( 2 );
-	const dx3 = dx1.div( 2 );
-	const dy3 = dy1.div( 2 );
+	const radiusScaled = radius.mul( texelSize.x );
+
+	// Use IGN to rotate sampling pattern per pixel (phi = IGN * 2π)
+	const phi = interleavedGradientNoise( screenCoordinate.xy ).mul( 6.28318530718 );
 
 
+	// 5 samples using Vogel disk distribution
 	return add(
 	return add(
-		depthCompare( shadowCoord.xy.add( vec2( dx0, dy0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx1, dy0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx2, dy2 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy2 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx3, dy2 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx0, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx2, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy, shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx3, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx1, 0 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx2, dy3 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy3 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx3, dy3 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx0, dy1 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( 0, dy1 ) ), shadowCoord.z ),
-		depthCompare( shadowCoord.xy.add( vec2( dx1, dy1 ) ), shadowCoord.z )
-	).mul( 1 / 17 );
+		depthCompare( shadowCoord.xy.add( vogelDiskSample( 0, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vogelDiskSample( 1, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vogelDiskSample( 2, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vogelDiskSample( 3, 5, phi ).mul( radiusScaled ) ), shadowCoord.z ),
+		depthCompare( shadowCoord.xy.add( vogelDiskSample( 4, 5, phi ).mul( radiusScaled ) ), shadowCoord.z )
+	).mul( 1 / 5 );
 
 
 } );
 } );
 
 

+ 33 - 1
src/nodes/utils/PostProcessingUtils.js

@@ -1,4 +1,5 @@
-import { abs, cross, float, Fn, normalize, ivec2, sub, vec2, vec3, vec4, fract, dot } from '../tsl/TSLBase.js';
+import { abs, cross, float, Fn, normalize, ivec2, sub, vec2, vec3, vec4, fract, dot, cos, sin } from '../tsl/TSLBase.js';
+import { sqrt } from '../math/MathNode.js';
 import { textureSize } from '../accessors/TextureSizeNode.js';
 import { textureSize } from '../accessors/TextureSizeNode.js';
 import { textureLoad } from '../accessors/TextureNode.js';
 import { textureLoad } from '../accessors/TextureNode.js';
 import { WebGPUCoordinateSystem } from '../../constants.js';
 import { WebGPUCoordinateSystem } from '../../constants.js';
@@ -120,3 +121,34 @@ export const interleavedGradientNoise = Fn( ( [ position ] ) => {
 		{ name: 'position', type: 'vec2' }
 		{ name: 'position', type: 'vec2' }
 	]
 	]
 } );
 } );
+
+/**
+ * Vogel disk sampling for uniform circular distribution.
+ *
+ * This function generates sample points distributed uniformly on a disk using the golden angle,
+ * resulting in an efficient low-discrepancy sequence for sampling. The rotation parameter (phi)
+ * allows randomizing the pattern per-pixel when combined with IGN.
+ *
+ * @tsl
+ * @function
+ * @param {Node<int>} sampleIndex - The index of the current sample (0-based).
+ * @param {Node<int>} samplesCount - The total number of samples.
+ * @param {Node<float>} phi - Rotation angle in radians (typically from IGN * 2π).
+ * @return {Node<vec2>} A 2D point on the unit disk.
+ */
+export const vogelDiskSample = Fn( ( [ sampleIndex, samplesCount, phi ] ) => {
+
+	const goldenAngle = float( 2.399963229728653 ); // 2π * (2 - φ) where φ is golden ratio
+	const r = sqrt( float( sampleIndex ).add( 0.5 ) ).div( sqrt( float( samplesCount ) ) );
+	const theta = float( sampleIndex ).mul( goldenAngle ).add( phi );
+	return vec2( cos( theta ), sin( theta ) ).mul( r );
+
+} ).setLayout( {
+	name: 'vogelDiskSample',
+	type: 'vec2',
+	inputs: [
+		{ name: 'sampleIndex', type: 'int' },
+		{ name: 'samplesCount', type: 'int' },
+		{ name: 'phi', type: 'float' }
+	]
+} );

粤ICP备19079148号