Browse Source

WebGPURenderer: Introduce `TiledLighting` (#29642)

* Renderer: move `.nodes.library` -> `.library`

* WGSLNodeBuilder: Remove incompatible types

* introduce lighting system

* introduce tiled lighting

* update imports

* cleanup

* revision

* `misc_controls_fly` : fix warning

* revision

* cleanup

* fix needsUpdate
sunag 1 năm trước cách đây
mục cha
commit
25ff4db3cb

+ 1 - 0
examples/files.json

@@ -339,6 +339,7 @@
 		"webgpu_lights_phong",
 		"webgpu_lights_rectarealight",
 		"webgpu_lights_selective",
+		"webgpu_lights_tiled",
 		"webgpu_lines_fat_wireframe",
 		"webgpu_lines_fat",
 		"webgpu_loader_gltf",

+ 18 - 0
examples/jsm/lighting/TiledLighting.js

@@ -0,0 +1,18 @@
+import { Lighting } from 'three';
+import { tiledLights } from '../tsl/lighting/TiledLightsNode.js';
+
+export class TiledLighting extends Lighting {
+
+	constructor() {
+
+		super();
+
+	}
+
+	createNode( lights = [] ) {
+
+		return tiledLights().setLights( lights );
+
+	}
+
+}

+ 2 - 2
examples/jsm/tsl/display/FilmNode.js

@@ -1,4 +1,4 @@
-import { TempNode, rand, Fn, fract, timerLocal, uv, clamp, mix, vec4, nodeProxy } from 'three/tsl';
+import { TempNode, rand, Fn, fract, time, uv, clamp, mix, vec4, nodeProxy } from 'three/tsl';
 
 class FilmNode extends TempNode {
 
@@ -25,7 +25,7 @@ class FilmNode extends TempNode {
 		const film = Fn( () => {
 
 			const base = this.inputNode.rgb;
-			const noise = rand( fract( uvNode.add( timerLocal() ) ) );
+			const noise = rand( fract( uvNode.add( time ) ) );
 
 			let color = base.add( base.mul( clamp( noise.add( 0.1 ), 0, 1 ) ) );
 

+ 392 - 0
examples/jsm/tsl/lighting/TiledLightsNode.js

@@ -0,0 +1,392 @@
+import {
+	storageObject, nodeProxy, int, float, vec2, ivec2, ivec4, uniform, Break, Loop,
+	Fn, If, Return, textureLoad, instanceIndex, screenCoordinate, directPointLight
+} from 'three/tsl';
+
+import * as THREE from 'three';
+
+export const circleIntersectsAABB = /*@__PURE__*/ Fn( ( [ circleCenter, radius, minBounds, maxBounds ] ) => {
+
+	// Find the closest point on the AABB to the circle's center using method chaining
+	const closestX = minBounds.x.max( circleCenter.x.min( maxBounds.x ) );
+	const closestY = minBounds.y.max( circleCenter.y.min( maxBounds.y ) );
+
+	// Compute the distance between the circle's center and the closest point
+	const distX = circleCenter.x.sub( closestX );
+	const distY = circleCenter.y.sub( closestY );
+
+	// Calculate the squared distance
+	const distSquared = distX.mul( distX ).add( distY.mul( distY ) );
+
+	return distSquared.lessThanEqual( radius.mul( radius ) );
+
+} ).setLayout( {
+	name: 'circleIntersectsAABB',
+	type: 'bool',
+	inputs: [
+		{ name: 'circleCenter', type: 'vec2' },
+		{ name: 'radius', type: 'float' },
+		{ name: 'minBounds', type: 'vec2' },
+		{ name: 'maxBounds', type: 'vec2' }
+	]
+} );
+
+const _vector3 = /*@__PURE__*/ new THREE.Vector3();
+const _size = /*@__PURE__*/ new THREE.Vector2();
+
+class TiledLightsNode extends THREE.LightsNode {
+
+	static get type() {
+
+		return 'TiledLightsNode';
+
+	}
+
+	constructor( maxLights = 1024, tileSize = 32 ) {
+
+		super();
+
+		this.materialLights = [];
+		this.tiledLights = [];
+
+		this.maxLights = maxLights;
+		this.tileSize = tileSize;
+
+		this.bufferSize = null;
+		this.lightIndexes = null;
+		this.screenTileIndex = null;
+		this.compute = null;
+		this.lightsTexture = null;
+
+		this.lightsCount = uniform( 0, 'int' );
+		this.tileLightCount = 8;
+		this.screenSize = uniform( new THREE.Vector2() );
+		this.cameraProjectionMatrix = uniform( 'mat4' );
+		this.cameraViewMatrix = uniform( 'mat4' );
+
+		this.updateBeforeType = THREE.NodeUpdateType.RENDER;
+
+	}
+
+	updateLightsTexture() {
+
+		const { lightsTexture, tiledLights } = this;
+
+		const data = lightsTexture.image.data;
+		const lineSize = lightsTexture.image.width * 4;
+
+		this.lightsCount.value = tiledLights.length;
+
+		for ( let i = 0; i < tiledLights.length; i ++ ) {
+
+			const light = tiledLights[ i ];
+
+			// world position
+
+			_vector3.setFromMatrixPosition( light.matrixWorld );
+
+			// store data
+
+			const offset = i * 4;
+
+			data[ offset + 0 ] = _vector3.x;
+			data[ offset + 1 ] = _vector3.y;
+			data[ offset + 2 ] = _vector3.z;
+			data[ offset + 3 ] = light.distance;
+
+			data[ lineSize + offset + 0 ] = light.color.r;
+			data[ lineSize + offset + 1 ] = light.color.g;
+			data[ lineSize + offset + 2 ] = light.color.b;
+			data[ lineSize + offset + 3 ] = light.decay;
+
+		}
+
+		lightsTexture.needsUpdate = true;
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer, camera } = frame;
+
+		this.updateProgram( renderer );
+
+		this.updateLightsTexture( camera );
+
+		this.cameraProjectionMatrix.value = camera.projectionMatrix;
+		this.cameraViewMatrix.value = camera.matrixWorldInverse;
+
+		renderer.getDrawingBufferSize( _size );
+		this.screenSize.value.copy( _size );
+
+		renderer.compute( this.compute );
+
+	}
+
+	setLights( lights ) {
+
+		const { tiledLights, materialLights } = this;
+
+		let materialindex = 0;
+		let tiledIndex = 0;
+
+		for ( const light of lights ) {
+
+			if ( light.isPointLight === true ) {
+
+				tiledLights[ tiledIndex ++ ] = light;
+
+			} else {
+
+				materialLights[ materialindex ++ ] = light;
+
+			}
+
+		}
+
+		materialLights.length = materialindex;
+		tiledLights.length = tiledIndex;
+
+		return super.setLights( materialLights );
+
+	}
+
+	getBlock( block = 0 ) {
+
+		return this.lightIndexes.element( this.screenTileIndex.mul( int( 2 ).add( int( block ) ) ) );
+
+	}
+
+	getTile( element ) {
+
+		element = int( element );
+
+		const stride = int( 4 );
+		const tileOffset = element.div( stride );
+		const tileIndex = this.screenTileIndex.mul( int( 2 ) ).add( tileOffset );
+
+		return this.lightIndexes.element( tileIndex ).element( element.modInt( stride ) );
+
+	}
+
+	getLightData( index ) {
+
+		index = int( index );
+
+		const dataA = textureLoad( this.lightsTexture, ivec2( index, 0 ) );
+		const dataB = textureLoad( this.lightsTexture, ivec2( index, 1 ) );
+
+		const position = dataA.xyz;
+		const viewPosition = this.cameraViewMatrix.mul( position );
+		const distance = dataA.w;
+		const color = dataB.rgb;
+		const decay = dataB.w;
+
+		return {
+			position,
+			viewPosition,
+			distance,
+			color,
+			decay
+		};
+
+	}
+
+	setupLights( builder, lightNodes ) {
+
+		this.updateProgram( builder.renderer );
+
+		//
+
+		const lightingModel = builder.context.reflectedLight;
+
+		// force declaration order, before of the loop
+		lightingModel.directDiffuse.append();
+		lightingModel.directSpecular.append();
+
+		Fn( () => {
+
+			Loop( this.tileLightCount, ( { i } ) => {
+
+				const lightIndex = this.getTile( i );
+
+				If( lightIndex.equal( int( 0 ) ), () => {
+
+					Break();
+
+				} );
+
+				const { color, decay, viewPosition, distance } = this.getLightData( lightIndex.sub( 1 ) );
+
+				directPointLight( {
+					color,
+					lightViewPosition: viewPosition,
+					cutoffDistance: distance,
+					decayExponent: decay
+				} ).append();
+
+			} );
+
+		} )().append();
+
+		// others lights
+
+		super.setupLights( builder, lightNodes );
+
+	}
+
+	getBufferFitSize( value ) {
+
+		const multiple = this.tileSize;
+
+		return Math.ceil( value / multiple ) * multiple;
+
+	}
+
+	setSize( width, height ) {
+
+		width = this.getBufferFitSize( width );
+		height = this.getBufferFitSize( height );
+
+		if ( ! this.bufferSize || this.bufferSize.width !== width || this.bufferSize.height !== height ) {
+
+			this.create( width, height );
+
+		}
+
+		return this;
+
+	}
+
+	updateProgram( renderer ) {
+
+		renderer.getDrawingBufferSize( _size );
+
+		const width = this.getBufferFitSize( _size.width );
+		const height = this.getBufferFitSize( _size.height );
+
+		if ( this.bufferSize === null ) {
+
+			this.create( width, height );
+
+		} else if ( this.bufferSize.width !== width || this.bufferSize.height !== height ) {
+
+			this.create( width, height );
+
+		}
+
+	}
+
+	create( width, height ) {
+
+		const { tileSize, maxLights } = this;
+
+		const bufferSize = new THREE.Vector2( width, height );
+		const lineSize = Math.floor( bufferSize.width / tileSize );
+		const count = Math.floor( ( bufferSize.width * bufferSize.height ) / tileSize );
+
+		// buffers
+
+		const lightsData = new Float32Array( maxLights * 4 * 2 ); // 2048 lights, 4 elements(rgba), 2 components, 1 component per line (position, distance, color, decay)
+		const lightsTexture = new THREE.DataTexture( lightsData, lightsData.length / 8, 2, THREE.RGBAFormat, THREE.FloatType );
+
+		const lightIndexesArray = new Int32Array( count * 4 * 2 );
+		const lightIndexesAttribute = new THREE.StorageBufferAttribute( lightIndexesArray, 4 );
+		const lightIndexes = storageObject( lightIndexesAttribute, 'ivec4', lightIndexesAttribute.count ).label( 'lightIndexes' );
+
+		// compute
+
+		const getBlock = ( index ) => {
+
+			const tileIndex = instanceIndex.mul( int( 2 ) ).add( int( index ) );
+
+			return lightIndexes.element( tileIndex );
+
+		};
+
+		const getTile = ( elementIndex ) => {
+
+			elementIndex = int( elementIndex );
+
+			const stride = int( 4 );
+			const tileOffset = elementIndex.div( stride );
+			const tileIndex = instanceIndex.mul( int( 2 ) ).add( tileOffset );
+
+			return lightIndexes.element( tileIndex ).element( elementIndex.modInt( stride ) );
+
+		};
+
+		const compute = Fn( () => {
+
+			const { cameraProjectionMatrix, bufferSize, screenSize } = this;
+
+			const tiledBufferSize = bufferSize.clone().divideScalar( tileSize ).floor();
+
+			const tileScreen = vec2(
+				instanceIndex.modInt( tiledBufferSize.width ),
+				instanceIndex.div( tiledBufferSize.width )
+			).mul( tileSize ).div( screenSize );
+
+			const blockSize = float( tileSize ).div( screenSize );
+			const minBounds = tileScreen;
+			const maxBounds = minBounds.add( blockSize );
+
+			const index = int( 0 ).toVar();
+
+			getBlock( 0 ).assign( ivec4( 0 ) );
+			getBlock( 1 ).assign( ivec4( 0 ) );
+
+			Loop( this.maxLights, ( { i } ) => {
+
+				If( index.greaterThanEqual( this.tileLightCount ).or( int( i ).greaterThanEqual( int( this.lightsCount ) ) ), () => {
+
+					Return();
+
+				} );
+
+				const { viewPosition, distance } = this.getLightData( i );
+
+				const projectedPosition = cameraProjectionMatrix.mul( viewPosition );
+				const ndc = projectedPosition.div( projectedPosition.w );
+				const screenPosition = ndc.xy.mul( 0.5 ).add( 0.5 ).flipY();
+
+				const distanceFromCamera = viewPosition.z;
+				const pointRadius = distance.div( distanceFromCamera );
+
+				If( circleIntersectsAABB( screenPosition, pointRadius, minBounds, maxBounds ), () => {
+
+					getTile( index ).assign( i.add( int( 1 ) ) );
+					index.addAssign( int( 1 ) );
+
+				} );
+
+			} );
+
+		} )().compute( count );
+
+		// screen coordinate lighting indexes
+
+		const screenTile = screenCoordinate.div( tileSize ).floor().toVar();
+		const screenTileIndex = screenTile.x.add( screenTile.y.mul( lineSize ) );
+
+		// assigns
+
+		this.bufferSize = bufferSize;
+		this.lightIndexes = lightIndexes;
+		this.screenTileIndex = screenTileIndex;
+		this.compute = compute;
+		this.lightsTexture = lightsTexture;
+
+	}
+
+	get hasLights() {
+
+		return super.hasLights || this.tiledLights.length > 0;
+
+	}
+
+}
+
+export default TiledLightsNode;
+
+export const tiledLights = /*@__PURE__*/ nodeProxy( TiledLightsNode );

BIN
examples/screenshots/webgpu_lights_tiled.jpg


+ 234 - 0
examples/webgpu_lights_tiled.html

@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - tiled 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 - Compute-based Tiled Lighting<br />
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { texture, uv, pass, uniform } from 'three/tsl';
+			import { bloom } from 'three/addons/tsl/display/BloomNode.js';
+
+			import { TiledLighting } from 'three/addons/lighting/TiledLighting.js';
+
+			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';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+
+			let camera, scene, renderer,
+				lights, lightDummy,
+				stats, controls,
+				compose, tileInfluence,
+				lighting,
+				count,
+				postProcessing;
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 600 );
+				camera.position.z = 200;
+				camera.position.y = 30;
+
+				scene = new THREE.Scene();
+				scene.fog = new THREE.Fog( 0x111111, 300, 500 );
+				scene.background = new THREE.Color( 0x111111 );
+
+				count = 1000;
+
+				const material = new THREE.MeshBasicMaterial();
+
+				lightDummy = new THREE.InstancedMesh( new THREE.SphereGeometry( 0.1, 16, 8 ), material, count );
+				lightDummy.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
+				scene.add( lightDummy );
+
+				// lights
+
+				lights = new THREE.Group();
+				scene.add( lights );
+
+				const addLight = ( hexColor, power = 10000, distance = 3 ) => {
+
+					const light = new THREE.PointLight( hexColor, 1, distance );
+					light.position.set( Math.random() * 300 - 150, 1, Math.random() * 300 - 150 );
+					light.power = power;
+					light.userData.fixedPosition = light.position.clone();
+					lights.add( light );
+
+					return light;
+
+				};
+
+				const color = new THREE.Color();
+
+				for ( let i = 0; i < count; i ++ ) {
+
+					const hex = ( Math.random() * 0xffffff ) + 0x666666;
+
+					lightDummy.setColorAt( i, color.setHex( hex ) );
+
+					addLight( hex );
+
+				}
+
+				//
+
+				const lightAmbient = new THREE.AmbientLight( 0xffffff, .1 );
+				scene.add( lightAmbient );
+
+				// textures
+
+				const textureLoader = new THREE.TextureLoader();
+
+				const floorColor = textureLoader.load( 'textures/floors/FloorsCheckerboard_S_Diffuse.jpg' );
+				floorColor.wrapS = THREE.RepeatWrapping;
+				floorColor.wrapT = THREE.RepeatWrapping;
+				floorColor.colorSpace = THREE.SRGBColorSpace;
+
+				const floorNormal = textureLoader.load( 'textures/floors/FloorsCheckerboard_S_Normal.jpg' );
+				floorNormal.wrapS = THREE.RepeatWrapping;
+				floorNormal.wrapT = THREE.RepeatWrapping;
+
+				const planeGeometry = new THREE.PlaneGeometry( 1000, 1000 );
+				const planeMaterial = new THREE.MeshPhongNodeMaterial( {
+					colorNode: texture( floorColor, uv().mul( 50 ) ),
+					normalMap: floorNormal
+				} );
+
+				const ground = new THREE.Mesh( planeGeometry, planeMaterial );
+				ground.rotation.x = - Math.PI / 2;
+				ground.position.y = 0;
+				ground.castShadow = true;
+				ground.receiveShadow = true;
+				scene.add( ground );
+
+				// renderer
+
+				lighting = new TiledLighting(); // ( maxLights = 1024, tileSize = 32 )
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.lighting = lighting; // set lighting system
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMappingExposure = 5;
+				renderer.lighting = lighting; // set lighting
+				document.body.appendChild( renderer.domElement );
+
+				// controls
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.maxDistance = 400;
+
+				// stats
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// post processing
+
+				const scenePass = pass( scene, camera );
+				const bloomPass = bloom( scenePass, 3, .9, .2 );
+
+				// compose
+
+				compose = scenePass.add( bloomPass );
+				tileInfluence = uniform( 0 );
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				updatePostProcessing();
+
+				// gui
+
+				const gui = new GUI();
+				gui.add( tileInfluence, 'value', 0, 1 ).name( 'tile indexes debug' );
+
+			}
+
+			function updatePostProcessing() {
+
+				// tile indexes debug, needs to be updated every time the renderer size changes
+
+				const debugBlockIndexes = lighting.getNode( scene, camera ).setSize( window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio ).getBlock().toColor().div( count * 2 );
+
+				postProcessing.outputNode = compose.add( debugBlockIndexes.mul( tileInfluence ) );
+				postProcessing.needsUpdate = true;
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				updatePostProcessing();
+
+			}
+
+			function animate() {
+
+				const time = performance.now() / 1000;
+
+				for ( let i = 0; i < lights.children.length; i ++ ) {
+
+					const light = lights.children[ i ];
+					const lightTime = ( time * 0.5 ) + light.id;
+
+					light.position.copy( light.userData.fixedPosition );
+					light.position.x += Math.sin( lightTime * 0.7 ) * 3;
+					light.position.y += Math.cos( lightTime * 0.5 ) * .5;
+					light.position.z += Math.cos( lightTime * 0.3 ) * 3;
+
+					lightDummy.setMatrixAt( i, light.matrixWorld );
+
+				}
+
+				postProcessing.render();
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
src/Three.WebGPU.Nodes.js

@@ -164,6 +164,7 @@ export * from './Three.Legacy.js';
 
 export * from './materials/nodes/NodeMaterials.js';
 export { default as WebGPURenderer } from './renderers/webgpu/WebGPURenderer.Nodes.js';
+export { default as Lighting } from './renderers/common/Lighting.js';
 export { default as QuadMesh } from './renderers/common/QuadMesh.js';
 export { default as PMREMGenerator } from './renderers/common/extras/PMREMGenerator.js';
 export { default as PostProcessing } from './renderers/common/PostProcessing.js';

+ 1 - 0
src/Three.WebGPU.js

@@ -164,6 +164,7 @@ export * from './Three.Legacy.js';
 
 export * from './materials/nodes/NodeMaterials.js';
 export { default as WebGPURenderer } from './renderers/webgpu/WebGPURenderer.js';
+export { default as Lighting } from './renderers/common/Lighting.js';
 export { default as BundleGroup } from './renderers/common/BundleGroup.js';
 export { default as QuadMesh } from './renderers/common/QuadMesh.js';
 export { default as PMREMGenerator } from './renderers/common/extras/PMREMGenerator.js';

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

@@ -13,7 +13,6 @@ import { materialReference } from '../../nodes/accessors/MaterialReferenceNode.j
 import { positionLocal, positionView } from '../../nodes/accessors/Position.js';
 import { skinningReference } from '../../nodes/accessors/SkinningNode.js';
 import { morphReference } from '../../nodes/accessors/MorphNode.js';
-import { lights } from '../../nodes/lighting/LightsNode.js';
 import { mix } from '../../nodes/math/MathNode.js';
 import { float, vec3, vec4 } from '../../nodes/tsl/TSLBase.js';
 import AONode from '../../nodes/lighting/AONode.js';
@@ -460,7 +459,7 @@ class NodeMaterial extends Material {
 
 		if ( materialLightsNode.length > 0 ) {
 
-			lightsN = lights( [ ...lightsN.getLights(), ...materialLightsNode ] );
+			lightsN = builder.renderer.lighting.createNode( [ ...lightsN.getLights(), ...materialLightsNode ] );
 
 		}
 
@@ -487,7 +486,7 @@ class NodeMaterial extends Material {
 
 		let outgoingLightNode = this.setupOutgoingLight( builder );
 
-		if ( lightsNode && lightsNode.getScope().getLights().length > 0 ) {
+		if ( lightsNode && lightsNode.getScope().hasLights ) {
 
 			const lightingModel = this.setupLightingModel( builder );
 

+ 1 - 0
src/nodes/TSL.js

@@ -132,6 +132,7 @@ export * from './accessors/Lights.js';
 export * from './lighting/LightsNode.js';
 export * from './lighting/LightingContextNode.js';
 export * from './lighting/ShadowNode.js';
+export * from './lighting/PointLightNode.js';
 
 // pmrem
 export * from './pmrem/PMREMNode.js';

+ 1 - 1
src/nodes/core/NodeBuilder.js

@@ -1324,7 +1324,7 @@ class NodeBuilder {
 
 		if ( material !== null ) {
 
-			let nodeMaterial = renderer.nodes.library.fromMaterial( material );
+			let nodeMaterial = renderer.library.fromMaterial( material );
 
 			if ( nodeMaterial === null ) {
 

+ 1 - 1
src/nodes/display/ColorSpaceNode.js

@@ -79,7 +79,7 @@ class ColorSpaceNode extends TempNode {
 
 		let outputNode = null;
 
-		const colorSpaceFn = renderer.nodes.library.getColorSpaceFunction( colorSpace );
+		const colorSpaceFn = renderer.library.getColorSpaceFunction( colorSpace );
 
 		if ( colorSpaceFn !== null ) {
 

+ 1 - 1
src/nodes/display/ToneMappingNode.js

@@ -39,7 +39,7 @@ class ToneMappingNode extends TempNode {
 
 		let outputNode = null;
 
-		const toneMappingFn = builder.renderer.nodes.library.getToneMappingFunction( toneMapping );
+		const toneMappingFn = builder.renderer.library.getToneMappingFunction( toneMapping );
 
 		if ( toneMappingFn !== null ) {
 

+ 24 - 12
src/nodes/lighting/LightsNode.js

@@ -1,5 +1,5 @@
 import Node from '../core/Node.js';
-import { nodeObject, nodeProxy, vec3 } from '../tsl/TSLBase.js';
+import { nodeObject, vec3 } from '../tsl/TSLBase.js';
 
 const sortLights = ( lights ) => {
 
@@ -33,7 +33,7 @@ class LightsNode extends Node {
 
 	}
 
-	constructor( lights = [] ) {
+	constructor() {
 
 		super( 'vec3' );
 
@@ -42,7 +42,7 @@ class LightsNode extends Node {
 
 		this.outgoingLightNode = vec3().toVar( 'outgoingLight' );
 
-		this._lights = lights;
+		this._lights = [];
 
 		this._lightNodes = null;
 		this._lightNodesHash = null;
@@ -61,7 +61,7 @@ class LightsNode extends Node {
 
 			for ( const lightNode of this._lightNodes ) {
 
-				hash.push( lightNode.getHash() );
+				hash.push( lightNode.getSelf().getHash() );
 
 			}
 
@@ -92,7 +92,7 @@ class LightsNode extends Node {
 		const previousLightNodes = this._lightNodes;
 
 		const lights = sortLights( this._lights );
-		const nodeLibrary = builder.renderer.nodes.library;
+		const nodeLibrary = builder.renderer.library;
 
 		for ( const light of lights ) {
 
@@ -125,7 +125,7 @@ class LightsNode extends Node {
 
 					if ( ! _lightsNodeRef.has( light ) ) {
 
-						lightNode = new lightNodeClass( light );
+						lightNode = nodeObject( new lightNodeClass( light ) );
 						_lightsNodeRef.set( light, lightNode );
 
 					} else {
@@ -146,6 +146,16 @@ class LightsNode extends Node {
 
 	}
 
+	setupLights( builder, lightNodes ) {
+
+		for ( const lightNode of lightNodes ) {
+
+			lightNode.build( builder );
+
+		}
+
+	}
+
 	setup( builder ) {
 
 		if ( this._lightNodes === null ) this.setupLightsNode( builder );
@@ -174,11 +184,7 @@ class LightsNode extends Node {
 
 			// lights
 
-			for ( const lightNode of _lightNodes ) {
-
-				lightNode.build( builder );
-
-			}
+			this.setupLights( builder, _lightNodes );
 
 			//
 
@@ -243,8 +249,14 @@ class LightsNode extends Node {
 
 	}
 
+	get hasLights() {
+
+		return this._lights.length > 0;
+
+	}
+
 }
 
 export default LightsNode;
 
-export const lights = /*@__PURE__*/ nodeProxy( LightsNode );
+export const lights = ( lights = [] ) => nodeObject( new LightsNode() ).setLights( lights );

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

@@ -3,7 +3,35 @@ import { getDistanceAttenuation } from './LightUtils.js';
 import { uniform } from '../core/UniformNode.js';
 import { lightViewPosition } from '../accessors/Lights.js';
 import { positionView } from '../accessors/Position.js';
-import { renderGroup } from '../TSL.js';
+import { Fn } from '../tsl/TSLBase.js';
+import { renderGroup } from '../core/UniformGroupNode.js';
+
+export const directPointLight = Fn( ( { color, lightViewPosition, cutoffDistance, decayExponent }, builder ) => {
+
+	const lightingModel = builder.context.lightingModel;
+
+	const lVector = lightViewPosition.sub( positionView ); // @TODO: Add it into LightNode
+
+	const lightDirection = lVector.normalize();
+	const lightDistance = lVector.length();
+
+	const lightAttenuation = getDistanceAttenuation( {
+		lightDistance,
+		cutoffDistance,
+		decayExponent
+	} );
+
+	const lightColor = color.mul( lightAttenuation );
+
+	const reflectedLight = builder.context.reflectedLight;
+
+	lightingModel.direct( {
+		lightDirection,
+		lightColor,
+		reflectedLight
+	}, builder.stack, builder );
+
+} );
 
 class PointLightNode extends AnalyticLightNode {
 
@@ -33,32 +61,14 @@ class PointLightNode extends AnalyticLightNode {
 
 	}
 
-	setup( builder ) {
-
-		const { colorNode, cutoffDistanceNode, decayExponentNode, light } = this;
-
-		const lightingModel = builder.context.lightingModel;
-
-		const lVector = lightViewPosition( light ).sub( positionView ); // @TODO: Add it into LightNode
-
-		const lightDirection = lVector.normalize();
-		const lightDistance = lVector.length();
-
-		const lightAttenuation = getDistanceAttenuation( {
-			lightDistance,
-			cutoffDistance: cutoffDistanceNode,
-			decayExponent: decayExponentNode
-		} );
-
-		const lightColor = colorNode.mul( lightAttenuation );
-
-		const reflectedLight = builder.context.reflectedLight;
+	setup() {
 
-		lightingModel.direct( {
-			lightDirection,
-			lightColor,
-			reflectedLight
-		}, builder.stack, builder );
+		directPointLight( {
+			color: this.colorNode,
+			lightViewPosition: lightViewPosition( this.light ),
+			cutoffDistance: this.cutoffDistanceNode,
+			decayExponent: this.decayExponentNode
+		} ).append();
 
 	}
 

+ 45 - 0
src/renderers/common/Lighting.js

@@ -0,0 +1,45 @@
+import { LightsNode } from '../../nodes/Nodes.js';
+import ChainMap from './ChainMap.js';
+
+const _defaultLights = /*@__PURE__*/ new LightsNode();
+
+class Lighting extends ChainMap {
+
+	constructor() {
+
+		super();
+
+	}
+
+	createNode( lights = [] ) {
+
+		return new LightsNode().setLights( lights );
+
+	}
+
+	getNode( scene, camera ) {
+
+		// ignore post-processing
+
+		if ( scene.isQuadMesh ) return _defaultLights;
+
+		// tiled lighting
+
+		const keys = [ scene, camera ];
+
+		let node = this.get( keys );
+
+		if ( node === undefined ) {
+
+			node = this.createNode();
+			this.set( keys, node );
+
+		}
+
+		return node;
+
+	}
+
+}
+
+export default Lighting;

+ 5 - 4
src/renderers/common/RenderList.js

@@ -1,5 +1,3 @@
-import { LightsNode } from '../../nodes/Nodes.js';
-
 function painterSortStable( a, b ) {
 
 	if ( a.groupOrder !== b.groupOrder ) {
@@ -50,7 +48,7 @@ function reversePainterSortStable( a, b ) {
 
 class RenderList {
 
-	constructor() {
+	constructor( lighting, scene, camera ) {
 
 		this.renderItems = [];
 		this.renderItemsIndex = 0;
@@ -59,9 +57,12 @@ class RenderList {
 		this.transparent = [];
 		this.bundles = [];
 
-		this.lightsNode = new LightsNode( [] );
+		this.lightsNode = lighting.getNode( scene, camera );
 		this.lightsArray = [];
 
+		this.scene = scene;
+		this.camera = camera;
+
 		this.occlusionQueryCount = 0;
 
 	}

+ 4 - 2
src/renderers/common/RenderLists.js

@@ -3,7 +3,9 @@ import RenderList from './RenderList.js';
 
 class RenderLists {
 
-	constructor() {
+	constructor( lighting ) {
+
+		this.lighting = lighting;
 
 		this.lists = new ChainMap();
 
@@ -18,7 +20,7 @@ class RenderLists {
 
 		if ( list === undefined ) {
 
-			list = new RenderList();
+			list = new RenderList( this.lighting, scene, camera );
 			lists.set( keys, list );
 
 		}

+ 5 - 2
src/renderers/common/Renderer.js

@@ -15,6 +15,7 @@ import ClippingContext from './ClippingContext.js';
 import QuadMesh from './QuadMesh.js';
 import RenderBundles from './RenderBundles.js';
 import NodeLibrary from './nodes/NodeLibrary.js';
+import Lighting from './Lighting.js';
 
 import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
 
@@ -82,11 +83,13 @@ class Renderer {
 		this.info = new Info();
 
 		this.nodes = {
-			library: new NodeLibrary(),
 			modelViewMatrix: null,
 			modelNormalViewMatrix: null
 		};
 
+		this.library = new NodeLibrary();
+		this.lighting = new Lighting();
+
 		// internals
 
 		this._getFallback = getFallback;
@@ -238,7 +241,7 @@ class Renderer {
 			this._pipelines = new Pipelines( backend, this._nodes );
 			this._bindings = new Bindings( backend, this._nodes, this._textures, this._attributes, this._pipelines, this.info );
 			this._objects = new RenderObjects( this, this._nodes, this._geometries, this._pipelines, this._bindings, this.info );
-			this._renderLists = new RenderLists();
+			this._renderLists = new RenderLists( this.lighting );
 			this._bundles = new RenderBundles();
 			this._renderContexts = new RenderContexts();
 

+ 1 - 1
src/renderers/webgpu/WebGPURenderer.Nodes.js

@@ -31,7 +31,7 @@ class WebGPURenderer extends Renderer {
 
 		super( backend, parameters );
 
-		this.nodes.library = new BasicNodeLibrary();
+		this.library = new BasicNodeLibrary();
 
 		this.isWebGPURenderer = true;
 

+ 1 - 1
src/renderers/webgpu/WebGPURenderer.js

@@ -45,7 +45,7 @@ class WebGPURenderer extends Renderer {
 		//super( new Proxy( backend, debugHandler ) );
 		super( backend, parameters );
 
-		this.nodes.library = new StandardNodeLibrary();
+		this.library = new StandardNodeLibrary();
 
 		this.isWebGPURenderer = true;
 

+ 1 - 12
src/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -57,19 +57,8 @@ const wgslTypeLib = {
 	bvec4: 'vec4<bool>',
 
 	mat2: 'mat2x2<f32>',
-	imat2: 'mat2x2<i32>',
-	umat2: 'mat2x2<u32>',
-	bmat2: 'mat2x2<bool>',
-
 	mat3: 'mat3x3<f32>',
-	imat3: 'mat3x3<i32>',
-	umat3: 'mat3x3<u32>',
-	bmat3: 'mat3x3<bool>',
-
-	mat4: 'mat4x4<f32>',
-	imat4: 'mat4x4<i32>',
-	umat4: 'mat4x4<u32>',
-	bmat4: 'mat4x4<bool>'
+	mat4: 'mat4x4<f32>'
 };
 
 const wgslPolyfill = {

+ 2 - 1
test/e2e/puppeteer.js

@@ -121,12 +121,14 @@ const exceptionList = [
 	'webgpu_sprites',
 	'webgpu_video_panorama',
 	'webgpu_postprocessing_bloom_emissive',
+	'webgpu_lights_tiled',
 
 	// Awaiting for WebGPU Backend support in Puppeteer
 	'webgpu_storage_buffer',
 	'webgpu_compute_sort_bitonic',
 
 	// WebGPURenderer: Unknown problem
+	'webgpu_backdrop_water',
 	'webgpu_camera_logarithmicdepthbuffer',
 	'webgpu_clipping',
 	'webgpu_lightprobe_cubecamera',
@@ -153,7 +155,6 @@ const exceptionList = [
 	'webgpu_tsl_vfx_linkedparticles',
 	'webgpu_tsl_vfx_tornado',
 	'webgpu_textures_anisotropy',
-	'webgpu_backdrop_water',
 
 	// WebGPU idleTime and parseTime too low
 	'webgpu_compute_particles',

粤ICP备19079148号