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

WebGPURenderer: Volumetric lighting (#30530)

* volumetric fog - wip

* performance-test

* revision

* Update webgpu_volume_cloud4.html

* revision shadow

* revisions

* rename

* volumetric lighting

* rename to live test

* improve names

* update shadow intensity

* rev

* update title

* update description

* description

* rev

* improve example parameters

* update parameters

* add smoke amount

* update example

* fix name

* add some description

* update parameters

* update screenshot

* improve ray marching approach and fixes TODO

* TSL: add `modelRadius` and `objectRadius()`

* revisions

* add some docs

* cleanup

* cleanup

* fix diameter

* revisions and `webgpu_volume_lighting_rectarea`

* Update MeshSSSNodeMaterial.js

* revision

* improve orbit controls limits

* cleanup

* Update webgpu_volume_lighting_rectarea.html

* Update TiledLightsNode.js

* Update WGSLNodeBuilder.js

* update examples
sunag 1 год назад
Родитель
Сommit
4bc99d47eb
33 измененных файлов с 1212 добавлено и 310 удалено
  1. 2 0
      examples/files.json
  2. 4 4
      examples/jsm/tsl/lighting/TiledLightsNode.js
  3. 9 0
      examples/jsm/tsl/math/Bayer.js
  4. BIN
      examples/screenshots/webgpu_volume_lighting.jpg
  5. BIN
      examples/screenshots/webgpu_volume_lighting_rectarea.jpg
  6. 307 0
      examples/webgpu_volume_lighting.html
  7. 320 0
      examples/webgpu_volume_lighting_rectarea.html
  8. 4 0
      src/Three.TSL.js
  9. 2 3
      src/materials/nodes/MeshSSSNodeMaterial.js
  10. 26 11
      src/materials/nodes/NodeMaterial.js
  11. 17 127
      src/materials/nodes/VolumeNodeMaterial.js
  12. 1 0
      src/nodes/TSL.js
  13. 6 13
      src/nodes/accessors/Lights.js
  14. 8 0
      src/nodes/accessors/ModelNode.js
  15. 32 0
      src/nodes/accessors/Object3DNode.js
  16. 17 17
      src/nodes/core/LightingModel.js
  17. 56 2
      src/nodes/display/PassNode.js
  18. 45 1
      src/nodes/functions/BSDF/LTC.js
  19. 5 8
      src/nodes/functions/BasicLightingModel.js
  20. 4 6
      src/nodes/functions/PhongLightingModel.js
  21. 21 23
      src/nodes/functions/PhysicalLightingModel.js
  22. 5 6
      src/nodes/functions/ToonLightingModel.js
  23. 183 0
      src/nodes/functions/VolumetricLightingModel.js
  24. 33 0
      src/nodes/lighting/AnalyticLightNode.js
  25. 2 11
      src/nodes/lighting/DirectionalLightNode.js
  26. 1 3
      src/nodes/lighting/LightUtils.js
  27. 3 3
      src/nodes/lighting/LightingContextNode.js
  28. 58 14
      src/nodes/lighting/LightsNode.js
  29. 11 26
      src/nodes/lighting/PointLightNode.js
  30. 3 8
      src/nodes/lighting/RectAreaLightNode.js
  31. 5 5
      src/nodes/lighting/ShadowBaseNode.js
  32. 15 1
      src/nodes/lighting/ShadowNode.js
  33. 7 18
      src/nodes/lighting/SpotLightNode.js

+ 2 - 0
examples/files.json

@@ -447,6 +447,8 @@
 		"webgpu_video_frame",
 		"webgpu_video_panorama",
 		"webgpu_volume_cloud",
+		"webgpu_volume_lighting",
+		"webgpu_volume_lighting_rectarea",
 		"webgpu_volume_perlin",
 		"webgpu_water",
 		"webgpu_xr_cubes"

+ 4 - 4
examples/jsm/tsl/lighting/TiledLightsNode.js

@@ -1,7 +1,7 @@
 import { DataTexture, FloatType, RGBAFormat, Vector2, Vector3, LightsNode, NodeUpdateType } from 'three/webgpu';
 
 import {
-	attributeArray, nodeProxy, int, float, vec2, ivec2, ivec4, uniform, Break, Loop,
+	attributeArray, nodeProxy, int, float, vec2, ivec2, ivec4, uniform, Break, Loop, positionView,
 	Fn, If, Return, textureLoad, instanceIndex, screenCoordinate, directPointLight
 } from 'three/tsl';
 
@@ -226,12 +226,12 @@ class TiledLightsNode extends LightsNode {
 
 				const { color, decay, viewPosition, distance } = this.getLightData( lightIndex.sub( 1 ) );
 
-				directPointLight( {
+				builder.lightsNode.setupDirectLight( builder, this, directPointLight( {
 					color,
-					lightViewPosition: viewPosition,
+					lightVector: viewPosition.sub( positionView ),
 					cutoffDistance: distance,
 					decayExponent: decay
-				} ).append();
+				} ) );
 
 			} );
 

Разница между файлами не показана из-за своего большого размера
+ 9 - 0
examples/jsm/tsl/math/Bayer.js


BIN
examples/screenshots/webgpu_volume_lighting.jpg


BIN
examples/screenshots/webgpu_volume_lighting_rectarea.jpg


+ 307 - 0
examples/webgpu_volume_lighting.html

@@ -0,0 +1,307 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - volumetric lighting</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="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - volumetric lighting
+			<br>Improve the quality/performance adjusting the parameters in the Controls
+		</div>
+
+		<video id="video" loop muted crossOrigin="anonymous" playsinline style="display:none">
+			<source src="textures/sintel.ogv" type='video/ogg; codecs="theora, vorbis"'>
+			<source src="textures/sintel.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
+		</video>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { vec3, Fn, time, texture3D, screenUV, uniform, screenCoordinate, pass } from 'three/tsl';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
+			import { TeapotGeometry } from 'three/addons/geometries/TeapotGeometry.js';
+
+			import { bayer16 } from 'three/addons/tsl/math/Bayer.js';
+			import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let renderer, scene, camera;
+			let volumetricMesh, teapot, pointLight, spotLight;
+			let postProcessing;
+			let stats;
+
+			init();
+
+			function createTexture3D() {
+
+				let i = 0;
+
+				const size = 128;
+				const data = new Uint8Array( size * size * size );
+
+				const scale = 10;
+				const perlin = new ImprovedNoise();
+
+				const repeatFactor = 5.0;
+
+				for ( let z = 0; z < size; z ++ ) {
+
+					for ( let y = 0; y < size; y ++ ) {
+
+						for ( let x = 0; x < size; x ++ ) {
+
+							const nx = ( x / size ) * repeatFactor;
+							const ny = ( y / size ) * repeatFactor;
+							const nz = ( z / size ) * repeatFactor;
+
+							const noiseValue = perlin.noise( nx * scale, ny * scale, nz * scale );
+
+							data[ i ] = ( 128 + 128 * noiseValue );
+
+							i ++;
+
+						}
+
+					}
+
+				}
+
+				const texture = new THREE.Data3DTexture( data, size, size, size );
+				texture.format = THREE.RedFormat;
+				texture.minFilter = THREE.LinearFilter;
+				texture.magFilter = THREE.LinearFilter;
+				texture.wrapS = THREE.RepeatWrapping;
+				texture.wrapT = THREE.RepeatWrapping;
+				texture.unpackAlignment = 1;
+				texture.needsUpdate = true;
+
+				return texture;
+
+			}
+
+			function init() {
+
+				const LAYER_VOLUMETRIC_LIGHTING = 10;
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMappingExposure = 2;
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( - 8, 1, - 6 );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.maxDistance = 40;
+				controls.minDistance = 2;
+
+				// Volumetric Fog Area
+
+				const noiseTexture3D = createTexture3D();
+
+				const smokeAmount = uniform( 2 );
+
+				const volumetricMaterial = new THREE.VolumeNodeMaterial();
+				volumetricMaterial.steps = 12;
+				volumetricMaterial.offsetNode = bayer16( screenCoordinate ); // Add dithering to reduce banding
+				volumetricMaterial.scatteringNode = Fn( ( { positionRay } ) => {
+
+					// Return the amount of fog based on the noise texture
+
+					const timeScaled = vec3( time, 0, time.mul( .3 ) );
+
+					const sampleGrain = ( scale, timeScale = 1 ) => texture3D( noiseTexture3D, positionRay.add( timeScaled.mul( timeScale ) ).mul( scale ).mod( 1 ), 0 ).r.add( .5 );
+
+					let density = sampleGrain( .1 );
+					density = density.mul( sampleGrain( .05, 1 ) );
+					density = density.mul( sampleGrain( .02, 2 ) );
+
+					return smokeAmount.mix( 1, density );
+
+				} );
+
+				volumetricMesh = new THREE.Mesh( new THREE.BoxGeometry( 20, 10, 20 ), volumetricMaterial );
+				volumetricMesh.receiveShadow = true;
+				volumetricMesh.position.y = 2;
+				volumetricMesh.layers.disableAll();
+				volumetricMesh.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				scene.add( volumetricMesh );
+
+				// Objects
+
+				teapot = new THREE.Mesh( new TeapotGeometry( .8, 18 ), new THREE.MeshStandardMaterial( { color: 0xffffff, side: THREE.DoubleSide } ) );
+				teapot.castShadow = true;
+				scene.add( teapot );
+
+				const floor = new THREE.Mesh( new THREE.PlaneGeometry( 100, 100 ), new THREE.MeshStandardMaterial( { color: 0xffffff } ) );
+				floor.rotation.x = - Math.PI / 2;
+				floor.position.y = - 3;
+				floor.receiveShadow = true;
+				scene.add( floor );
+
+				// Lights
+
+				pointLight = new THREE.PointLight( 0xf9bb50, 3, 100 );
+				pointLight.castShadow = true;
+				pointLight.position.set( 0, 1.4, 0 );
+				pointLight.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				//lightBase.add( new THREE.Mesh( new THREE.SphereGeometry( 0.1, 16, 16 ), new THREE.MeshBasicMaterial( { color: 0xf9bb50 } ) ) );
+				scene.add( pointLight );
+
+				spotLight = new THREE.SpotLight( 0xffffff, 100 );
+				spotLight.position.set( 2.5, 5, 2.5 );
+				spotLight.angle = Math.PI / 6;
+				spotLight.penumbra = 1;
+				spotLight.decay = 2;
+				spotLight.distance = 0;
+				spotLight.map = new THREE.TextureLoader().setPath( 'textures/' ).load( 'colors.png' );
+				spotLight.castShadow = true;
+				spotLight.shadow.intensity = .98;
+				spotLight.shadow.mapSize.width = 1024;
+				spotLight.shadow.mapSize.height = 1024;
+				spotLight.shadow.camera.near = 1;
+				spotLight.shadow.camera.far = 15;
+				spotLight.shadow.focus = 1;
+				spotLight.shadow.bias = - .003;
+				spotLight.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				//sunLight.add( new THREE.Mesh( new THREE.SphereGeometry( 0.1, 16, 16 ), new THREE.MeshBasicMaterial( { color: 0xffffff } ) ) );
+				scene.add( spotLight );
+
+				// Post-Proccessing
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				// Layers
+
+				const volumetricLightingIntensity = uniform( 1 );
+
+				const volumetricLayer = new THREE.Layers();
+				volumetricLayer.disableAll();
+				volumetricLayer.enable( LAYER_VOLUMETRIC_LIGHTING );
+
+				// Scene Pass
+
+				const scenePass = pass( scene, camera );
+				const sceneLinearDepth = scenePass.getTextureNode( 'depth' );
+
+				// Material - Apply oclussion depth of volumetric lighting based on the scene depth
+
+				volumetricMaterial.depthNode = sceneLinearDepth.sample( screenUV );
+
+				// Volumetric Lighting Pass
+
+				const volumetricPass = pass( scene, camera, { depthBuffer: false } );
+				volumetricPass.setLayers( volumetricLayer );
+				volumetricPass.setResolution( .25 );
+
+				// Compose and Denoise
+
+				const denoiseStrength = uniform( .6 );
+
+				const blurredVolumetricPass = gaussianBlur( volumetricPass, denoiseStrength );
+
+				const scenePassColor = scenePass.add( blurredVolumetricPass.mul( volumetricLightingIntensity ) );
+
+				postProcessing.outputNode = scenePassColor;
+
+				// GUI
+
+				const params = {
+					quality: 1,
+					resolution: volumetricPass.getResolution(),
+					denoise: true
+				};
+
+				const gui = new GUI();
+
+				const rayMarching = gui.addFolder( 'Ray Marching' ).close();
+				rayMarching.add( params, 'resolution', .1, .5 ).onChange( ( resolution ) => {
+
+					volumetricPass.setResolution( resolution );
+
+				} );
+				rayMarching.add( volumetricMaterial, 'steps', 2, 12 ).name( 'step count' );
+				rayMarching.add( denoiseStrength, 'value', 0, 1 ).name( 'denoise strength' );
+				rayMarching.add( params, 'denoise' ).onChange( ( denoise ) => {
+
+					const volumetric = denoise ? blurredVolumetricPass : volumetricPass;
+
+					const scenePassColor = scenePass.add( volumetric.mul( volumetricLightingIntensity ) );
+
+					postProcessing.outputNode = scenePassColor;
+					postProcessing.needsUpdate = true;
+
+				} );
+
+				const lighting = gui.addFolder( 'Lighting / Scene' ).close();
+				lighting.add( pointLight, 'intensity', 0, 6 ).name( 'light intensity' );
+				lighting.add( spotLight, 'intensity', 0, 200 ).name( 'spot intensity' );
+				lighting.add( volumetricLightingIntensity, 'value', 0, 2 ).name( 'fog intensity' );
+				lighting.add( smokeAmount, 'value', 0, 3 ).name( 'smoke amount' );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				stats.update();
+
+				const time = performance.now() * 0.001;
+				const scale = 2.4;
+
+				pointLight.position.x = Math.sin( time * 0.7 ) * scale;
+				pointLight.position.y = Math.cos( time * 0.5 ) * scale;
+				pointLight.position.z = Math.cos( time * 0.3 ) * scale;
+
+				spotLight.position.x = Math.cos( time * 0.3 ) * scale;
+				spotLight.lookAt( 0, 0, 0 );
+
+				teapot.rotation.y = time * 0.2;
+
+				postProcessing.render();
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 320 - 0
examples/webgpu_volume_lighting_rectarea.html

@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - volumetric lighting rect area</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="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - volumetric lighting rect area
+			<br>Improve the quality/performance adjusting the parameters in the Controls
+		</div>
+
+		<video id="video" loop muted crossOrigin="anonymous" playsinline style="display:none">
+			<source src="textures/sintel.ogv" type='video/ogg; codecs="theora, vorbis"'>
+			<source src="textures/sintel.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
+		</video>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { vec3, Fn, time, texture3D, screenUV, uniform, screenCoordinate, pass } from 'three/tsl';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
+			import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
+
+			import { bayer16 } from 'three/addons/tsl/math/Bayer.js';
+			import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let renderer, scene, camera;
+			let volumetricMesh, meshKnot;
+			let rectLight1, rectLight2, rectLight3;
+			let clock;
+			let postProcessing;
+			let stats;
+
+			init();
+
+			function createTexture3D() {
+
+				let i = 0;
+
+				const size = 128;
+				const data = new Uint8Array( size * size * size );
+
+				const scale = 10;
+				const perlin = new ImprovedNoise();
+
+				const repeatFactor = 5.0;
+
+				for ( let z = 0; z < size; z ++ ) {
+
+					for ( let y = 0; y < size; y ++ ) {
+
+						for ( let x = 0; x < size; x ++ ) {
+
+							const nx = ( x / size ) * repeatFactor;
+							const ny = ( y / size ) * repeatFactor;
+							const nz = ( z / size ) * repeatFactor;
+
+							const noiseValue = perlin.noise( nx * scale, ny * scale, nz * scale );
+
+							data[ i ] = ( 128 + 128 * noiseValue );
+
+							i ++;
+
+						}
+
+					}
+
+				}
+
+				const texture = new THREE.Data3DTexture( data, size, size, size );
+				texture.format = THREE.RedFormat;
+				texture.minFilter = THREE.LinearFilter;
+				texture.magFilter = THREE.LinearFilter;
+				texture.wrapS = THREE.RepeatWrapping;
+				texture.wrapT = THREE.RepeatWrapping;
+				texture.unpackAlignment = 1;
+				texture.needsUpdate = true;
+
+				return texture;
+
+			}
+
+			function init() {
+
+				THREE.RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
+
+				const LAYER_VOLUMETRIC_LIGHTING = 10;
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				clock = new THREE.Clock();
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMappingExposure = 2;
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 250 );
+				camera.position.set( 0, 5, - 15 );
+
+				// Volumetric Fog Area
+
+				const noiseTexture3D = createTexture3D();
+
+				const smokeAmount = uniform( 2 );
+
+				const volumetricMaterial = new THREE.VolumeNodeMaterial();
+				volumetricMaterial.steps = 12;
+				volumetricMaterial.offsetNode = bayer16( screenCoordinate ); // Add dithering to reduce banding
+				volumetricMaterial.scatteringNode = Fn( ( { positionRay } ) => {
+
+					// Return the amount of fog based on the noise texture
+
+					const timeScaled = vec3( time, 0, time.mul( .3 ) );
+
+					const sampleGrain = ( scale, timeScale = 1 ) => texture3D( noiseTexture3D, positionRay.add( timeScaled.mul( timeScale ) ).mul( scale ).mod( 1 ), 0 ).r.add( .5 );
+
+					let density = sampleGrain( .1 );
+					density = density.mul( sampleGrain( .05, 1 ) );
+					density = density.mul( sampleGrain( .02, 2 ) );
+
+					return smokeAmount.mix( 1, density );
+
+				} );
+
+				volumetricMesh = new THREE.Mesh( new THREE.BoxGeometry( 50, 40, 50 ), volumetricMaterial );
+				volumetricMesh.receiveShadow = true;
+				volumetricMesh.position.y = 20;
+				volumetricMesh.layers.disableAll();
+				volumetricMesh.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				scene.add( volumetricMesh );
+
+				// Objects
+
+				rectLight1 = new THREE.RectAreaLight( 0xff0000, 5, 4, 10 );
+				rectLight1.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				rectLight1.position.set( - 5, 5, 5 );
+				scene.add( rectLight1 );
+
+				rectLight2 = new THREE.RectAreaLight( 0x00ff00, 5, 4, 10 );
+				rectLight2.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				rectLight2.position.set( 0, 5, 5 );
+				scene.add( rectLight2 );
+
+				rectLight3 = new THREE.RectAreaLight( 0x0000ff, 5, 4, 10 );
+				rectLight3.layers.enable( LAYER_VOLUMETRIC_LIGHTING );
+				rectLight3.position.set( 5, 5, 5 );
+				scene.add( rectLight3 );
+
+				//
+
+				const createRectLightMesh = ( rectLight ) => {
+
+					const geometry = new THREE.PlaneGeometry( 4, 10 );
+					const frontMaterial = new THREE.MeshBasicMaterial( { color: rectLight.color, side: THREE.BackSide } );
+					const backMaterial = new THREE.MeshStandardMaterial( { color: 0x111111 } );
+
+					const backSide = new THREE.Mesh( geometry, backMaterial );
+					backSide.position.set( 0, 0, .08 );
+
+					const frontSide = new THREE.Mesh( geometry, frontMaterial );
+					frontSide.position.set( 0, 0, .01 );
+
+					rectLight.add( backSide );
+					rectLight.add( frontSide );
+
+				};
+
+				createRectLightMesh( rectLight1 );
+				createRectLightMesh( rectLight2 );
+				createRectLightMesh( rectLight3 );
+
+				//
+
+				const geoFloor = new THREE.BoxGeometry( 2000, 0.1, 2000 );
+				const matStdFloor = new THREE.MeshStandardMaterial( { color: 0xbcbcbc, roughness: 0.1, metalness: 0 } );
+				const mshStdFloor = new THREE.Mesh( geoFloor, matStdFloor );
+				scene.add( mshStdFloor );
+
+				const geoKnot = new THREE.TorusKnotGeometry( 1.5, 0.5, 200, 16 );
+				const matKnot = new THREE.MeshStandardMaterial( { color: 0xffffff, roughness: 0, metalness: 0 } );
+				meshKnot = new THREE.Mesh( geoKnot, matKnot );
+				meshKnot.position.set( 0, 5, 0 );
+				scene.add( meshKnot );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 5;
+				controls.maxDistance = 200;
+				controls.target.copy( meshKnot.position );
+				controls.update();
+
+				// Post-Proccessing
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				// Layers
+
+				const volumetricLightingIntensity = uniform( 1 );
+
+				const volumetricLayer = new THREE.Layers();
+				volumetricLayer.disableAll();
+				volumetricLayer.enable( LAYER_VOLUMETRIC_LIGHTING );
+
+				// Scene Pass
+
+				const scenePass = pass( scene, camera );
+				const sceneLinearDepth = scenePass.getTextureNode( 'depth' );
+
+				// Material - Apply oclussion depth of volumetric lighting based on the scene depth
+
+				volumetricMaterial.depthNode = sceneLinearDepth.sample( screenUV );
+
+				// Volumetric Lighting Pass
+
+				const volumetricPass = pass( scene, camera, { depthBuffer: false } );
+				volumetricPass.setLayers( volumetricLayer );
+				volumetricPass.setResolution( .25 );
+
+				// Compose and Denoise
+
+				const denoiseStrength = uniform( .6 );
+
+				const blurredVolumetricPass = gaussianBlur( volumetricPass, denoiseStrength );
+
+				const scenePassColor = scenePass.add( blurredVolumetricPass.mul( volumetricLightingIntensity ) );
+
+				postProcessing.outputNode = scenePassColor;
+
+				// GUI
+
+				const params = {
+					quality: 1,
+					resolution: volumetricPass.getResolution(),
+					denoise: true
+				};
+
+				const gui = new GUI();
+
+				const rayMarching = gui.addFolder( 'Ray Marching' ).close();
+				rayMarching.add( params, 'resolution', .1, .5 ).onChange( ( resolution ) => {
+
+					volumetricPass.setResolution( resolution );
+
+				} );
+				rayMarching.add( volumetricMaterial, 'steps', 2, 12 ).name( 'step count' );
+				rayMarching.add( denoiseStrength, 'value', 0, 1 ).name( 'denoise strength' );
+				rayMarching.add( params, 'denoise' ).onChange( ( denoise ) => {
+
+					const volumetric = denoise ? blurredVolumetricPass : volumetricPass;
+
+					const scenePassColor = scenePass.add( volumetric.mul( volumetricLightingIntensity ) );
+
+					postProcessing.outputNode = scenePassColor;
+					postProcessing.needsUpdate = true;
+
+				} );
+
+				const lighting = gui.addFolder( 'Lighting / Scene' ).close();
+				lighting.add( volumetricLightingIntensity, 'value', 0, 2 ).name( 'fog intensity' );
+				lighting.add( smokeAmount, 'value', 0, 3 ).name( 'smoke amount' );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				const delta = clock.getDelta();
+
+				stats.update();
+
+				rectLight1.rotation.y += - delta;
+				rectLight2.rotation.y += delta * .5;
+				rectLight3.rotation.y += delta;
+
+				postProcessing.render();
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 4 - 0
src/Three.TSL.js

@@ -285,6 +285,7 @@ export const modInt = TSL.modInt;
 export const modelDirection = TSL.modelDirection;
 export const modelNormalMatrix = TSL.modelNormalMatrix;
 export const modelPosition = TSL.modelPosition;
+export const modelRadius = TSL.modelRadius;
 export const modelScale = TSL.modelScale;
 export const modelViewMatrix = TSL.modelViewMatrix;
 export const modelViewPosition = TSL.modelViewPosition;
@@ -336,6 +337,7 @@ export const numWorkgroups = TSL.numWorkgroups;
 export const objectDirection = TSL.objectDirection;
 export const objectGroup = TSL.objectGroup;
 export const objectPosition = TSL.objectPosition;
+export const objectRadius = TSL.objectRadius;
 export const objectScale = TSL.objectScale;
 export const objectViewPosition = TSL.objectViewPosition;
 export const objectWorldMatrix = TSL.objectWorldMatrix;
@@ -380,6 +382,7 @@ export const range = TSL.range;
 export const rangeFog = TSL.rangeFog;
 export const rangeFogFactor = TSL.rangeFogFactor;
 export const reciprocal = TSL.reciprocal;
+export const lightProjectionUV = TSL.lightProjectionUV;
 export const reference = TSL.reference;
 export const referenceBuffer = TSL.referenceBuffer;
 export const reflect = TSL.reflect;
@@ -416,6 +419,7 @@ export const select = TSL.select;
 export const setCurrentStack = TSL.setCurrentStack;
 export const shaderStages = TSL.shaderStages;
 export const shadow = TSL.shadow;
+export const pointShadow = TSL.pointShadow;
 export const shadowPositionWorld = TSL.shadowPositionWorld;
 export const sharedUniformGroup = TSL.sharedUniformGroup;
 export const shapeCircle = TSL.shapeCircle;

+ 2 - 3
src/materials/nodes/MeshSSSNodeMaterial.js

@@ -42,10 +42,9 @@ class SSSLightingModel extends PhysicalLightingModel {
 	 * Reference: [Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look]{@link https://colinbarrebrisebois.com/2011/03/07/gdc-2011-approximating-translucency-for-a-fast-cheap-and-convincing-subsurface-scattering-look/}
 	 *
 	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	direct( { lightDirection, lightColor, reflectedLight }, stack, builder ) {
+	direct( { lightDirection, lightColor, reflectedLight }, builder ) {
 
 		if ( this.useSSS === true ) {
 
@@ -61,7 +60,7 @@ class SSSLightingModel extends PhysicalLightingModel {
 
 		}
 
-		super.direct( { lightDirection, lightColor, reflectedLight }, stack, builder );
+		super.direct( { lightDirection, lightColor, reflectedLight }, builder );
 
 	}
 

+ 26 - 11
src/materials/nodes/NodeMaterial.js

@@ -292,7 +292,7 @@ class NodeMaterial extends Material {
 		 * 	return shadow.mix( color( 0xff0000 ), 1 ); // modify shadow color
 		 * } );
 		 *
-		 * @type {?Node<vec4>}
+		 * @type {?(Function<vec4>|FunctionNode<vec4>)}
 		 * @default null
 		 */
 		this.receivedShadowNode = null;
@@ -965,7 +965,7 @@ class NodeMaterial extends Material {
 
 		if ( lightsNode && lightsNode.getScope().hasLights ) {
 
-			const lightingModel = this.setupLightingModel( builder );
+			const lightingModel = this.setupLightingModel( builder ) || null;
 
 			outgoingLightNode = lightingContext( lightsNode, lightingModel, backdropNode, backdropAlphaNode );
 
@@ -990,27 +990,42 @@ class NodeMaterial extends Material {
 	}
 
 	/**
-	 * Setups the output node.
+	 * Setup the fog.
 	 *
 	 * @param {NodeBuilder} builder - The current node builder.
 	 * @param {Node<vec4>} outputNode - The existing output node.
 	 * @return {Node<vec4>} The output node.
 	 */
-	setupOutput( builder, outputNode ) {
+	setupFog( builder, outputNode ) {
 
-		// FOG
+		const fogNode = builder.fogNode;
 
-		if ( this.fog === true ) {
+		if ( fogNode ) {
+
+			output.assign( outputNode );
 
-			const fogNode = builder.fogNode;
+			outputNode = vec4( fogNode );
 
-			if ( fogNode ) {
+		}
 
-				output.assign( outputNode );
+		return outputNode;
 
-				outputNode = vec4( fogNode );
+	}
 
-			}
+	/**
+	 * Setups the output node.
+	 *
+	 * @param {NodeBuilder} builder - The current node builder.
+	 * @param {Node<vec4>} outputNode - The existing output node.
+	 * @return {Node<vec4>} The output node.
+	 */
+	setupOutput( builder, outputNode ) {
+
+		// FOG
+
+		if ( this.fog === true ) {
+
+			outputNode = this.setupFog( builder, outputNode );
 
 		}
 

+ 17 - 127
src/materials/nodes/VolumeNodeMaterial.js

@@ -1,18 +1,9 @@
 import NodeMaterial from './NodeMaterial.js';
-import { property } from '../../nodes/core/PropertyNode.js';
-import { materialReference } from '../../nodes/accessors/MaterialReferenceNode.js';
-import { modelWorldMatrixInverse } from '../../nodes/accessors/ModelNode.js';
-import { cameraPosition } from '../../nodes/accessors/Camera.js';
-import { positionGeometry } from '../../nodes/accessors/Position.js';
-import { Fn, varying, float, vec2, vec3, vec4 } from '../../nodes/tsl/TSLBase.js';
-import { min, max } from '../../nodes/math/MathNode.js';
-import { Loop, Break } from '../../nodes/utils/LoopNode.js';
-import { texture3D } from '../../nodes/accessors/Texture3DNode.js';
-import { Color } from '../../math/Color.js';
+import VolumetricLightingModel from '../../nodes/functions/VolumetricLightingModel.js';
+import { BackSide } from '../../constants.js';
 
 /**
- * Node material intended for volume rendering. The volumetric data are
- * defined with an instance of {@link Data3DTexture}.
+ * Volume node material.
  *
  * @augments NodeMaterial
  */
@@ -43,137 +34,36 @@ class VolumeNodeMaterial extends NodeMaterial {
 		this.isVolumeNodeMaterial = true;
 
 		/**
-		 * The base color of the volume.
+		 * Number of steps used for raymarching.
 		 *
-		 * @type {Color}
-		 * @default 100
+		 * @type {number}
+		 * @default 25
 		 */
-		this.base = new Color( 0xffffff );
+		this.steps = 25;
 
 		/**
-		 * A 3D data texture holding the volumetric data.
+		 * Node used for scattering calculations.
 		 *
-		 * @type {?Data3DTexture}
+		 * @type {Function<vec4>|FunctionNode<vec4>}
 		 * @default null
 		 */
-		this.map = null;
+		this.scatteringNode = null;
 
-		/**
-		 * This number of samples for each ray that hits the mesh's surface
-		 * and travels through the volume.
-		 *
-		 * @type {number}
-		 * @default 100
-		 */
-		this.steps = 100;
+		this.lights = true;
 
-		/**
-		 * Callback for {@link VolumeNodeMaterial#testNode}.
-		 *
-		 * @callback testNodeCallback
-		 * @param {Data3DTexture<float>} map - The 3D texture.
-		 * @param {Node<float>} mapValue - The sampled value inside the volume.
-		 * @param {Node<vec3>} probe - The probe which is the entry point of the ray on the mesh's surface.
-		 * @param {Node<vec4>} finalColor - The final color.
-		 */
+		this.transparent = true;
+		this.side = BackSide;
 
-		/**
-		 * The volume rendering of this material works by shooting rays
-		 * from the camera position through each fragment of the mesh's
-		 * surface and sample the inner volume in a raymarching fashion
-		 * multiple times.
-		 *
-		 * This node can be used to assign a callback function of type `Fn`
-		 * that will be executed per sample. The callback receives the
-		 * texture, the sampled texture value as well as position on the surface
-		 * where the rays enters the volume. The last parameter is a color
-		 * that allows the callback to determine the final color.
-		 *
-		 * @type {?testNodeCallback}
-		 * @default null
-		 */
-		this.testNode = null;
+		this.depthTest = false;
+		this.depthWrite = false;
 
 		this.setValues( parameters );
 
 	}
 
-	/**
-	 * Setups the vertex and fragment stage of this node material.
-	 *
-	 * @param {NodeBuilder} builder - The current node builder.
-	 */
-	setup( builder ) {
-
-		const map = texture3D( this.map, null, 0 );
-
-		const hitBox = Fn( ( { orig, dir } ) => {
-
-			const box_min = vec3( - 0.5 );
-			const box_max = vec3( 0.5 );
-
-			const inv_dir = dir.reciprocal();
-
-			const tmin_tmp = box_min.sub( orig ).mul( inv_dir );
-			const tmax_tmp = box_max.sub( orig ).mul( inv_dir );
-
-			const tmin = min( tmin_tmp, tmax_tmp );
-			const tmax = max( tmin_tmp, tmax_tmp );
-
-			const t0 = max( tmin.x, max( tmin.y, tmin.z ) );
-			const t1 = min( tmax.x, min( tmax.y, tmax.z ) );
-
-			return vec2( t0, t1 );
-
-		} );
-
-		this.fragmentNode = Fn( () => {
-
-			const vOrigin = varying( vec3( modelWorldMatrixInverse.mul( vec4( cameraPosition, 1.0 ) ) ) );
-			const vDirection = varying( positionGeometry.sub( vOrigin ) );
-
-			const rayDir = vDirection.normalize();
-			const bounds = vec2( hitBox( { orig: vOrigin, dir: rayDir } ) ).toVar();
-
-			bounds.x.greaterThan( bounds.y ).discard();
-
-			bounds.assign( vec2( max( bounds.x, 0.0 ), bounds.y ) );
-
-			const p = vec3( vOrigin.add( bounds.x.mul( rayDir ) ) ).toVar();
-			const inc = vec3( rayDir.abs().reciprocal() ).toVar();
-			const delta = float( min( inc.x, min( inc.y, inc.z ) ) ).toVar( 'delta' ); // used 'delta' name in loop
-
-			delta.divAssign( materialReference( 'steps', 'float' ) );
-
-			const ac = vec4( materialReference( 'base', 'color' ), 0.0 ).toVar();
-
-			Loop( { type: 'float', start: bounds.x, end: bounds.y, update: '+= delta' }, () => {
-
-				const d = property( 'float', 'd' ).assign( map.sample( p.add( 0.5 ) ).r );
-
-				if ( this.testNode !== null ) {
-
-					this.testNode( { map: map, mapValue: d, probe: p, finalColor: ac } ).append();
-
-				} else {
-
-					// default to show surface of mesh
-					ac.a.assign( 1 );
-					Break();
-
-				}
-
-				p.addAssign( rayDir.mul( delta ) );
-
-			} );
-
-			ac.a.equal( 0 ).discard();
-
-			return vec4( ac );
-
-		} )();
+	setupLightingModel() {
 
-		super.setup( builder );
+		return new VolumetricLightingModel();
 
 	}
 

+ 1 - 0
src/nodes/TSL.js

@@ -134,6 +134,7 @@ export * from './lighting/LightsNode.js';
 export * from './lighting/LightingContextNode.js';
 export * from './lighting/ShadowBaseNode.js';
 export * from './lighting/ShadowNode.js';
+export * from './lighting/PointShadowNode.js';
 export * from './lighting/PointLightNode.js';
 
 // pmrem

+ 6 - 13
src/nodes/accessors/Lights.js

@@ -51,22 +51,15 @@ export function lightShadowMatrix( light ) {
  * @tsl
  * @function
  * @param {Light} light -The light source.
+ * @param {Node<vec3>} [position=positionWorld] -The position to project.
  * @returns {Node<vec3>} The projected uvs.
  */
-export function lightProjectionUV( light ) {
+export function lightProjectionUV( light, position = positionWorld ) {
 
-	const data = getLightData( light );
-
-	if ( data.projectionUV === undefined ) {
-
-		const spotLightCoord = lightShadowMatrix( light ).mul( positionWorld );
-
-		data.projectionUV = spotLightCoord.xyz.div( spotLightCoord.w );
+	const spotLightCoord = lightShadowMatrix( light ).mul( position );
+	const projectionUV = spotLightCoord.xyz.div( spotLightCoord.w );
 
-
-	}
-
-	return data.projectionUV;
+	return projectionUV;
 
 }
 
@@ -107,7 +100,7 @@ export function lightTargetPosition( light ) {
  *
  * @tsl
  * @function
- * @param {Light} light -The light source.
+ * @param {Light} light - The light source.
  * @returns {UniformNode<vec3>} The light's position in view space.
  */
 export function lightViewPosition( light ) {

+ 8 - 0
src/nodes/accessors/ModelNode.js

@@ -91,6 +91,14 @@ export const modelScale = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.SCAL
  */
 export const modelViewPosition = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.VIEW_POSITION );
 
+/**
+ * TSL object that represents the object's radius.
+ *
+ * @tsl
+ * @type {ModelNode<float>}
+ */
+export const modelRadius = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.RADIUS );
+
 /**
  * TSL object that represents the object's normal matrix.
  *

+ 32 - 0
src/nodes/accessors/Object3DNode.js

@@ -3,6 +3,9 @@ import { NodeUpdateType } from '../core/constants.js';
 import UniformNode from '../core/UniformNode.js';
 import { nodeProxy } from '../tsl/TSLBase.js';
 import { Vector3 } from '../../math/Vector3.js';
+import { Sphere } from '../../math/Sphere.js';
+
+const _sphere = /*@__PURE__*/ new Sphere();
 
 /**
  * This node can be used to access transformation related metrics of 3D objects.
@@ -85,6 +88,10 @@ class Object3DNode extends Node {
 
 			return 'vec3';
 
+		} else if ( scope === Object3DNode.RADIUS ) {
+
+			return 'float';
+
 		}
 
 	}
@@ -131,6 +138,16 @@ class Object3DNode extends Node {
 
 			uniformNode.value.applyMatrix4( camera.matrixWorldInverse );
 
+		} else if ( scope === Object3DNode.RADIUS ) {
+
+			const geometry = frame.object.geometry;
+
+			if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
+
+			_sphere.copy( geometry.boundingSphere ).applyMatrix4( object.matrixWorld );
+
+			uniformNode.value = _sphere.radius;
+
 		}
 
 	}
@@ -154,6 +171,10 @@ class Object3DNode extends Node {
 
 			this._uniformNode.nodeType = 'vec3';
 
+		} else if ( scope === Object3DNode.RADIUS ) {
+
+			this._uniformNode.nodeType = 'float';
+
 		}
 
 		return this._uniformNode.build( builder );
@@ -183,6 +204,7 @@ Object3DNode.POSITION = 'position';
 Object3DNode.SCALE = 'scale';
 Object3DNode.VIEW_POSITION = 'viewPosition';
 Object3DNode.DIRECTION = 'direction';
+Object3DNode.RADIUS = 'radius';
 
 export default Object3DNode;
 
@@ -235,3 +257,13 @@ export const objectScale = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.S
  * @returns {Object3DNode<vec3>}
  */
 export const objectViewPosition = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.VIEW_POSITION );
+
+/**
+ * TSL function for creating an object 3D node that represents the object's radius.
+ *
+ * @tsl
+ * @function
+ * @param {?Object3D} [object3d=null] - The 3D object.
+ * @returns {Object3DNode<vec3>}
+ */
+export const objectRadius = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.RADIUS );

+ 17 - 17
src/nodes/core/LightingModel.js

@@ -11,54 +11,56 @@ class LightingModel {
 	 * which are later used in the evaluation process.
 	 *
 	 * @abstract
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	start( /*input, stack, builder*/ ) { }
+	start( builder ) {
+
+		// lights ( direct )
+
+		builder.lightsNode.setupLights( builder, builder.lightsNode.getLightNodes( builder ) );
+
+		// indirect
+
+		this.indirect( builder );
+
+	}
 
 	/**
 	 * This method is intended for executing final tasks like final updates
 	 * to the outgoing light.
 	 *
 	 * @abstract
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	finish( /*input, stack, builder*/ ) { }
+	finish( /*builder*/ ) { }
 
 	/**
 	 * This method is intended for implementing the direct light term and
 	 * executed during the build process of directional, point and spot light nodes.
 	 *
 	 * @abstract
-	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
+	 * @param {Object} lightData - The light data.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	direct( /*input, stack, builder*/ ) { }
+	direct( /*lightData, builder*/ ) { }
 
 	/**
 	 * This method is intended for implementing the direct light term for
 	 * rect area light nodes.
 	 *
 	 * @abstract
-	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
+	 * @param {Object} lightData - The light data.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	directRectArea( /*input, stack, builder*/ ) {}
+	directRectArea( /*lightData, builder*/ ) {}
 
 	/**
 	 * This method is intended for implementing the indirect light term.
 	 *
 	 * @abstract
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirect( /*input, stack, builder*/ ) { }
+	indirect( /*builder*/ ) { }
 
 	/**
 	 * This method is intended for implementing the ambient occlusion term.
@@ -66,8 +68,6 @@ class LightingModel {
 	 * model in its indirect term.
 	 *
 	 * @abstract
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
 	ambientOcclusion( /*input, stack, builder*/ ) { }

+ 56 - 2
src/nodes/display/PassNode.js

@@ -314,6 +314,10 @@ class PassNode extends TempNode {
 		 */
 		this._mrt = null;
 
+		this._layers = null;
+
+		this._resolution = 1;
+
 		/**
 		 * This flag can be used for type testing.
 		 *
@@ -334,6 +338,47 @@ class PassNode extends TempNode {
 
 	}
 
+	/**
+	 * Sets the resolution for the pass.
+	 * The resolution is a factor that is multiplied with the renderer's width and height.
+	 *
+	 * @param {number} resolution - The resolution to set. A value of `1` means full resolution.
+	 * @return {PassNode} A reference to this pass.
+	 */
+	setResolution( resolution ) {
+
+		this._resolution = resolution;
+
+		return this;
+
+	}
+
+	/**
+	 * Gets the current resolution of the pass.
+	 *
+	 * @return {number} The current resolution. A value of `1` means full resolution.
+	 * @default 1
+	 */
+	getResolution() {
+
+		return this._resolution;
+
+	}
+
+	setLayers( layers ) {
+
+		this._layers = layers;
+
+		return this;
+
+	}
+
+	getLayers() {
+
+		return this._layers;
+
+	}
+
 	/**
 	 * Sets the given MRT node to setup MRT for this pass.
 	 *
@@ -591,10 +636,17 @@ class PassNode extends TempNode {
 
 		const currentRenderTarget = renderer.getRenderTarget();
 		const currentMRT = renderer.getMRT();
+		const currentMask = camera.layers.mask;
 
 		this._cameraNear.value = camera.near;
 		this._cameraFar.value = camera.far;
 
+		if ( this._layers !== null ) {
+
+			camera.layers.mask = this._layers.mask;
+
+		}
+
 		for ( const name in this._previousTextures ) {
 
 			this.toggleTexture( name );
@@ -609,6 +661,8 @@ class PassNode extends TempNode {
 		renderer.setRenderTarget( currentRenderTarget );
 		renderer.setMRT( currentMRT );
 
+		camera.layers.mask = currentMask;
+
 	}
 
 	/**
@@ -622,8 +676,8 @@ class PassNode extends TempNode {
 		this._width = width;
 		this._height = height;
 
-		const effectiveWidth = this._width * this._pixelRatio;
-		const effectiveHeight = this._height * this._pixelRatio;
+		const effectiveWidth = this._width * this._pixelRatio * this._resolution;
+		const effectiveHeight = this._height * this._pixelRatio * this._resolution;
 
 		this.renderTarget.setSize( effectiveWidth, effectiveHeight );
 

+ 45 - 1
src/nodes/functions/BSDF/LTC.js

@@ -127,5 +127,49 @@ const LTC_Evaluate = /*@__PURE__*/ Fn( ( { N, V, P, mInv, p0, p1, p2, p3 } ) =>
 	]
 } );
 
+const LTC_Evaluate_Volume = /*@__PURE__*/ Fn( ( { P, p0, p1, p2, p3 } ) => {
 
-export { LTC_Evaluate, LTC_Uv };
+	// bail if point is on back side of plane of light
+	// assumes ccw winding order of light vertices
+	const v1 = p1.sub( p0 ).toVar();
+	const v2 = p3.sub( p0 ).toVar();
+
+	const lightNormal = v1.cross( v2 );
+	const result = vec3().toVar();
+
+	If( lightNormal.dot( P.sub( p0 ) ).greaterThanEqual( 0.0 ), () => {
+
+		// transform rect
+		// & project rect onto sphere
+		const coords0 = p0.sub( P ).normalize().toVar();
+		const coords1 = p1.sub( P ).normalize().toVar();
+		const coords2 = p2.sub( P ).normalize().toVar();
+		const coords3 = p3.sub( P ).normalize().toVar();
+
+		// calculate vector form factor
+		const vectorFormFactor = vec3( 0 ).toVar();
+		vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords0, v2: coords1 } ) );
+		vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords1, v2: coords2 } ) );
+		vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords2, v2: coords3 } ) );
+		vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords3, v2: coords0 } ) );
+
+		// adjust for horizon clipping
+		result.assign( vec3( LTC_ClippedSphereFormFactor( { f: vectorFormFactor.abs() } ) ) );
+
+	} );
+
+	return result;
+
+} ).setLayout( {
+	name: 'LTC_Evaluate',
+	type: 'vec3',
+	inputs: [
+		{ name: 'P', type: 'vec3' },
+		{ name: 'p0', type: 'vec3' },
+		{ name: 'p1', type: 'vec3' },
+		{ name: 'p2', type: 'vec3' },
+		{ name: 'p3', type: 'vec3' }
+	]
+} );
+
+export { LTC_Evaluate, LTC_Evaluate_Volume, LTC_Uv };

+ 5 - 8
src/nodes/functions/BasicLightingModel.js

@@ -26,15 +26,13 @@ class BasicLightingModel extends LightingModel {
 	/**
 	 * Implements the baked indirect lighting with its modulation.
 	 *
-	 * @param {ContextNode} context - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirect( context, stack, builder ) {
+	indirect( { context } ) {
 
 		const ambientOcclusion = context.ambientOcclusion;
 		const reflectedLight = context.reflectedLight;
-		const irradianceLightMap = builder.context.irradianceLightMap;
+		const irradianceLightMap = context.irradianceLightMap;
 
 		reflectedLight.indirectDiffuse.assign( vec4( 0.0 ) );
 
@@ -61,13 +59,12 @@ class BasicLightingModel extends LightingModel {
 	/**
 	 * Implements the environment mapping.
 	 *
-	 * @param {ContextNode} context - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	finish( context, stack, builder ) {
+	finish( builder ) {
+
+		const { material, context } = builder;
 
-		const material = builder.material;
 		const outgoingLight = context.outgoingLight;
 		const envNode = builder.context.environment;
 

+ 4 - 6
src/nodes/functions/PhongLightingModel.js

@@ -62,9 +62,7 @@ class PhongLightingModel extends BasicLightingModel {
 	 * Implements the direct lighting. The specular portion is optional an can be controlled
 	 * with the {@link PhongLightingModel#specular} flag.
 	 *
-	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
-	 * @param {NodeBuilder} builder - The current node builder.
+	 * @param {Object} lightData - The light data.
 	 */
 	direct( { lightDirection, lightColor, reflectedLight } ) {
 
@@ -84,11 +82,11 @@ class PhongLightingModel extends BasicLightingModel {
 	/**
 	 * Implements the indirect lighting.
 	 *
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirect( { ambientOcclusion, irradiance, reflectedLight } ) {
+	indirect( builder ) {
+
+		const { ambientOcclusion, irradiance, reflectedLight } = builder.context;
 
 		reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
 

+ 21 - 23
src/nodes/functions/PhysicalLightingModel.js

@@ -473,9 +473,9 @@ class PhysicalLightingModel extends LightingModel {
 	 * Depending on what features are requested, the method prepares certain node variables
 	 * which are later used for lighting computations.
 	 *
-	 * @param {ContextNode} context - The current node context.
+	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	start( context ) {
+	start( builder ) {
 
 		if ( this.clearcoat === true ) {
 
@@ -514,6 +514,8 @@ class PhysicalLightingModel extends LightingModel {
 			const v = cameraPosition.sub( positionWorld ).normalize(); // TODO: Create Node for this, same issue in MaterialX
 			const n = transformedNormalWorld;
 
+			const context = builder.context;
+
 			context.backdrop = getIBLVolumeRefraction(
 				n,
 				v,
@@ -538,6 +540,8 @@ class PhysicalLightingModel extends LightingModel {
 
 		}
 
+		super.start( builder );
+
 	}
 
 	// Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
@@ -568,8 +572,7 @@ class PhysicalLightingModel extends LightingModel {
 	/**
 	 * Implements the direct light.
 	 *
-	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
+	 * @param {Object} lightData - The light data.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
 	direct( { lightDirection, lightColor, reflectedLight } ) {
@@ -603,7 +606,6 @@ class PhysicalLightingModel extends LightingModel {
 	 * rect area light nodes.
 	 *
 	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
 	directRectArea( { lightColor, lightPosition, halfWidth, halfHeight, reflectedLight, ltc_1, ltc_2 } ) {
@@ -641,26 +643,24 @@ class PhysicalLightingModel extends LightingModel {
 	/**
 	 * Implements the indirect lighting.
 	 *
-	 * @param {ContextNode} context - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirect( context, stack, builder ) {
+	indirect( builder ) {
 
-		this.indirectDiffuse( context, stack, builder );
-		this.indirectSpecular( context, stack, builder );
-		this.ambientOcclusion( context, stack, builder );
+		this.indirectDiffuse( builder );
+		this.indirectSpecular( builder );
+		this.ambientOcclusion( builder );
 
 	}
 
 	/**
 	 * Implements the indirect diffuse term.
 	 *
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirectDiffuse( { irradiance, reflectedLight } ) {
+	indirectDiffuse( builder ) {
+
+		const { irradiance, reflectedLight } = builder.context;
 
 		reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
 
@@ -669,11 +669,11 @@ class PhysicalLightingModel extends LightingModel {
 	/**
 	 * Implements the indirect specular term.
 	 *
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirectSpecular( { radiance, iblIrradiance, reflectedLight } ) {
+	indirectSpecular( builder ) {
+
+		const { radiance, iblIrradiance, reflectedLight } = builder.context;
 
 		if ( this.sheen === true ) {
 
@@ -725,11 +725,11 @@ class PhysicalLightingModel extends LightingModel {
 	/**
 	 * Implements the ambient occlusion term.
 	 *
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	ambientOcclusion( { ambientOcclusion, reflectedLight } ) {
+	ambientOcclusion( builder ) {
+
+		const { ambientOcclusion, reflectedLight } = builder.context;
 
 		const dotNV = transformedNormalView.dot( positionViewDirection ).clamp(); // @ TODO: Move to core dotNV
 
@@ -758,11 +758,9 @@ class PhysicalLightingModel extends LightingModel {
 	/**
 	 * Used for final lighting accumulations depending on the requested features.
 	 *
-	 * @param {ContextNode} context - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	finish( context ) {
+	finish( { context } ) {
 
 		const { outgoingLight } = context;
 

+ 5 - 6
src/nodes/functions/ToonLightingModel.js

@@ -39,11 +39,10 @@ class ToonLightingModel extends LightingModel {
 	 * Implements the direct lighting. Instead of using a conventional smooth irradiance, the irradiance is
 	 * reduced to a small number of discrete shades to create a comic-like, flat look.
 	 *
-	 * @param {Object} input - The input data.
-	 * @param {StackNode} stack - The current stack.
+	 * @param {Object} lightData - The light data.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	direct( { lightDirection, lightColor, reflectedLight }, stack, builder ) {
+	direct( { lightDirection, lightColor, reflectedLight }, builder ) {
 
 		const irradiance = getGradientIrradiance( { normal: normalGeometry, lightDirection, builder } ).mul( lightColor );
 
@@ -54,11 +53,11 @@ class ToonLightingModel extends LightingModel {
 	/**
 	 * Implements the indirect lighting.
 	 *
-	 * @param {ContextNode} input - The current node context.
-	 * @param {StackNode} stack - The current stack.
 	 * @param {NodeBuilder} builder - The current node builder.
 	 */
-	indirect( { ambientOcclusion, irradiance, reflectedLight } ) {
+	indirect( builder ) {
+
+		const { ambientOcclusion, irradiance, reflectedLight } = builder.context;
 
 		reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
 

+ 183 - 0
src/nodes/functions/VolumetricLightingModel.js

@@ -0,0 +1,183 @@
+import LightingModel from '../core/LightingModel.js';
+import { property } from '../core/PropertyNode.js';
+import { float, If, uniform, vec3, vec4 } from '../tsl/TSLBase.js';
+import { positionWorld } from '../accessors/Position.js';
+import { cameraFar, cameraNear, cameraPosition, cameraViewMatrix } from '../accessors/Camera.js';
+import { Loop } from '../utils/LoopNode.js';
+import { linearDepth, viewZToPerspectiveDepth } from '../display/ViewportDepthNode.js';
+import { modelRadius } from '../accessors/ModelNode.js';
+import { LTC_Evaluate_Volume } from './BSDF/LTC.js';
+
+const scatteringDensity = property( 'vec3' );
+const linearDepthRay = property( 'vec3' );
+const outgoingRayLight = property( 'vec3' );
+
+/**
+ * VolumetricLightingModel class extends the LightingModel to implement volumetric lighting effects.
+ * This model calculates the scattering and transmittance of light through a volumetric medium.
+ * It dynamically adjusts the direction of the ray based on the camera and object positions.
+ * The model supports custom scattering and depth nodes to enhance the lighting effects.
+ *
+ * @augments LightingModel
+ */
+class VolumetricLightingModel extends LightingModel {
+
+	constructor() {
+
+		super();
+
+	}
+
+	start( builder ) {
+
+		const { material, context } = builder;
+
+		const startPos = property( 'vec3' );
+		const endPos = property( 'vec3' );
+
+		// This approach dynamically changes the direction of the ray,
+		// prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away.
+
+		If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {
+
+			startPos.assign( cameraPosition );
+			endPos.assign( positionWorld );
+
+		} ).Else( () => {
+
+			startPos.assign( positionWorld );
+			endPos.assign( cameraPosition );
+
+		} );
+
+		//
+
+		const viewVector = endPos.sub( startPos );
+
+		const steps = uniform( 'int' ).onRenderUpdate( ( { material } ) => material.steps );
+		const stepSize = viewVector.length().div( steps ).toVar();
+
+		const rayDir = viewVector.normalize().toVar(); // TODO: toVar() should be automatic here ( in loop )
+
+		const distTravelled = float( 0.0 ).toVar();
+		const transmittance = vec3( 1 ).toVar();
+
+		if ( material.offsetNode ) {
+
+			// reduce banding
+
+			distTravelled.addAssign( material.offsetNode.mul( stepSize ) );
+
+		}
+
+		Loop( steps, () => {
+
+			const positionRay = startPos.add( rayDir.mul( distTravelled ) );
+			const positionViewRay = cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz;
+
+			if ( material.depthNode !== null ) {
+
+				linearDepthRay.assign( linearDepth( viewZToPerspectiveDepth( positionViewRay.z, cameraNear, cameraFar ) ) );
+
+				context.sceneDepthNode = linearDepth( material.depthNode ).toVar();
+
+			}
+
+			context.positionWorld = positionRay;
+			context.shadowPositionWorld = positionRay;
+			context.positionView = positionViewRay;
+
+			scatteringDensity.assign( 0 );
+
+			let scatteringNode;
+
+			if ( material.scatteringNode ) {
+
+				scatteringNode = material.scatteringNode( {
+					positionRay
+				} );
+
+			}
+
+			super.start( builder );
+
+			if ( scatteringNode ) {
+
+				scatteringDensity.mulAssign( scatteringNode );
+
+			}
+
+			// beer's law
+
+			const falloff = scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp();
+			transmittance.mulAssign( falloff );
+
+			// move along the ray
+
+			distTravelled.addAssign( stepSize );
+
+		} );
+
+		outgoingRayLight.addAssign( transmittance.saturate().oneMinus() );
+
+	}
+
+	scaterringLight( lightColor, builder ) {
+
+		const sceneDepthNode = builder.context.sceneDepthNode;
+
+		if ( sceneDepthNode ) {
+
+			If( sceneDepthNode.greaterThanEqual( linearDepthRay ), () => {
+
+				scatteringDensity.addAssign( lightColor );
+
+			} );
+
+		} else {
+
+			scatteringDensity.addAssign( lightColor );
+
+		}
+
+	}
+
+	direct( { lightNode, lightColor }, builder ) {
+
+		// Ignore lights with infinite distance
+
+		if ( lightNode.light.distance === undefined ) return;
+
+		// TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approches
+
+		const directLight = lightColor.xyz.toVar();
+		directLight.mulAssign( lightNode.shadowNode ); // it no should be necessary if used in the same render pass
+
+		this.scaterringLight( directLight, builder );
+
+	}
+
+	directRectArea( { lightColor, lightPosition, halfWidth, halfHeight }, builder ) {
+
+		const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
+		const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
+		const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
+		const p3 = lightPosition.add( halfWidth ).add( halfHeight );
+
+		const P = builder.context.positionView;
+
+		const directLight = lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow( 1.5 );
+
+		this.scaterringLight( directLight, builder );
+
+	}
+
+	finish( builder ) {
+
+		builder.context.outgoingLight.assign( outgoingRayLight );
+
+	}
+
+}
+
+export default VolumetricLightingModel;

+ 33 - 0
src/nodes/lighting/AnalyticLightNode.js

@@ -6,6 +6,8 @@ import { renderGroup } from '../core/UniformGroupNode.js';
 import { hash } from '../core/NodeUtils.js';
 import { shadow } from './ShadowNode.js';
 import { nodeObject } from '../tsl/TSLCore.js';
+import { lightViewPosition } from '../accessors/Lights.js';
+import { positionView } from '../accessors/Position.js';
 
 /**
  * Base class for analytic light nodes.
@@ -115,6 +117,22 @@ class AnalyticLightNode extends LightingNode {
 
 	}
 
+	getLightVector( builder ) {
+
+		return lightViewPosition( this.light ).sub( builder.context.positionView || positionView );
+
+	}
+
+	/**
+	 * Sets up the direct lighting for the analytic light node.
+	 *
+	 * @abstract
+	 * @param {NodeBuilder} builder - The builder object used for setting up the light.
+	 */
+	setupDirect( /*builder*/ ) { }
+
+	setupDirectRectArea( /*builder*/ ) { }
+
 	/**
 	 * Setups the shadow node for this light. The method exists so concrete light classes
 	 * can setup different types of shadow nodes.
@@ -199,6 +217,21 @@ class AnalyticLightNode extends LightingNode {
 
 		}
 
+		const directLightData = this.setupDirect( builder );
+		const directRectAreaLightData = this.setupDirectRectArea( builder );
+
+		if ( directLightData ) {
+
+			builder.lightsNode.setupDirectLight( builder, this, directLightData );
+
+		}
+
+		if ( directRectAreaLightData ) {
+
+			builder.lightsNode.setupDirectRectAreaLight( builder, this, directRectAreaLightData );
+
+		}
+
 	}
 
 	/**

+ 2 - 11
src/nodes/lighting/DirectionalLightNode.js

@@ -25,21 +25,12 @@ class DirectionalLightNode extends AnalyticLightNode {
 
 	}
 
-	setup( builder ) {
-
-		super.setup( builder );
-
-		const lightingModel = builder.context.lightingModel;
+	setupDirect() {
 
 		const lightColor = this.colorNode;
 		const lightDirection = lightTargetDirection( this.light );
-		const reflectedLight = builder.context.reflectedLight;
 
-		lightingModel.direct( {
-			lightDirection,
-			lightColor,
-			reflectedLight
-		}, builder.stack, builder );
+		return { lightDirection, lightColor };
 
 	}
 

+ 1 - 3
src/nodes/lighting/LightUtils.js

@@ -10,9 +10,7 @@ import { Fn } from '../tsl/TSLBase.js';
  * @param {Node<float>} inputs.decayExponent - The light's decay exponent.
  * @return {Node<float>} The distance falloff.
  */
-export const getDistanceAttenuation = /*@__PURE__*/ Fn( ( inputs ) => {
-
-	const { lightDistance, cutoffDistance, decayExponent } = inputs;
+export const getDistanceAttenuation = /*@__PURE__*/ Fn( ( { lightDistance, cutoffDistance, decayExponent } ) => {
 
 	// based upon Frostbite 3 Moving to Physically-based Rendering
 	// page 32, equation 26: E[window1]

+ 3 - 3
src/nodes/lighting/LightingContextNode.js

@@ -19,14 +19,14 @@ class LightingContextNode extends ContextNode {
 	/**
 	 * Constructs a new lighting context node.
 	 *
-	 * @param {LightsNode} node - The lights node.
+	 * @param {LightsNode} lightsNode - The lights node.
 	 * @param {?LightingModel} [lightingModel=null] - The current lighting model.
 	 * @param {?Node<vec3>} [backdropNode=null] - A backdrop node.
 	 * @param {?Node<float>} [backdropAlphaNode=null] - A backdrop alpha node.
 	 */
-	constructor( node, lightingModel = null, backdropNode = null, backdropAlphaNode = null ) {
+	constructor( lightsNode, lightingModel = null, backdropNode = null, backdropAlphaNode = null ) {
 
-		super( node );
+		super( lightsNode );
 
 		/**
 		 * The current lighting model.

+ 58 - 14
src/nodes/lighting/LightsNode.js

@@ -236,6 +236,37 @@ class LightsNode extends Node {
 
 	}
 
+	/**
+	 * Sets up a direct light in the lighting model.
+	 *
+	 * @param {Object} builder - The builder object containing the context and stack.
+	 * @param {Object} lightNode - The light node.
+	 * @param {Object} lightData - The light object containing color and direction properties.
+	 */
+	setupDirectLight( builder, lightNode, lightData ) {
+
+		const { lightingModel, reflectedLight } = builder.context;
+
+		lightingModel.direct( {
+			...lightData,
+			lightNode,
+			reflectedLight
+		}, builder );
+
+	}
+
+	setupDirectRectAreaLight( builder, lightNode, lightData ) {
+
+		const { lightingModel, reflectedLight } = builder.context;
+
+		lightingModel.directRectArea( {
+			...lightData,
+			lightNode,
+			reflectedLight
+		}, builder );
+
+	}
+
 	/**
 	 * Setups the internal lights by building all respective
 	 * light nodes.
@@ -253,6 +284,14 @@ class LightsNode extends Node {
 
 	}
 
+	getLightNodes( builder ) {
+
+		if ( this._lightNodes === null ) this.setupLightsNode( builder );
+
+		return this._lightNodes;
+
+	}
+
 	/**
 	 * The implementation makes sure that for each light in the scene
 	 * there is a corresponding light node. By building the light nodes
@@ -263,16 +302,22 @@ class LightsNode extends Node {
 	 */
 	setup( builder ) {
 
-		if ( this._lightNodes === null ) this.setupLightsNode( builder );
+		const currentLightsNode = builder.lightsNode;
+
+		builder.lightsNode = this;
+
+		//
+
+		let outgoingLightNode = this.outgoingLightNode;
 
 		const context = builder.context;
 		const lightingModel = context.lightingModel;
 
-		let outgoingLightNode = this.outgoingLightNode;
+		const properties = builder.getDataFromNode( this );
 
 		if ( lightingModel ) {
 
-			const { _lightNodes, totalDiffuseNode, totalSpecularNode } = this;
+			const { totalDiffuseNode, totalSpecularNode } = this;
 
 			context.outgoingLight = outgoingLightNode;
 
@@ -280,20 +325,11 @@ class LightsNode extends Node {
 
 			//
 
-			const properties = builder.getDataFromNode( this );
 			properties.nodes = stack.nodes;
 
 			//
 
-			lightingModel.start( context, stack, builder );
-
-			// lights
-
-			this.setupLights( builder, _lightNodes );
-
-			//
-
-			lightingModel.indirect( context, stack, builder );
+			lightingModel.start( builder );
 
 			//
 
@@ -325,14 +361,22 @@ class LightsNode extends Node {
 
 			//
 
-			lightingModel.finish( context, stack, builder );
+			lightingModel.finish( builder );
 
 			//
 
 			outgoingLightNode = outgoingLightNode.bypass( builder.removeStack() );
 
+		} else {
+
+			properties.nodes = [];
+
 		}
 
+		//
+
+		builder.lightsNode = currentLightsNode;
+
 		return outgoingLightNode;
 
 	}

+ 11 - 26
src/nodes/lighting/PointLightNode.js

@@ -1,38 +1,25 @@
 import AnalyticLightNode from './AnalyticLightNode.js';
 import { getDistanceAttenuation } from './LightUtils.js';
 import { uniform } from '../core/UniformNode.js';
-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 ) => {
+export const directPointLight = ( { color, lightVector, cutoffDistance, decayExponent } ) => {
 
-	const lightingModel = builder.context.lightingModel;
+	const lightDirection = lightVector.normalize();
+	const lightDistance = lightVector.length();
 
-	const lVector = lightViewPosition.sub( positionView ); // @TODO: Add it into LightNode
-
-	const lightDirection = lVector.normalize();
-	const lightDistance = lVector.length();
-
-	const lightAttenuation = getDistanceAttenuation( {
+	const attenuation = getDistanceAttenuation( {
 		lightDistance,
 		cutoffDistance,
 		decayExponent
 	} );
 
-	const lightColor = color.mul( lightAttenuation );
+	const lightColor = color.mul( attenuation );
 
-	const reflectedLight = builder.context.reflectedLight;
+	return { lightDirection, lightColor };
 
-	lightingModel.direct( {
-		lightDirection,
-		lightColor,
-		reflectedLight
-	}, builder.stack, builder );
-
-} );
+};
 
 /**
  * Module for representing point lights as nodes.
@@ -99,16 +86,14 @@ class PointLightNode extends AnalyticLightNode {
 
 	}
 
-	setup( builder ) {
-
-		super.setup( builder );
+	setupDirect( builder ) {
 
-		directPointLight( {
+		return directPointLight( {
 			color: this.colorNode,
-			lightViewPosition: lightViewPosition( this.light ),
+			lightVector: this.getLightVector( builder ),
 			cutoffDistance: this.cutoffDistanceNode,
 			decayExponent: this.decayExponentNode
-		} ).append();
+		} );
 
 	}
 

+ 3 - 8
src/nodes/lighting/RectAreaLightNode.js

@@ -86,9 +86,7 @@ class RectAreaLightNode extends AnalyticLightNode {
 
 	}
 
-	setup( builder ) {
-
-		super.setup( builder );
+	setupDirectRectArea( builder ) {
 
 		let ltc_1, ltc_2;
 
@@ -105,20 +103,17 @@ class RectAreaLightNode extends AnalyticLightNode {
 		}
 
 		const { colorNode, light } = this;
-		const lightingModel = builder.context.lightingModel;
 
 		const lightPosition = lightViewPosition( light );
-		const reflectedLight = builder.context.reflectedLight;
 
-		lightingModel.directRectArea( {
+		return {
 			lightColor: colorNode,
 			lightPosition,
 			halfWidth: this.halfWidth,
 			halfHeight: this.halfHeight,
-			reflectedLight,
 			ltc_1,
 			ltc_2
-		}, builder.stack, builder );
+		};
 
 	}
 

+ 5 - 5
src/nodes/lighting/ShadowBaseNode.js

@@ -1,6 +1,6 @@
 import Node from '../core/Node.js';
 import { NodeUpdateType } from '../core/constants.js';
-import { vec3 } from '../tsl/TSLBase.js';
+import { property } from '../tsl/TSLBase.js';
 import { positionWorld } from '../accessors/Position.js';
 
 /**
@@ -58,13 +58,13 @@ class ShadowBaseNode extends Node {
 	/**
 	 * Setups the shadow position node which is by default the predefined TSL node object `shadowPositionWorld`.
 	 *
-	 * @param {(NodeBuilder|{Material})} object - A configuration object that must at least hold a material reference.
+	 * @param {NodeBuilder} object - A configuration object that must at least hold a material reference.
 	 */
-	setupShadowPosition( { material } ) {
+	setupShadowPosition( { context, material } ) {
 
 		// Use assign inside an Fn()
 
-		shadowPositionWorld.assign( material.shadowPositionNode || positionWorld );
+		shadowPositionWorld.assign( material.shadowPositionNode || context.shadowPositionWorld || positionWorld );
 
 	}
 
@@ -87,6 +87,6 @@ class ShadowBaseNode extends Node {
  * @tsl
  * @type {Node<vec3>}
  */
-export const shadowPositionWorld = /*@__PURE__*/ vec3().toVar( 'shadowPositionWorld' );
+export const shadowPositionWorld = /*@__PURE__*/ property( 'vec3', 'shadowPositionWorld' );
 
 export default ShadowBaseNode;

+ 15 - 1
src/nodes/lighting/ShadowNode.js

@@ -395,6 +395,8 @@ class ShadowNode extends ShadowBaseNode {
 		 */
 		this._node = null;
 
+		this._cameraFrameId = new WeakMap();
+
 		/**
 		 * This flag can be used for type testing.
 		 *
@@ -766,7 +768,19 @@ class ShadowNode extends ShadowBaseNode {
 
 		const { shadow } = this;
 
-		const needsUpdate = shadow.needsUpdate || shadow.autoUpdate;
+		let needsUpdate = shadow.needsUpdate || shadow.autoUpdate;
+
+		if ( needsUpdate ) {
+
+			if ( this._cameraFrameId[ frame.camera ] === frame.frameId ) {
+
+				needsUpdate = false;
+
+			}
+
+			this._cameraFrameId[ frame.camera ] = frame.frameId;
+
+		}
 
 		if ( needsUpdate ) {
 

+ 7 - 18
src/nodes/lighting/SpotLightNode.js

@@ -2,9 +2,8 @@ import AnalyticLightNode from './AnalyticLightNode.js';
 import { getDistanceAttenuation } from './LightUtils.js';
 import { uniform } from '../core/UniformNode.js';
 import { smoothstep } from '../math/MathNode.js';
-import { positionView } from '../accessors/Position.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
-import { lightViewPosition, lightTargetDirection, lightProjectionUV } from '../accessors/Lights.js';
+import { lightTargetDirection, lightProjectionUV } from '../accessors/Lights.js';
 import { texture } from '../accessors/TextureNode.js';
 
 /**
@@ -92,21 +91,17 @@ class SpotLightNode extends AnalyticLightNode {
 
 	}
 
-	setup( builder ) {
-
-		super.setup( builder );
-
-		const lightingModel = builder.context.lightingModel;
+	setupDirect( builder ) {
 
 		const { colorNode, cutoffDistanceNode, decayExponentNode, light } = this;
 
-		const lVector = lightViewPosition( light ).sub( positionView ); // @TODO: Add it into LightNode
+		const lightVector = this.getLightVector( builder );
 
-		const lightDirection = lVector.normalize();
+		const lightDirection = lightVector.normalize();
 		const angleCos = lightDirection.dot( lightTargetDirection( light ) );
 		const spotAttenuation = this.getSpotAttenuation( angleCos );
 
-		const lightDistance = lVector.length();
+		const lightDistance = lightVector.length();
 
 		const lightAttenuation = getDistanceAttenuation( {
 			lightDistance,
@@ -118,7 +113,7 @@ class SpotLightNode extends AnalyticLightNode {
 
 		if ( light.map ) {
 
-			const spotLightCoord = lightProjectionUV( light );
+			const spotLightCoord = lightProjectionUV( light, builder.context.positionWorld );
 			const projectedTexture = texture( light.map, spotLightCoord.xy ).onRenderUpdate( () => light.map );
 
 			const inSpotLightMap = spotLightCoord.mul( 2. ).sub( 1. ).abs().lessThan( 1. ).all();
@@ -127,13 +122,7 @@ class SpotLightNode extends AnalyticLightNode {
 
 		}
 
-		const reflectedLight = builder.context.reflectedLight;
-
-		lightingModel.direct( {
-			lightDirection,
-			lightColor,
-			reflectedLight
-		}, builder.stack, builder );
+		return { lightColor, lightDirection };
 
 	}
 

Некоторые файлы не были показаны из-за большого количества измененных файлов

粤ICP备19079148号