Skip to content

Commit ba6fa5a

Browse files
authoredMar 21, 2025
NodeMaterial: Add support for compute() integrated into the material (#30768)
* add `computeSkinning` and remove `skinningReference` * Node: Show warning for recursive code generate * fix `attributeName` undefined * add compute() support for NodeMaterial * update example using `computeSkinning` * cleanup
1 parent 03cedd8 commit ba6fa5a

File tree

8 files changed

+126
-55
lines changed

8 files changed

+126
-55
lines changed
 
12.2 KB
Loading

‎examples/webgpu_skinning_points.html

+39-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
<body>
1010

1111
<div id="info">
12-
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - skinning points
12+
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - skinning points</br>
13+
colors and scale of the points are based on the speed of the skinning
1314
</div>
1415

1516
<script type="importmap">
@@ -26,12 +27,11 @@
2627
<script type="module">
2728

2829
import * as THREE from 'three';
29-
import { uniform, skinning } from 'three/tsl';
30+
import { color, computeSkinning, objectWorldMatrix, instancedArray, instanceIndex, Fn, shapeCircle } from 'three/tsl';
3031

3132
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3233

3334
let camera, scene, renderer;
34-
3535
let mixer, clock;
3636

3737
init();
@@ -42,8 +42,11 @@
4242
camera.position.set( 0, 300, - 85 );
4343

4444
scene = new THREE.Scene();
45+
scene.background = new THREE.Color( 0x111111 );
4546
camera.lookAt( 0, 0, - 85 );
4647

48+
scene.add( new THREE.AmbientLight( 0xffffff, 10 ) );
49+
4750
clock = new THREE.Clock();
4851

4952
const loader = new GLTFLoader();
@@ -61,17 +64,47 @@
6164

6265
child.visible = false;
6366

67+
const countOfPoints = child.geometry.getAttribute( 'position' ).count;
68+
69+
const pointPositionArray = instancedArray( countOfPoints, 'vec3' ).setPBO( true );
70+
const pointSpeedArray = instancedArray( countOfPoints, 'vec3' ).setPBO( true );
71+
72+
const pointSpeedAttribute = pointSpeedArray.toAttribute();
73+
const skinningPosition = computeSkinning( child );
74+
6475
const materialPoints = new THREE.PointsNodeMaterial();
65-
materialPoints.colorNode = uniform( new THREE.Color() );
66-
materialPoints.positionNode = skinning( child );
76+
materialPoints.colorNode = pointSpeedAttribute.mul( .6 ).mix( color( 0x0066ff ), color( 0xff9000 ) );
77+
materialPoints.opacityNode = shapeCircle();
78+
materialPoints.sizeNode = pointSpeedAttribute.length().exp().min( 5 ).mul( 5 ).add( 1 );
79+
materialPoints.sizeAttenuation = false;
80+
81+
materialPoints.positionNode = Fn( () => {
82+
83+
const pointPosition = pointPositionArray.element( instanceIndex );
84+
const pointSpeed = pointSpeedArray.element( instanceIndex );
6785

68-
const pointCloud = new THREE.Points( child.geometry, materialPoints );
86+
const skinningWorldPosition = objectWorldMatrix( child ).mul( skinningPosition );
87+
88+
const skinningSpeed = skinningWorldPosition.sub( pointPosition );
89+
90+
pointSpeed.assign( skinningSpeed );
91+
pointPosition.assign( skinningWorldPosition );
92+
93+
return pointPositionArray.toAttribute();
94+
95+
} )().compute( countOfPoints );
96+
97+
const pointCloud = new THREE.Sprite( materialPoints );
98+
pointCloud.count = countOfPoints;
6999
scene.add( pointCloud );
70100

71101
}
72102

73103
} );
74104

105+
object.scale.set( 100, 100, 100 );
106+
object.rotation.x = - Math.PI / 2;
107+
75108
scene.add( object );
76109

77110
} );

‎src/Three.TSL.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export const color = TSL.color;
116116
export const colorSpaceToWorking = TSL.colorSpaceToWorking;
117117
export const colorToDirection = TSL.colorToDirection;
118118
export const compute = TSL.compute;
119+
export const computeSkinning = TSL.computeSkinning;
119120
export const cond = TSL.cond;
120121
export const Const = TSL.Const;
121122
export const context = TSL.context;
@@ -433,7 +434,6 @@ export const sign = TSL.sign;
433434
export const sin = TSL.sin;
434435
export const sinc = TSL.sinc;
435436
export const skinning = TSL.skinning;
436-
export const skinningReference = TSL.skinningReference;
437437
export const smoothstep = TSL.smoothstep;
438438
export const smoothstepElement = TSL.smoothstepElement;
439439
export const specularColor = TSL.specularColor;

‎src/materials/nodes/NodeMaterial.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { instancedMesh } from '../../nodes/accessors/InstancedMeshNode.js';
1111
import { batch } from '../../nodes/accessors/BatchNode.js';
1212
import { materialReference } from '../../nodes/accessors/MaterialReferenceNode.js';
1313
import { positionLocal, positionView } from '../../nodes/accessors/Position.js';
14-
import { skinningReference } from '../../nodes/accessors/SkinningNode.js';
14+
import { skinning } from '../../nodes/accessors/SkinningNode.js';
1515
import { morphReference } from '../../nodes/accessors/MorphNode.js';
1616
import { mix } from '../../nodes/math/MathNode.js';
1717
import { float, vec3, vec4 } from '../../nodes/tsl/TSLBase.js';
@@ -699,7 +699,7 @@ class NodeMaterial extends Material {
699699

700700
if ( object.isSkinnedMesh === true ) {
701701

702-
skinningReference( object ).append();
702+
skinning( object ).append();
703703

704704
}
705705

‎src/nodes/accessors/SkinningNode.js

+46-37
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { tangentLocal } from './Tangent.js';
1010
import { uniform } from '../core/UniformNode.js';
1111
import { buffer } from './BufferNode.js';
1212
import { getDataFromObject } from '../core/NodeUtils.js';
13+
import { storage } from './StorageBufferNode.js';
14+
import { InstancedBufferAttribute } from '../../core/InstancedBufferAttribute.js';
15+
import { instanceIndex } from '../core/IndexNode.js';
1316

1417
const _frameId = new WeakMap();
1518

@@ -31,9 +34,8 @@ class SkinningNode extends Node {
3134
* Constructs a new skinning node.
3235
*
3336
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
34-
* @param {boolean} [useReference=false] - Whether to use reference nodes for internal skinned mesh related data or not.
3537
*/
36-
constructor( skinnedMesh, useReference = false ) {
38+
constructor( skinnedMesh ) {
3739

3840
super( 'void' );
3941

@@ -44,14 +46,6 @@ class SkinningNode extends Node {
4446
*/
4547
this.skinnedMesh = skinnedMesh;
4648

47-
/**
48-
* Whether to use reference nodes for internal skinned mesh related data or not.
49-
* TODO: Explain the purpose of the property.
50-
*
51-
* @type {boolean}
52-
*/
53-
this.useReference = useReference;
54-
5549
/**
5650
* The update type overwritten since skinning nodes are updated per object.
5751
*
@@ -75,42 +69,40 @@ class SkinningNode extends Node {
7569
*/
7670
this.skinWeightNode = attribute( 'skinWeight', 'vec4' );
7771

78-
let bindMatrixNode, bindMatrixInverseNode, boneMatricesNode;
79-
80-
if ( useReference ) {
81-
82-
bindMatrixNode = reference( 'bindMatrix', 'mat4' );
83-
bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' );
84-
boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length );
85-
86-
} else {
87-
88-
bindMatrixNode = uniform( skinnedMesh.bindMatrix, 'mat4' );
89-
bindMatrixInverseNode = uniform( skinnedMesh.bindMatrixInverse, 'mat4' );
90-
boneMatricesNode = buffer( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length );
91-
92-
}
93-
9472
/**
9573
* The bind matrix node.
9674
*
9775
* @type {Node<mat4>}
9876
*/
99-
this.bindMatrixNode = bindMatrixNode;
77+
this.bindMatrixNode = reference( 'bindMatrix', 'mat4' );
10078

10179
/**
10280
* The bind matrix inverse node.
10381
*
10482
* @type {Node<mat4>}
10583
*/
106-
this.bindMatrixInverseNode = bindMatrixInverseNode;
84+
this.bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' );
10785

10886
/**
10987
* The bind matrices as a uniform buffer node.
11088
*
11189
* @type {Node}
11290
*/
113-
this.boneMatricesNode = boneMatricesNode;
91+
this.boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length );
92+
93+
/**
94+
* The current vertex position in local space.
95+
*
96+
* @type {Node<vec3>}
97+
*/
98+
this.positionNode = positionLocal;
99+
100+
/**
101+
* The result of vertex position in local space.
102+
*
103+
* @type {Node<vec3>}
104+
*/
105+
this.toPositionNode = positionLocal;
114106

115107
/**
116108
* The previous bind matrices as a uniform buffer node.
@@ -127,10 +119,10 @@ class SkinningNode extends Node {
127119
* Transforms the given vertex position via skinning.
128120
*
129121
* @param {Node} [boneMatrices=this.boneMatricesNode] - The bone matrices
130-
* @param {Node<vec3>} [position=positionLocal] - The vertex position in local space.
122+
* @param {Node<vec3>} [position=this.positionNode] - The vertex position in local space.
131123
* @return {Node<vec3>} The transformed vertex position.
132124
*/
133-
getSkinnedPosition( boneMatrices = this.boneMatricesNode, position = positionLocal ) {
125+
getSkinnedPosition( boneMatrices = this.boneMatricesNode, position = this.positionNode ) {
134126

135127
const { skinIndexNode, skinWeightNode, bindMatrixNode, bindMatrixInverseNode } = this;
136128

@@ -225,6 +217,7 @@ class SkinningNode extends Node {
225217
* Setups the skinning node by assigning the transformed vertex data to predefined node variables.
226218
*
227219
* @param {NodeBuilder} builder - The current node builder.
220+
* @return {Node<vec3>} The transformed vertex position.
228221
*/
229222
setup( builder ) {
230223

@@ -236,8 +229,9 @@ class SkinningNode extends Node {
236229

237230
const skinPosition = this.getSkinnedPosition();
238231

232+
if ( this.toPositionNode ) this.toPositionNode.assign( skinPosition );
239233

240-
positionLocal.assign( skinPosition );
234+
//
241235

242236
if ( builder.hasGeometryAttribute( 'normal' ) ) {
243237

@@ -253,6 +247,8 @@ class SkinningNode extends Node {
253247

254248
}
255249

250+
return skinPosition;
251+
256252
}
257253

258254
/**
@@ -266,7 +262,7 @@ class SkinningNode extends Node {
266262

267263
if ( output !== 'void' ) {
268264

269-
return positionLocal.build( builder, output );
265+
return super.generate( builder, output );
270266

271267
}
272268

@@ -279,8 +275,7 @@ class SkinningNode extends Node {
279275
*/
280276
update( frame ) {
281277

282-
const object = this.useReference ? frame.object : this.skinnedMesh;
283-
const skeleton = object.skeleton;
278+
const skeleton = frame.object && frame.object.skeleton ? frame.object.skeleton : this.skinnedMesh.skeleton;
284279

285280
if ( _frameId.get( skeleton ) === frame.frameId ) return;
286281

@@ -307,11 +302,25 @@ export default SkinningNode;
307302
export const skinning = ( skinnedMesh ) => nodeObject( new SkinningNode( skinnedMesh ) );
308303

309304
/**
310-
* TSL function for creating a skinning node with reference usage.
305+
* TSL function for computing skinning.
311306
*
312307
* @tsl
313308
* @function
314309
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
310+
* @param {Node<vec3>} [toPosition=null] - The target position.
315311
* @returns {SkinningNode}
316312
*/
317-
export const skinningReference = ( skinnedMesh ) => nodeObject( new SkinningNode( skinnedMesh, true ) );
313+
export const computeSkinning = ( skinnedMesh, toPosition = null ) => {
314+
315+
const node = new SkinningNode( skinnedMesh );
316+
node.positionNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'position' ).array, 3 ), 'vec3' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
317+
node.skinIndexNode = storage( new InstancedBufferAttribute( new Uint32Array( skinnedMesh.geometry.getAttribute( 'skinIndex' ).array ), 4 ), 'uvec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
318+
node.skinWeightNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'skinWeight' ).array, 4 ), 'vec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
319+
node.bindMatrixNode = uniform( skinnedMesh.bindMatrix, 'mat4' );
320+
node.bindMatrixInverseNode = uniform( skinnedMesh.bindMatrixInverse, 'mat4' );
321+
node.boneMatricesNode = buffer( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length );
322+
node.toPositionNode = toPosition;
323+
324+
return nodeObject( node );
325+
326+
};

‎src/nodes/core/Node.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -674,9 +674,21 @@ class Node extends EventDispatcher {
674674

675675
if ( result === undefined ) {
676676

677-
result = this.generate( builder ) || '';
677+
if ( nodeData.generated === undefined ) {
678678

679-
nodeData.snippet = result;
679+
nodeData.generated = true;
680+
681+
result = this.generate( builder ) || '';
682+
683+
nodeData.snippet = result;
684+
685+
} else {
686+
687+
console.warn( 'THREE.Node: Recursion detected.', this );
688+
689+
result = '';
690+
691+
}
680692

681693
} else if ( nodeData.flowCodes !== undefined && builder.context.nodeBlock !== undefined ) {
682694

‎src/nodes/gpgpu/ComputeNode.js

+22-4
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,35 @@ class ComputeNode extends Node {
163163

164164
}
165165

166-
generate( builder ) {
166+
setup( builder ) {
167+
168+
const result = this.computeNode.setup( builder );
169+
170+
const properties = builder.getNodeProperties( this );
171+
properties.outputComputeNode = result.outputNode;
172+
173+
result.outputNode = null;
174+
175+
return result;
176+
177+
}
178+
179+
generate( builder, output ) {
167180

168181
const { shaderStage } = builder;
169182

170183
if ( shaderStage === 'compute' ) {
171184

172-
const snippet = this.computeNode.build( builder, 'void' );
185+
this.computeNode.build( builder, 'void' );
186+
187+
} else {
188+
189+
const properties = builder.getNodeProperties( this );
190+
const outputComputeNode = properties.outputComputeNode;
173191

174-
if ( snippet !== '' ) {
192+
if ( outputComputeNode ) {
175193

176-
builder.addLineFlowCode( snippet, this );
194+
return outputComputeNode.build( builder, output );
177195

178196
}
179197

‎src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ ${ flowData.code }
775775

776776
const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
777777

778-
snippet += `${flat} out ${type} ${varying.name};\n`;
778+
snippet += `${flat}out ${type} ${varying.name};\n`;
779779

780780
} else {
781781

@@ -1073,10 +1073,9 @@ ${ flowData.code }
10731073
for ( let i = 0; i < transforms.length; i ++ ) {
10741074

10751075
const transform = transforms[ i ];
1076-
10771076
const attributeName = this.getPropertyName( transform.attributeNode );
10781077

1079-
snippet += `${ transform.varyingName } = ${ attributeName };\n\t`;
1078+
if ( attributeName ) snippet += `${ transform.varyingName } = ${ attributeName };\n\t`;
10801079

10811080
}
10821081

0 commit comments

Comments
 (0)
Failed to load comments.