فهرست منبع

WebGPURenderer: RectAreaLight support (#28580)

* WebGPURenderer: RectAreaLight support

* detect float32 texture filetring support

* cache runtime availbility checks

* remove testing code

* update screenshot

* excude from test

---------

Co-authored-by: aardgoose <[email protected]>
aardgoose 1 سال پیش
والد
کامیت
2f335437df

+ 1 - 0
examples/files.json

@@ -331,6 +331,7 @@
 		"webgpu_lights_custom",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
+		"webgpu_lights_rectarealight",
 		"webgpu_lights_selective",
 		"webgpu_lines_fat",
 		"webgpu_loader_gltf",

+ 1 - 0
examples/jsm/nodes/Nodes.js

@@ -151,6 +151,7 @@ export { default as ComputeNode, compute } from './gpgpu/ComputeNode.js';
 export { default as LightNode, lightTargetDirection } from './lighting/LightNode.js';
 export { default as PointLightNode } from './lighting/PointLightNode.js';
 export { default as DirectionalLightNode } from './lighting/DirectionalLightNode.js';
+export { default as RectAreaLightNode } from './lighting/RectAreaLightNode.js';
 export { default as SpotLightNode } from './lighting/SpotLightNode.js';
 export { default as IESSpotLightNode } from './lighting/IESSpotLightNode.js';
 export { default as AmbientLightNode } from './lighting/AmbientLightNode.js';

+ 2 - 0
examples/jsm/nodes/core/LightingModel.js

@@ -6,6 +6,8 @@ class LightingModel {
 
 	direct( /*input, stack, builder*/ ) { }
 
+	directRectArea( /*input, stack, builder*/ ) {}
+
 	indirectDiffuse( /*input, stack, builder*/ ) { }
 
 	indirectSpecular( /*input, stack, builder*/ ) { }

+ 131 - 0
examples/jsm/nodes/functions/BSDF/LTC.js

@@ -0,0 +1,131 @@
+import { tslFn, If, mat3, vec2, vec3 } from '../../shadernode/ShaderNode.js';
+import { max } from '../../math/MathNode.js';
+
+// Rect Area Light
+
+// Real-Time Polygonal-Light Shading with Linearly Transformed Cosines
+// by Eric Heitz, Jonathan Dupuy, Stephen Hill and David Neubelt
+// code: https://github.com/selfshadow/ltc_code/
+
+const LTC_Uv = tslFn( ( { N, V, roughness } ) => {
+
+	const LUT_SIZE = 64.0;
+	const LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;
+	const LUT_BIAS = 0.5 / LUT_SIZE;
+
+	const dotNV = N.dot( V ).saturate();
+
+	// texture parameterized by sqrt( GGX alpha ) and sqrt( 1 - cos( theta ) )
+	const uv = vec2( roughness, dotNV.oneMinus().sqrt() );
+
+	uv.assign( uv.mul( LUT_SCALE ).add( LUT_BIAS ) );
+
+	return uv;
+
+} ).setLayout( {
+	name: 'LTC_Uv',
+	type: 'vec2',
+	inputs: [
+		{ name: 'N', type: 'vec3' },
+		{ name: 'V', type: 'vec3' },
+		{ name: 'roughness', type: 'float' }
+	]
+} );
+
+const LTC_ClippedSphereFormFactor = tslFn( ( { f } ) => {
+
+	// Real-Time Area Lighting: a Journey from Research to Production (p.102)
+	// An approximation of the form factor of a horizon-clipped rectangle.
+
+	const l = f.length();
+
+	return max( l.mul( l ).add( f.z ).div( l.add( 1.0 ) ), 0 );
+
+} ).setLayout( {
+	name: 'LTC_ClippedSphereFormFactor',
+	type: 'float',
+	inputs: [
+		{ name: 'f', type: 'vec3' }
+	]
+} );
+
+const LTC_EdgeVectorFormFactor = tslFn( ( { v1, v2 } ) => {
+
+	const x = v1.dot( v2 );
+	const y = x.abs().toVar();
+
+	// rational polynomial approximation to theta / sin( theta ) / 2PI
+	const a = y.mul( 0.0145206 ).add( 0.4965155 ).mul( y ).add( 0.8543985 ).toVar();
+	const b = y.add( 4.1616724 ).mul( y ).add( 3.4175940 ).toVar();
+	const v = a.div( b );
+
+	const theta_sintheta = x.greaterThan( 0.0 ).cond( v, max( x.mul( x ).oneMinus(), 1e-7 ).inverseSqrt().mul( 0.5 ).sub( v ) );
+
+	return v1.cross( v2 ).mul( theta_sintheta );
+
+} ).setLayout( {
+	name: 'LTC_EdgeVectorFormFactor',
+	type: 'vec3',
+	inputs: [
+		{ name: 'v1', type: 'vec3' },
+		{ name: 'v2', type: 'vec3' }
+	]
+} );
+
+const LTC_Evaluate = tslFn( ( { N, V, P, mInv, p0, p1, p2, p3 } ) => {
+
+	// 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 ), () => {
+
+		// construct orthonormal basis around N
+		const T1 = V.sub( N.mul( V.dot( N ) ) ).normalize();
+		const T2 = N.cross( T1 ).negate(); // negated from paper; possibly due to a different handedness of world coordinate system
+
+		// compute transform
+		const mat = mInv.mul( mat3( T1, T2, N ).transpose() ).toVar();
+
+		// transform rect
+		// & project rect onto sphere
+		const coords0 = mat.mul( p0.sub( P ) ).normalize().toVar();
+		const coords1 = mat.mul( p1.sub( P ) ).normalize().toVar();
+		const coords2 = mat.mul( p2.sub( P ) ).normalize().toVar();
+		const coords3 = mat.mul( 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 } ) ) );
+
+	} );
+
+	return result;
+
+} ).setLayout( {
+	name: 'LTC_Evaluate',
+	type: 'vec3',
+	inputs: [
+		{ name: 'N', type: 'vec3' },
+		{ name: 'V', type: 'vec3' },
+		{ name: 'P', type: 'vec3' },
+		{ name: 'mInv', type: 'mat3' },
+		{ name: 'p0', type: 'vec3' },
+		{ name: 'p1', type: 'vec3' },
+		{ name: 'p2', type: 'vec3' },
+		{ name: 'p3', type: 'vec3' }
+	]
+} );
+
+
+export { LTC_Evaluate, LTC_Uv };

+ 34 - 1
examples/jsm/nodes/functions/PhysicalLightingModel.js

@@ -5,10 +5,11 @@ import EnvironmentBRDF from './BSDF/EnvironmentBRDF.js';
 import F_Schlick from './BSDF/F_Schlick.js';
 import Schlick_to_F0 from './BSDF/Schlick_to_F0.js';
 import BRDF_Sheen from './BSDF/BRDF_Sheen.js';
+import { LTC_Evaluate, LTC_Uv } from './BSDF/LTC.js';
 import LightingModel from '../core/LightingModel.js';
 import { diffuseColor, specularColor, specularF90, roughness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, ior, thickness, transmission, attenuationDistance, attenuationColor, dispersion } from '../core/PropertyNode.js';
 import { transformedNormalView, transformedClearcoatNormalView, transformedNormalWorld } from '../accessors/NormalNode.js';
-import { positionViewDirection, positionWorld } from '../accessors/PositionNode.js';
+import { positionViewDirection, positionView, positionWorld } from '../accessors/PositionNode.js';
 import { tslFn, float, vec2, vec3, vec4, mat3, If } from '../shadernode/ShaderNode.js';
 import { cond } from '../math/CondNode.js';
 import { mix, normalize, refract, length, clamp, log2, log, exp, smoothstep } from '../math/MathNode.js';
@@ -474,6 +475,38 @@ class PhysicalLightingModel extends LightingModel {
 
 	}
 
+	directRectArea( { lightColor, lightPosition, halfWidth, halfHeight, reflectedLight, ltc_1, ltc_2 } ) {
+
+		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 N = transformedNormalView;
+		const V = positionViewDirection;
+		const P = positionView.toVar();
+
+		const uv = LTC_Uv( { N, V, roughness } );
+
+		const t1 = ltc_1.uv( uv ).toVar();
+		const t2 = ltc_2.uv( uv ).toVar();
+
+		const mInv = mat3(
+			vec3( t1.x, 0, t1.y ),
+			vec3( 0, 1, 0 ),
+			vec3( t1.z, 0, t1.w )
+		).toVar();
+
+		// LTC Fresnel Approximation by Stephen Hill
+		// http://blog.selfshadow.com/publications/s2016-advances/s2016_ltc_fresnel.pdf
+		const fresnel = specularColor.mul( t2.x ).add( specularColor.oneMinus().mul( t2.y ) ).toVar();
+
+		reflectedLight.directSpecular.addAssign( lightColor.mul( fresnel ).mul( LTC_Evaluate( { N, V, P, mInv, p0, p1, p2, p3 } ) ) );
+
+		reflectedLight.directDiffuse.addAssign( lightColor.mul( diffuseColor ).mul( LTC_Evaluate( { N, V, P, mInv: mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 ), p0, p1, p2, p3 } ) ) );
+
+	}
+
 	indirectDiffuse( { irradiance, reflectedLight } ) {
 
 		reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );

+ 90 - 0
examples/jsm/nodes/lighting/RectAreaLightNode.js

@@ -0,0 +1,90 @@
+import AnalyticLightNode from './AnalyticLightNode.js';
+import { addLightNode } from './LightsNode.js';
+import { texture } from '../accessors/TextureNode.js';
+import { uniform } from '../core/UniformNode.js';
+import { objectViewPosition } from '../accessors/Object3DNode.js';
+import { addNodeClass } from '../core/Node.js';
+
+import { RectAreaLight, Matrix4, Vector3, UniformsLib } from 'three';
+
+const _matrix41 = new Matrix4();
+const _matrix42 = new Matrix4();
+let ltc_1, ltc_2;
+
+class RectAreaLightNode extends AnalyticLightNode {
+
+	constructor( light = null ) {
+
+		super( light );
+
+		this.halfHeight = uniform( new Vector3() );
+		this.halfWidth = uniform( new Vector3() );
+
+	}
+
+	update( frame ) {
+
+		super.update( frame );
+
+		const { light } = this;
+
+		const viewMatrix = frame.camera.matrixWorldInverse;
+
+		_matrix42.identity();
+		_matrix41.copy( light.matrixWorld );
+		_matrix41.premultiply( viewMatrix );
+		_matrix42.extractRotation( _matrix41 );
+
+		this.halfWidth.value.set( light.width * 0.5, 0.0, 0.0 );
+		this.halfHeight.value.set( 0.0, light.height * 0.5, 0.0 );
+
+		this.halfWidth.value.applyMatrix4( _matrix42 );
+		this.halfHeight.value.applyMatrix4( _matrix42 );
+
+	}
+
+	setup( builder ) {
+
+		super.setup( builder );
+
+		if ( ltc_1 === undefined ) {
+
+			if ( builder.isAvailable( 'float32Filterable' ) ) {
+
+				ltc_1 = texture( UniformsLib.LTC_FLOAT_1 );
+				ltc_2 = texture( UniformsLib.LTC_FLOAT_2 );
+
+			} else {
+
+				ltc_1 = texture( UniformsLib.LTC_HALF_1 );
+				ltc_2 = texture( UniformsLib.LTC_HALF_2 );
+
+			}
+
+		}
+
+		const { colorNode, light } = this;
+		const lightingModel = builder.context.lightingModel;
+
+		const lightPosition = objectViewPosition( light );
+		const reflectedLight = builder.context.reflectedLight;
+
+		lightingModel.directRectArea( {
+			lightColor: colorNode,
+			lightPosition,
+			halfWidth: this.halfWidth,
+			halfHeight: this.halfHeight,
+			reflectedLight,
+			ltc_1,
+			ltc_2
+		}, builder.stack, builder );
+
+	}
+
+}
+
+export default RectAreaLightNode;
+
+addNodeClass( 'RectAreaLightNode', RectAreaLightNode );
+
+addLightNode( RectAreaLight, RectAreaLightNode );

+ 28 - 2
examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js

@@ -20,7 +20,8 @@ const precisionLib = {
 };
 
 const supports = {
-	swizzleAssign: true
+	swizzleAssign: true,
+	storageBuffer: false
 };
 
 const defaultPrecisions = `
@@ -606,7 +607,32 @@ ${ flowData.code }
 
 	isAvailable( name ) {
 
-		return supports[ name ] === true;
+		let result = supports[ name ];
+
+		if ( result === undefined ) {
+
+			if ( name === 'float32Filterable' ) {
+
+				const extentions = this.renderer.backend.extensions;
+
+				if ( extentions.has( 'OES_texture_float_linear' ) )  {
+
+					extentions.get( 'OES_texture_float_linear' );
+					result = true;
+
+				} else {
+
+					result = false;
+
+				}
+
+			}
+
+			supports[ name ] = result;
+
+		}
+
+		return result;
 
 	}
 

+ 16 - 1
examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -26,6 +26,7 @@ const gpuShaderStageLib = {
 };
 
 const supports = {
+	swizzleAssign: false,
 	storageBuffer: true
 };
 
@@ -1010,7 +1011,21 @@ ${ flowData.code }
 
 	isAvailable( name ) {
 
-		return supports[ name ] === true;
+		let result = supports[ name ];
+
+		if ( result === undefined ) {
+
+			if ( name === 'float32Filterable' ) {
+
+				result = this.renderer.hasFeature( 'float32-filterable' );
+
+			}
+
+			supports[ name ] = result;
+
+		}
+
+		return result;
 
 	}
 

BIN
examples/screenshots/webgpu_lights_rectarealight.jpg


+ 118 - 0
examples/webgpu_lights_rectarealight.html

@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webGPU - lights - rect area light</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 - THREE.RectAreaLight<br/>
+			by <a href="http://github.com/abelnation" target="_blank" rel="noopener">abelnation</a>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
+			import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			let renderer, scene, camera;
+			let stats, meshKnot;
+
+			init();
+
+			function init() {
+
+				RectAreaLightUniformsLib.init();
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animation );
+				document.body.appendChild( renderer.domElement );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( 0, 5, - 15 );
+
+				scene = new THREE.Scene();
+
+				const rectLight1 = new THREE.RectAreaLight( 0xff0000, 5, 4, 10 );
+				rectLight1.position.set( - 5, 5, 5 );
+				scene.add( rectLight1 );
+
+				const rectLight2 = new THREE.RectAreaLight( 0x00ff00, 5, 4, 10 );
+				rectLight2.position.set( 0, 5, 5 );
+				scene.add( rectLight2 );
+
+				const rectLight3 = new THREE.RectAreaLight( 0x0000ff, 5, 4, 10 );
+				rectLight3.position.set( 5, 5, 5 );
+				scene.add( rectLight3 );
+
+				scene.add( new RectAreaLightHelper( rectLight1 ) );
+				scene.add( new RectAreaLightHelper( rectLight2 ) );
+				scene.add( new RectAreaLightHelper( 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.target.copy( meshKnot.position );
+				controls.update();
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+			}
+
+			function onWindowResize() {
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				camera.aspect = ( window.innerWidth / window.innerHeight );
+				camera.updateProjectionMatrix();
+
+			}
+
+			function animation( time ) {
+
+				meshKnot.rotation.y = time / 1000;
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -145,6 +145,7 @@ const exceptionList = [
 	'webgpu_mesh_batch',
 	'webgpu_texturegrad',
 	'webgpu_performance_renderbundle',
+	'webgpu_lights_rectarealight',
 
 	// WebGPU idleTime and parseTime too low
 	'webgpu_compute_particles',