Преглед на файлове

WebGPURenderer: Morph Textures (#27207)

sunag преди 1 година
родител
ревизия
86328907bc

+ 1 - 0
examples/files.json

@@ -342,6 +342,7 @@
 		"webgpu_materials_video",
 		"webgpu_multiple_rendertargets",
 		"webgpu_morphtargets",
+		"webgpu_morphtargets_face",
 		"webgpu_occlusion",
 		"webgpu_particles",
 		"webgpu_rtt",

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

@@ -84,13 +84,13 @@ export { default as NormalNode, normalGeometry, normalLocal, normalView, normalW
 export { default as Object3DNode, objectDirection, objectViewMatrix, objectNormalMatrix, objectWorldMatrix, objectPosition, objectScale, objectViewPosition } from './accessors/Object3DNode.js';
 export { default as PointUVNode, pointUV } from './accessors/PointUVNode.js';
 export { default as PositionNode, positionGeometry, positionLocal, positionWorld, positionWorldDirection, positionView, positionViewDirection } from './accessors/PositionNode.js';
-export { default as ReferenceNode, reference } from './accessors/ReferenceNode.js';
+export { default as ReferenceNode, reference, referenceIndex } from './accessors/ReferenceNode.js';
 export { default as ReflectVectorNode, reflectVector } from './accessors/ReflectVectorNode.js';
 export { default as SkinningNode, skinning } from './accessors/SkinningNode.js';
 export { default as SceneNode, backgroundBlurriness, backgroundIntensity } from './accessors/SceneNode.js';
 export { default as StorageBufferNode, storage } from './accessors/StorageBufferNode.js';
 export { default as TangentNode, tangentGeometry, tangentLocal, tangentView, tangentWorld, transformedTangentView, transformedTangentWorld } from './accessors/TangentNode.js';
-export { default as TextureNode, texture, /*textureLevel,*/ sampler } from './accessors/TextureNode.js';
+export { default as TextureNode, texture, textureLoad, /*textureLevel,*/ sampler } from './accessors/TextureNode.js';
 export { default as TextureStoreNode, textureStore } from './accessors/TextureStoreNode.js';
 export { default as UVNode, uv } from './accessors/UVNode.js';
 export { default as UserDataNode, userData } from './accessors/UserDataNode.js';

+ 191 - 16
examples/jsm/nodes/accessors/MorphNode.js

@@ -1,10 +1,157 @@
 import Node, { addNodeClass } from '../core/Node.js';
 import { NodeUpdateType } from '../core/constants.js';
-import { nodeProxy } from '../shadernode/ShaderNode.js';
+import { nodeProxy, tslFn } from '../shadernode/ShaderNode.js';
 import { uniform } from '../core/UniformNode.js';
-import { reference } from './ReferenceNode.js';
-import { bufferAttribute } from './BufferAttributeNode.js';
+import { referenceIndex } from './ReferenceNode.js';
 import { positionLocal } from './PositionNode.js';
+import { normalLocal } from './NormalNode.js';
+import { textureLoad } from './TextureNode.js';
+import { vertexIndex } from '../core/IndexNode.js';
+import { ivec2, int } from '../shadernode/ShaderNode.js';
+import { DataArrayTexture, Vector2, Vector4, FloatType } from 'three';
+
+const morphTextures = new WeakMap();
+const morphVec4 = new Vector4();
+
+const getMorph = tslFn( ( { bufferMap, influence, stride, width, depth, offset } ) => {
+
+	const texelIndex = int( vertexIndex ).mul( stride ).add( offset );
+
+	const y = texelIndex.div( width );
+	const x = texelIndex.sub( y.mul( width ) );
+
+	const bufferAttrib = textureLoad( bufferMap, ivec2( x, y ) ).depth( depth );
+
+	return bufferAttrib.mul( influence );
+
+} );
+
+function getEntry( geometry ) {
+
+	const hasMorphPosition = geometry.morphAttributes.position !== undefined;
+	const hasMorphNormals = geometry.morphAttributes.normal !== undefined;
+	const hasMorphColors = geometry.morphAttributes.color !== undefined;
+
+	// instead of using attributes, the WebGL 2 code path encodes morph targets
+	// into an array of data textures. Each layer represents a single morph target.
+
+	const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
+	const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;
+
+	let entry = morphTextures.get( geometry );
+
+	if ( entry === undefined || entry.count !== morphTargetsCount ) {
+
+		if ( entry !== undefined ) entry.texture.dispose();
+
+		const morphTargets = geometry.morphAttributes.position || [];
+		const morphNormals = geometry.morphAttributes.normal || [];
+		const morphColors = geometry.morphAttributes.color || [];
+
+		let vertexDataCount = 0;
+
+		if ( hasMorphPosition === true ) vertexDataCount = 1;
+		if ( hasMorphNormals === true ) vertexDataCount = 2;
+		if ( hasMorphColors === true ) vertexDataCount = 3;
+
+		let width = geometry.attributes.position.count * vertexDataCount;
+		let height = 1;
+
+		const maxTextureSize = 4096; // @TODO: Use 'capabilities.maxTextureSize'
+
+		if ( width > maxTextureSize ) {
+
+			height = Math.ceil( width / maxTextureSize );
+			width = maxTextureSize;
+
+		}
+
+		const buffer = new Float32Array( width * height * 4 * morphTargetsCount );
+
+		const bufferTexture = new DataArrayTexture( buffer, width, height, morphTargetsCount );
+		bufferTexture.type = FloatType;
+		bufferTexture.needsUpdate = true;
+
+		// fill buffer
+
+		const vertexDataStride = vertexDataCount * 4;
+
+		for ( let i = 0; i < morphTargetsCount; i ++ ) {
+
+			const morphTarget = morphTargets[ i ];
+			const morphNormal = morphNormals[ i ];
+			const morphColor = morphColors[ i ];
+
+			const offset = width * height * 4 * i;
+
+			for ( let j = 0; j < morphTarget.count; j ++ ) {
+
+				const stride = j * vertexDataStride;
+
+				if ( hasMorphPosition === true ) {
+
+					morphVec4.fromBufferAttribute( morphTarget, j );
+
+					buffer[ offset + stride + 0 ] = morphVec4.x;
+					buffer[ offset + stride + 1 ] = morphVec4.y;
+					buffer[ offset + stride + 2 ] = morphVec4.z;
+					buffer[ offset + stride + 3 ] = 0;
+
+				}
+
+				if ( hasMorphNormals === true ) {
+
+					morphVec4.fromBufferAttribute( morphNormal, j );
+
+					buffer[ offset + stride + 4 ] = morphVec4.x;
+					buffer[ offset + stride + 5 ] = morphVec4.y;
+					buffer[ offset + stride + 6 ] = morphVec4.z;
+					buffer[ offset + stride + 7 ] = 0;
+
+				}
+
+				if ( hasMorphColors === true ) {
+
+					morphVec4.fromBufferAttribute( morphColor, j );
+
+					buffer[ offset + stride + 8 ] = morphVec4.x;
+					buffer[ offset + stride + 9 ] = morphVec4.y;
+					buffer[ offset + stride + 10 ] = morphVec4.z;
+					buffer[ offset + stride + 11 ] = ( morphColor.itemSize === 4 ) ? morphVec4.w : 1;
+
+				}
+
+			}
+
+		}
+
+		entry = {
+			count: morphTargetsCount,
+			texture: bufferTexture,
+			stride: vertexDataCount,
+			size: new Vector2( width, height )
+		};
+
+		morphTextures.set( geometry, entry );
+
+		function disposeTexture() {
+
+			bufferTexture.dispose();
+
+			morphTextures.delete( geometry );
+
+			geometry.removeEventListener( 'dispose', disposeTexture );
+
+		}
+
+		geometry.addEventListener( 'dispose', disposeTexture );
+
+	}
+
+	return entry;
+
+}
+
 
 class MorphNode extends Node {
 
@@ -19,29 +166,57 @@ class MorphNode extends Node {
 
 	}
 
-	setupAttribute( name, assignNode = positionLocal ) {
+	setup( builder ) {
 
-		const mesh = this.mesh;
-		const attributes = mesh.geometry.morphAttributes[ name ];
+		const { geometry } = builder;
 
-		assignNode.mulAssign( this.morphBaseInfluence );
+		const hasMorphPosition = geometry.morphAttributes.position !== undefined;
+		const hasMorphNormals = geometry.morphAttributes.normal !== undefined;
 
-		for ( let i = 0; i < attributes.length; i ++ ) {
+		const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
+		const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;
 
-			const attribute = attributes[ i ];
+		// nodes
 
-			const bufferAttrib = bufferAttribute( attribute.array, 'vec3' );
-			const influence = reference( i, 'float', mesh.morphTargetInfluences );
+		const { texture: bufferMap, stride, size } = getEntry( geometry );
 
-			assignNode.addAssign( bufferAttrib.mul( influence ) );
+		if ( hasMorphPosition === true ) positionLocal.mulAssign( this.morphBaseInfluence );
+		if ( hasMorphNormals === true ) normalLocal.mulAssign( this.morphBaseInfluence );
 
-		}
+		const width = int( size.width );
 
-	}
+		for ( let i = 0; i < morphTargetsCount; i ++ ) {
+
+			const influence = referenceIndex( 'morphTargetInfluences', i, 'float' );
+			const depth = int( i );
+
+			if ( hasMorphPosition === true ) {
+
+				positionLocal.addAssign( getMorph( {
+					bufferMap,
+					influence,
+					stride,
+					width,
+					depth,
+					offset: int( 0 )
+				} ) );
 
-	setup( /*builder*/ ) {
+			}
 
-		this.setupAttribute( 'position' );
+			if ( hasMorphNormals === true ) {
+
+				normalLocal.addAssign( getMorph( {
+					bufferMap,
+					influence,
+					stride,
+					width,
+					depth,
+					offset: int( 1 )
+				} ) );
+
+			}
+
+		}
 
 	}
 

+ 25 - 1
examples/jsm/nodes/accessors/ReferenceNode.js

@@ -11,6 +11,7 @@ class ReferenceNode extends Node {
 		super();
 
 		this.property = property;
+		this.index = null;
 
 		this.uniformType = uniformType;
 
@@ -33,6 +34,20 @@ class ReferenceNode extends Node {
 
 	}
 
+	setIndex( index ) {
+
+		this.index = index;
+
+		return this;
+
+	}
+
+	getIndex() {
+
+		return this.index;
+
+	}
+
 	setNodeType( uniformType ) {
 
 		let node = null;
@@ -59,7 +74,15 @@ class ReferenceNode extends Node {
 
 	update( /*frame*/ ) {
 
-		this.node.value = this.reference[ this.property ];
+		let value = this.reference[ this.property ];
+
+		if ( this.index !== null ) {
+
+			value = value[ this.index ];
+
+		}
+
+		this.node.value = value;
 
 	}
 
@@ -74,5 +97,6 @@ class ReferenceNode extends Node {
 export default ReferenceNode;
 
 export const reference = ( name, type, object ) => nodeObject( new ReferenceNode( name, type, object ) );
+export const referenceIndex = ( name, index, type, object ) => nodeObject( new ReferenceNode( name, type, object ).setIndex( index ) );
 
 addNodeClass( 'ReferenceNode', ReferenceNode );

+ 8 - 3
examples/jsm/nodes/accessors/TextureNode.js

@@ -163,7 +163,7 @@ class TextureNode extends UniformNode {
 
 		}
 
-		return uvNode.build( builder, this.sampler === true ? 'vec2' : 'uvec2' );
+		return uvNode.build( builder, this.sampler === true ? 'vec2' : 'ivec2' );
 
 	}
 
@@ -201,7 +201,7 @@ class TextureNode extends UniformNode {
 
 				const uvSnippet = this.generateUV( builder, uvNode );
 				const levelSnippet = levelNode ? levelNode.build( builder, 'float' ) : null;
-				const depthSnippet = depthNode ? depthNode.build( builder, 'uint' ) : null;
+				const depthSnippet = depthNode ? depthNode.build( builder, 'int' ) : null;
 				const compareSnippet = compareNode ? compareNode.build( builder, 'float' ) : null;
 
 				const nodeVar = builder.getVarFromNode( this );
@@ -335,7 +335,10 @@ class TextureNode extends UniformNode {
 
 	clone() {
 
-		return new this.constructor( this.value, this.uvNode, this.levelNode );
+		const newNode = new this.constructor( this.value, this.uvNode, this.levelNode );
+		newNode.sampler = this.sampler;
+
+		return newNode;
 
 	}
 
@@ -344,6 +347,8 @@ class TextureNode extends UniformNode {
 export default TextureNode;
 
 export const texture = nodeProxy( TextureNode );
+export const textureLoad = ( ...params ) => texture( ...params ).setSampler( false );
+
 //export const textureLevel = ( value, uv, level ) => texture( value, uv ).level( level );
 
 export const sampler = ( aTexture ) => ( aTexture.isNode === true ? aTexture : texture( aTexture ) ).convert( 'sampler' );

+ 7 - 1
examples/jsm/renderers/common/RenderObject.js

@@ -106,7 +106,7 @@ export default class RenderObject {
 
 	getMaterialCacheKey() {
 
-		const material = this.material;
+		const { object, material } = this;
 
 		let cacheKey = material.customProgramCacheKey();
 
@@ -129,6 +129,12 @@ export default class RenderObject {
 
 		}
 
+		if ( object.morphTargetInfluences ) {
+
+			cacheKey += object.morphTargetInfluences.length + ',';
+
+		}
+
 		return cacheKey;
 
 	}

+ 24 - 7
examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js

@@ -20,6 +20,13 @@ const supports = {
 	instance: true
 };
 
+const defaultPrecisions = `
+precision highp float;
+precision highp int;
+precision mediump sampler2DArray;
+precision lowp sampler2DShadow;
+`;
+
 class GLSLNodeBuilder extends NodeBuilder {
 
 	constructor( object, renderer, scene = null ) {
@@ -74,6 +81,20 @@ ${ flowData.code }
 
 	}
 
+	generateTextureLoad( texture, textureProperty, uvIndexSnippet, depthSnippet, levelSnippet = '0' ) {
+
+		if ( depthSnippet ) {
+
+			return `texelFetch( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), ${ levelSnippet } )`;
+
+		} else {
+
+			return `texelFetch( ${ textureProperty }, ${ uvIndexSnippet }, ${ levelSnippet } )`;
+
+		}
+
+	}
+
 	generateTexture( texture, textureProperty, uvSnippet, depthSnippet ) {
 
 		if ( texture.isTextureCube ) {
@@ -337,7 +358,7 @@ ${ flowData.code }
 
 	getVertexIndex() {
 
-		return 'gl_VertexID';
+		return 'uint( gl_VertexID )';
 
 	}
 
@@ -388,8 +409,7 @@ ${vars}
 ${ this.getSignature() }
 
 // precision
-precision highp float;
-precision highp int;
+${ defaultPrecisions }
 
 // uniforms
 ${shaderData.uniforms}
@@ -425,10 +445,7 @@ void main() {
 ${ this.getSignature() }
 
 // precision
-precision highp float;
-precision highp int;
-precision highp sampler2DArray;
-precision lowp sampler2DShadow;
+${ defaultPrecisions }
 
 // uniforms
 ${shaderData.uniforms}

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

@@ -193,6 +193,20 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 	}
 
+	generateTextureLoad( texture, textureProperty, uvIndexSnippet, depthSnippet, levelSnippet = '0u' ) {
+
+		if ( depthSnippet ) {
+
+			return `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, ${ levelSnippet } )`;
+
+		} else {
+
+			return `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ levelSnippet } )`;
+
+		}
+
+	}
+
 	isUnfilterable( texture ) {
 
 		return texture.isDataTexture === true && texture.type === FloatType;

BIN
examples/screenshots/webgpu_morphtargets_face.jpg


+ 165 - 0
examples/webgpu_morphtargets_face.html

@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - morph targets - face</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">
+		<style>
+			body {
+				background-color: #666666;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - morph targets - face<br/>
+			model by <a href="https://www.bannaflak.com/face-cap" target="_blank" rel="noopener">Face Cap</a>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGL from 'three/addons/capabilities/WebGL.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
+			import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false && WebGL.isWebGL2Available() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU or WebGL2 support' );
+
+				}
+
+				let mixer;
+
+				const clock = new THREE.Clock();
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 20 );
+				camera.position.set( - 1.8, 0.8, 3 );
+
+				const scene = new THREE.Scene();
+				scene.add( new THREE.HemisphereLight( 0xFFFFFF, 0x443333, 2 ) );
+
+				const renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+
+				container.appendChild( renderer.domElement );
+
+				const ktx2Loader = new KTX2Loader()
+					.setTranscoderPath( 'jsm/libs/basis/' )
+					.detectSupport( renderer );
+
+				new GLTFLoader()
+					.setKTX2Loader( ktx2Loader )
+					.setMeshoptDecoder( MeshoptDecoder )
+					.load( 'models/gltf/facecap.glb', ( gltf ) => {
+
+						const mesh = gltf.scene.children[ 0 ];
+
+						scene.add( mesh );
+
+						mixer = new THREE.AnimationMixer( mesh );
+
+						mixer.clipAction( gltf.animations[ 0 ] ).play();
+
+						// GUI
+
+						const head = mesh.getObjectByName( 'mesh_2' );
+						const influences = head.morphTargetInfluences;
+
+						//head.morphTargetInfluences = null;
+
+						// WebGPURenderer: Unsupported texture format. 33776
+						head.material.map = null;
+
+						const gui = new GUI();
+						gui.close();
+
+						for ( const [ key, value ] of Object.entries( head.morphTargetDictionary ) ) {
+
+							gui.add( influences, value, 0, 1, 0.01 )
+								.name( key.replace( 'blendShape1.', '' ) )
+								.listen();
+
+						}
+
+					} );
+
+				scene.background = new THREE.Color( 0x666666 );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableDamping = true;
+				controls.minDistance = 2.5;
+				controls.maxDistance = 5;
+				controls.minAzimuthAngle = - Math.PI / 2;
+				controls.maxAzimuthAngle = Math.PI / 2;
+				controls.maxPolarAngle = Math.PI / 1.8;
+				controls.target.set( 0, 0.15, - 0.2 );
+
+				const stats = new Stats();
+				container.appendChild( stats.dom );
+
+				renderer.setAnimationLoop( () => {
+
+					const delta = clock.getDelta();
+
+					if ( mixer ) {
+
+						mixer.update( delta );
+
+					}
+
+					renderer.render( scene, camera );
+
+					controls.update();
+
+					stats.update();
+
+				} );
+
+				window.addEventListener( 'resize', () => {
+
+					camera.aspect = window.innerWidth / window.innerHeight;
+					camera.updateProjectionMatrix();
+
+					renderer.setSize( window.innerWidth, window.innerHeight );
+
+				} );
+
+			}
+		</script>
+	</body>
+</html>