Explorar el Código

Nodes: Add `GTAONode`. (#28844)

* Nodes: Add `GTAONode`.

* GTAONode: Clean up.

* set derivative_uniformity diagnostic to off

* GTAONode: Add internal pass with correct clear color.

* Clean up.

* GTAONode: Couple of fixes.

* GTAONode: Clean up.

* GTAONode: Fix loop.

* GTAONode: Fix normal buffer values.

* GTAONode: Use correct camera.

* GTAONode: Use `positionView`.

* GTAONode: Use `transformedNormalView`.

* Clean up.

* GTAONode: Add multiple fixes.

* GTANode: More fixes.

* GTAONode: Use `vec4` in `getSceneUvAndDepth()`.

* GTAONode: Fix assignment of `ao`.

* GTAONode: Attempt to fix `getSceneUvAndDepth()`.

* GTAONode: Fix y-sampling.

* E2E: Update screenshot.

* Examples: Add back color space conversion.

* E2E: Add example to exception list.

---------

Co-authored-by: sunag <[email protected]>
Michael Herzog hace 1 año
padre
commit
3230889efa

+ 1 - 0
examples/files.json

@@ -373,6 +373,7 @@
 		"webgpu_postprocessing_3dlut",
 		"webgpu_postprocessing_afterimage",
 		"webgpu_postprocessing_anamorphic",
+		"webgpu_postprocessing_ao",
 		"webgpu_postprocessing_dof",
 		"webgpu_postprocessing_sobel",
 		"webgpu_postprocessing",

BIN
examples/screenshots/webgpu_postprocessing_ao.jpg


+ 138 - 0
examples/webgpu_postprocessing_ao.html

@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - ambient occlusion (GTAO)</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>
+		<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 { pass, mrt, output, transformedNormalView } from 'three/tsl';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
+			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, postProcessing, controls, clock, stats, mixer;
+
+			init();
+
+			async function init() {
+
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 100 );
+				camera.position.set( 5, 2, 8 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xbfe3dd );
+
+				clock = new THREE.Clock();
+
+				const hdrloader = new RGBELoader();
+				const texture = await hdrloader.loadAsync( 'textures/equirectangular/quarry_01_1k.hdr' );
+				texture.mapping = THREE.EquirectangularReflectionMapping;
+
+				scene.environment = texture;
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 0.5, 0 );
+				controls.update();
+				controls.enablePan = false;
+				controls.enableDamping = true;
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				//
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				const scenePass = pass( scene, camera );
+				scenePass.setMRT( mrt( {
+					output: output,
+					normal: transformedNormalView
+				} ) );
+
+				const scenePassColor = scenePass.getTextureNode( 'output' );
+				const scenePassNormal = scenePass.getTextureNode( 'normal' );
+				const scenePassDepth = scenePass.getTextureNode( 'depth' );
+
+				const aoPass = scenePassColor.ao( scenePassDepth, scenePassNormal, camera );
+
+				postProcessing.outputNode = aoPass;
+
+				//
+
+				const dracoLoader = new DRACOLoader();
+				dracoLoader.setDecoderPath( 'jsm/libs/draco/' );
+				dracoLoader.setDecoderConfig( { type: 'js' } );
+				const loader = new GLTFLoader();
+				loader.setDRACOLoader( dracoLoader );
+				loader.setPath( 'models/gltf/' );
+
+				const gltf = await loader.loadAsync( 'LittlestTokyo.glb' );
+
+				const model = gltf.scene;
+				model.position.set( 1, 1, 0 );
+				model.scale.set( 0.01, 0.01, 0.01 );
+				scene.add( model );
+
+				mixer = new THREE.AnimationMixer( model );
+				mixer.clipAction( gltf.animations[ 0 ] ).play();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+			}
+
+			function animate() {
+
+				const delta = clock.getDelta();
+
+				if ( mixer ) {
+
+					mixer.update( delta );
+
+				}
+
+				controls.update();
+
+				postProcessing.render();
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
src/nodes/Nodes.js

@@ -134,6 +134,7 @@ export { default as DotScreenNode, dotScreen } from './display/DotScreenNode.js'
 export { default as RGBShiftNode, rgbShift } from './display/RGBShiftNode.js';
 export { default as FilmNode, film } from './display/FilmNode.js';
 export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
+export { default as GTAONode, ao } from './display/GTAONode.js';
 export { default as RenderOutputNode, renderOutput } from './display/RenderOutputNode.js';
 
 export { default as PassNode, pass, passTexture, depthPass } from './display/PassNode.js';

+ 336 - 0
src/nodes/display/GTAONode.js

@@ -0,0 +1,336 @@
+import TempNode from '../core/TempNode.js';
+import { texture } from '../accessors/TextureNode.js';
+import { textureSize } from '../accessors/TextureSizeNode.js';
+import { uv } from '../accessors/UVNode.js';
+import { addNodeElement, nodeObject, tslFn, mat3, vec2, vec3, vec4, float, int, If } from '../shadernode/ShaderNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { uniform } from '../core/UniformNode.js';
+import { DataTexture } from '../../textures/DataTexture.js';
+import { Vector2 } from '../../math/Vector2.js';
+import { Vector3 } from '../../math/Vector3.js';
+import { PI, cos, sin, pow, clamp, abs, max, mix, sqrt, acos, dot, normalize, cross } from '../math/MathNode.js';
+import { div, mul, add, sub } from '../math/OperatorNode.js';
+import { loop } from '../utils/LoopNode.js';
+import { passTexture } from './PassNode.js';
+import { RepeatWrapping } from '../../constants.js';
+import QuadMesh from '../../renderers/common/QuadMesh.js';
+import { RenderTarget } from '../../core/RenderTarget.js';
+import { Color } from '../../math/Color.js';
+
+const _quadMesh = new QuadMesh();
+const _currentClearColor = new Color();
+
+class GTAONode extends TempNode {
+
+	constructor( textureNode, depthNode, normalNode, camera ) {
+
+		super();
+
+		this.textureNode = textureNode;
+		this.depthNode = depthNode;
+		this.normalNode = normalNode;
+
+		this.radius = uniform( 0.25 );
+		this.resolution = uniform( new Vector2() );
+		this.thickness = uniform( 1 );
+		this.distanceExponent = uniform( 1 );
+		this.distanceFallOff = uniform( 1 );
+		this.scale = uniform( 1 );
+		this.noiseNode = texture( generateMagicSquareNoise() );
+
+		this.cameraProjectionMatrix = uniform( camera.projectionMatrix );
+		this.cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
+
+		this.SAMPLES = uniform( 16 );
+
+		this._aoRenderTarget = new RenderTarget();
+		this._aoRenderTarget.texture.name = 'GTAONode.AO';
+
+		this._material = null;
+		this._textureNode = passTexture( this, this._aoRenderTarget.texture );
+
+		this.updateBeforeType = NodeUpdateType.RENDER;
+
+	}
+
+	getTextureNode() {
+
+		return this._textureNode;
+
+	}
+
+	setSize( width, height ) {
+
+		this.resolution.value.set( width, height );
+		this._aoRenderTarget.setSize( width, height );
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+
+		const textureNode = this.textureNode;
+		const map = textureNode.value;
+
+		const currentRenderTarget = renderer.getRenderTarget();
+		const currentMRT = renderer.getMRT();
+		renderer.getClearColor( _currentClearColor );
+		const currentClearAlpha = renderer.getClearAlpha();
+
+
+		const currentTexture = textureNode.value;
+
+		_quadMesh.material = this._material;
+
+		this.setSize( map.image.width, map.image.height );
+
+
+		const textureType = map.type;
+
+		this._aoRenderTarget.texture.type = textureType;
+
+		// clear
+
+		renderer.setMRT( null );
+		renderer.setClearColor( 0xffffff, 1 );
+
+		// ao
+
+		renderer.setRenderTarget( this._aoRenderTarget );
+		_quadMesh.render( renderer );
+
+		// restore
+
+		renderer.setRenderTarget( currentRenderTarget );
+		renderer.setMRT( currentMRT );
+		renderer.setClearColor( _currentClearColor, currentClearAlpha );
+		textureNode.value = currentTexture;
+
+	}
+
+	setup( builder ) {
+
+		const { textureNode } = this;
+
+		const uvNode = uv();
+
+		// const sampleTexture = ( uv ) => textureNode.uv( uv );
+		const sampleDepth = ( uv ) => this.depthNode.uv( uv ).x;
+		const sampleNoise = ( uv ) => this.noiseNode.uv( uv );
+
+		const getSceneUvAndDepth = tslFn( ( [ sampleViewPos ] )=> {
+
+			const sampleClipPos = this.cameraProjectionMatrix.mul( vec4( sampleViewPos, 1.0 ) );
+			let sampleUv = sampleClipPos.xy.div( sampleClipPos.w ).mul( 0.5 ).add( 0.5 ).toVar();
+			sampleUv = vec2( sampleUv.x, sampleUv.y.oneMinus() );
+			const sampleSceneDepth = sampleDepth( sampleUv );
+			return vec3( sampleUv, sampleSceneDepth );
+
+		} );
+
+		const getViewPosition = tslFn( ( [ screenPosition, depth ] ) => {
+
+			screenPosition = vec2( screenPosition.x, screenPosition.y.oneMinus() ).mul( 2.0 ).sub( 1.0 );
+
+			const clipSpacePosition = vec4( vec3( screenPosition, depth ), 1.0 );
+			const viewSpacePosition = vec4( this.cameraProjectionMatrixInverse.mul( clipSpacePosition ) );
+
+			return viewSpacePosition.xyz.div( viewSpacePosition.w );
+
+		} );
+
+		const ao = tslFn( () => {
+
+			const depth = sampleDepth( uvNode );
+
+			depth.greaterThanEqual( 1.0 ).discard();
+
+			const viewPosition = getViewPosition( uvNode, depth );
+			const viewNormal = this.normalNode.rgb.normalize();
+
+			const radiusToUse = this.radius;
+
+			const noiseResolution = textureSize( this.noiseNode, 0 );
+			let noiseUv = vec2( uvNode.x, uvNode.y.oneMinus() );
+			noiseUv = noiseUv.mul( this.resolution.div( noiseResolution ) );
+			const noiseTexel = sampleNoise( noiseUv );
+			const randomVec = noiseTexel.xyz.mul( 2.0 ).sub( 1.0 );
+			const tangent = vec3( randomVec.xy, 0.0 ).normalize();
+			const bitangent = vec3( tangent.y.mul( - 1.0 ), tangent.x, 0.0 );
+			const kernelMatrix = mat3( tangent, bitangent, vec3( 0.0, 0.0, 1.0 ) );
+
+			const DIRECTIONS = this.SAMPLES.lessThan( 30 ).cond( 3, 5 );
+			const STEPS = add( this.SAMPLES, DIRECTIONS.sub( 1 ) ).div( DIRECTIONS );
+
+			const ao = float( 0 ).toVar();
+
+			loop( { start: int( 0 ), end: DIRECTIONS, type: 'int', condition: '<' }, ( { i } ) => {
+
+				const angle = float( i ).div( float( DIRECTIONS ) ).mul( PI );
+				const sampleDir = vec4( cos( angle ), sin( angle ), 0., add( 0.5, mul( 0.5, noiseTexel.w ) ) );
+				sampleDir.xyz = normalize( kernelMatrix.mul( sampleDir.xyz ) );
+
+				const viewDir = normalize( viewPosition.xyz.negate() );
+				const sliceBitangent = normalize( cross( sampleDir.xyz, viewDir ) );
+				const sliceTangent = cross( sliceBitangent, viewDir );
+				const normalInSlice = normalize( viewNormal.sub( sliceBitangent.mul( dot( viewNormal, sliceBitangent ) ) ) );
+
+				const tangentToNormalInSlice = cross( normalInSlice, sliceBitangent );
+				const cosHorizons = vec2( dot( viewDir, tangentToNormalInSlice ), dot( viewDir, tangentToNormalInSlice.negate() ) ).toVar();
+
+				loop( { end: STEPS, type: 'int', name: 'j', condition: '<' }, ( { j } ) => {
+
+					const sampleViewOffset = sampleDir.xyz.mul( radiusToUse ).mul( sampleDir.w ).mul( pow( div( float( j ).add( 1.0 ), float( STEPS ) ), this.distanceExponent ) );
+
+					// x
+
+					const sampleSceneUvDepthX = getSceneUvAndDepth( viewPosition.add( sampleViewOffset ) );
+					const sampleSceneViewPositionX = getViewPosition( sampleSceneUvDepthX.xy, sampleSceneUvDepthX.z );
+					const viewDeltaX = sampleSceneViewPositionX.sub( viewPosition );
+
+					If( abs( viewDeltaX.z ).lessThan( this.thickness ), () => {
+
+						const sampleCosHorizon = dot( viewDir, normalize( viewDeltaX ) );
+						cosHorizons.x.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.x ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) );
+
+					} );
+
+					// y
+
+					const sampleSceneUvDepthY = getSceneUvAndDepth( viewPosition.sub( sampleViewOffset ) );
+					const sampleSceneViewPositionY = getViewPosition( sampleSceneUvDepthY.xy, sampleSceneUvDepthY.z );
+					const viewDeltaY = sampleSceneViewPositionY.sub( viewPosition );
+
+					If( abs( viewDeltaY.z ).lessThan( this.thickness ), () => {
+
+						const sampleCosHorizon = dot( viewDir, normalize( viewDeltaY ) );
+						cosHorizons.y.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.y ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) );
+
+					} );
+
+				} );
+
+				const sinHorizons = sqrt( sub( 1.0, cosHorizons.mul( cosHorizons ) ) );
+				const nx = dot( normalInSlice, sliceTangent );
+				const ny = dot( normalInSlice, viewDir );
+				const nxb = mul( 0.5, acos( cosHorizons.y ).sub( acos( cosHorizons.x ) ).add( sinHorizons.x.mul( cosHorizons.x ).sub( sinHorizons.y.mul( cosHorizons.y ) ) ) );
+				const nyb = mul( 0.5, sub( 2.0, cosHorizons.x.mul( cosHorizons.x ) ).sub( cosHorizons.y.mul( cosHorizons.y ) ) );
+				const occlusion = nx.mul( nxb ).add( ny.mul( nyb ) );
+				ao.addAssign( occlusion );
+
+			} );
+
+			ao.assign( clamp( ao.div( DIRECTIONS ), 0, 1 ) );
+			ao.assign( pow( ao, this.scale ) );
+
+			return vec4( vec3( ao ), 1.0 );
+
+		} );
+
+		const material = this._material || ( this._material = builder.createNodeMaterial() );
+		material.fragmentNode = ao().context( builder.getSharedContext() );
+		material.needsUpdate = true;
+
+		//
+
+		const properties = builder.getNodeProperties( this );
+		properties.textureNode = textureNode;
+
+		//
+
+		return this._textureNode;
+
+	}
+
+}
+
+function generateMagicSquareNoise( size = 5 ) {
+
+	const noiseSize = Math.floor( size ) % 2 === 0 ? Math.floor( size ) + 1 : Math.floor( size );
+	const magicSquare = generateMagicSquare( noiseSize );
+	const noiseSquareSize = magicSquare.length;
+	const data = new Uint8Array( noiseSquareSize * 4 );
+
+	for ( let inx = 0; inx < noiseSquareSize; ++ inx ) {
+
+		const iAng = magicSquare[ inx ];
+		const angle = ( 2 * Math.PI * iAng ) / noiseSquareSize;
+		const randomVec = new Vector3(
+			Math.cos( angle ),
+			Math.sin( angle ),
+			0
+		).normalize();
+		data[ inx * 4 ] = ( randomVec.x * 0.5 + 0.5 ) * 255;
+		data[ inx * 4 + 1 ] = ( randomVec.y * 0.5 + 0.5 ) * 255;
+		data[ inx * 4 + 2 ] = 127;
+		data[ inx * 4 + 3 ] = 255;
+
+	}
+
+	const noiseTexture = new DataTexture( data, noiseSize, noiseSize );
+	noiseTexture.wrapS = RepeatWrapping;
+	noiseTexture.wrapT = RepeatWrapping;
+	noiseTexture.needsUpdate = true;
+
+	return noiseTexture;
+
+}
+
+function generateMagicSquare( size ) {
+
+	const noiseSize = Math.floor( size ) % 2 === 0 ? Math.floor( size ) + 1 : Math.floor( size );
+	const noiseSquareSize = noiseSize * noiseSize;
+	const magicSquare = Array( noiseSquareSize ).fill( 0 );
+	let i = Math.floor( noiseSize / 2 );
+	let j = noiseSize - 1;
+
+	for ( let num = 1; num <= noiseSquareSize; ) {
+
+		if ( i === - 1 && j === noiseSize ) {
+
+			j = noiseSize - 2;
+			i = 0;
+
+		} else {
+
+			if ( j === noiseSize ) {
+
+				j = 0;
+
+			}
+
+			if ( i < 0 ) {
+
+				i = noiseSize - 1;
+
+			}
+
+		}
+
+		if ( magicSquare[ i * noiseSize + j ] !== 0 ) {
+
+			j -= 2;
+			i ++;
+			continue;
+
+		} else {
+
+			magicSquare[ i * noiseSize + j ] = num ++;
+
+		}
+
+		j ++;
+		i --;
+
+	}
+
+	return magicSquare;
+
+}
+
+export const ao = ( node, depthNode, normalNode, camera ) => nodeObject( new GTAONode( nodeObject( node ).toTexture(), nodeObject( depthNode ), nodeObject( normalNode ), camera ) );
+
+addNodeElement( 'ao', ao );
+
+export default GTAONode;

+ 2 - 0
src/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -1157,6 +1157,8 @@ fn main( ${shaderData.attributes} ) -> VaryingsStruct {
 
 		return `${ this.getSignature() }
 
+diagnostic( off, derivative_uniformity );
+
 // uniforms
 ${shaderData.uniforms}
 

+ 1 - 0
test/e2e/puppeteer.js

@@ -126,6 +126,7 @@ const exceptionList = [
 	// WebGPURenderer: Unknown problem
 	'webgpu_postprocessing_afterimage',
 	'webgpu_postprocessing_3dlut',
+	'webgpu_postprocessing_ao',
 	'webgpu_backdrop_water',
 	'webgpu_camera_logarithmicdepthbuffer',
 	'webgpu_clipping',