Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TSL: Introduce struct #30394

Merged
merged 13 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@
"webgpu_sky",
"webgpu_sprites",
"webgpu_storage_buffer",
"webgpu_struct_drawindirect",
"webgpu_texturegrad",
"webgpu_textures_2d-array",
"webgpu_textures_2d-array_compressed",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"webgpu_compute_sort_bitonic": [ "gpgpu" ],
"webgpu_compute_texture": [ "gpgpu" ],
"webgpu_compute_texture_pingpong": [ "gpgpu" ],
"webgpu_compute_water": [ "gpgpu", "struct" ],
"webgpu_depth_texture": [ "renderTarget" ],
"webgpu_loader_gltf_dispersion": [ "transmission" ],
"webgpu_materials_lightmap": [ "shadow" ],
Expand Down
31 changes: 18 additions & 13 deletions examples/webgpu_compute_water.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

import * as THREE from 'three';

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';
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';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import Stats from 'three/addons/libs/stats.module.js';
Expand Down Expand Up @@ -279,30 +279,35 @@
const sphereMaterial = new THREE.MeshPhongMaterial( { color: 0xFFFF00 } );

// Initialize sphere mesh instance position and velocity.
const spherePositionArray = new Float32Array( NUM_SPHERES * 3 );

// position<vec3> + velocity<vec2> + unused<vec3> = 8 floats per sphere.
// for structs arrays must be enclosed in multiple of 4

const sphereStride = 8;
const sphereArray = new Float32Array( NUM_SPHERES * sphereStride );

// Only hold velocity in x and z directions.
// The sphere is wedded to the surface of the water, and will only move vertically with the water.
const sphereVelocityArray = new Float32Array( NUM_SPHERES * 2 );

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

spherePositionArray[ i * 3 + 0 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
spherePositionArray[ i * 3 + 1 ] = 0;
spherePositionArray[ i * 3 + 2 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
sphereArray[ i * sphereStride + 0 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
sphereArray[ i * sphereStride + 1 ] = 0;
sphereArray[ i * sphereStride + 2 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;

}

sphereVelocityArray.fill( 0.0 );
const SphereStruct = struct( {
position: 'vec3',
velocity: 'vec2'
} );

// Sphere Instance Storage
const sphereInstancePositionStorage = instancedArray( spherePositionArray, 'vec3' ).label( 'SpherePosition' );
const sphereVelocityStorage = instancedArray( sphereVelocityArray, 'vec2' ).label( 'SphereVelocity' );
const sphereVelocityStorage = instancedArray( sphereArray, SphereStruct ).label( 'SphereData' );

computeSphere = Fn( () => {

const instancePosition = sphereInstancePositionStorage.element( instanceIndex );
const velocity = sphereVelocityStorage.element( instanceIndex );
const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );
const velocity = sphereVelocityStorage.element( instanceIndex ).get( 'velocity' );

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

sphereMaterial.positionNode = Fn( () => {

const instancePosition = sphereInstancePositionStorage.element( instanceIndex );
const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );

const newPosition = positionLocal.add( instancePosition );

Expand Down
258 changes: 258 additions & 0 deletions examples/webgpu_struct_drawindirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - struct drawIndirect</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 - struct drawIndirect<br />
</div>

<script type="importmap">
{
"imports": {
"three": "../src/three.webgpu.js",
"three/webgpu": "../src/three.webgpu.js",
"three/tsl": "../src/three.tsl.js",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three';
import { struct, storage, wgslFn, instanceIndex, time, varyingProperty, attribute } from 'three/tsl';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0x000000 );
renderer.setClearAlpha( 0 );
document.body.appendChild( renderer.domElement );

const aspect = window.innerWidth / window.innerHeight;

const camera = new THREE.PerspectiveCamera( 50.0, aspect, 0.1, 10000 );
const scene = new THREE.Scene();

scene.background = new THREE.Color( 0x00001f );
camera.position.set( 1, 1, 1 );
const controls = new OrbitControls( camera, renderer.domElement );

let computeDrawBuffer, computeInitDrawBuffer;

init();

async function init() {

await renderer.init();

// geometry

const vector = new THREE.Vector4();

const instances = 100000;

const positions = [];
const offsets = [];
const colors = [];
const orientationsStart = [];
const orientationsEnd = [];

positions.push( 0.025, - 0.025, 0 );
positions.push( - 0.025, 0.025, 0 );
positions.push( 0, 0, 0.025 );

// instanced attributes

for ( let i = 0; i < instances; i ++ ) {

// offsets

offsets.push( Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 );

// colors

colors.push( Math.random(), Math.random(), Math.random(), Math.random() );

// orientation start

vector.set( Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1 );
vector.normalize();

orientationsStart.push( vector.x, vector.y, vector.z, vector.w );

// orientation end

vector.set( Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1 );
vector.normalize();

orientationsEnd.push( vector.x, vector.y, vector.z, vector.w );

}

const geometry = new THREE.InstancedBufferGeometry();
geometry.instanceCount = instances;

geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
geometry.setAttribute( 'offset', new THREE.InstancedBufferAttribute( new Float32Array( offsets ), 3 ) );
geometry.setAttribute( 'color', new THREE.InstancedBufferAttribute( new Float32Array( colors ), 4 ) );
geometry.setAttribute( 'orientationStart', new THREE.InstancedBufferAttribute( new Float32Array( orientationsStart ), 4 ) );
geometry.setAttribute( 'orientationEnd', new THREE.InstancedBufferAttribute( new Float32Array( orientationsEnd ), 4 ) );

const drawBuffer = new THREE.IndirectStorageBufferAttribute( new Uint32Array( 5 ), 5 );
geometry.setIndirect( drawBuffer );

const drawBufferStruct = struct( {
vertexCount: 'uint',
instanceCount: { type: 'uint', atomic: true },
firstVertex: 'uint',
firstInstance: 'uint',
offset: 'uint'
}, 'DrawBuffer' );

const writeDrawBuffer = wgslFn( `
fn compute(
index: u32,
drawBuffer: ptr<storage, DrawBuffer, read_write>,
instances: f32,
time: f32,
) -> void {

let instanceCount = max( instances * pow( sin( time * 0.5 ) + 1, 4.0 ), 100 );

atomicStore( &drawBuffer.instanceCount, u32( instanceCount ) );
}
` );

computeDrawBuffer = writeDrawBuffer( {
drawBuffer: storage( drawBuffer, drawBufferStruct, drawBuffer.count ),
instances: instances,
index: instanceIndex,
time: time
} ).compute( instances ); // not neccessary in this case but normally one wants to run through all instances

const initDrawBuffer = wgslFn( `
fn compute(
drawBuffer: ptr< storage, DrawBuffer, read_write >,
) -> void {

drawBuffer.vertexCount = 3u;
atomicStore(&drawBuffer.instanceCount, 0u);
drawBuffer.firstVertex = 0u;
drawBuffer.firstInstance = 0u;
drawBuffer.offset = 0u;
}
` );

computeInitDrawBuffer = initDrawBuffer( {
drawBuffer: storage( drawBuffer, drawBufferStruct, drawBuffer.count ),
} ).compute( 1 );

const vPosition = varyingProperty( 'vec3', 'vPosition' );
const vColor = varyingProperty( 'vec4', 'vColor' );

const positionShaderParams = {
position: attribute( 'position' ),
offset: attribute( 'offset' ),
color: attribute( 'color' ),
orientationStart: attribute( 'orientationStart' ),
orientationEnd: attribute( 'orientationEnd' ),
time: time
};

const positionShader = wgslFn( `
fn main_vertex(
position: vec3<f32>,
offset: vec3<f32>,
color: vec4<f32>,
orientationStart: vec4<f32>,
orientationEnd: vec4<f32>,
time: f32
) -> vec4<f32> {

var vPosition = offset * max( abs( sin( time * 0.5 ) * 2.0 + 1.0 ), 0.5 ) + position;
var orientation = normalize( mix( orientationStart, orientationEnd, sin( time * 0.5 ) ) );
var vcV = cross( orientation.xyz, vPosition );
vPosition = vcV * ( 2.0 * orientation.w ) + ( cross( orientation.xyz, vcV ) * 2.0 + vPosition );

var vColor = color;

var outPosition = vec4f(vPosition, 1);

varyings.vPosition = vPosition;
varyings.vColor = vColor;

return outPosition;
}
`, [ vPosition, vColor ] );

const fragmentShaderParams = {
time: time,
vPosition: vPosition,
vColor: vColor
};

const fragmentShader = wgslFn( `
fn main_fragment(
time: f32,
vPosition: vec3<f32>,
vColor: vec4<f32>
) -> vec4<f32> {

var color = vec4f( vColor );
color.r += sin( vPosition.x * 10.0 + time ) * 0.5;

return color;
}
` );

const material = new THREE.MeshBasicNodeMaterial( {
side: THREE.DoubleSide,
forceSinglePass: true,
transparent: true
} );

material.positionNode = positionShader( positionShaderParams );
material.fragmentNode = fragmentShader( fragmentShaderParams );

const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

renderer.setAnimationLoop( render );

window.addEventListener( 'resize', onWindowResize, false );

}

function render() {

controls.update();

renderer.render( scene, camera );

renderer.compute( computeInitDrawBuffer );
renderer.compute( computeDrawBuffer );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );

}

</script>

</body>
</html>
1 change: 1 addition & 0 deletions src/Three.TSL.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ export const storageBarrier = TSL.storageBarrier;
export const storageObject = TSL.storageObject;
export const storageTexture = TSL.storageTexture;
export const string = TSL.string;
export const struct = TSL.struct;
export const sub = TSL.sub;
export const subgroupIndex = TSL.subgroupIndex;
export const subgroupSize = TSL.subgroupSize;
Expand Down
3 changes: 3 additions & 0 deletions src/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export { default as TempNode } from './core/TempNode.js';
export { default as UniformGroupNode } from './core/UniformGroupNode.js';
export { default as UniformNode } from './core/UniformNode.js';
export { default as VaryingNode } from './core/VaryingNode.js';
export { default as StructNode } from './core/StructNode.js';
export { default as StructTypeNode } from './core/StructTypeNode.js';
export { default as OutputStructNode } from './core/OutputStructNode.js';
export { default as MRTNode } from './core/MRTNode.js';

Expand All @@ -53,6 +55,7 @@ export { default as StorageArrayElementNode } from './utils/StorageArrayElementN
export { default as TriplanarTexturesNode } from './utils/TriplanarTexturesNode.js';
export { default as ReflectorNode } from './utils/ReflectorNode.js';
export { default as RTTNode } from './utils/RTTNode.js';
export { default as MemberNode } from './utils/MemberNode.js';

// accessors
export { default as UniformArrayNode } from './accessors/UniformArrayNode.js';
Expand Down
1 change: 1 addition & 0 deletions src/nodes/TSL.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './core/IndexNode.js';
export * from './core/ParameterNode.js';
export * from './core/PropertyNode.js';
export * from './core/StackNode.js';
export * from './core/StructNode.js';
export * from './core/UniformGroupNode.js';
export * from './core/UniformNode.js';
export * from './core/VaryingNode.js';
Expand Down
Loading