Explorar o código

WebGPU: add `webgpu_cubemap_adjustments` example (#24206)

* ConvertNode: fix cache if used .tempRead = false

* add webgpu_cubemap_adjusts example

* update title

* webgpu_cubemap_adjustments: name and update example

* add ColorAdjustmentNode

* Revert webgpu_cubemap_mix

* WebGPU: add atan2()

* Nodes: fixes multipass flow

* MaxMipLevelNode: Fix check array if images not loaded

* cleanup
sunag %!s(int64=3) %!d(string=hai) anos
pai
achega
18fb056c4a

+ 1 - 0
examples/files.json

@@ -309,6 +309,7 @@
 	],
 	"webgpu": [
 		"webgpu_compute",
+		"webgpu_cubemap_adjustments",
 		"webgpu_cubemap_mix",
 		"webgpu_depth_texture",
 		"webgpu_instance_mesh",

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

@@ -49,6 +49,7 @@ import UserDataNode from './accessors/UserDataNode.js';
 import ComputeNode from './gpgpu/ComputeNode.js';
 
 // display
+import ColorAdjustmentNode from './display/ColorAdjustmentNode.js';
 import ColorSpaceNode from './display/ColorSpaceNode.js';
 import FrontFacingNode from './display/FrontFacingNode.js';
 import NormalMapNode from './display/NormalMapNode.js';
@@ -153,6 +154,7 @@ const nodeLib = {
 	UserDataNode,
 
 	// display
+	ColorAdjustmentNode,
 	ColorSpaceNode,
 	FrontFacingNode,
 	NormalMapNode,
@@ -256,6 +258,7 @@ export {
 	UserDataNode,
 
 	// display
+	ColorAdjustmentNode,
 	ColorSpaceNode,
 	FrontFacingNode,
 	NormalMapNode,

+ 2 - 2
examples/jsm/nodes/accessors/CubeTextureNode.js

@@ -20,7 +20,7 @@ class CubeTextureNode extends TextureNode {
 
 	getConstructHash( builder ) {
 
-		return `${ this.uuid }-${ builder.context.environmentContext?.uuid || '' }`;
+		return `${ this.uuid } / ${ builder.context.environmentContext?.uuid || '' }`;
 
 	}
 
@@ -72,7 +72,7 @@ class CubeTextureNode extends TextureNode {
 
 			let snippet = nodeData.snippet;
 
-			if ( builder.context.tempRead === false || snippet === undefined ) {
+			if ( snippet === undefined || builder.context.tempRead === false ) {
 
 				const uvSnippet = uvNode.build( builder, 'vec3' );
 

+ 6 - 10
examples/jsm/nodes/core/Node.js

@@ -131,15 +131,13 @@ class Node {
 
 	}
 
-	generate( builder ) {
+	generate( builder, output ) {
 
 		const { outputNode } = builder.getNodeProperties( this );
 
 		if ( outputNode?.isNode === true ) {
 
-			const type = this.getNodeType( builder );
-
-			return outputNode.build( builder, type );
+			return outputNode.build( builder, output );
 
 		}
 
@@ -167,7 +165,7 @@ class Node {
 		/* expected return:
 			- "construct"	-> Node
 			- "analyze"		-> null
-			- "generat"		-> String
+			- "generate"	-> String
 		*/
 		let result = null;
 
@@ -176,12 +174,10 @@ class Node {
 		if ( buildStage === 'construct' ) {
 
 			const properties = builder.getNodeProperties( this );
-			const nodeData = builder.getDataFromNode( this );
-
-			if ( properties.initied !== true ) {
 
-				nodeData.initied = true;
+			if ( properties.initialized !== true || builder.context.tempRead === false ) {
 
+				properties.initialized = true;
 				properties.outputNode = this.construct( builder );
 
 				for ( const childNode of Object.values( properties ) ) {
@@ -211,7 +207,7 @@ class Node {
 
 				result = nodeData.snippet;
 
-				if ( result === undefined ) {
+				if ( result === undefined /*|| builder.context.tempRead === false*/ ) {
 
 					result = this.generate( builder ) || '';
 

+ 94 - 0
examples/jsm/nodes/display/ColorAdjustmentNode.js

@@ -0,0 +1,94 @@
+import TempNode from '../core/TempNode.js';
+import { ShaderNode, vec3, mat3, add, sub, mul, max, div, dot, float, mix, cos, sin, atan2, sqrt } from '../shadernode/ShaderNodeBaseElements.js';
+
+const luminanceNode = new ShaderNode( ( { color } ) => {
+
+	const LUMA = vec3( 0.2125, 0.7154, 0.0721 );
+
+	return dot( color, LUMA );
+
+} );
+
+const saturationNode = new ShaderNode( ( { color, adjustment } ) => {
+
+	const intensityNode = luminanceNode.call( { color } );
+
+	return mix( intensityNode, color, adjustment );
+
+} );
+
+const vibranceNode = new ShaderNode( ( { color, adjustment } ) => {
+
+	const average = div( add( color.r, color.g, color.b ), 3.0 );
+
+	const mx = max( color.r, max( color.g, color.b ) );
+	const amt = mul( sub( mx, average ), mul( -3.0, adjustment ) );
+
+	return mix( color.rgb, vec3( mx ), amt );
+
+} );
+
+const hueNode = new ShaderNode( ( { color, adjustment } ) => {
+
+	const RGBtoYIQ = mat3( 0.299, 0.587, 0.114, 0.595716, -0.274453, -0.321263, 0.211456, -0.522591, 0.311135 );
+	const YIQtoRGB = mat3( 1.0, 0.9563, 0.6210, 1.0, -0.2721, -0.6474, 1.0, -1.107, 1.7046 );
+
+	const yiq = mul( RGBtoYIQ, color );
+
+	const hue = add( atan2( yiq.z, yiq.y ), adjustment );
+	const chroma = sqrt( add( mul( yiq.z, yiq.z ), mul( yiq.y, yiq.y ) ) );
+
+	return mul( YIQtoRGB, vec3( yiq.x, mul( chroma, cos( hue ) ), mul( chroma, sin( hue ) ) ) );
+
+} );
+
+class ColorAdjustmentNode extends TempNode {
+
+	static SATURATION = 'saturation';
+	static VIBRANCE = 'vibrance';
+	static HUE = 'hue';
+
+	constructor( method, colorNode, adjustmentNode = float( 1 ) ) {
+
+		super( 'vec3' );
+
+		this.method = method;
+
+		this.colorNode = colorNode;
+		this.adjustmentNode = adjustmentNode;
+
+	}
+
+	construct() {
+
+		const { method, colorNode, adjustmentNode } = this;
+
+		const callParams = { color: colorNode, adjustment: adjustmentNode };
+
+		let outputNode = null;
+
+		if ( method === ColorAdjustmentNode.SATURATION ) {
+
+			outputNode = saturationNode.call( callParams );
+
+		} else if ( method === ColorAdjustmentNode.VIBRANCE ) {
+
+			outputNode = vibranceNode.call( callParams );
+
+		} else if ( method === ColorAdjustmentNode.HUE ) {
+
+			outputNode = hueNode.call( callParams );
+
+		} else {
+
+			console.error( `${ this.type }: Method "${ this.method }" not supported!` );
+
+		}
+
+		return outputNode;
+
+	}
+
+}
+
+export default ColorAdjustmentNode;

+ 6 - 2
examples/jsm/nodes/lighting/EnvironmentNode.js

@@ -15,7 +15,7 @@ const getSpecularMIPLevel = new ShaderNode( ( { texture, levelNode } ) => {
 
 } );
 
-class EnvironmentLightNode extends LightingNode {
+class EnvironmentNode extends LightingNode {
 
 	constructor( envNode = null ) {
 
@@ -28,6 +28,7 @@ class EnvironmentLightNode extends LightingNode {
 	construct( builder ) {
 
 		const envNode = this.envNode;
+		const properties = builder.getNodeProperties( this );
 
 		const flipNormalWorld = vec3( negate( transformedNormalWorld.x ), transformedNormalWorld.yz );
 
@@ -58,8 +59,11 @@ class EnvironmentLightNode extends LightingNode {
 
 		builder.context.iblIrradiance.add( mul( Math.PI, irradianceContext ) );
 
+		properties.radianceContext = radianceContext;
+		properties.irradianceContext = irradianceContext;
+
 	}
 
 }
 
-export default EnvironmentLightNode;
+export default EnvironmentNode;

+ 1 - 0
examples/jsm/nodes/math/MathNode.js

@@ -38,6 +38,7 @@ class MathNode extends TempNode {
 
 	// 2 inputs
 
+	static ATAN2 = 'atan2';
 	static MIN = 'min';
 	static MAX = 'max';
 	static MOD = 'mod';

+ 1 - 0
examples/jsm/nodes/shadernode/ShaderNodeBaseElements.js

@@ -236,6 +236,7 @@ export const dFdy = nodeProxy( MathNode, MathNode.DFDY );
 export const saturate = nodeProxy( MathNode, MathNode.SATURATE );
 export const round = nodeProxy( MathNode, MathNode.ROUND );
 
+export const atan2 = nodeProxy( MathNode, MathNode.ATAN2 );
 export const min = nodeProxy( MathNode, MathNode.MIN );
 export const max = nodeProxy( MathNode, MathNode.MAX );
 export const mod = nodeProxy( MathNode, MathNode.MOD );

+ 5 - 0
examples/jsm/nodes/shadernode/ShaderNodeElements.js

@@ -5,6 +5,7 @@ import ReflectNode from '../accessors/ReflectNode.js';
 import SkinningNode from '../accessors/SkinningNode.js';
 
 // display
+import ColorAdjustmentNode from '../display/ColorAdjustmentNode.js';
 import ColorSpaceNode from '../display/ColorSpaceNode.js';
 import NormalMapNode from '../display/NormalMapNode.js';
 import ToneMappingNode from '../display/ToneMappingNode.js';
@@ -68,6 +69,10 @@ export const skinning = nodeProxy( SkinningNode );
 
 // display
 
+export const saturation = nodeProxy( ColorAdjustmentNode, ColorAdjustmentNode.SATURATION );
+export const vibrance = nodeProxy( ColorAdjustmentNode, ColorAdjustmentNode.VIBRANCE );
+export const hue = nodeProxy( ColorAdjustmentNode, ColorAdjustmentNode.HUE );
+
 export const colorSpace = ( node, encoding ) => nodeObject( new ColorSpaceNode( null, nodeObject( node ) ).fromEncoding( encoding ) );
 export const normalMap = nodeProxy( NormalMapNode );
 export const toneMapping = ( mapping, exposure, color ) => nodeObject( new ToneMappingNode( mapping, nodeObject( exposure ), nodeObject( color ) ) );

+ 8 - 3
examples/jsm/nodes/utils/ConvertNode.js

@@ -17,23 +17,28 @@ class ConvertNode extends Node {
 
 	}
 
-	generate( builder ) {
+	generate( builder, output ) {
 
 		const convertTo = this.convertTo;
 		const node = this.node;
+		const type = this.getNodeType( builder );
+
+		let snippet = null;
 
 		if ( builder.isReference( convertTo ) === false ) {
 
 			const nodeSnippet = node.build( builder, convertTo );
 
-			return builder.format( nodeSnippet, this.getNodeType( builder ), convertTo );
+			snippet = builder.format( nodeSnippet, type, convertTo );
 
 		} else {
 
-			return node.build( builder, convertTo );
+			snippet = node.build( builder, convertTo );
 
 		}
 
+		return builder.format( snippet, type, output );
+
 	}
 
 }

+ 7 - 7
examples/jsm/nodes/utils/JoinNode.js

@@ -1,6 +1,6 @@
-import Node from '../core/Node.js';
+import TempNode from '../core/Node.js';
 
-class JoinNode extends Node {
+class JoinNode extends TempNode {
 
 	constructor( nodes = [] ) {
 
@@ -16,16 +16,14 @@ class JoinNode extends Node {
 
 	}
 
-	generate( builder ) {
+	generate( builder, output ) {
 
 		const type = this.getNodeType( builder );
 		const nodes = this.nodes;
 
 		const snippetValues = [];
 
-		for ( let i = 0; i < nodes.length; i ++ ) {
-
-			const input = nodes[ i ];
+		for ( const input of nodes ) {
 
 			const inputSnippet = input.build( builder );
 
@@ -33,7 +31,9 @@ class JoinNode extends Node {
 
 		}
 
-		return `${ builder.getType( type ) }( ${ snippetValues.join( ', ' ) } )`;
+		const snippet = `${ builder.getType( type ) }( ${ snippetValues.join( ', ' ) } )`;
+
+		return builder.format( snippet, type, output );
 
 	}
 

+ 1 - 1
examples/jsm/nodes/utils/MaxMipLevelNode.js

@@ -15,7 +15,7 @@ class MaxMipLevelNode extends UniformNode {
 
 	update() {
 
-		const image = this.texture.images ? this.texture.images[ 0 ].image || this.texture.images[ 0 ] : this.texture.image;
+		const image = this.texture.images && this.texture.images.length > 0 ? this.texture.images[ 0 ].image || this.texture.images[ 0 ] : this.texture.image;
 
 		if ( image?.width !== undefined ) {
 

BIN=BIN
examples/screenshots/webgpu_cubemap_adjustments.jpg


+ 213 - 0
examples/webgpu_cubemap_adjustments.html

@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - cubemap adjustments</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> - Env. Adjustments<br />
+			Battle Damaged Sci-fi Helmet by
+			<a href="https://sketchfab.com/theblueturtle_" target="_blank" rel="noopener">theblueturtle_</a><br />
+		</div>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three-nodes/": "./jsm/nodes/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import * as Nodes from 'three-nodes/Nodes.js';
+
+			import { uniform, mix, cubeTexture, mul, reference, add, positionWorld, normalWorld, saturate, saturation, hue, reflectCube } from 'three-nodes/Nodes.js';
+
+			import WebGPU from './jsm/capabilities/WebGPU.js';
+			import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
+
+			import { RGBMLoader } from './jsm/loaders/RGBMLoader.js';
+
+			import { OrbitControls } from './jsm/controls/OrbitControls.js';
+			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
+
+			import { GUI } from './jsm/libs/lil-gui.module.min.js';
+
+			let camera, scene, renderer;
+
+			init().then( render ).catch( error );
+
+			async function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				const initialDistance = 2;
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
+				camera.position.set( - 1.8 * initialDistance, 0.6 * initialDistance, 2.7 * initialDistance );
+
+				scene = new THREE.Scene();
+
+				// cube textures
+
+				const rgbmUrls = [ 'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png' ];
+				const cube1Texture = new RGBMLoader()
+					.setMaxRange( 16 )
+					.setPath( './textures/cube/pisaRGBM16/' )
+					.loadCubemap( rgbmUrls );
+
+				cube1Texture.generateMipmaps = true;
+				cube1Texture.minFilter = THREE.LinearMipmapLinearFilter;
+
+				const cube2Urls = [ 'posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg' ];
+				const cube2Texture = new THREE.CubeTextureLoader()
+					.setPath( './textures/cube/Park2/' )
+					.load( cube2Urls );
+
+				cube2Texture.generateMipmaps = true;
+				cube2Texture.minFilter = THREE.LinearMipmapLinearFilter;
+
+				// nodes and environment
+
+				const adjustments = {
+					mix: 0,
+					procedural: 0,
+					brightness: 0,
+					contrast: 1,
+					hue: 0,
+					saturation: 1
+				};
+
+				const mixNode = reference( 'mix', 'float', adjustments );
+				const proceduralNode = reference( 'procedural', 'float', adjustments );
+				const brightnessNode = reference( 'brightness', 'float', adjustments );
+				const contrastNode = reference( 'contrast', 'float', adjustments );
+				const hueNode = reference( 'hue', 'float', adjustments );
+				const saturationNode = reference( 'saturation', 'float', adjustments );
+
+				const rotateY1Matrix = new THREE.Matrix4();
+				const rotateY2Matrix = new THREE.Matrix4();
+
+				const custom1UV = mul( reflectCube, uniform( rotateY1Matrix ) );
+				const custom2UV = mul( reflectCube, uniform( rotateY2Matrix ) );
+
+				const mixCubeMaps = mix( cubeTexture( cube1Texture, custom1UV ), cubeTexture( cube2Texture, custom2UV ), saturate( add( positionWorld.y, mixNode ) ) );
+				const proceduralEnv = mix( mixCubeMaps, normalWorld, proceduralNode );
+				const brightnessFilter = add( proceduralEnv, brightnessNode );
+				const contrastFilter = mul( brightnessFilter, contrastNode );
+				const hueFilter = hue( contrastFilter, hueNode );
+				const saturationFilter = saturation( hueFilter, saturationNode );
+
+				scene.environmentNode = saturationFilter;
+
+				// scene objects
+
+				const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );
+				loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
+
+					scene.add( gltf.scene );
+
+					render();
+
+				} );
+
+				const sphereGeometry = new THREE.SphereGeometry( .5, 64, 32 );
+
+				const sphereRightView = new THREE.Mesh( sphereGeometry, new THREE.MeshStandardMaterial( { roughness: 0, metalness: 1 } ) );
+				sphereRightView.position.x += 2;
+
+				const sphereLeftView = new THREE.Mesh( sphereGeometry, new THREE.MeshStandardMaterial( { roughness: 1, metalness: 1 } ) );
+				sphereLeftView.position.x -= 2;
+
+				scene.add( sphereLeftView );
+				scene.add( sphereRightView );
+
+				// renderer and controls
+
+				renderer = new WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.toneMappingNode = new Nodes.ToneMappingNode( THREE.LinearToneMapping, 1 );
+				renderer.outputEncoding = THREE.sRGBEncoding;
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// gui
+
+				const gui = new GUI();
+
+				gui.add( { offsetCube1: 0 }, 'offsetCube1', 0, Math.PI * 2, 0.01 ).onChange( value => {
+
+					rotateY1Matrix.makeRotationY( value );
+
+				} );
+				gui.add( { offsetCube2: 0 }, 'offsetCube2', 0, Math.PI * 2, 0.01 ).onChange( value => {
+
+					rotateY2Matrix.makeRotationY( value );
+
+				} );
+				gui.add( adjustments, 'mix', - 1, 2, 0.01 );
+				gui.add( adjustments, 'procedural', 0, 1, 0.01 );
+				gui.add( adjustments, 'brightness', 0, 1, 0.01 );
+				gui.add( adjustments, 'contrast', 0, 3, 0.01 );
+				gui.add( adjustments, 'hue', 0, Math.PI * 2, 0.01 );
+				gui.add( adjustments, 'saturation', 0, 2, 0.01 );
+
+				return renderer.init();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function render() {
+
+				requestAnimationFrame( render );
+
+				renderer.render( scene, camera );
+
+			}
+
+			function error( error ) {
+
+				console.error( error );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -44,6 +44,7 @@ const exceptionList = [
 	'webxr_ar_lighting',
 	// webgpu
 	'webgpu_compute',
+	'webgpu_cubemap_adjustments',
 	'webgpu_cubemap_mix',
 	'webgpu_depth_texture',
 	'webgpu_instance_mesh',