Skip to content

Commit 2dc9a7e

Browse files
sunagSpiri0Attila Schroeder
authored
TSL: Introduce struct (#30394)
* introduce struct * add alternative style * added member support * uniform buffer name revision * add initial struct array support * update to use struct * update build patch * add `webgpu_struct_drawindirect` example * add tags * cleanup * cleanup * Update puppeteer.js * Update webgpu_compute_water.html --------- Co-authored-by: 69024222 <Spiri0@users.noreply.github.com> Co-authored-by: Attila Schroeder <attila-schroeder.79@gmail.com>
1 parent 2353764 commit 2dc9a7e

23 files changed

+795
-128
lines changed

examples/files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@
424424
"webgpu_sky",
425425
"webgpu_sprites",
426426
"webgpu_storage_buffer",
427+
"webgpu_struct_drawindirect",
427428
"webgpu_texturegrad",
428429
"webgpu_textures_2d-array",
429430
"webgpu_textures_2d-array_compressed",
Loading

examples/tags.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"webgpu_compute_sort_bitonic": [ "gpgpu" ],
124124
"webgpu_compute_texture": [ "gpgpu" ],
125125
"webgpu_compute_texture_pingpong": [ "gpgpu" ],
126+
"webgpu_compute_water": [ "gpgpu", "struct" ],
126127
"webgpu_depth_texture": [ "renderTarget" ],
127128
"webgpu_loader_gltf_dispersion": [ "transmission" ],
128129
"webgpu_materials_lightmap": [ "shadow" ],

examples/webgpu_compute_water.html

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
import * as THREE from 'three';
3030

31-
import { color, instanceIndex, If, varyingProperty, uint, int, negate, floor, float, length, clamp, vec2, cos, vec3, vertexIndex, Fn, uniform, instancedArray, min, max, positionLocal, transformNormalToView } from 'three/tsl';
31+
import { color, instanceIndex, struct, If, varyingProperty, uint, int, negate, floor, float, length, clamp, vec2, cos, vec3, vertexIndex, Fn, uniform, instancedArray, min, max, positionLocal, transformNormalToView } from 'three/tsl';
3232
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
3333
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
3434
import Stats from 'three/addons/libs/stats.module.js';
@@ -279,30 +279,35 @@
279279
const sphereMaterial = new THREE.MeshPhongMaterial( { color: 0xFFFF00 } );
280280

281281
// Initialize sphere mesh instance position and velocity.
282-
const spherePositionArray = new Float32Array( NUM_SPHERES * 3 );
283-
282+
// position<vec3> + velocity<vec2> + unused<vec3> = 8 floats per sphere.
283+
// for structs arrays must be enclosed in multiple of 4
284+
285+
const sphereStride = 8;
286+
const sphereArray = new Float32Array( NUM_SPHERES * sphereStride );
287+
284288
// Only hold velocity in x and z directions.
285289
// The sphere is wedded to the surface of the water, and will only move vertically with the water.
286-
const sphereVelocityArray = new Float32Array( NUM_SPHERES * 2 );
287290

288291
for ( let i = 0; i < NUM_SPHERES; i ++ ) {
289292

290-
spherePositionArray[ i * 3 + 0 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
291-
spherePositionArray[ i * 3 + 1 ] = 0;
292-
spherePositionArray[ i * 3 + 2 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
293+
sphereArray[ i * sphereStride + 0 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
294+
sphereArray[ i * sphereStride + 1 ] = 0;
295+
sphereArray[ i * sphereStride + 2 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
293296

294297
}
295298

296-
sphereVelocityArray.fill( 0.0 );
299+
const SphereStruct = struct( {
300+
position: 'vec3',
301+
velocity: 'vec2'
302+
} );
297303

298304
// Sphere Instance Storage
299-
const sphereInstancePositionStorage = instancedArray( spherePositionArray, 'vec3' ).label( 'SpherePosition' );
300-
const sphereVelocityStorage = instancedArray( sphereVelocityArray, 'vec2' ).label( 'SphereVelocity' );
305+
const sphereVelocityStorage = instancedArray( sphereArray, SphereStruct ).label( 'SphereData' );
301306

302307
computeSphere = Fn( () => {
303308

304-
const instancePosition = sphereInstancePositionStorage.element( instanceIndex );
305-
const velocity = sphereVelocityStorage.element( instanceIndex );
309+
const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );
310+
const velocity = sphereVelocityStorage.element( instanceIndex ).get( 'velocity' );
306311

307312
// Bring position from range of [ -BOUNDS/2, BOUNDS/2 ] to [ 0, BOUNDS ]
308313
const tempX = instancePosition.x.add( BOUNDS_HALF );
@@ -372,7 +377,7 @@
372377

373378
sphereMaterial.positionNode = Fn( () => {
374379

375-
const instancePosition = sphereInstancePositionStorage.element( instanceIndex );
380+
const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );
376381

377382
const newPosition = positionLocal.add( instancePosition );
378383

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgpu - struct drawIndirect</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link type="text/css" rel="stylesheet" href="main.css">
8+
</head>
9+
<body>
10+
11+
<div id="info">
12+
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - struct drawIndirect<br />
13+
</div>
14+
15+
<script type="importmap">
16+
{
17+
"imports": {
18+
"three": "../src/three.webgpu.js",
19+
"three/webgpu": "../src/three.webgpu.js",
20+
"three/tsl": "../src/three.tsl.js",
21+
"three/addons/": "./jsm/"
22+
}
23+
}
24+
</script>
25+
26+
<script type="module">
27+
28+
import * as THREE from 'three';
29+
import { struct, storage, wgslFn, instanceIndex, time, varyingProperty, attribute } from 'three/tsl';
30+
31+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
32+
33+
const renderer = new THREE.WebGPURenderer( { antialias: true } );
34+
renderer.outputColorSpace = THREE.SRGBColorSpace;
35+
renderer.setPixelRatio( window.devicePixelRatio );
36+
renderer.setSize( window.innerWidth, window.innerHeight );
37+
renderer.setClearColor( 0x000000 );
38+
renderer.setClearAlpha( 0 );
39+
document.body.appendChild( renderer.domElement );
40+
41+
const aspect = window.innerWidth / window.innerHeight;
42+
43+
const camera = new THREE.PerspectiveCamera( 50.0, aspect, 0.1, 10000 );
44+
const scene = new THREE.Scene();
45+
46+
scene.background = new THREE.Color( 0x00001f );
47+
camera.position.set( 1, 1, 1 );
48+
const controls = new OrbitControls( camera, renderer.domElement );
49+
50+
let computeDrawBuffer, computeInitDrawBuffer;
51+
52+
init();
53+
54+
async function init() {
55+
56+
await renderer.init();
57+
58+
// geometry
59+
60+
const vector = new THREE.Vector4();
61+
62+
const instances = 100000;
63+
64+
const positions = [];
65+
const offsets = [];
66+
const colors = [];
67+
const orientationsStart = [];
68+
const orientationsEnd = [];
69+
70+
positions.push( 0.025, - 0.025, 0 );
71+
positions.push( - 0.025, 0.025, 0 );
72+
positions.push( 0, 0, 0.025 );
73+
74+
// instanced attributes
75+
76+
for ( let i = 0; i < instances; i ++ ) {
77+
78+
// offsets
79+
80+
offsets.push( Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 );
81+
82+
// colors
83+
84+
colors.push( Math.random(), Math.random(), Math.random(), Math.random() );
85+
86+
// orientation start
87+
88+
vector.set( Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1 );
89+
vector.normalize();
90+
91+
orientationsStart.push( vector.x, vector.y, vector.z, vector.w );
92+
93+
// orientation end
94+
95+
vector.set( Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1 );
96+
vector.normalize();
97+
98+
orientationsEnd.push( vector.x, vector.y, vector.z, vector.w );
99+
100+
}
101+
102+
const geometry = new THREE.InstancedBufferGeometry();
103+
geometry.instanceCount = instances;
104+
105+
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
106+
geometry.setAttribute( 'offset', new THREE.InstancedBufferAttribute( new Float32Array( offsets ), 3 ) );
107+
geometry.setAttribute( 'color', new THREE.InstancedBufferAttribute( new Float32Array( colors ), 4 ) );
108+
geometry.setAttribute( 'orientationStart', new THREE.InstancedBufferAttribute( new Float32Array( orientationsStart ), 4 ) );
109+
geometry.setAttribute( 'orientationEnd', new THREE.InstancedBufferAttribute( new Float32Array( orientationsEnd ), 4 ) );
110+
111+
const drawBuffer = new THREE.IndirectStorageBufferAttribute( new Uint32Array( 5 ), 5 );
112+
geometry.setIndirect( drawBuffer );
113+
114+
const drawBufferStruct = struct( {
115+
vertexCount: 'uint',
116+
instanceCount: { type: 'uint', atomic: true },
117+
firstVertex: 'uint',
118+
firstInstance: 'uint',
119+
offset: 'uint'
120+
}, 'DrawBuffer' );
121+
122+
const writeDrawBuffer = wgslFn( `
123+
fn compute(
124+
index: u32,
125+
drawBuffer: ptr<storage, DrawBuffer, read_write>,
126+
instances: f32,
127+
time: f32,
128+
) -> void {
129+
130+
let instanceCount = max( instances * pow( sin( time * 0.5 ) + 1, 4.0 ), 100 );
131+
132+
atomicStore( &drawBuffer.instanceCount, u32( instanceCount ) );
133+
}
134+
` );
135+
136+
computeDrawBuffer = writeDrawBuffer( {
137+
drawBuffer: storage( drawBuffer, drawBufferStruct, drawBuffer.count ),
138+
instances: instances,
139+
index: instanceIndex,
140+
time: time
141+
} ).compute( instances ); // not neccessary in this case but normally one wants to run through all instances
142+
143+
const initDrawBuffer = wgslFn( `
144+
fn compute(
145+
drawBuffer: ptr< storage, DrawBuffer, read_write >,
146+
) -> void {
147+
148+
drawBuffer.vertexCount = 3u;
149+
atomicStore(&drawBuffer.instanceCount, 0u);
150+
drawBuffer.firstVertex = 0u;
151+
drawBuffer.firstInstance = 0u;
152+
drawBuffer.offset = 0u;
153+
}
154+
` );
155+
156+
computeInitDrawBuffer = initDrawBuffer( {
157+
drawBuffer: storage( drawBuffer, drawBufferStruct, drawBuffer.count ),
158+
} ).compute( 1 );
159+
160+
const vPosition = varyingProperty( 'vec3', 'vPosition' );
161+
const vColor = varyingProperty( 'vec4', 'vColor' );
162+
163+
const positionShaderParams = {
164+
position: attribute( 'position' ),
165+
offset: attribute( 'offset' ),
166+
color: attribute( 'color' ),
167+
orientationStart: attribute( 'orientationStart' ),
168+
orientationEnd: attribute( 'orientationEnd' ),
169+
time: time
170+
};
171+
172+
const positionShader = wgslFn( `
173+
fn main_vertex(
174+
position: vec3<f32>,
175+
offset: vec3<f32>,
176+
color: vec4<f32>,
177+
orientationStart: vec4<f32>,
178+
orientationEnd: vec4<f32>,
179+
time: f32
180+
) -> vec4<f32> {
181+
182+
var vPosition = offset * max( abs( sin( time * 0.5 ) * 2.0 + 1.0 ), 0.5 ) + position;
183+
var orientation = normalize( mix( orientationStart, orientationEnd, sin( time * 0.5 ) ) );
184+
var vcV = cross( orientation.xyz, vPosition );
185+
vPosition = vcV * ( 2.0 * orientation.w ) + ( cross( orientation.xyz, vcV ) * 2.0 + vPosition );
186+
187+
var vColor = color;
188+
189+
var outPosition = vec4f(vPosition, 1);
190+
191+
varyings.vPosition = vPosition;
192+
varyings.vColor = vColor;
193+
194+
return outPosition;
195+
}
196+
`, [ vPosition, vColor ] );
197+
198+
const fragmentShaderParams = {
199+
time: time,
200+
vPosition: vPosition,
201+
vColor: vColor
202+
};
203+
204+
const fragmentShader = wgslFn( `
205+
fn main_fragment(
206+
time: f32,
207+
vPosition: vec3<f32>,
208+
vColor: vec4<f32>
209+
) -> vec4<f32> {
210+
211+
var color = vec4f( vColor );
212+
color.r += sin( vPosition.x * 10.0 + time ) * 0.5;
213+
214+
return color;
215+
}
216+
` );
217+
218+
const material = new THREE.MeshBasicNodeMaterial( {
219+
side: THREE.DoubleSide,
220+
forceSinglePass: true,
221+
transparent: true
222+
} );
223+
224+
material.positionNode = positionShader( positionShaderParams );
225+
material.fragmentNode = fragmentShader( fragmentShaderParams );
226+
227+
const mesh = new THREE.Mesh( geometry, material );
228+
scene.add( mesh );
229+
230+
renderer.setAnimationLoop( render );
231+
232+
window.addEventListener( 'resize', onWindowResize, false );
233+
234+
}
235+
236+
function render() {
237+
238+
controls.update();
239+
240+
renderer.render( scene, camera );
241+
242+
renderer.compute( computeInitDrawBuffer );
243+
renderer.compute( computeDrawBuffer );
244+
245+
}
246+
247+
function onWindowResize() {
248+
249+
camera.aspect = window.innerWidth / window.innerHeight;
250+
camera.updateProjectionMatrix();
251+
renderer.setSize( window.innerWidth, window.innerHeight );
252+
253+
}
254+
255+
</script>
256+
257+
</body>
258+
</html>

src/Three.TSL.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ export const storageBarrier = TSL.storageBarrier;
444444
export const storageObject = TSL.storageObject;
445445
export const storageTexture = TSL.storageTexture;
446446
export const string = TSL.string;
447+
export const struct = TSL.struct;
447448
export const sub = TSL.sub;
448449
export const subgroupIndex = TSL.subgroupIndex;
449450
export const subgroupSize = TSL.subgroupSize;

src/nodes/Nodes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export { default as TempNode } from './core/TempNode.js';
2929
export { default as UniformGroupNode } from './core/UniformGroupNode.js';
3030
export { default as UniformNode } from './core/UniformNode.js';
3131
export { default as VaryingNode } from './core/VaryingNode.js';
32+
export { default as StructNode } from './core/StructNode.js';
33+
export { default as StructTypeNode } from './core/StructTypeNode.js';
3234
export { default as OutputStructNode } from './core/OutputStructNode.js';
3335
export { default as MRTNode } from './core/MRTNode.js';
3436

@@ -53,6 +55,7 @@ export { default as StorageArrayElementNode } from './utils/StorageArrayElementN
5355
export { default as TriplanarTexturesNode } from './utils/TriplanarTexturesNode.js';
5456
export { default as ReflectorNode } from './utils/ReflectorNode.js';
5557
export { default as RTTNode } from './utils/RTTNode.js';
58+
export { default as MemberNode } from './utils/MemberNode.js';
5659

5760
// accessors
5861
export { default as UniformArrayNode } from './accessors/UniformArrayNode.js';

src/nodes/TSL.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './core/IndexNode.js';
1111
export * from './core/ParameterNode.js';
1212
export * from './core/PropertyNode.js';
1313
export * from './core/StackNode.js';
14+
export * from './core/StructNode.js';
1415
export * from './core/UniformGroupNode.js';
1516
export * from './core/UniformNode.js';
1617
export * from './core/VaryingNode.js';

0 commit comments

Comments
 (0)