Просмотр исходного кода

WebGPURenderer: Add `PointShadowNode` (#29849)

* Add `PointShadowNode` (WIP)

* use webgpu

* fix hash

* cleanup

* Update ShadowNode.js

* revisions

* revision

* Update webgpu_lights_physical.jpg

* ShadowNode: Add `getShadowFilterFn`

* Add `Fn` sufix for shadow filters TSL function

* PointShadowNode: add percentage-closer filtering

* update example

* add __PURE__

* rename shadow filters and `export`

* Update webgpu_lights_physical.html
sunag 1 год назад
Родитель
Сommit
63949026de

+ 1 - 0
examples/files.json

@@ -335,6 +335,7 @@
 		"webgpu_lights_custom",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
+		"webgpu_lights_physical",
 		"webgpu_lights_rectarealight",
 		"webgpu_lights_selective",
 		"webgpu_lights_tiled",

BIN
examples/screenshots/webgpu_lights_physical.jpg


+ 297 - 0
examples/webgpu_lights_physical.html

@@ -0,0 +1,297 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - physical lights</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="container"></div>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Physically accurate incandescent bulb by <a href="http://clara.io" target="_blank" rel="noopener">Ben Houston</a><br />
+			Real world scale: Brick cube is 50 cm in size. Globe is 50 cm in diameter.<br/>
+			Reinhard inline tonemapping with real-world light falloff (decay = 2).
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let camera, scene, renderer, bulbLight, bulbMat, hemiLight, stats;
+			let ballMat, cubeMat, floorMat;
+
+			let previousShadowMap = false;
+
+			// ref for lumens: http://www.power-sure.com/lumens.htm
+			const bulbLuminousPowers = {
+				'110000 lm (1000W)': 110000,
+				'3500 lm (300W)': 3500,
+				'1700 lm (100W)': 1700,
+				'800 lm (60W)': 800,
+				'400 lm (40W)': 400,
+				'180 lm (25W)': 180,
+				'20 lm (4W)': 20,
+				'Off': 0
+			};
+
+			// ref for solar irradiances: https://en.wikipedia.org/wiki/Lux
+			const hemiLuminousIrradiances = {
+				'0.0001 lx (Moonless Night)': 0.0001,
+				'0.002 lx (Night Airglow)': 0.002,
+				'0.5 lx (Full Moon)': 0.5,
+				'3.4 lx (City Twilight)': 3.4,
+				'50 lx (Living Room)': 50,
+				'100 lx (Very Overcast)': 100,
+				'350 lx (Office Room)': 350,
+				'400 lx (Sunrise/Sunset)': 400,
+				'1000 lx (Overcast)': 1000,
+				'18000 lx (Daylight)': 18000,
+				'50000 lx (Direct Sun)': 50000
+			};
+
+			const params = {
+				shadows: true,
+				exposure: 0.68,
+				bulbPower: Object.keys( bulbLuminousPowers )[ 4 ],
+				hemiIrradiance: Object.keys( hemiLuminousIrradiances )[ 0 ]
+			};
+
+			init();
+
+			function init() {
+
+				const container = document.getElementById( 'container' );
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				//
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.x = - 4;
+				camera.position.z = 4;
+				camera.position.y = 2;
+
+				scene = new THREE.Scene();
+
+				const bulbGeometry = new THREE.SphereGeometry( 0.02, 16, 8 );
+				bulbLight = new THREE.PointLight( 0xffee88, 1, 100, 2 );
+
+				bulbMat = new THREE.MeshStandardMaterial( {
+					emissive: 0xffffee,
+					emissiveIntensity: 1,
+					color: 0x000000
+				} );
+				bulbLight.add( new THREE.Mesh( bulbGeometry, bulbMat ) );
+				bulbLight.position.set( 0, 2, 0 );
+				bulbLight.castShadow = true;
+				scene.add( bulbLight );
+
+				hemiLight = new THREE.HemisphereLight( 0xddeeff, 0x0f0e0d, 0.02 );
+				scene.add( hemiLight );
+
+				floorMat = new THREE.MeshStandardMaterial( {
+					roughness: 0.8,
+					color: 0xffffff,
+					metalness: 0.2,
+					bumpScale: 1
+				} );
+				const textureLoader = new THREE.TextureLoader();
+				textureLoader.load( 'textures/hardwood2_diffuse.jpg', function ( map ) {
+
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 10, 24 );
+					map.colorSpace = THREE.SRGBColorSpace;
+					floorMat.map = map;
+					floorMat.needsUpdate = true;
+
+				} );
+				textureLoader.load( 'textures/hardwood2_bump.jpg', function ( map ) {
+
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 10, 24 );
+					floorMat.bumpMap = map;
+					floorMat.needsUpdate = true;
+
+				} );
+				textureLoader.load( 'textures/hardwood2_roughness.jpg', function ( map ) {
+
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 10, 24 );
+					floorMat.roughnessMap = map;
+					floorMat.needsUpdate = true;
+
+				} );
+
+				cubeMat = new THREE.MeshStandardMaterial( {
+					roughness: 0.7,
+					color: 0xffffff,
+					bumpScale: 1,
+					metalness: 0.2
+				} );
+				textureLoader.load( 'textures/brick_diffuse.jpg', function ( map ) {
+
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 1, 1 );
+					map.colorSpace = THREE.SRGBColorSpace;
+					cubeMat.map = map;
+					cubeMat.needsUpdate = true;
+
+				} );
+				textureLoader.load( 'textures/brick_bump.jpg', function ( map ) {
+
+					map.wrapS = THREE.RepeatWrapping;
+					map.wrapT = THREE.RepeatWrapping;
+					map.anisotropy = 4;
+					map.repeat.set( 1, 1 );
+					cubeMat.bumpMap = map;
+					cubeMat.needsUpdate = true;
+
+				} );
+
+				ballMat = new THREE.MeshStandardMaterial( {
+					color: 0xffffff,
+					roughness: 0.5,
+					metalness: 1.0
+				} );
+				textureLoader.load( 'textures/planets/earth_atmos_2048.jpg', function ( map ) {
+
+					map.anisotropy = 4;
+					map.colorSpace = THREE.SRGBColorSpace;
+					ballMat.map = map;
+					ballMat.needsUpdate = true;
+
+				} );
+				textureLoader.load( 'textures/planets/earth_specular_2048.jpg', function ( map ) {
+
+					map.anisotropy = 4;
+					map.colorSpace = THREE.SRGBColorSpace;
+					ballMat.metalnessMap = map;
+					ballMat.needsUpdate = true;
+
+				} );
+
+				const floorGeometry = new THREE.PlaneGeometry( 20, 20 );
+				const floorMesh = new THREE.Mesh( floorGeometry, floorMat );
+				floorMesh.receiveShadow = true;
+				floorMesh.rotation.x = - Math.PI / 2.0;
+				scene.add( floorMesh );
+
+				const ballGeometry = new THREE.SphereGeometry( 0.25, 32, 32 );
+				const ballMesh = new THREE.Mesh( ballGeometry, ballMat );
+				ballMesh.position.set( 1, 0.25, 1 );
+				ballMesh.rotation.y = Math.PI;
+				ballMesh.castShadow = true;
+				scene.add( ballMesh );
+
+				const boxGeometry = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
+				const boxMesh = new THREE.Mesh( boxGeometry, cubeMat );
+				boxMesh.position.set( - 0.5, 0.25, - 1 );
+				boxMesh.castShadow = true;
+				scene.add( boxMesh );
+
+				const boxMesh2 = new THREE.Mesh( boxGeometry, cubeMat );
+				boxMesh2.position.set( 0, 0.25, - 5 );
+				boxMesh2.castShadow = true;
+				scene.add( boxMesh2 );
+
+				const boxMesh3 = new THREE.Mesh( boxGeometry, cubeMat );
+				boxMesh3.position.set( 7, 0.25, 0 );
+				boxMesh3.castShadow = true;
+				scene.add( boxMesh3 );
+
+				//
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.shadowMap.enabled = true;
+				renderer.toneMapping = THREE.ReinhardToneMapping;
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 1;
+				controls.maxDistance = 20;
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				//
+
+				const gui = new GUI();
+
+				gui.add( params, 'hemiIrradiance', Object.keys( hemiLuminousIrradiances ) );
+				gui.add( params, 'bulbPower', Object.keys( bulbLuminousPowers ) );
+				gui.add( params, 'exposure', 0, 1 );
+				gui.add( params, 'shadows' );
+				gui.open();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				renderer.toneMappingExposure = Math.pow( params.exposure, 5.0 ); // to allow for very bright scenes.
+				renderer.shadowMap.enabled = params.shadows;
+				bulbLight.castShadow = params.shadows;
+
+				if ( params.shadows !== previousShadowMap ) {
+
+					ballMat.needsUpdate = true;
+					cubeMat.needsUpdate = true;
+					floorMat.needsUpdate = true;
+					previousShadowMap = params.shadows;
+
+				}
+
+				bulbLight.power = bulbLuminousPowers[ params.bulbPower ];
+				bulbMat.emissiveIntensity = bulbLight.intensity / Math.pow( 0.02, 2.0 ); // convert from intensity to irradiance at bulb surface
+
+				hemiLight.intensity = hemiLuminousIrradiances[ params.hemiIrradiance ];
+				const time = Date.now() * 0.0005;
+
+				bulbLight.position.y = Math.cos( time ) * 0.75 + 1.25;
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 13 - 10
src/materials/nodes/manager/NodeMaterialObserver.js

@@ -85,7 +85,7 @@ class NodeMaterialObserver {
 
 		if ( data === undefined ) {
 
-			const { geometry, material } = renderObject;
+			const { geometry, material, object } = renderObject;
 
 			data = {
 				material: this.getMaterialData( material ),
@@ -94,18 +94,18 @@ class NodeMaterialObserver {
 					indexVersion: geometry.index ? geometry.index.version : null,
 					drawRange: { start: geometry.drawRange.start, count: geometry.drawRange.count }
 				},
-				worldMatrix: renderObject.object.matrixWorld.clone()
+				worldMatrix: object.matrixWorld.clone()
 			};
 
-			if ( renderObject.object.center ) {
+			if ( object.center ) {
 
-				data.center = renderObject.object.center.clone();
+				data.center = object.center.clone();
 
 			}
 
-			if ( renderObject.object.morphTargetInfluences ) {
+			if ( object.morphTargetInfluences ) {
 
-				data.morphTargetInfluences = renderObject.object.morphTargetInfluences.slice();
+				data.morphTargetInfluences = object.morphTargetInfluences.slice();
 
 			}
 
@@ -289,7 +289,8 @@ class NodeMaterialObserver {
 
 		}
 
-		// Compare each attribute
+		// compare each attribute
+
 		for ( const name of storedAttributeNames ) {
 
 			const storedAttributeData = storedAttributes[ name ];
@@ -297,7 +298,7 @@ class NodeMaterialObserver {
 
 			if ( attribute === undefined ) {
 
-				// Attribute was removed
+				// attribute was removed
 				delete storedAttributes[ name ];
 				return false;
 
@@ -312,7 +313,8 @@ class NodeMaterialObserver {
 
 		}
 
-		// Check index
+		// check index
+
 		const index = geometry.index;
 		const storedIndexVersion = storedGeometryData.indexVersion;
 		const currentIndexVersion = index ? index.version : null;
@@ -324,7 +326,8 @@ class NodeMaterialObserver {
 
 		}
 
-		// Check drawRange
+		// check drawRange
+
 		if ( storedGeometryData.drawRange.start !== geometry.drawRange.start || storedGeometryData.drawRange.count !== geometry.drawRange.count ) {
 
 			storedGeometryData.drawRange.start = geometry.drawRange.start;

+ 7 - 1
src/nodes/lighting/AnalyticLightNode.js

@@ -47,6 +47,12 @@ class AnalyticLightNode extends LightingNode {
 
 	}
 
+	setupShadowNode() {
+
+		return shadow( this.light );
+
+	}
+
 	setupShadow( builder ) {
 
 		const { renderer } = builder;
@@ -67,7 +73,7 @@ class AnalyticLightNode extends LightingNode {
 
 			} else {
 
-				shadowNode = shadow( this.light );
+				shadowNode = this.setupShadowNode( builder );
 
 			}
 

+ 10 - 1
src/nodes/lighting/PointLightNode.js

@@ -5,6 +5,7 @@ import { lightViewPosition } from '../accessors/Lights.js';
 import { positionView } from '../accessors/Position.js';
 import { Fn } from '../tsl/TSLBase.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
+import { pointShadow } from './PointShadowNode.js';
 
 export const directPointLight = Fn( ( { color, lightViewPosition, cutoffDistance, decayExponent }, builder ) => {
 
@@ -61,7 +62,15 @@ class PointLightNode extends AnalyticLightNode {
 
 	}
 
-	setup() {
+	setupShadowNode() {
+
+		return pointShadow( this.light );
+
+	}
+
+	setup( builder ) {
+
+		super.setup( builder );
 
 		directPointLight( {
 			color: this.colorNode,

+ 254 - 0
src/nodes/lighting/PointShadowNode.js

@@ -0,0 +1,254 @@
+import ShadowNode from './ShadowNode.js';
+import { uniform } from '../core/UniformNode.js';
+import { float, vec2, If, Fn, nodeObject } from '../tsl/TSLBase.js';
+import { reference } from '../accessors/ReferenceNode.js';
+import { texture } from '../accessors/TextureNode.js';
+import { max, abs, sign } from '../math/MathNode.js';
+import { sub, div } from '../math/OperatorNode.js';
+import { renderGroup } from '../core/UniformGroupNode.js';
+import { Vector2 } from '../../math/Vector2.js';
+import { Vector4 } from '../../math/Vector4.js';
+import { Color } from '../../math/Color.js';
+import { BasicShadowMap } from '../../constants.js';
+
+const _clearColor = /*@__PURE__*/ new Color();
+
+// 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 scaleToCube = div( 1.0, max( absV.x, max( absV.y, absV.z ) ) );
+	absV.mulAssign( scaleToCube );
+
+	// 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 = texelSizeY.mul( 1.5 );
+	const almostOne = almostATexel.oneMinus();
+
+	If( absV.z.greaterThanEqual( almostOne ), () => {
+
+		If( v.z.greaterThan( 0.0 ), () => {
+
+			planar.x.assign( sub( 4.0, v.x ) );
+
+		} );
+
+	} ).ElseIf( absV.x.greaterThanEqual( almostOne ), () => {
+
+		const signX = sign( v.x );
+		planar.x.assign( v.z.mul( signX ).add( signX.mul( 2.0 ) ) );
+
+	} ).ElseIf( absV.y.greaterThanEqual( almostOne ), () => {
+
+		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 ) ).flipY();
+
+} ).setLayout( {
+	name: 'cubeToUV',
+	type: 'vec2',
+	inputs: [
+		{ name: 'pos', type: 'vec3' },
+		{ name: 'texelSizeY', type: 'float' }
+	]
+} );
+
+export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize } ) => {
+
+	return texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp );
+
+} );
+
+export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize, shadow } ) => {
+
+	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
+	const offset = vec2( - 1.0, 1.0 ).mul( radius ).mul( texelSize.y );
+
+	return texture( depthTexture, cubeToUV( bd3D.add( offset.xyy ), texelSize.y ) ).compare( dp )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyy ), texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xyx ), texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyx ), texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxy ), texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxy ), texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxx ), texelSize.y ) ).compare( dp ) )
+		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxx ), texelSize.y ) ).compare( dp ) )
+		.mul( 1.0 / 9.0 );
+
+} );
+
+const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, 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();
+
+	const cameraNearLocal = uniform( 'float' ).onRenderUpdate( () => shadow.camera.near );
+	const cameraFarLocal = uniform( 'float' ).onRenderUpdate( () => shadow.camera.far );
+	const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
+	const mapSize = uniform( shadow.mapSize ).setGroup( renderGroup );
+
+	const result = float( 1.0 ).toVar();
+
+	If( lightToPositionLength.sub( cameraFarLocal ).lessThanEqual( 0.0 ).and( lightToPositionLength.sub( cameraNearLocal ).greaterThanEqual( 0.0 ) ), () => {
+
+		// 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 ) ) );
+
+		// percentage-closer filtering
+		result.assign( filterFn( { depthTexture, bd3D, dp, texelSize, shadow } ) );
+
+	} );
+
+	return result;
+
+} );
+
+const _viewport = /*@__PURE__*/ new Vector4();
+const _viewportSize = /*@__PURE__*/ new Vector2();
+const _shadowMapSize = /*@__PURE__*/ new Vector2();
+
+//
+
+class PointShadowNode extends ShadowNode {
+
+	static get type() {
+
+		return 'PointShadowNode';
+
+	}
+
+	constructor( light, shadow = null ) {
+
+		super( light, shadow );
+
+	}
+
+	getShadowFilterFn( type ) {
+
+		return type === BasicShadowMap ? BasicPointShadowFilter : PointShadowFilter;
+
+	}
+
+	setupShadowCoord( builder, shadowPosition ) {
+
+		return shadowPosition;
+
+	}
+
+	setupShadowFilter( builder, { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } ) {
+
+		return pointShadowFilter( { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } );
+
+	}
+
+	renderShadow( frame ) {
+
+		const { shadow, shadowMap, light } = this;
+		const { renderer, scene } = frame;
+
+		const shadowFrameExtents = shadow.getFrameExtents();
+
+		_shadowMapSize.copy( shadow.mapSize );
+		_shadowMapSize.multiply( shadowFrameExtents );
+
+		shadowMap.setSize( _shadowMapSize.width, _shadowMapSize.height );
+
+		_viewportSize.copy( shadow.mapSize );
+
+		//
+
+		const previousAutoClear = renderer.autoClear;
+
+		const previousClearColor = renderer.getClearColor( _clearColor );
+		const previousClearAlpha = renderer.getClearAlpha();
+
+		renderer.autoClear = false;
+		renderer.setClearColor( shadow.clearColor, shadow.clearAlpha );
+		renderer.clear();
+
+		const viewportCount = shadow.getViewportCount();
+
+		for ( let vp = 0; vp < viewportCount; vp ++ ) {
+
+			const viewport = shadow.getViewport( vp );
+
+			const x = _viewportSize.x * viewport.x;
+			const y = _shadowMapSize.y - _viewportSize.y - ( _viewportSize.y * viewport.y );
+
+			_viewport.set(
+				x,
+				y,
+				_viewportSize.x * viewport.z,
+				_viewportSize.y * viewport.w
+			);
+
+			shadowMap.viewport.copy( _viewport );
+
+			shadow.updateMatrices( light, vp );
+
+			renderer.render( scene, shadow.camera );
+
+		}
+
+		//
+
+		renderer.autoClear = previousAutoClear;
+		renderer.setClearColor( previousClearColor, previousClearAlpha );
+
+	}
+
+}
+
+export default PointShadowNode;
+
+export const pointShadow = ( light, shadow ) => nodeObject( new PointShadowNode( light, shadow ) );

+ 129 - 61
src/nodes/lighting/ShadowNode.js

@@ -16,16 +16,40 @@ 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 { objectPosition } from '../accessors/Object3DNode.js';
 
-const shadowPosition = vec3().toVar( 'shadowPosition' );
+const shadowWorldPosition = /*@__PURE__*/ vec3().toVar( 'shadowWorldPosition' );
 
-const BasicShadowMap = Fn( ( { depthTexture, shadowCoord } ) => {
+const linearDistance = /*@__PURE__*/ Fn( ( [ position, cameraNear, cameraFar ] ) => {
+
+	let dist = positionWorld.sub( position ).length();
+	dist = dist.sub( cameraNear ).div( cameraFar.sub( cameraNear ) );
+	dist = dist.saturate(); // clamp to [ 0, 1 ]
+
+	return dist;
+
+} );
+
+const linearShadowDistance = ( light ) => {
+
+	const camera = light.shadow.camera;
+
+	const nearDistance = reference( 'near', 'float', camera ).setGroup( renderGroup );
+	const farDistance = reference( 'far', 'float', camera ).setGroup( renderGroup );
+
+	const referencePosition = objectPosition( light );
+
+	return linearDistance( referencePosition, nearDistance, farDistance );
+
+};
+
+export const BasicShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord } ) => {
 
 	return texture( depthTexture, shadowCoord.xy ).compare( shadowCoord.z );
 
 } );
 
-const PCFShadowMap = Fn( ( { depthTexture, shadowCoord, shadow } ) => {
+export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow } ) => {
 
 	const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );
 
@@ -64,7 +88,7 @@ const PCFShadowMap = Fn( ( { depthTexture, shadowCoord, shadow } ) => {
 
 } );
 
-const PCFSoftShadowMap = Fn( ( { depthTexture, shadowCoord, shadow } ) => {
+export const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow } ) => {
 
 	const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );
 
@@ -122,7 +146,7 @@ const PCFSoftShadowMap = Fn( ( { depthTexture, shadowCoord, shadow } ) => {
 
 // VSM
 
-const VSMShadowMapNode = Fn( ( { depthTexture, shadowCoord } ) => {
+export const VSMShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord } ) => {
 
 	const occlusion = float( 1 ).toVar();
 
@@ -144,7 +168,7 @@ const VSMShadowMapNode = Fn( ( { depthTexture, shadowCoord } ) => {
 
 } );
 
-const VSMPassVertical = Fn( ( { samples, radius, size, shadowPass } ) => {
+const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass } ) => {
 
 	const mean = float( 0 ).toVar();
 	const squaredMean = float( 0 ).toVar();
@@ -170,7 +194,7 @@ const VSMPassVertical = Fn( ( { samples, radius, size, shadowPass } ) => {
 
 } );
 
-const VSMPassHorizontal = Fn( ( { samples, radius, size, shadowPass } ) => {
+const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass } ) => {
 
 	const mean = float( 0 ).toVar();
 	const squaredMean = float( 0 ).toVar();
@@ -196,7 +220,7 @@ const VSMPassHorizontal = Fn( ( { samples, radius, size, shadowPass } ) => {
 
 } );
 
-const _shadowFilterLib = [ BasicShadowMap, PCFShadowMap, PCFSoftShadowMap, VSMShadowMapNode ];
+const _shadowFilterLib = [ BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter ];
 
 //
 
@@ -233,22 +257,93 @@ class ShadowNode extends Node {
 
 	}
 
+	setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
+
+		const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
+			.and( shadowCoord.x.lessThanEqual( 1 ) )
+			.and( shadowCoord.y.greaterThanEqual( 0 ) )
+			.and( shadowCoord.y.lessThanEqual( 1 ) )
+			.and( shadowCoord.z.lessThanEqual( 1 ) );
+
+		const shadowNode = filterFn( { depthTexture, shadowCoord, shadow } );
+
+		return frustumTest.select( shadowNode, float( 1 ) );
+
+	}
+
+	setupShadowCoord( builder, shadowPosition ) {
+
+		const { shadow } = this;
+		const { renderer } = builder;
+
+		const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
+
+		let shadowCoord = shadowPosition;
+		let coordZ;
+
+		if ( shadow.camera.isOrthographicCamera || renderer.logarithmicDepthBuffer !== true ) {
+
+			shadowCoord = shadowCoord.xyz.div( shadowCoord.w );
+
+			coordZ = shadowCoord.z;
+
+			if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {
+
+				coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Conversion [ 0, 1 ] to [ - 1, 1 ]
+
+			}
+
+		} else {
+
+			const w = shadowCoord.w;
+			shadowCoord = shadowCoord.xy.div( w ); // <-- Only divide X/Y coords since we don't need Z
+
+			// The normally available "cameraNear" and "cameraFar" nodes cannot be used here because they do not get
+			// updated to use the shadow camera. So, we have to declare our own "local" ones here.
+			// TODO: How do we get the cameraNear/cameraFar nodes to use the shadow camera so we don't have to declare local ones here?
+			const cameraNearLocal = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup );
+			const cameraFarLocal = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup );
+
+			coordZ = viewZToLogarithmicDepth( w.negate(), cameraNearLocal, cameraFarLocal );
+
+		}
+
+		shadowCoord = vec3(
+			shadowCoord.x,
+			shadowCoord.y.oneMinus(), // follow webgpu standards
+			coordZ.add( bias )
+		);
+
+		return shadowCoord;
+
+	}
+
+	getShadowFilterFn( type ) {
+
+		return _shadowFilterLib[ type ];
+
+	}
+
 	setupShadow( builder ) {
 
 		const { renderer } = builder;
 
+		const { light, shadow } = this;
+
+		const shadowMapType = renderer.shadowMap.type;
+
 		if ( _overrideMaterial === null ) {
 
+			const depthNode = light.isPointLight ? linearShadowDistance( light ) : null;
+
 			_overrideMaterial = new NodeMaterial();
 			_overrideMaterial.fragmentNode = vec4( 0, 0, 0, 1 );
+			_overrideMaterial.depthNode = depthNode;
 			_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;
 
@@ -286,55 +381,14 @@ class ShadowNode extends Node {
 		//
 
 		const shadowIntensity = reference( 'intensity', 'float', shadow ).setGroup( renderGroup );
-		const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
 		const normalBias = reference( 'normalBias', 'float', shadow ).setGroup( renderGroup );
 
-		let shadowCoord = uniform( shadow.matrix ).setGroup( renderGroup ).mul( shadowPosition.add( transformedNormalWorld.mul( normalBias ) ) );
-
-		let coordZ;
-
-		if ( shadow.camera.isOrthographicCamera || renderer.logarithmicDepthBuffer !== true ) {
-
-			shadowCoord = shadowCoord.xyz.div( shadowCoord.w );
-
-			coordZ = shadowCoord.z;
-
-			if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {
-
-				coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Conversion [ 0, 1 ] to [ - 1, 1 ]
-
-			}
-
-		} else {
-
-			const w = shadowCoord.w;
-			shadowCoord = shadowCoord.xy.div( w ); // <-- Only divide X/Y coords since we don't need Z
-
-			// The normally available "cameraNear" and "cameraFar" nodes cannot be used here because they do not get
-			// updated to use the shadow camera. So, we have to declare our own "local" ones here.
-			// TODO: How do we get the cameraNear/cameraFar nodes to use the shadow camera so we don't have to declare local ones here?
-			const cameraNearLocal = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup );
-			const cameraFarLocal = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup );
-
-			coordZ = viewZToLogarithmicDepth( w.negate(), cameraNearLocal, cameraFarLocal );
-
-		}
-
-		shadowCoord = vec3(
-			shadowCoord.x,
-			shadowCoord.y.oneMinus(), // follow webgpu standards
-			coordZ.add( bias )
-		);
-
-		const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
-			.and( shadowCoord.x.lessThanEqual( 1 ) )
-			.and( shadowCoord.y.greaterThanEqual( 0 ) )
-			.and( shadowCoord.y.lessThanEqual( 1 ) )
-			.and( shadowCoord.z.lessThanEqual( 1 ) );
+		const shadowPosition = uniform( shadow.matrix ).setGroup( renderGroup ).mul( shadowWorldPosition.add( transformedNormalWorld.mul( normalBias ) ) );
+		const shadowCoord = this.setupShadowCoord( builder, shadowPosition );
 
 		//
 
-		const filterFn = shadow.filterNode || _shadowFilterLib[ renderer.shadowMap.type ] || null;
+		const filterFn = shadow.filterNode || this.getShadowFilterFn( renderer.shadowMap.type ) || null;
 
 		if ( filterFn === null ) {
 
@@ -342,8 +396,11 @@ class ShadowNode extends Node {
 
 		}
 
+		const shadowDepthTexture = ( shadowMapType === VSMShadowMap ) ? this.vsmShadowMapHorizontal.texture : depthTexture;
+
+		const shadowNode = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture: shadowDepthTexture, shadowCoord, shadow } );
+
 		const shadowColor = texture( shadowMap.texture, shadowCoord );
-		const shadowNode = frustumTest.select( filterFn( { depthTexture: ( shadowMapType === VSMShadowMap ) ? this.vsmShadowMapHorizontal.texture : depthTexture, shadowCoord, shadow } ), float( 1 ) );
 		const shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar();
 
 		this.shadowMap = shadowMap;
@@ -359,7 +416,7 @@ class ShadowNode extends Node {
 
 		return Fn( ( { material } ) => {
 
-			shadowPosition.assign( material.shadowPositionNode || positionWorld );
+			shadowWorldPosition.assign( material.shadowPositionNode || positionWorld );
 
 			let node = this._node;
 
@@ -387,6 +444,19 @@ class ShadowNode extends Node {
 
 	}
 
+	renderShadow( frame ) {
+
+		const { shadow, shadowMap, light } = this;
+		const { renderer, scene } = frame;
+
+		shadow.updateMatrices( light );
+
+		shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height );
+
+		renderer.render( scene, shadow.camera );
+
+	}
+
 	updateShadow( frame ) {
 
 		const { shadowMap, light, shadow } = this;
@@ -401,9 +471,6 @@ class ShadowNode extends Node {
 
 		scene.overrideMaterial = _overrideMaterial;
 
-		shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height );
-
-		shadow.updateMatrices( light );
 		shadow.camera.layers.mask = camera.layers.mask;
 
 		const currentRenderTarget = renderer.getRenderTarget();
@@ -420,7 +487,8 @@ class ShadowNode extends Node {
 		} );
 
 		renderer.setRenderTarget( shadowMap );
-		renderer.render( scene, shadow.camera );
+
+		this.renderShadow( frame );
 
 		renderer.setRenderObjectFunction( currentRenderObjectFunction );
 

+ 2 - 0
src/renderers/common/RenderObject.js

@@ -379,6 +379,8 @@ export default class RenderObject {
 
 		}
 
+		cacheKey += object.receiveShadow + ',';
+
 		return hashString( cacheKey );
 
 	}

粤ICP备19079148号