Browse Source

WebGPU: SkinnedMesh support (#22610)

* WebGPURenderer: Add skinning.

* WebGPU: Skinning support and updates

* fix others examples

* disable alert now

* Create webgpu_skinning.jpg

Co-authored-by: Mugen87 <[email protected]>
sunag 4 years ago
parent
commit
e57b777078

+ 2 - 1
examples/files.json

@@ -322,7 +322,8 @@
 		"webgpu_lights_selective",
 		"webgpu_materials",
 		"webgpu_rtt",
-		"webgpu_sandbox"
+		"webgpu_sandbox",
+		"webgpu_skinning"
 	],
 	"webaudio": [
 		"webaudio_orientation",

+ 13 - 1
examples/jsm/renderers/nodes/accessors/NormalNode.js

@@ -1,5 +1,6 @@
 import Node from '../core/Node.js';
 import AttributeNode from '../core/AttributeNode.js';
+import NodeKeywords from '../core/NodeKeywords.js';
 import VaryNode from '../core/VaryNode.js';
 import ModelNode from '../accessors/ModelNode.js';
 import CameraNode from '../accessors/CameraNode.js';
@@ -9,6 +10,7 @@ import { inverseTransformDirection } from '../functions/MathFunctions.js';
 
 class NormalNode extends Node {
 
+	static GEOMETRY = 'geometry';
 	static LOCAL = 'local';
 	static WORLD = 'world';
 	static VIEW = 'view';
@@ -21,16 +23,26 @@ class NormalNode extends Node {
 
 	}
 
+	getHash( /*builder*/ ) {
+
+		return `normal-${this.scope}`;
+
+	}
+
 	generate( builder ) {
 
 		const scope = this.scope;
 
 		let outputNode = null;
 
-		if ( scope === NormalNode.LOCAL ) {
+		if ( scope === NormalNode.GEOMETRY ) {
 
 			outputNode = new AttributeNode( 'normal', 'vec3' );
 
+		} else if ( scope === NormalNode.LOCAL ) {
+
+			outputNode = new VaryNode( new NormalNode( NormalNode.GEOMETRY ) );
+
 		} else if ( scope === NormalNode.VIEW ) {
 
 			const vertexNormalNode = new OperatorNode( '*', new ModelNode( ModelNode.NORMAL_MATRIX ), new NormalNode( NormalNode.LOCAL ) );

+ 15 - 3
examples/jsm/renderers/nodes/accessors/PositionNode.js

@@ -1,5 +1,6 @@
 import Node from '../core/Node.js';
 import AttributeNode from '../core/AttributeNode.js';
+import NodeKeywords from '../core/NodeKeywords.js';
 import VaryNode from '../core/VaryNode.js';
 import ModelNode from '../accessors/ModelNode.js';
 import MathNode from '../math/MathNode.js';
@@ -8,6 +9,7 @@ import { transformDirection } from '../functions/MathFunctions.js';
 
 class PositionNode extends Node {
 
+	static GEOMETRY = 'geometry';
 	static LOCAL = 'local';
 	static WORLD = 'world';
 	static VIEW = 'view';
@@ -21,16 +23,26 @@ class PositionNode extends Node {
 
 	}
 
+	getHash( /*builder*/ ) {
+
+		return `position-${this.scope}`;
+
+	}
+
 	generate( builder ) {
 
 		const scope = this.scope;
 
 		let outputNode = null;
 
-		if ( scope === PositionNode.LOCAL ) {
-			
+		if ( scope === PositionNode.GEOMETRY ) {
+
 			outputNode = new AttributeNode( 'position', 'vec3' );
-			
+
+		} else if ( scope === PositionNode.LOCAL ) {
+
+			outputNode = new VaryNode( new PositionNode( PositionNode.GEOMETRY ) );
+
 		} else if ( scope === PositionNode.WORLD ) {
 
 			const vertexPositionNode = transformDirection.call( { dir: new PositionNode( PositionNode.LOCAL ), matrix: new ModelNode( ModelNode.WORLD_MATRIX ) } );

+ 105 - 0
examples/jsm/renderers/nodes/accessors/SkinningNode.js

@@ -0,0 +1,105 @@
+import Node from '../core/Node.js';
+import AttributeNode from '../core/AttributeNode.js';
+import ConstNode from '../core/ConstNode.js';
+import PositionNode from '../accessors/PositionNode.js';
+import NormalNode from '../accessors/NormalNode.js';
+import FunctionNode from '../core/FunctionNode.js';
+import Matrix4Node from '../inputs/Matrix4Node.js';
+import BufferNode from '../inputs/BufferNode.js';
+
+import { NodeUpdateType } from '../core/constants.js';
+
+const Skinning = new FunctionNode( `
+	void ( inout vec3 position, inout vec3 normal, const in vec4 index, const in vec4 weight, const in mat4 bindMatrix, const in mat4 bindMatrixInverse ) {
+
+		mat4 boneMatX = BoneMatrices[ int( index.x ) ];
+		mat4 boneMatY = BoneMatrices[ int( index.y ) ];
+		mat4 boneMatZ = BoneMatrices[ int( index.z ) ];
+		mat4 boneMatW = BoneMatrices[ int( index.w ) ];
+
+		// POSITION
+
+		vec4 skinVertex = bindMatrix * vec4( position, 1.0 );
+
+		vec4 skinned = vec4( 0.0 );
+		skinned += boneMatX * skinVertex * weight.x;
+		skinned += boneMatY * skinVertex * weight.y;
+		skinned += boneMatZ * skinVertex * weight.z;
+		skinned += boneMatW * skinVertex * weight.w;
+
+		position = ( bindMatrixInverse * skinned ).xyz;
+
+		// NORMAL
+
+		mat4 skinMatrix = mat4( 0.0 );
+		skinMatrix += skinWeight.x * boneMatX;
+		skinMatrix += skinWeight.y * boneMatY;
+		skinMatrix += skinWeight.z * boneMatZ;
+		skinMatrix += skinWeight.w * boneMatW;
+		skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;
+
+		normal = vec4( skinMatrix * vec4( normal, 0.0 ) ).xyz;
+
+	}`
+);
+
+class SkinningNode extends Node {
+
+	constructor( skinnedMesh ) {
+
+		super( 'void' );
+
+		this.skinnedMesh = skinnedMesh;
+
+		this.updateType = NodeUpdateType.Object;
+
+		//
+
+		this.skinIndexNode = new AttributeNode( 'skinIndex', 'uvec4' );
+		this.skinWeightNode = new AttributeNode( 'skinWeight', 'vec4' );
+
+		this.bindMatrixNode = new Matrix4Node( skinnedMesh.bindMatrix );
+		this.bindMatrixInverseNode = new Matrix4Node( skinnedMesh.bindMatrixInverse );
+		this.boneMatricesNode = new BufferNode( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length );
+
+	}
+
+	generate( builder ) {
+
+		const keywords = builder.getContextValue( 'keywords' );
+
+		keywords.addKeyword( 'BoneMatrices', () => {
+
+			return new ConstNode( this.boneMatricesNode.build( builder ), 'mat4', 'BoneMatrices' );
+
+		} );
+
+		// inout nodes
+		const position = new PositionNode( PositionNode.LOCAL );
+		const normal = new NormalNode( NormalNode.LOCAL );
+
+		const index = this.skinIndexNode;
+		const weight = this.skinWeightNode;
+		const bindMatrix = this.bindMatrixNode;
+		const bindMatrixInverse = this.bindMatrixInverseNode;
+
+		return Skinning.call( {
+			position,
+			normal,
+			index,
+			weight,
+			bindMatrix,
+			bindMatrixInverse
+		} ).build( builder );
+
+	}
+
+	update() {
+
+		this.skinnedMesh.skeleton.update();
+
+	}
+
+}
+
+export default SkinningNode;

+ 6 - 0
examples/jsm/renderers/nodes/core/AttributeNode.js

@@ -11,6 +11,12 @@ class AttributeNode extends Node {
 
 	}
 
+	getHash( builder ) {
+
+		return this.getAttributeName( builder );
+
+	}
+
 	setAttributeName( attributeName ) {
 
 		this._attributeName = attributeName;

+ 32 - 0
examples/jsm/renderers/nodes/core/BypassNode.js

@@ -0,0 +1,32 @@
+import Node from './Node.js';
+
+class BypassNode extends Node {
+
+	constructor( returnNode, callNode ) {
+
+		super();
+
+		this.outputNode = returnNode;
+		this.callNode = callNode;
+
+	}
+
+	getNodeType( builder ) {
+
+		return this.outputNode.getNodeType( builder );
+
+	}
+
+	generate( builder, output ) {
+
+		builder.addFlowCode( this.callNode.build( builder, 'void' ) );
+
+		return this.outputNode.build( builder, output );
+
+	}
+
+}
+
+BypassNode.prototype.isBypassNode = true;
+
+export default BypassNode;

+ 31 - 3
examples/jsm/renderers/nodes/core/Node.js

@@ -1,5 +1,7 @@
 import { NodeUpdateType } from './constants.js';
 
+import { MathUtils } from 'three';
+
 class Node {
 
 	constructor( nodeType = null ) {
@@ -8,6 +10,8 @@ class Node {
 
 		this.updateType = NodeUpdateType.None;
 
+		this.uuid = MathUtils.generateUUID();
+
 	}
 
 	get type() {
@@ -16,6 +20,12 @@ class Node {
 
 	}
 
+	getHash( /*builder*/ ) {
+
+		return this.uuid;
+
+	}
+
 	getUpdateType( /*builder*/ ) {
 
 		return this.updateType;
@@ -48,16 +58,28 @@ class Node {
 
 	build( builder, output = null ) {
 
+		const hash = this.getHash( builder );
+		const sharedNode = builder.getNodeFromHash( hash );
+
+		if ( sharedNode !== undefined && this !== sharedNode ) {
+
+			return sharedNode.build( builder, output );
+
+		}
+
 		builder.addNode( this );
+		builder.addStack( this );
 
 		const isGenerateOnce = this.generate.length === 1;
 
+		let snippet = null;
+
 		if ( isGenerateOnce ) {
 
 			const type = this.getNodeType( builder );
 			const nodeData = builder.getDataFromNode( this );
 
-			let snippet = nodeData.snippet;
+			snippet = nodeData.snippet;
 
 			if ( snippet === undefined ) {
 
@@ -67,11 +89,17 @@ class Node {
 
 			}
 
-			return builder.format( snippet, type, output );
+			snippet = builder.format( snippet, type, output );
+
+		} else {
+
+			snippet = this.generate( builder, output );
 
 		}
 
-		return this.generate( builder, output );
+		builder.removeStack( this );
+
+		return snippet;
 
 	}
 

+ 63 - 18
examples/jsm/renderers/nodes/core/NodeBuilder.js

@@ -10,13 +10,15 @@ import { LinearEncoding } from 'three';
 
 class NodeBuilder {
 
-	constructor( material, renderer ) {
+	constructor( object, renderer ) {
 
-		this.material = material;
+		this.object = object;
+		this.material = object.material;
 		this.renderer = renderer;
 
 		this.nodes = [];
 		this.updateNodes = [];
+		this.hashNodes = {};
 
 		this.vertexShader = null;
 		this.fragmentShader = null;
@@ -29,10 +31,11 @@ class NodeBuilder {
 		this.varys = [];
 		this.vars = { vertex: [], fragment: [] };
 		this.flow = { code: '' };
+		this.stack = [];
 
 		this.context = {
 			keywords: new NodeKeywords(),
-			material: material
+			material: object.material
 		};
 
 		this.nodesData = new WeakMap();
@@ -42,6 +45,30 @@ class NodeBuilder {
 
 	}
 
+	addStack( node ) {
+/*
+		if ( this.stack.indexOf( node ) !== - 1 ) {
+
+			console.warn( 'Recursive node: ', node );
+
+		}
+*/
+		this.stack.push( node );
+
+	}
+
+	removeStack( node ) {
+
+		const lastStack = this.stack.pop();
+
+		if ( lastStack !== node ) {
+
+			throw new Error( 'NodeBuilder: Invalid node stack!' );
+
+		}
+
+	}
+
 	addNode( node ) {
 
 		if ( this.nodes.indexOf( node ) === - 1 ) {
@@ -56,10 +83,18 @@ class NodeBuilder {
 
 			this.nodes.push( node );
 
+			this.hashNodes[ node.getHash( this ) ] = node;
+
 		}
 
 	}
 
+	getNodeFromHash( hash ) {
+
+		return this.hashNodes[ hash ];
+
+	}
+
 	addSlot( shaderStage, slot ) {
 
 		this.slots[ shaderStage ].push( slot );
@@ -503,15 +538,25 @@ class NodeBuilder {
 
 	build() {
 
-		const shaderStages = [ 'vertex', 'fragment' ];
+		const shaderStages = [ 'fragment', 'vertex' ];
 		const shaderData = {};
 
 		for ( const shaderStage of shaderStages ) {
 
-			this.setShaderStage( shaderStage );
-
 			this.define( shaderStage, 'NODE_CODE', '' );
 
+		}
+
+		if ( this.context.vertex !== undefined && this.context.vertex !== null ) {
+
+			this.flowNodeFromShaderStage( 'vertex', this.context.vertex );
+
+		}
+
+		for ( const shaderStage of shaderStages ) {
+
+			this.setShaderStage( shaderStage );
+
 			const slots = this.slots[ shaderStage ];
 
 			for ( const slot of slots ) {
@@ -582,26 +627,26 @@ class NodeBuilder {
 			case 'float to vec4' : return `vec4( vec3( ${snippet} ), 1.0 )`;
 
 			case 'vec2 to float' : return `${snippet}.x`;
-			case 'vec2 to vec3'  : return `vec3( ${snippet}, 0.0 )`;
-			case 'vec2 to vec4'  : return `vec4( ${snippet}.xy, 0.0, 1.0 )`;
+			case 'vec2 to vec3' : return `vec3( ${snippet}, 0.0 )`;
+			case 'vec2 to vec4' : return `vec4( ${snippet}.xy, 0.0, 1.0 )`;
 
 			case 'vec3 to float' : return `${snippet}.x`;
-			case 'vec3 to vec2'  : return `${snippet}.xy`;
-			case 'vec3 to vec4'  : return `vec4( ${snippet}, 1.0 )`;
+			case 'vec3 to vec2' : return `${snippet}.xy`;
+			case 'vec3 to vec4' : return `vec4( ${snippet}, 1.0 )`;
 
 			case 'vec4 to float' : return `${snippet}.x`;
-			case 'vec4 to vec2'  : return `${snippet}.xy`;
-			case 'vec4 to vec3'  : return `${snippet}.xyz`;
+			case 'vec4 to vec2' : return `${snippet}.xy`;
+			case 'vec4 to vec3' : return `${snippet}.xyz`;
 
 			case 'mat3 to float' : return `( ${snippet} * vec3( 1.0 ) ).x`;
-			case 'mat3 to vec2'  : return `( ${snippet} * vec3( 1.0 ) ).xy`;
-			case 'mat3 to vec3'  : return `( ${snippet} * vec3( 1.0 ) ).xyz`;
-			case 'mat3 to vec4'  : return `vec4( ${snippet} * vec3( 1.0 ), 1.0 )`;
+			case 'mat3 to vec2' : return `( ${snippet} * vec3( 1.0 ) ).xy`;
+			case 'mat3 to vec3' : return `( ${snippet} * vec3( 1.0 ) ).xyz`;
+			case 'mat3 to vec4' : return `vec4( ${snippet} * vec3( 1.0 ), 1.0 )`;
 
 			case 'mat4 to float' : return `( ${snippet} * vec4( 1.0 ) ).x`;
-			case 'mat4 to vec2'  : return `( ${snippet} * vec4( 1.0 ) ).xy`;
-			case 'mat4 to vec3'  : return `( ${snippet} * vec4( 1.0 ) ).xyz`;
-			case 'mat4 to vec4'  : return `( ${snippet} * vec4( 1.0 ) )`;
+			case 'mat4 to vec2' : return `( ${snippet} * vec4( 1.0 ) ).xy`;
+			case 'mat4 to vec3' : return `( ${snippet} * vec4( 1.0 ) ).xyz`;
+			case 'mat4 to vec4' : return `( ${snippet} * vec4( 1.0 ) )`;
 
 		}
 

+ 24 - 2
examples/jsm/renderers/nodes/core/NodeKeywords.js

@@ -71,6 +71,8 @@ class NodeKeywords {
 
 		this.nodes = [];
 
+		this.keywordsCallback = {};
+
 	}
 
 	getNode( name ) {
@@ -79,6 +81,16 @@ class NodeKeywords {
 
 		if ( node === undefined ) {
 
+			if ( this.keywordsCallback[ name ] !== undefined ) {
+				
+				node = this.keywordsCallback[ name ]( name );
+				
+				this.nodes[ name ] = node;
+				
+				return node;
+				
+			}
+
 			switch ( name ) {
 
 				case NodeKeywords.PI:
@@ -113,7 +125,7 @@ class NodeKeywords {
 
 				case NodeKeywords.PositionLocal:
 
-					node = new VarNode( new PositionNode( PositionNode.LOCAL ), name );
+					node = new VarNode( new PositionNode( PositionNode.GEOMETRY ), name );
 
 					break;
 
@@ -137,7 +149,7 @@ class NodeKeywords {
 
 				case NodeKeywords.NormalLocal:
 
-					node = new VarNode( new NormalNode( NormalNode.LOCAL ), name );
+					node = new VarNode( new NormalNode( NormalNode.GEOMETRY ), name );
 
 					break;
 
@@ -194,6 +206,16 @@ class NodeKeywords {
 
 	}
 
+	addKeyword( name, callback ) {
+		
+		this.keywords.push( name );
+		
+		this.keywordsCallback[ name ] = callback;
+		
+		return this;
+		
+	}
+
 	parse( code ) {
 
 		const keywordNames = this.keywords;

+ 10 - 1
examples/jsm/renderers/nodes/core/VaryNode.js

@@ -3,11 +3,12 @@ import { NodeShaderStage } from './constants.js';
 
 class VaryNode extends Node {
 
-	constructor( value ) {
+	constructor( value, name = '' ) {
 
 		super();
 
 		this.value = value;
+		this.name = name;
 
 	}
 
@@ -23,8 +24,16 @@ class VaryNode extends Node {
 
 		const type = this.getNodeType( builder );
 		const value = this.value;
+		const name = this.name;
 
 		const nodeVary = builder.getVaryFromNode( this, type );
+
+		if ( name !== '' ) {
+
+			nodeVary.name = name;
+
+		}
+
 		const propertyName = builder.getPropertyName( nodeVary );
 
 		// force nodeVary.snippet work in vertex stage

+ 22 - 0
examples/jsm/renderers/nodes/inputs/BufferNode.js

@@ -0,0 +1,22 @@
+import InputNode from '../core/InputNode.js';
+import ExpressionNode from '../core/ExpressionNode.js';
+import UVNode from '../accessors/UVNode.js';
+import ColorSpaceNode from '../display/ColorSpaceNode.js';
+
+class BufferNode extends InputNode {
+
+	constructor( value, bufferType, bufferCount = 0 ) {
+
+		super( 'buffer' );
+
+		this.value = value;
+		this.bufferType = bufferType;
+		this.bufferCount = bufferCount;
+
+	}
+
+}
+
+BufferNode.prototype.isBufferNode = true;
+
+export default BufferNode;

+ 4 - 4
examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js

@@ -20,17 +20,17 @@ function getShaderStageProperty( shaderStage ) {
 
 class WebGLNodeBuilder extends NodeBuilder {
 
-	constructor( material, renderer, shader ) {
+	constructor( object, renderer, shader ) {
 
-		super( material, renderer );
+		super( object, renderer );
 
 		this.shader = shader;
 
-		this._parseMaterial();
+		this._parseObject();
 
 	}
 
-	_parseMaterial() {
+	_parseObject() {
 
 		const material = this.material;
 

+ 2 - 2
examples/jsm/renderers/webgl/nodes/WebGLNodes.js

@@ -6,9 +6,9 @@ import { Material } from 'three';
 const builders = new WeakMap();
 export const nodeFrame = new NodeFrame();
 
-Material.prototype.onBuild = function ( parameters, renderer ) {
+Material.prototype.onBuild = function ( object, parameters, renderer ) {
 
-	builders.set( this, new WebGLNodeBuilder( this, renderer, parameters ).build() );
+	builders.set( this, new WebGLNodeBuilder( object, renderer, parameters ).build() );
 
 };
 

+ 0 - 13
examples/jsm/renderers/webgpu/WebGPURenderer.js

@@ -693,19 +693,6 @@ class WebGPURenderer {
 
 			} else if ( object.isMesh || object.isLine || object.isPoints ) {
 
-				if ( object.isSkinnedMesh ) {
-
-					// update skeleton only once in a frame
-
-					if ( object.skeleton.frame !== info.render.frame ) {
-
-						object.skeleton.update();
-						object.skeleton.frame = info.render.frame;
-
-					}
-
-				}
-
 				if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {
 
 					if ( this.sortObjects === true ) {

+ 48 - 11
examples/jsm/renderers/webgpu/nodes/WebGPUNodeBuilder.js

@@ -6,22 +6,26 @@ import {
 import WebGPUNodeSampler from './WebGPUNodeSampler.js';
 import { WebGPUNodeSampledTexture } from './WebGPUNodeSampledTexture.js';
 
+import WebGPUUniformBuffer from '../WebGPUUniformBuffer.js';
 import { getVectorLength, getStrideLength } from '../WebGPUBufferUtils.js';
 
 import NodeSlot from '../../nodes/core/NodeSlot.js';
 import VarNode from '../../nodes/core/VarNode.js';
+import BypassNode from '../../nodes/core/BypassNode.js';
 import NodeBuilder from '../../nodes/core/NodeBuilder.js';
 import MaterialNode from '../../nodes/accessors/MaterialNode.js';
+import PositionNode from '../../nodes/accessors/PositionNode.js';
 import NormalNode from '../../nodes/accessors/NormalNode.js';
 import ModelViewProjectionNode from '../../nodes/accessors/ModelViewProjectionNode.js';
+import SkinningNode from '../../nodes/accessors/SkinningNode.js';
 import LightContextNode from '../../nodes/lights/LightContextNode.js';
 import ShaderLib from './ShaderLib.js';
 
 class WebGPUNodeBuilder extends NodeBuilder {
 
-	constructor( material, renderer, lightNode = null ) {
+	constructor( object, renderer, lightNode = null ) {
 
-		super( material, renderer );
+		super( object, renderer );
 
 		this.lightNode = lightNode;
 
@@ -32,12 +36,13 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 		this.nativeShader = null;
 
-		this._parseMaterial();
+		this._parseObject();
 
 	}
 
-	_parseMaterial() {
+	_parseObject() {
 
+		const object = this.object;
 		const material = this.material;
 
 		// get shader
@@ -60,23 +65,31 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 		if ( material.isMeshStandardMaterial || material.isMeshBasicMaterial || material.isPointsMaterial || material.isLineBasicMaterial ) {
 
-			const mvpNode = new ModelViewProjectionNode();
-
 			let lightNode = material.lightNode;
 
+			let vertex = new PositionNode( PositionNode.GEOMETRY );
+
 			if ( lightNode === null && this.lightNode && this.lightNode.hasLights === true ) {
 
 				lightNode = this.lightNode;
 
 			}
 
-			if ( material.positionNode && material.positionNode.isNode ) {
+			if ( material.positionNode !== undefined ) {
+
+				vertex = material.positionNode;
+
+			}
+
+			if ( object.isSkinnedMesh === true ) {
 
-				mvpNode.position = material.positionNode;
+				vertex = new BypassNode( vertex, new SkinningNode( object ) );
 
 			}
 
-			this.addSlot( 'vertex', new NodeSlot( mvpNode, 'MVP', 'vec4' ) );
+			this.context.vertex = vertex;
+
+			this.addSlot( 'vertex', new NodeSlot( new ModelViewProjectionNode(), 'MVP', 'vec4' ) );
 
 			if ( material.alphaTestNode && material.alphaTestNode.isNode ) {
 
@@ -205,6 +218,10 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 				return name;
 
+			} else if ( type === 'buffer' ) {
+
+				return `${name}.value`;
+
 			} else {
 
 				return `nodeUniforms.${name}`;
@@ -249,6 +266,18 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 				uniformGPU = [ sampler, texture ];
 
+			} else if ( type === 'buffer' ) {
+
+				const buffer = new WebGPUUniformBuffer( 'NodeBuffer', node.value );
+
+				// add first textures in sequence and group for last
+				const lastBinding = bindings[ bindings.length - 1 ];
+				const index = lastBinding && lastBinding.isUniformsGroup ? bindings.length - 1 : bindings.length;
+
+				bindings.splice( index, 0, buffer );
+
+				uniformGPU = buffer;
+
 			} else {
 
 				let uniformsGroup = this.uniformsGroup[ shaderStage ];
@@ -363,6 +392,14 @@ class WebGPUNodeBuilder extends NodeBuilder {
 				snippet += `layout(set = 0, binding = ${index ++}) uniform sampler ${uniform.name}_sampler; `;
 				snippet += `layout(set = 0, binding = ${index ++}) uniform texture2D ${uniform.name}; `;
 
+			} else if ( uniform.type === 'buffer' ) {
+
+				const bufferNode = uniform.node;
+				const bufferType = bufferNode.bufferType;
+				const bufferCount = bufferNode.bufferCount;
+
+				snippet += `layout(set = 0, binding = ${index ++}) uniform NodeBuffer { uniform ${bufferType}[ ${bufferCount} ] value; } ${uniform.name}; `;
+
 			} else {
 
 				const vectorType = this.getVectorType( uniform.type );
@@ -397,11 +434,11 @@ class WebGPUNodeBuilder extends NodeBuilder {
 
 		const keywords = this.getContextValue( 'keywords' );
 
-		for ( const shaderStage of [ 'vertex', 'fragment' ] ) {
+		for ( const shaderStage of [ 'fragment', 'vertex' ] ) {
 
 			this.shaderStage = shaderStage;
 
-			keywords.include( this, this.nativeShader.fragmentShader );
+			keywords.include( this, this.nativeShader[ shaderStage + 'Shader' ] );
 
 		}
 

+ 1 - 1
examples/jsm/renderers/webgpu/nodes/WebGPUNodes.js

@@ -19,7 +19,7 @@ class WebGPUNodes {
 
 		if ( nodeBuilder === undefined ) {
 
-			nodeBuilder = new WebGPUNodeBuilder( object.material, this.renderer, lightNode ).build();
+			nodeBuilder = new WebGPUNodeBuilder( object, this.renderer, lightNode ).build();
 
 			this.builders.set( object, nodeBuilder );
 

BIN
examples/screenshots/webgpu_skinning.jpg


+ 131 - 0
examples/webgpu_skinning.html

@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Skinning</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> - WebGPU - Skinning<br />
+			(Chrome Canary with flag: --enable-unsafe-webgpu)
+		</div>
+
+		<script type="importmap">
+		{
+			"imports": {
+				"three": "../build/three.module.js"
+			}
+		}
+		</script>
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { FBXLoader } from './jsm/loaders/FBXLoader.js';
+
+			import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
+			import WebGPU from './jsm/renderers/webgpu/WebGPU.js';
+
+			import LightsNode from './jsm/renderers/nodes/lights/LightsNode.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 '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 light = new THREE.PointLight( 0xffffff );
+				camera.add( light );
+				scene.add( camera );
+
+				const lightNode = LightsNode.fromLights( [ light ] );
+
+				const loader = new FBXLoader();
+				loader.load( 'models/fbx/Samba Dancing.fbx', function ( object ) {
+
+					mixer = new THREE.AnimationMixer( object );
+
+					const action = mixer.clipAction( object.animations[ 0 ] );
+					action.play();
+
+					object.traverse( function ( child ) {
+
+						if ( child.isMesh ) {
+
+							child.material = new THREE.MeshStandardMaterial();
+							child.material.lightNode = lightNode;
+
+						}
+
+					} );
+
+					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>

+ 1 - 1
src/renderers/WebGLRenderer.js

@@ -1418,7 +1418,7 @@ function WebGLRenderer( parameters = {} ) {
 
 			parameters.uniforms = programCache.getUniforms( material );
 
-			material.onBuild( parameters, _this );
+			material.onBuild( object, parameters, _this );
 
 			material.onBeforeCompile( parameters, _this );