Prechádzať zdrojové kódy

WebGPU: Instancing (#23835)

* WebGPU: instancing support

* add instancing examples

* update thumbnails

* cleanup

* cleanup

* use native inverseSqrt
sunag 3 rokov pred
rodič
commit
e63acfc762

+ 2 - 0
examples/files.json

@@ -309,6 +309,7 @@
 	"webgpu": [
 		"webgpu_compute",
 		"webgpu_depth_texture",
+		"webgpu_instance_mesh",
 		"webgpu_instance_uniform",
 		"webgpu_lights_custom",
 		"webgpu_lights_selective",
@@ -317,6 +318,7 @@
 		"webgpu_rtt",
 		"webgpu_sandbox",
 		"webgpu_skinning",
+		"webgpu_skinning_instancing",
 		"webgpu_skinning_points"
 	],
 	"webaudio": [

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

@@ -8,6 +8,7 @@ import ContextNode from './core/ContextNode.js';
 import ExpressionNode from './core/ExpressionNode.js';
 import FunctionCallNode from './core/FunctionCallNode.js';
 import FunctionNode from './core/FunctionNode.js';
+import InstanceIndexNode from './core/InstanceIndexNode.js';
 import Node from './core/Node.js';
 import NodeAttribute from './core/NodeAttribute.js';
 import NodeBuilder from './core/NodeBuilder.js';
@@ -81,9 +82,6 @@ import FogRangeNode from './fog/FogRangeNode.js';
 // core
 export * from './core/constants.js';
 
-// functions
-export * from './functions/BSDFs.js';
-
 // materials
 export * from './materials/Materials.js';
 
@@ -101,6 +99,7 @@ const nodeLib = {
 	ExpressionNode,
 	FunctionCallNode,
 	FunctionNode,
+	InstanceIndexNode,
 	Node,
 	NodeAttribute,
 	NodeBuilder,
@@ -190,6 +189,7 @@ export {
 	ExpressionNode,
 	FunctionCallNode,
 	FunctionNode,
+	InstanceIndexNode,
 	Node,
 	NodeAttribute,
 	NodeBuilder,

+ 58 - 0
examples/jsm/nodes/accessors/InstanceNode.js

@@ -0,0 +1,58 @@
+import Node from '../core/Node.js';
+import {
+	vec3,
+	mat3,
+	mul,
+	assign,
+	buffer,
+	element,
+	dot,
+	div,
+	temp,
+	instanceIndex,
+	positionLocal,
+	normalLocal
+} from '../shadernode/ShaderNodeElements.js';
+
+class InstanceNode extends Node {
+
+	constructor( instanceMesh ) {
+
+		super( 'void' );
+
+		this.instanceMesh = instanceMesh;
+
+		//
+
+		const instanceBufferNode = buffer( instanceMesh.instanceMatrix.array, 'mat4', instanceMesh.count );
+
+		this.instanceMatrixNode = temp( element( instanceBufferNode, instanceIndex ) );
+
+	}
+
+	generate( builder ) {
+
+		const { instanceMatrixNode } = this;
+
+		// POSITION
+
+		const instancePosition = mul( instanceMatrixNode, positionLocal ).xyz;
+
+		// NORMAL
+
+		const m = mat3( instanceMatrixNode[ 0 ].xyz, instanceMatrixNode[ 1 ].xyz, instanceMatrixNode[ 2 ].xyz );
+
+		const transformedNormal = div( normalLocal, vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) ) );
+
+		const instanceNormal = mul( m, transformedNormal ).xyz;
+
+		// ASSIGNS
+
+		assign( positionLocal, instancePosition ).build( builder );
+		assign( normalLocal, instanceNormal ).build( builder );
+
+	}
+
+}
+
+export default InstanceNode;

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

@@ -0,0 +1,21 @@
+import Node from './Node.js';
+
+class InstanceIndexNode extends Node {
+
+	constructor() {
+
+		super( 'uint' );
+
+	}
+
+	generate( builder ) {
+
+		return builder.getInstanceIndex();
+
+	}
+
+}
+
+InstanceIndexNode.prototype.isInstanceIndexNode = true;
+
+export default InstanceIndexNode;

+ 27 - 8
examples/jsm/nodes/core/NodeBuilder.js

@@ -12,7 +12,7 @@ export const shaderStages = [ 'fragment', 'vertex' ];
 export const vector = [ 'x', 'y', 'z', 'w' ];
 
 const toFloat = ( value ) => {
-	
+
 	value = Number( value );
 
 	return value + ( value % 1 ? '' : '.0' );
@@ -142,6 +142,18 @@ class NodeBuilder {
 
 	}
 
+	isAvailable( /*name*/ ) {
+
+		return false;
+
+	}
+
+	getInstanceIndex( /*shaderStage*/ ) {
+
+		console.warn( 'Abstract function.' );
+
+	}
+
 	getTexture( /* textureProperty, uvSnippet */ ) {
 
 		console.warn( 'Abstract function.' );
@@ -193,6 +205,10 @@ class NodeBuilder {
 
 			return `${ this.getType( type ) }( ${ getConst( value.x ) }, ${ getConst( value.y ) }, ${ getConst( value.z ) }, ${ getConst( value.w ) } )`;
 
+		} else if ( typeLength > 4 ) {
+
+			return `${ this.getType( type ) }()`;
+
 		}
 
 		throw new Error( `NodeBuilder: Type '${type}' not found in generate constant attempt.` );
@@ -257,7 +273,7 @@ class NodeBuilder {
 
 	isReference( type ) {
 
-		return type === 'void' || type === 'property' || type === 'sampler';
+		return type === 'void' || type === 'property' || type === 'sampler' || type === 'texture' || type === 'cubeTexture';
 
 	}
 
@@ -320,6 +336,8 @@ class NodeBuilder {
 		if ( type === 2 ) return 'vec2';
 		if ( type === 3 ) return 'vec3';
 		if ( type === 4 ) return 'vec4';
+		if ( type === 9 ) return 'mat3';
+		if ( type === 16 ) return 'mat4';
 
 		return 0;
 
@@ -332,6 +350,8 @@ class NodeBuilder {
 
 		if ( vecNum !== null ) return Number( vecNum[ 1 ] );
 		if ( vecType === 'float' || vecType === 'bool' || vecType === 'int' || vecType === 'uint' ) return 1;
+		if ( /mat3/.test( type ) === true ) return 9;
+		if ( /mat4/.test( type ) === true ) return 16;
 
 		return 0;
 
@@ -660,18 +680,17 @@ class NodeBuilder {
 		const fromTypeLength = this.getTypeLength( fromType );
 		const toTypeLength = this.getTypeLength( toType );
 
-		if ( fromTypeLength === 0 ) { // fromType is matrix-like
+		if ( fromTypeLength > 4 ) { // fromType is matrix-like
 
-			const vectorType = this.getVectorFromMatrix( fromType );
+			// @TODO: ignore for now
 
-			return this.format( `( ${ snippet } * ${ this.getType( vectorType ) }( 1.0 ) )`, vectorType, toType );
+			return snippet;
 
 		}
 
-		if ( toTypeLength === 0 ) { // toType is matrix-like
+		if ( toTypeLength > 4 ) { // toType is matrix-like
 
-			// ignore for now
-			//return `${ this.getType( toType ) }( ${ snippet } )`;
+			// @TODO: ignore for now
 
 			return snippet;
 

+ 25 - 9
examples/jsm/nodes/materials/NodeMaterial.js

@@ -4,7 +4,7 @@ import ExpressionNode from '../core/ExpressionNode.js';
 import {
 	float, vec3, vec4,
 	assign, label, mul, add, mix, bypass,
-	positionLocal, skinning, modelViewProjection, lightContext, colorSpace,
+	positionLocal, skinning, instance, modelViewProjection, lightContext, colorSpace,
 	materialAlphaTest, materialColor, materialOpacity
 } from '../shadernode/ShaderNodeElements.js';
 
@@ -30,18 +30,35 @@ class NodeMaterial extends ShaderMaterial {
 
 	generateMain( builder ) {
 
-		// VERTEX STAGE
+		const object = builder.object;
+
+		// < VERTEX STAGE >
 
 		let vertex = positionLocal;
 
-		if ( this.positionNode ) vertex = bypass( vertex, assign( positionLocal, this.positionNode ) );
-		if ( builder.object.isSkinnedMesh ) vertex = bypass( vertex, skinning( builder.object ) );
+		if ( this.positionNode !== null ) {
+
+			vertex = bypass( vertex, assign( vertex, this.positionNode ) );
+
+		}
+
+		if ( object.isInstancedMesh === true && builder.isAvailable( 'instance' ) === true ) {
+
+			vertex = bypass( vertex, instance( object ) );
+
+		}
+
+		if ( object.isSkinnedMesh === true ) {
+
+			vertex = bypass( vertex, skinning( object ) );
+
+		}
 
 		builder.context.vertex = vertex;
 
 		builder.addFlow( 'vertex', modelViewProjection() );
 
-		// FRAGMENT STAGE
+		// < FRAGMENT STAGE >
 
 		let colorNode = vec4( this.colorNode || materialColor );
 		let opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;
@@ -63,8 +80,9 @@ class NodeMaterial extends ShaderMaterial {
 			const alphaTestNode = this.alphaTestNode ? float( this.alphaTestNode ) : materialAlphaTest;
 
 			builder.addFlow( 'fragment', label( alphaTestNode, 'AlphaTest' ) );
+
+			// @TODO: remove ExpressionNode here and then possibly remove it completely
 			builder.addFlow( 'fragment', new ExpressionNode( 'if ( DiffuseColor.a <= AlphaTest ) { discard; }' ) );
-																	// TODO: remove ExpressionNode here and then possibly remove it completely
 
 		}
 
@@ -106,11 +124,9 @@ class NodeMaterial extends ShaderMaterial {
 		// This approach is to reuse the native refreshUniforms*
 		// and turn available the use of features like transmission and environment in core
 
-		let value;
-
 		for ( const property in values ) {
 
-			value = values[ property ];
+			const value = values[ property ];
 
 			if ( this[ property ] === undefined ) {
 

+ 3 - 3
examples/jsm/nodes/math/MathNode.js

@@ -75,9 +75,9 @@ class MathNode extends TempNode {
 		const bType = this.bNode ? this.bNode.getNodeType( builder ) : null;
 		const cType = this.cNode ? this.cNode.getNodeType( builder ) : null;
 
-		const aLen = builder.getTypeLength( aType );
-		const bLen = builder.getTypeLength( bType );
-		const cLen = builder.getTypeLength( cType );
+		const aLen = builder.isMatrix( aType ) ? 0 : builder.getTypeLength( aType );
+		const bLen = builder.isMatrix( bType ) ? 0 : builder.getTypeLength( bType );
+		const cLen = builder.isMatrix( cType ) ? 0 : builder.getTypeLength( cType );
 
 		if ( aLen > bLen && aLen > cLen ) {
 

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

@@ -4,6 +4,7 @@ import VarNode from '../core/VarNode.js';
 import AttributeNode from '../core/AttributeNode.js';
 import UniformNode from '../core/UniformNode.js';
 import BypassNode from '../core/BypassNode.js';
+import InstanceIndexNode from '../core/InstanceIndexNode.js';
 
 // accessor nodes
 import BufferNode from '../accessors/BufferNode.js';
@@ -16,6 +17,7 @@ import PositionNode from '../accessors/PositionNode.js';
 import SkinningNode from '../accessors/SkinningNode.js';
 import TextureNode from '../accessors/TextureNode.js';
 import UVNode from '../accessors/UVNode.js';
+import InstanceNode from '../accessors/InstanceNode.js';
 
 // math nodes
 import OperatorNode from '../math/OperatorNode.js';
@@ -137,6 +139,7 @@ export const shiftLeft = nodeProxy( OperatorNode, '<<' );
 export const shiftRight = nodeProxy( OperatorNode, '>>' );
 
 export const element = nodeProxy( ArrayElementNode );
+export const instanceIndex = nodeObject( new InstanceIndexNode() );
 
 export const modelViewProjection = nodeProxy( ModelViewProjectionNode );
 
@@ -170,6 +173,7 @@ export const materialRoughness = nodeObject( new MaterialNode( MaterialNode.ROUG
 export const materialMetalness = nodeObject( new MaterialNode( MaterialNode.METALNESS ) );
 
 export const skinning = nodeProxy( SkinningNode );
+export const instance = nodeProxy( InstanceNode );
 
 export const lightContext = nodeProxy( LightContextNode );
 

+ 3 - 3
examples/jsm/nodes/utils/ArrayElementNode.js

@@ -1,6 +1,6 @@
-import Node from '../core/Node.js';
+import TempNode from '../core/Node.js';
 
-class ArrayElementNode extends Node {
+class ArrayElementNode extends TempNode {
 
 	constructor( node, indexNode ) {
 
@@ -20,7 +20,7 @@ class ArrayElementNode extends Node {
 	generate( builder ) {
 
 		const nodeSnippet = this.node.build( builder );
-		const indexSnippet = this.indexNode.build( builder, 'int' );
+		const indexSnippet = this.indexNode.build( builder, 'uint' );
 
 		return `${nodeSnippet}[ ${indexSnippet} ]`;
 

+ 1 - 2
examples/jsm/nodes/utils/ConvertNode.js

@@ -24,10 +24,9 @@ class ConvertNode extends Node {
 
 		if ( builder.isReference( convertTo ) === false ) {
 
-			const convertToSnippet = builder.getType( convertTo );
 			const nodeSnippet = node.build( builder, convertTo );
 
-			return `${ builder.getVectorType( convertToSnippet ) }( ${ nodeSnippet } )`;
+			return builder.format( nodeSnippet, this.getNodeType( builder ), convertTo );
 
 		} else {
 

+ 2 - 1
examples/jsm/renderers/webgpu/WebGPURenderer.js

@@ -847,7 +847,8 @@ class WebGPURenderer {
 
 		const drawRange = geometry.drawRange;
 		const firstVertex = drawRange.start;
-		const instanceCount = ( geometry.isInstancedBufferGeometry ) ? geometry.instanceCount : 1;
+
+		const instanceCount = geometry.isInstancedBufferGeometry ? geometry.instanceCount : ( object.isInstancedMesh ? object.count : 1 );
 
 		if ( hasIndex === true ) {
 

+ 37 - 14
examples/jsm/renderers/webgpu/nodes/WebGPUNodeBuilder.js

@@ -16,6 +16,10 @@ import CodeNode from 'three-nodes/core/CodeNode.js';
 
 import { NodeMaterial } from 'three-nodes/materials/Materials.js';
 
+const supports = {
+	instance: true
+};
+
 const wgslTypeLib = {
 	float: 'f32',
 	int: 'i32',
@@ -50,7 +54,8 @@ const wgslTypeLib = {
 
 const wgslMethods = {
 	dFdx: 'dpdx',
-	dFdy: 'dpdy'
+	dFdy: 'dpdy',
+	inversesqrt: 'inverseSqrt'
 };
 
 const wgslPolyfill = {
@@ -75,13 +80,6 @@ fn repeatWrapping( uv : vec2<f32>, dimension : vec2<i32> ) -> vec2<i32> {
 
 	return ( ( uvScaled % dimension ) + dimension ) % dimension;
 
-}
-` ),
-	inversesqrt: new CodeNode( `
-fn inversesqrt( x : f32 ) -> f32 {
-
-	return 1.0 / sqrt( x );
-
 }
 ` )
 };
@@ -100,6 +98,8 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 		this.uniformsGroup = {};
 
+		this.builtins = new Set();
+
 	}
 
 	build() {
@@ -173,7 +173,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 			} else if ( type === 'buffer' ) {
 
-				return `NodeBuffer.${name}`;
+				return `NodeBuffer_${node.node.id}.${name}`;
 
 			} else {
 
@@ -240,10 +240,9 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 				}
 
-
 			} else if ( type === 'buffer' ) {
 
-				const buffer = new WebGPUUniformBuffer( 'NodeBuffer', node.value );
+				const buffer = new WebGPUUniformBuffer( 'NodeBuffer_' + node.id, node.value );
 
 				// add first textures in sequence and group for last
 				const lastBinding = bindings[ bindings.length - 1 ];
@@ -315,12 +314,30 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 	}
 
+	getInstanceIndex( shaderStage = this.shaderStage ) {
+
+		this.builtins.add( 'instance_index' );
+
+		if ( shaderStage === 'vertex' ) {
+
+			return 'instanceIndex';
+
+		}
+
+	}
+
 	getAttributes( shaderStage ) {
 
 		const snippets = [];
 
 		if ( shaderStage === 'vertex' ) {
 
+			if ( this.builtins.has( 'instance_index' ) ) {
+
+				snippets.push( `@builtin( instance_index ) instanceIndex : u32` );
+
+			}
+
 			const attributes = this.attributes;
 			const length = attributes.length;
 
@@ -394,7 +411,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 		const code = snippets.join( ',\n\t' );
 
-		return shaderStage === 'vertex' ? this._getWGSLStruct( 'NodeVarysStruct', code ) : code;
+		return shaderStage === 'vertex' ? this._getWGSLStruct( 'NodeVarysStruct', '\t' + code ) : code;
 
 	}
 
@@ -438,7 +455,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 				const bufferSnippet = `\t${uniform.name} : array< ${bufferType}, ${bufferCount} >\n`;
 
-				bufferSnippets.push( this._getWGSLUniforms( 'NodeBuffer', bufferSnippet, index ++ ) );
+				bufferSnippets.push( this._getWGSLUniforms( 'NodeBuffer_' + bufferNode.id, bufferSnippet, index ++ ) );
 
 			} else {
 
@@ -461,7 +478,7 @@ class WebGPUNodeBuilder extends NodeBuilder {
 		}
 
 		let code = bindingSnippets.join( '\n' );
-		code += bufferSnippets.join( ',\n' );
+		code += bufferSnippets.join( '\n' );
 
 		if ( groupSnippets.length > 0 ) {
 
@@ -555,6 +572,12 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 	}
 
+	isAvailable( name ) {
+
+		return supports[ name ] === true;
+
+	}
+
 	_include( name ) {
 
 		wgslPolyfill[ name ].build( this );

BIN
examples/screenshots/webgpu_instance_mesh.jpg


BIN
examples/screenshots/webgpu_materials.jpg


BIN
examples/screenshots/webgpu_rtt.jpg


BIN
examples/screenshots/webgpu_skinning_instancing.jpg


+ 172 - 0
examples/webgpu_instance_mesh.html

@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Instance Mesh</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">
+		<!-- WebGPU (For Chrome 94-101), expires 09/01/2022 -->
+		<meta http-equiv="origin-trial" content="AoS1pSJwCV3KRe73TO0YgJkK9FZ/qhmvKeafztp0ofiE8uoGrnKzfxGVKKICvoBfL8dgE0zpkp2g/oEJNS0fDgkAAABeeyJvcmlnaW4iOiJodHRwczovL3RocmVlanMub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJHUFUiLCJleHBpcnkiOjE2NTI4MzE5OTksImlzU3ViZG9tYWluIjp0cnVlfQ==">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - WebGPU - Instance Mesh
+		</div>
+
+		<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 Stats from './jsm/libs/stats.module.js';
+			import { GUI } from './jsm/libs/lil-gui.module.min.js';
+
+			import { normalWorld } from 'three-nodes/Nodes.js';
+
+			import WebGPU from './jsm/capabilities/WebGPU.js';
+			import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
+
+			let camera, scene, renderer, stats;
+
+			let mesh;
+			const amount = parseInt( window.location.search.slice( 1 ) ) || 10;
+			const count = Math.pow( amount, 3 );
+			const dummy = new THREE.Object3D();
+
+			init().then( animate ).catch( error );
+
+			async function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( amount * 0.9, amount * 0.9, amount * 0.9 );
+				camera.lookAt( 0, 0, 0 );
+
+				scene = new THREE.Scene();
+
+				const material = new THREE.MeshBasicMaterial();
+				material.colorNode = normalWorld;
+
+				const loader = new THREE.BufferGeometryLoader();
+				loader.load( 'models/json/suzanne_buffergeometry.json', function ( geometry ) {
+
+					geometry.computeVertexNormals();
+					geometry.scale( 0.5, 0.5, 0.5 );
+
+					mesh = new THREE.InstancedMesh( geometry, material, count );
+					scene.add( mesh );
+
+					//
+
+					const gui = new GUI();
+					gui.add( mesh, 'count', 0, count );
+
+				} );
+
+				//
+
+				renderer = new WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				//
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				return renderer.init();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				render();
+
+				stats.update();
+
+			}
+
+			function render() {
+
+				if ( mesh ) {
+
+					const time = Date.now() * 0.001;
+
+					mesh.rotation.x = Math.sin( time / 4 );
+					mesh.rotation.y = Math.sin( time / 2 );
+
+					let i = 0;
+					const offset = ( amount - 1 ) / 2;
+
+					for ( let x = 0; x < amount; x ++ ) {
+
+						for ( let y = 0; y < amount; y ++ ) {
+
+							for ( let z = 0; z < amount; z ++ ) {
+
+								dummy.position.set( offset - x, offset - y, offset - z );
+								dummy.rotation.y = ( Math.sin( x / 4 + time ) + Math.sin( y / 4 + time ) + Math.sin( z / 4 + time ) );
+								dummy.rotation.z = dummy.rotation.y * 2;
+
+								dummy.updateMatrix();
+
+								mesh.setMatrixAt( i ++, dummy.matrix );
+
+							}
+
+						}
+
+					}
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+			function error( error ) {
+
+				console.error( error );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 155 - 0
examples/webgpu_skinning_instancing.html

@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Skinning Instancing</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">
+		<!-- WebGPU (For Chrome 94-101), expires 09/01/2022 -->
+		<meta http-equiv="origin-trial" content="AoS1pSJwCV3KRe73TO0YgJkK9FZ/qhmvKeafztp0ofiE8uoGrnKzfxGVKKICvoBfL8dgE0zpkp2g/oEJNS0fDgkAAABeeyJvcmlnaW4iOiJodHRwczovL3RocmVlanMub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJHUFUiLCJleHBpcnkiOjE2NTI4MzE5OTksImlzU3ViZG9tYWluIjp0cnVlfQ==">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - WebGPU - Skinning Instancing
+		</div>
+
+		<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 { FBXLoader } from './jsm/loaders/FBXLoader.js';
+
+			import WebGPU from './jsm/capabilities/WebGPU.js';
+			import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
+
+			let camera, scene, renderer;
+
+			let mixer, clock;
+
+			init().then( animate ).catch( error );
+
+			async function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( 100, 200, 300 );
+
+				scene = new THREE.Scene();
+				camera.lookAt( 0, 100, 0 );
+
+				clock = new THREE.Clock();
+
+				//lights
+
+				const centerLight = new THREE.PointLight( 0xffffff, .8, 7000 );
+				centerLight.position.y = 450;
+				centerLight.position.z = - 200;
+				scene.add( centerLight );
+
+				const cameraLight = new THREE.PointLight( 0x0099ff, .7, 7000 );
+				camera.add( cameraLight );
+				scene.add( camera );
+
+				const loader = new FBXLoader();
+				loader.load( 'models/fbx/Samba Dancing.fbx', ( object ) => {
+
+					mixer = new THREE.AnimationMixer( object );
+
+					const action = mixer.clipAction( object.animations[ 0 ] );
+					action.play();
+
+					const instanceCount = 50;
+					const dummy = new THREE.Object3D();
+
+					object.traverse( ( child ) => {
+
+						if ( child.isMesh ) {
+
+							child.material = new Nodes.MeshStandardNodeMaterial();
+
+							child.isInstancedMesh = true;
+							child.instanceMatrix = new THREE.InstancedBufferAttribute( new Float32Array( instanceCount * 16 ), 16 );
+							child.count = instanceCount;
+
+							for ( let i = 0; i < instanceCount; i ++ ) {
+
+								dummy.position.x = - 200 + ( ( i % 5 ) * 70 );
+								dummy.position.z = Math.floor( i / 5 ) * - 200;
+
+								dummy.updateMatrix();
+
+								dummy.matrix.toArray( child.instanceMatrix.array, i * 16 );
+
+							}
+
+						}
+
+					} );
+
+					scene.add( object );
+
+				} );
+
+				//renderer
+
+				renderer = new WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				return renderer.init();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				const delta = clock.getDelta();
+
+				if ( mixer ) mixer.update( delta );
+
+				renderer.render( scene, camera );
+
+			}
+
+			function error( error ) {
+
+				console.error( error );
+
+			}
+
+		</script>
+	</body>
+</html>