Browse Source

WebGPURenderer: MorphNode 1/2 and updates (#26325)

* Added: instancedBufferAttribute() and instancedDynamicBufferAttribute()

* TSL: vertexIndex

* Background: Fix color conversion

* NodeMaterial: Added flatShading

* Added MorphNode

* Added `webgpu_morphtargets` example

* Update examples/jsm/nodes/accessors/BufferAttributeNode.js

Co-authored-by: Levi Pesin <[email protected]>

---------

Co-authored-by: Levi Pesin <[email protected]>
sunag 2 years ago
parent
commit
c944e5acd2

+ 1 - 0
examples/files.json

@@ -325,6 +325,7 @@
 		"webgpu_loader_gltf_compressed",
 		"webgpu_materials",
 		"webgpu_materials_video",
+		"webgpu_morphtargets",
 		"webgpu_particles",
 		"webgpu_rtt",
 		"webgpu_sandbox",

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

@@ -11,7 +11,7 @@ export { default as BypassNode, bypass } from './core/BypassNode.js';
 export { default as CacheNode, cache } from './core/CacheNode.js';
 export { default as ConstNode } from './core/ConstNode.js';
 export { default as ContextNode, context, label } from './core/ContextNode.js';
-export { default as InstanceIndexNode, instanceIndex } from './core/InstanceIndexNode.js';
+export { default as IndexNode, vertexIndex, instanceIndex } from './core/IndexNode.js';
 export { default as LightingModel, lightingModel } from './core/LightingModel.js';
 export { default as Node, addNodeClass, createNodeFromType } from './core/Node.js';
 export { default as NodeAttribute } from './core/NodeAttribute.js';
@@ -63,7 +63,7 @@ export * from './shadernode/ShaderNode.js';
 
 // accessors
 export { default as BitangentNode, bitangentGeometry, bitangentLocal, bitangentView, bitangentWorld, transformedBitangentView, transformedBitangentWorld } from './accessors/BitangentNode.js';
-export { default as BufferAttributeNode, bufferAttribute, dynamicBufferAttribute } from './accessors/BufferAttributeNode.js';
+export { default as BufferAttributeNode, bufferAttribute, dynamicBufferAttribute, instancedBufferAttribute, instancedDynamicBufferAttribute } from './accessors/BufferAttributeNode.js';
 export { default as BufferNode, buffer } from './accessors/BufferNode.js';
 export { default as CameraNode, cameraProjectionMatrix, cameraViewMatrix, cameraNormalMatrix, cameraWorldMatrix, cameraPosition, cameraNear, cameraFar } from './accessors/CameraNode.js';
 export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTextureNode.js';
@@ -71,6 +71,7 @@ export { default as ExtendedMaterialNode, materialNormal } from './accessors/Ext
 export { default as InstanceNode, instance } from './accessors/InstanceNode.js';
 export { default as MaterialNode, materialUV, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialReflectivity, materialRoughness, materialMetalness, materialRotation } from './accessors/MaterialNode.js';
 export { default as MaterialReferenceNode, materialReference } from './accessors/MaterialReferenceNode.js';
+export { default as MorphNode, morph } from './accessors/MorphNode.js';
 export { default as TextureBicubicNode, textureBicubic } from './accessors/TextureBicubicNode.js';
 export { default as ModelNode, modelDirection, modelViewMatrix, modelNormalMatrix, modelWorldMatrix, modelPosition, modelViewPosition, modelScale } from './accessors/ModelNode.js';
 export { default as ModelViewProjectionNode, modelViewProjection } from './accessors/ModelViewProjectionNode.js';

+ 21 - 8
examples/jsm/nodes/accessors/BufferAttributeNode.js

@@ -17,6 +17,7 @@ class BufferAttributeNode extends InputNode {
 		this.bufferOffset = bufferOffset;
 
 		this.usage = StaticDrawUsage;
+		this.instanced = false;
 
 	}
 
@@ -34,7 +35,7 @@ class BufferAttributeNode extends InputNode {
 		buffer.setUsage( this.usage );
 
 		this.attribute = bufferAttribute;
-		this.attribute.isInstancedBufferAttribute = true; // @TODO: Add a possible: InstancedInterleavedBufferAttribute
+		this.attribute.isInstancedBufferAttribute = this.instanced; // @TODO: Add a possible: InstancedInterleavedBufferAttribute
 
 	}
 
@@ -69,18 +70,30 @@ class BufferAttributeNode extends InputNode {
 
 	}
 
+	setUsage( value ) {
+
+		this.usage = value;
+
+		return this;
+
+	}
+
+	setInstanced( value ) {
+
+		this.instanced = value;
+
+		return this;
+
+	}
+
 }
 
 export default BufferAttributeNode;
 
 export const bufferAttribute = ( array, type, stride, offset ) => nodeObject( new BufferAttributeNode( array, type, stride, offset ) );
-export const dynamicBufferAttribute = ( array, type, stride, offset ) => {
-
-	const node = bufferAttribute( array, type, stride, offset );
-	node.usage = DynamicDrawUsage;
-
-	return node;
+export const dynamicBufferAttribute = ( array, type, stride, offset ) => bufferAttribute( array, type, stride, offset ).setUsage( DynamicDrawUsage );
 
-};
+export const instancedBufferAttribute = ( array, type, stride, offset ) => bufferAttribute( array, type, stride, offset ).setInstanced( true );
+export const instancedDynamicBufferAttribute = ( array, type, stride, offset ) => dynamicBufferAttribute( array, type, stride, offset ).setInstanced( true );
 
 addNodeClass( BufferAttributeNode );

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

@@ -1,5 +1,5 @@
 import Node, { addNodeClass } from '../core/Node.js';
-import { bufferAttribute, dynamicBufferAttribute } from './BufferAttributeNode.js';
+import { instancedBufferAttribute, instancedDynamicBufferAttribute } from './BufferAttributeNode.js';
 import { normalLocal } from './NormalNode.js';
 import { positionLocal } from './PositionNode.js';
 import { nodeProxy, vec3, mat3, mat4 } from '../shadernode/ShaderNode.js';
@@ -27,7 +27,7 @@ class InstanceNode extends Node {
 			const instaceAttribute = instanceMesh.instanceMatrix;
 			const array = instaceAttribute.array;
 
-			const bufferFn = instaceAttribute.usage === DynamicDrawUsage ? dynamicBufferAttribute : bufferAttribute;
+			const bufferFn = instaceAttribute.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;
 
 			const instanceBuffers = [
 				// F.Signature -> bufferAttribute( array, type, stride, offset )

+ 70 - 0
examples/jsm/nodes/accessors/MorphNode.js

@@ -0,0 +1,70 @@
+import Node, { addNodeClass } from '../core/Node.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { nodeProxy } from '../shadernode/ShaderNode.js';
+import { uniform } from '../core/UniformNode.js';
+import { reference } from './ReferenceNode.js';
+import { bufferAttribute } from './BufferAttributeNode.js';
+import { positionLocal } from './PositionNode.js';
+
+class MorphNode extends Node {
+
+	constructor( mesh ) {
+
+		super( 'void' );
+
+		this.mesh = mesh;
+		this.morphBaseInfluence = uniform( 1 );
+
+		this.updateType = NodeUpdateType.OBJECT;
+
+	}
+
+	constructAttribute( builder, name, assignNode = positionLocal ) {
+
+		const mesh = this.mesh;
+		const attributes = mesh.geometry.morphAttributes[ name ];
+
+		builder.stack.assign( assignNode, assignNode.mul( this.morphBaseInfluence ) );
+
+		for ( let i = 0; i < attributes.length; i ++ ) {
+
+			const attribute = attributes[ i ];
+
+			const bufferAttrib = bufferAttribute( attribute.array, 'vec3' );
+			const influence = reference( i, 'float', mesh.morphTargetInfluences );
+
+			builder.stack.assign( assignNode, assignNode.add( bufferAttrib.mul( influence ) ) );
+
+		}
+
+	}
+
+	construct( builder ) {
+
+		this.constructAttribute( builder, 'position' );
+
+	}
+
+	update() {
+
+		const morphBaseInfluence = this.morphBaseInfluence;
+
+		if ( this.mesh.geometry.morphTargetsRelative ) {
+
+			morphBaseInfluence.value = 1;
+
+		} else {
+
+			morphBaseInfluence.value = 1 - this.mesh.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 );
+
+		}
+
+	}
+
+}
+
+export default MorphNode;
+
+export const morph = nodeProxy( MorphNode );
+
+addNodeClass( MorphNode );

+ 66 - 0
examples/jsm/nodes/core/IndexNode.js

@@ -0,0 +1,66 @@
+import Node, { addNodeClass } from './Node.js';
+import { varying } from './VaryingNode.js';
+import { nodeImmutable } from '../shadernode/ShaderNode.js';
+
+class IndexNode extends Node {
+
+	constructor( scope ) {
+
+		super( 'uint' );
+
+		this.scope = scope;
+
+		this.isInstanceIndexNode = true;
+
+	}
+
+	generate( builder ) {
+
+		const nodeType = this.getNodeType( builder );
+		const scope = this.scope;
+
+		let propertyName;
+
+		if ( scope === IndexNode.VERTEX ) {
+
+			propertyName = builder.getVertexIndex();
+
+		} else if ( scope === IndexNode.INSTANCE ) {
+
+			propertyName = builder.getInstanceIndex();
+
+		} else {
+
+			throw new Error( 'THREE.IndexNode: Unknown scope: ' + scope );
+
+		}
+
+		let output;
+
+		if ( builder.shaderStage === 'vertex' || builder.shaderStage === 'compute' ) {
+
+			output = propertyName;
+
+		} else {
+
+			const nodeVarying = varying( this );
+
+			output = nodeVarying.build( builder, nodeType );
+
+		}
+
+		return output;
+
+	}
+
+}
+
+IndexNode.VERTEX = 'vertex';
+IndexNode.INSTANCE = 'instance';
+
+export default IndexNode;
+
+export const vertexIndex = nodeImmutable( IndexNode, IndexNode.VERTEX );
+export const instanceIndex = nodeImmutable( IndexNode, IndexNode.INSTANCE );
+
+addNodeClass( IndexNode );

+ 0 - 45
examples/jsm/nodes/core/InstanceIndexNode.js

@@ -1,45 +0,0 @@
-import Node, { addNodeClass } from './Node.js';
-import { varying } from '../core/VaryingNode.js';
-import { nodeImmutable } from '../shadernode/ShaderNode.js';
-
-class InstanceIndexNode extends Node {
-
-	constructor() {
-
-		super( 'uint' );
-
-		this.isInstanceIndexNode = true;
-
-	}
-
-	generate( builder ) {
-
-		const nodeType = this.getNodeType( builder );
-
-		const propertyName = builder.getInstanceIndex();
-
-		let output = null;
-
-		if ( builder.shaderStage === 'vertex' || builder.shaderStage === 'compute' ) {
-
-			output = propertyName;
-
-		} else {
-
-			const nodeVarying = varying( this );
-
-			output = nodeVarying.build( builder, nodeType );
-
-		}
-
-		return output;
-
-	}
-
-}
-
-export default InstanceIndexNode;
-
-export const instanceIndex = nodeImmutable( InstanceIndexNode );
-
-addNodeClass( InstanceIndexNode );

+ 6 - 0
examples/jsm/nodes/core/NodeBuilder.js

@@ -234,6 +234,12 @@ class NodeBuilder {
 
 	}
 
+	getVertexIndex() {
+
+		console.warn( 'Abstract function.' );
+
+	}
+
 	getInstanceIndex() {
 
 		console.warn( 'Abstract function.' );

+ 1 - 1
examples/jsm/nodes/geometry/RangeNode.js

@@ -2,7 +2,7 @@ import Node, { addNodeClass } from '../core/Node.js';
 import { getValueType } from '../core/NodeUtils.js';
 import { buffer } from '../accessors/BufferNode.js';
 //import { bufferAttribute } from '../accessors/BufferAttributeNode.js';
-import { instanceIndex } from '../core/InstanceIndexNode.js';
+import { instanceIndex } from '../core/IndexNode.js';
 import { nodeProxy, float } from '../shadernode/ShaderNode.js';
 
 import { Vector4, MathUtils } from 'three';

+ 23 - 5
examples/jsm/nodes/materials/NodeMaterial.js

@@ -7,12 +7,13 @@ import { materialAlphaTest, materialColor, materialOpacity, materialEmissive } f
 import { modelViewProjection } from '../accessors/ModelViewProjectionNode.js';
 import { transformedNormalView } from '../accessors/NormalNode.js';
 import { instance } from '../accessors/InstanceNode.js';
-import { positionLocal } from '../accessors/PositionNode.js';
+import { positionLocal, positionView } from '../accessors/PositionNode.js';
 import { skinning } from '../accessors/SkinningNode.js';
+import { morph } from '../accessors/MorphNode.js';
 import { texture } from '../accessors/TextureNode.js';
 import { cubeTexture } from '../accessors/CubeTextureNode.js';
 import { lightsWithoutWrap } from '../lighting/LightsNode.js';
-import { mix } from '../math/MathNode.js';
+import { mix, dFdx, dFdy } from '../math/MathNode.js';
 import { float, vec3, vec4 } from '../shadernode/ShaderNode.js';
 import AONode from '../lighting/AONode.js';
 import EnvironmentNode from '../lighting/EnvironmentNode.js';
@@ -99,9 +100,16 @@ class NodeMaterial extends ShaderMaterial {
 	constructPosition( builder ) {
 
 		const object = builder.object;
+		const geometry = object.geometry;
 
 		builder.addStack();
 
+		if ( geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color ) {
+
+			builder.stack.add( morph( object ) );
+
+		}
+
 		if ( object.isSkinnedMesh === true ) {
 
 			builder.stack.add( skinning( object ) );
@@ -169,11 +177,21 @@ class NodeMaterial extends ShaderMaterial {
 
 		// NORMAL VIEW
 
-		const normalNode = this.normalNode ? vec3( this.normalNode ) : materialNormal;
+		if ( this.flatShading === true ) {
+
+			const fdx = dFdx( positionView );
+			const fdy = dFdy( positionView.negate() ); // use -positionView ?
+			const normalNode = fdx.cross( fdy ).normalize();
 
-		stack.assign( transformedNormalView, normalNode );
+			stack.assign( transformedNormalView, normalNode );
 
-		return normalNode;
+		} else {
+
+			const normalNode = this.normalNode ? vec3( this.normalNode ) : materialNormal;
+
+			stack.assign( transformedNormalView, normalNode );
+
+		}
 
 	}
 

+ 2 - 2
examples/jsm/renderers/common/Background.js

@@ -30,14 +30,14 @@ class Background extends DataMap {
 
 			// no background settings, use clear color configuration from the renderer
 
-			_clearColor.copy( renderer._clearColor );
+			_clearColor.copyLinearToSRGB( renderer._clearColor );
 			_clearAlpha = renderer._clearAlpha;
 
 		} else if ( background.isColor === true ) {
 
 			// background is an opaque color
 
-			_clearColor.copy( background );
+			_clearColor.copyLinearToSRGB( background );
 			_clearAlpha = 1;
 			forceClear = true;
 

+ 6 - 0
examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js

@@ -160,6 +160,12 @@ class GLSLNodeBuilder extends NodeBuilder {
 
 	}
 
+	getVertexIndex() {
+
+		return 'gl_VertexID';
+
+	}
+
 	getFrontFacing() {
 
 		return 'gl_FrontFacing';

+ 6 - 0
examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js

@@ -573,6 +573,12 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 	}
 
+	getVertexIndex() {
+
+		return 'gl_VertexID';
+
+	}
+
 	getFrontFacing() {
 
 		return 'gl_FrontFacing';

+ 12 - 0
examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -400,6 +400,18 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 	}
 
+	getVertexIndex() {
+
+		if ( this.shaderStage === 'vertex' ) {
+
+			return this.getBuiltin( 'vertex_index', 'vertexIndex', 'u32', 'attribute' );
+
+		}
+
+		return 'vertexIndex';
+
+	}
+
 	getInstanceIndex() {
 
 		if ( this.shaderStage === 'vertex' ) {

BIN
examples/screenshots/webgpu_morphtargets.jpg


+ 182 - 0
examples/webgpu_morphtargets.html

@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - morph targets</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="container"></div>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - morph targets<br/>
+			by <a href="https://discoverthreejs.com/" target="_blank" rel="noopener">Discover three.js</a>
+		</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/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			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';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			let container, camera, scene, renderer, mesh;
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				container = document.getElementById( 'container' );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x8FBCD4 );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 20 );
+				camera.position.z = 10;
+				scene.add( camera );
+
+				scene.add( new THREE.AmbientLight( 0x8FBCD4, 1.5 ) );
+
+				const pointLight = new THREE.PointLight( 0xffffff, 200 );
+				camera.add( pointLight );
+
+				const geometry = createGeometry();
+
+				const material = new THREE.MeshPhongMaterial( {
+					color: 0xff0000,
+					flatShading: true
+				} );
+
+				mesh = new THREE.Mesh( geometry, material );
+				scene.add( mesh );
+
+				initGUI();
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( function () {
+
+					renderer.render( scene, camera );
+
+				} );
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableZoom = false;
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function createGeometry() {
+
+				const geometry = new THREE.BoxGeometry( 2, 2, 2, 32, 32, 32 );
+
+				// create an empty array to hold targets for the attribute we want to morph
+				// morphing positions and normals is supported
+				geometry.morphAttributes.position = [];
+
+				// the original positions of the cube's vertices
+				const positionAttribute = geometry.attributes.position;
+
+				// for the first morph target we'll move the cube's vertices onto the surface of a sphere
+				const spherePositions = [];
+
+				// for the second morph target, we'll twist the cubes vertices
+				const twistPositions = [];
+				const direction = new THREE.Vector3( 1, 0, 0 );
+				const vertex = new THREE.Vector3();
+
+				for ( let i = 0; i < positionAttribute.count; i ++ ) {
+
+					const x = positionAttribute.getX( i );
+					const y = positionAttribute.getY( i );
+					const z = positionAttribute.getZ( i );
+
+					spherePositions.push(
+
+						x * Math.sqrt( 1 - ( y * y / 2 ) - ( z * z / 2 ) + ( y * y * z * z / 3 ) ),
+						y * Math.sqrt( 1 - ( z * z / 2 ) - ( x * x / 2 ) + ( z * z * x * x / 3 ) ),
+						z * Math.sqrt( 1 - ( x * x / 2 ) - ( y * y / 2 ) + ( x * x * y * y / 3 ) )
+
+					);
+
+					// stretch along the x-axis so we can see the twist better
+					vertex.set( x * 2, y, z );
+
+					vertex.applyAxisAngle( direction, Math.PI * x / 2 ).toArray( twistPositions, twistPositions.length );
+
+				}
+
+				// add the spherical positions as the first morph target
+				geometry.morphAttributes.position[ 0 ] = new THREE.Float32BufferAttribute( spherePositions, 3 );
+
+				// add the twisted positions as the second morph target
+				geometry.morphAttributes.position[ 1 ] = new THREE.Float32BufferAttribute( twistPositions, 3 );
+
+				return geometry;
+
+			}
+
+			function initGUI() {
+
+				// Set up dat.GUI to control targets
+				const params = {
+					Spherify: 0,
+					Twist: 0,
+				};
+				const gui = new GUI( { title: 'Morph Targets' } );
+
+				gui.add( params, 'Spherify', 0, 1 ).step( 0.01 ).onChange( function ( value ) {
+
+					mesh.morphTargetInfluences[ 0 ] = value;
+
+				} );
+				gui.add( params, 'Twist', 0, 1 ).step( 0.01 ).onChange( function ( value ) {
+
+					mesh.morphTargetInfluences[ 1 ] = value;
+
+				} );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -123,6 +123,7 @@ const exceptionList = [
 	'webgpu_loader_gltf_compressed',
 	'webgpu_materials',
 	'webgpu_materials_video',
+	'webgpu_morphtargets',
 	'webgpu_particles',
 	'webgpu_rtt',
 	'webgpu_sandbox',