Explorar o código

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>
sunag hai 1 ano
pai
achega
2dc9a7ed82

+ 1 - 0
examples/files.json

@@ -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",

BIN=BIN
examples/screenshots/webgpu_struct_drawindirect.jpg


+ 1 - 0
examples/tags.json

@@ -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" ],

+ 18 - 13
examples/webgpu_compute_water.html

@@ -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';
@@ -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 );
@@ -372,7 +377,7 @@
 
 				sphereMaterial.positionNode = Fn( () => {
 
-					const instancePosition = sphereInstancePositionStorage.element( instanceIndex );
+					const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );
 
 					const newPosition = positionLocal.add( instancePosition );
 

+ 258 - 0
examples/webgpu_struct_drawindirect.html

@@ -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",
+					"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 - 0
src/Three.TSL.js

@@ -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;

+ 3 - 0
src/nodes/Nodes.js

@@ -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';
 
@@ -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';

+ 1 - 0
src/nodes/TSL.js

@@ -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';

+ 28 - 6
src/nodes/accessors/Arrays.js

@@ -10,13 +10,24 @@ import { getLengthFromType, getTypedArrayFromType } from '../core/NodeUtils.js';
  *
  * @function
  * @param {Number|TypedArray} count - The data count. It is also valid to pass a typed array as an argument.
- * @param {String} [type='float'] - The data type.
+ * @param {String|Struct} [type='float'] - The data type.
  * @returns {StorageBufferNode}
  */
 export const attributeArray = ( count, type = 'float' ) => {
 
-	const itemSize = getLengthFromType( type );
-	const typedArray = getTypedArrayFromType( type );
+	let itemSize, typedArray;
+
+	if ( type.isStruct === true ) {
+
+		itemSize = type.layout.getLength();
+		typedArray = getTypedArrayFromType( 'float' );
+
+	} else {
+
+		itemSize = getLengthFromType( type );
+		typedArray = getTypedArrayFromType( type );
+
+	}
 
 	const buffer = new StorageBufferAttribute( count, itemSize, typedArray );
 	const node = storage( buffer, type, count );
@@ -30,13 +41,24 @@ export const attributeArray = ( count, type = 'float' ) => {
  *
  * @function
  * @param {Number|TypedArray} count - The data count. It is also valid to pass a typed array as an argument.
- * @param {String} [type='float'] - The data type.
+ * @param {String|Struct} [type='float'] - The data type.
  * @returns {StorageBufferNode}
  */
 export const instancedArray = ( count, type = 'float' ) => {
 
-	const itemSize = getLengthFromType( type );
-	const typedArray = getTypedArrayFromType( type );
+	let itemSize, typedArray;
+
+	if ( type.isStruct === true ) {
+
+		itemSize = type.layout.getLength();
+		typedArray = getTypedArrayFromType( 'float' );
+
+	} else {
+
+		itemSize = getLengthFromType( type );
+		typedArray = getTypedArrayFromType( type );
+
+	}
 
 	const buffer = new StorageInstancedBufferAttribute( count, itemSize, typedArray );
 	const node = storage( buffer, type, count );

+ 33 - 5
src/nodes/accessors/StorageBufferNode.js

@@ -50,19 +50,30 @@ class StorageBufferNode extends BufferNode {
 	 * Constructs a new storage buffer node.
 	 *
 	 * @param {StorageBufferAttribute|StorageInstancedBufferAttribute|BufferAttribute} value - The buffer data.
-	 * @param {String?} [bufferType=null] - The buffer type (e.g. `'vec3'`).
+	 * @param {String|Struct?} [bufferType=null] - The buffer type (e.g. `'vec3'`).
 	 * @param {Number} [bufferCount=0] - The buffer count.
 	 */
 	constructor( value, bufferType = null, bufferCount = 0 ) {
 
-		if ( bufferType === null && ( value.isStorageBufferAttribute || value.isStorageInstancedBufferAttribute ) ) {
+		let nodeType, structTypeNode = null;
 
-			bufferType = getTypeFromLength( value.itemSize );
+		if ( bufferType && bufferType.isStruct ) {
+
+			nodeType = 'struct';
+			structTypeNode = bufferType.layout;
+
+		} else if ( bufferType === null && ( value.isStorageBufferAttribute || value.isStorageInstancedBufferAttribute ) ) {
+
+			nodeType = getTypeFromLength( value.itemSize );
 			bufferCount = value.count;
 
+		} else {
+
+			nodeType = bufferType;
+
 		}
 
-		super( value, bufferType, bufferCount );
+		super( value, nodeType, bufferCount );
 
 		/**
 		 * This flag can be used for type testing.
@@ -73,6 +84,15 @@ class StorageBufferNode extends BufferNode {
 		 */
 		this.isStorageBufferNode = true;
 
+
+		/**
+		 * The buffer struct type.
+		 *
+		 * @type {structTypeNode?}
+		 * @default null
+		 */
+		this.structTypeNode = structTypeNode;
+
 		/**
 		 * The access type of the texture node.
 		 *
@@ -293,6 +313,12 @@ class StorageBufferNode extends BufferNode {
 	 */
 	getNodeType( builder ) {
 
+		if ( this.structTypeNode !== null ) {
+
+			return this.structTypeNode.getNodeType( builder );
+
+		}
+
 		if ( builder.isAvailable( 'storageBuffer' ) || builder.isAvailable( 'indirectStorageBuffer' ) ) {
 
 			return super.getNodeType( builder );
@@ -313,6 +339,8 @@ class StorageBufferNode extends BufferNode {
 	 */
 	generate( builder ) {
 
+		if ( this.structTypeNode !== null ) this.structTypeNode.build( builder );
+
 		if ( builder.isAvailable( 'storageBuffer' ) || builder.isAvailable( 'indirectStorageBuffer' ) ) {
 
 			return super.generate( builder );
@@ -338,7 +366,7 @@ export default StorageBufferNode;
  *
  * @function
  * @param {StorageBufferAttribute|StorageInstancedBufferAttribute|BufferAttribute} value - The buffer data.
- * @param {String?} [type=null] - The buffer type (e.g. `'vec3'`).
+ * @param {String|Struct?} [type=null] - The buffer type (e.g. `'vec3'`).
  * @param {Number} [count=0] - The buffer count.
  * @returns {StorageBufferNode}
  */

+ 13 - 0
src/nodes/core/Node.js

@@ -416,6 +416,19 @@ class Node extends EventDispatcher {
 
 	}
 
+	/**
+	 * Returns the node member type for the given name.
+	 *
+	 * @param {NodeBuilder} builder - The current node builder.
+	 * @param {String} name - The name of the member.
+	 * @return {String} The type of the node.
+	 */
+	getMemberType( /*uilder, name*/ ) {
+
+		return 'void';
+
+	}
+
 	/**
 	 * Returns the node's type.
 	 *

+ 58 - 6
src/nodes/core/NodeBuilder.js

@@ -5,7 +5,7 @@ import NodeVar from './NodeVar.js';
 import NodeCode from './NodeCode.js';
 import NodeCache from './NodeCache.js';
 import ParameterNode from './ParameterNode.js';
-import StructTypeNode from './StructTypeNode.js';
+import StructType from './StructType.js';
 import FunctionNode from '../code/FunctionNode.js';
 import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
 import { getTypeFromLength } from './NodeUtils.js';
@@ -1102,6 +1102,39 @@ class NodeBuilder {
 
 	}
 
+	/**
+	 * Generates the struct shader string.
+	 *
+	 * @param {String} type - The type.
+	 * @param {Array<Object>} [membersLayout] - The count.
+	 * @param {Array<Node>?} [values=null] - The default values.
+	 * @return {String} The generated value as a shader string.
+	 */
+	generateStruct( type, membersLayout, values = null ) {
+
+		const snippets = [];
+
+		for ( const member of membersLayout ) {
+
+			const { name, type } = member;
+
+			if ( values && values[ name ] && values[ name ].isNode ) {
+
+				snippets.push( values[ name ].build( this, type ) );
+
+			} else {
+
+				snippets.push( this.generateConst( type ) );
+
+			}
+
+		}
+
+		return type + '( ' + snippets.join( ', ' ) + ' )';
+
+	}
+
+
 	/**
 	 * Generates the shader string for the given type and value.
 	 *
@@ -1593,14 +1626,15 @@ class NodeBuilder {
 	}
 
 	/**
-	 * Returns an instance of {@link StructTypeNode} for the given output struct node.
+	 * Returns an instance of {@link StructType} for the given output struct node.
 	 *
 	 * @param {OutputStructNode} node - The output struct node.
-	 * @param {Array<String>} types - The output struct types.
+	 * @param {Array<Object>} membersLayout - The output struct types.
+	 * @param {String?} [name=null] - The name of the struct.
 	 * @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
-	 * @return {StructTypeNode} The struct type attribute.
+	 * @return {StructType} The struct type attribute.
 	 */
-	getStructTypeFromNode( node, types, shaderStage = this.shaderStage ) {
+	getStructTypeFromNode( node, membersLayout, name = null, shaderStage = this.shaderStage ) {
 
 		const nodeData = this.getDataFromNode( node, shaderStage );
 
@@ -1610,7 +1644,9 @@ class NodeBuilder {
 
 			const index = this.structs.index ++;
 
-			structType = new StructTypeNode( 'StructType' + index, types );
+			if ( name === null ) name = 'StructType' + index;
+
+			structType = new StructType( name, membersLayout );
 
 			this.structs[ shaderStage ].push( structType );
 
@@ -1622,6 +1658,22 @@ class NodeBuilder {
 
 	}
 
+	/**
+	 * Returns an instance of {@link StructType} for the given output struct node.
+	 *
+	 * @param {OutputStructNode} node - The output struct node.
+	 * @param {Array<Object>} membersLayout - The output struct types.
+	 * @return {StructType} The struct type attribute.
+	 */
+	getOutputStructTypeFromNode( node, membersLayout ) {
+
+		const structType = this.getStructTypeFromNode( node, membersLayout, 'OutputType', 'fragment' );
+		structType.output = true;
+
+		return structType;
+
+	}
+
 	/**
 	 * Returns an instance of {@link NodeUniform} for the given uniform node.
 	 *

+ 19 - 9
src/nodes/core/OutputStructNode.js

@@ -44,24 +44,34 @@ class OutputStructNode extends Node {
 
 	}
 
-	setup( builder ) {
+	getNodeType( builder ) {
 
-		super.setup( builder );
+		const properties = builder.getNodeProperties( this );
 
-		const members = this.members;
-		const types = [];
+		if ( properties.membersLayout === undefined ) {
 
-		for ( let i = 0; i < members.length; i ++ ) {
+			const members = this.members;
+			const membersLayout = [];
+
+			for ( let i = 0; i < members.length; i ++ ) {
+
+				const name = 'm' + i;
+				const type = members[ i ].getNodeType( builder );
+
+				membersLayout.push( { name, type, index: i } );
+
+			}
 
-			types.push( members[ i ].getNodeType( builder ) );
+			properties.membersLayout = membersLayout;
+			properties.structType = builder.getOutputStructTypeFromNode( this, properties.membersLayout );
 
 		}
 
-		this.nodeType = builder.getStructTypeFromNode( this, types ).name;
+		return properties.structType.name;
 
 	}
 
-	generate( builder, output ) {
+	generate( builder ) {
 
 		const propertyName = builder.getOutputStructName();
 		const members = this.members;
@@ -70,7 +80,7 @@ class OutputStructNode extends Node {
 
 		for ( let i = 0; i < members.length; i ++ ) {
 
-			const snippet = members[ i ].build( builder, output );
+			const snippet = members[ i ].build( builder );
 
 			builder.addLineFlowCode( `${ structPrefix }m${ i } = ${ snippet }`, this );
 

+ 6 - 0
src/nodes/core/StackNode.js

@@ -76,6 +76,12 @@ class StackNode extends Node {
 
 	}
 
+	getMemberType( builder, name ) {
+
+		return this.outputNode ? this.outputNode.getMemberType( builder, name ) : 'void';
+
+	}
+
 	/**
 	 * Adds a node to this stack.
 	 *

+ 93 - 0
src/nodes/core/StructNode.js

@@ -0,0 +1,93 @@
+import Node from './Node.js';
+import StructTypeNode from './StructTypeNode.js';
+import { nodeObject } from '../tsl/TSLCore.js';
+
+/** @module StructNode **/
+
+class StructNode extends Node {
+
+	static get type() {
+
+		return 'StructNode';
+
+	}
+
+	constructor( structLayoutNode, values ) {
+
+		super( 'vec3' );
+
+		this.structLayoutNode = structLayoutNode;
+		this.values = values;
+
+		this.isStructNode = true;
+
+	}
+
+	getNodeType( builder ) {
+
+		return this.structLayoutNode.getNodeType( builder );
+
+	}
+
+	getMemberType( builder, name ) {
+
+		return this.structLayoutNode.getMemberType( builder, name );
+
+	}
+
+	generate( builder ) {
+
+		const nodeVar = builder.getVarFromNode( this );
+		const structType = nodeVar.type;
+		const propertyName = builder.getPropertyName( nodeVar );
+
+		builder.addLineFlowCode( `${ propertyName } = ${ builder.generateStruct( structType, this.structLayoutNode.membersLayout, this.values ) }`, this );
+
+		return nodeVar.name;
+
+	}
+
+}
+
+export default StructNode;
+
+export const struct = ( membersLayout, name = null ) => {
+
+	const structLayout = new StructTypeNode( membersLayout, name );
+
+	const struct = ( ...params ) => {
+
+		let values = null;
+
+		if ( params.length > 0 ) {
+
+			if ( params[ 0 ].isNode ) {
+
+				values = {};
+
+				const names = Object.keys( membersLayout );
+
+				for ( let i = 0; i < params.length; i ++ ) {
+
+					values[ names[ i ] ] = params[ i ];
+
+				}
+
+			} else {
+
+				values = params[ 0 ];
+
+			}
+
+		}
+
+		return nodeObject( new StructNode( structLayout, values ) );
+
+	};
+
+	struct.layout = structLayout;
+	struct.isStruct = true;
+
+	return struct;
+
+};

+ 13 - 0
src/nodes/core/StructType.js

@@ -0,0 +1,13 @@
+class StructType {
+
+	constructor( name, members ) {
+
+		this.name = name;
+		this.members = members;
+		this.output = false;
+
+	}
+
+}
+
+export default StructType;

+ 55 - 42
src/nodes/core/StructTypeNode.js

@@ -1,11 +1,24 @@
 import Node from './Node.js';
+import { getLengthFromType } from './NodeUtils.js';
+
+/** @module StructTypeNode **/
+
+function getMembersLayout( members ) {
+
+	return Object.entries( members ).map( ( [ name, value ] ) => {
+
+		if ( typeof value === 'string' ) {
+
+			return { name, type: value, atomic: false };
+
+		}
+
+		return { name, type: value.type, atomic: value.atomic || false };
+
+	} );
+
+}
 
-/**
- * {@link NodeBuilder} is going to create instances of this class during the build process
- * of nodes. They represent the final shader struct data that are going to be generated
- * by the builder. A dictionary of struct types is maintained in {@link NodeBuilder#structs}
- * for this purpose.
- */
 class StructTypeNode extends Node {
 
 	static get type() {
@@ -14,50 +27,50 @@ class StructTypeNode extends Node {
 
 	}
 
-	/**
-	 * Constructs a new struct type node.
-	 *
-	 * @param {String} name - The name of the struct.
-	 * @param {Array<String>} types - An array of types.
-	 */
-	constructor( name, types ) {
-
-		super();
-
-		/**
-		 * The name of the struct.
-		 *
-		 * @type {String}
-		 */
+	constructor( membersLayout, name = null ) {
+
+		super( 'struct' );
+
+		this.membersLayout = getMembersLayout( membersLayout );
 		this.name = name;
 
+		this.isStructLayoutNode = true;
+
+	}
+
+	getLength() {
+
+		let length = 0;
+
+		for ( const member of this.membersLayout ) {
+
+			length += getLengthFromType( member.type );
+
+		}
+
+		return length;
+
+	}
+
+	getMemberType( builder, name ) {
+
+		const member = this.membersLayout.find( m => m.name === name );
+
+		return member ? member.type : 'void';
+
+	}
+
+	getNodeType( builder ) {
 
-		/**
-		 * An array of types.
-		 *
-		 * @type {Array<String>}
-		 */
-		this.types = types;
+		const structType = builder.getStructTypeFromNode( this, this.membersLayout, this.name );
 
-		/**
-		 * This flag can be used for type testing.
-		 *
-		 * @type {Boolean}
-		 * @readonly
-		 * @default true
-		 */
-		this.isStructTypeNode = true;
+		return structType.name;
 
 	}
 
-	/**
-	 * Returns the member types.
-	 *
-	 * @return {Array<String>} The types.
-	 */
-	getMemberTypes() {
+	generate( builder ) {
 
-		return this.types;
+		return this.getNodeType( builder );
 
 	}
 

+ 13 - 0
src/nodes/tsl/TSLCore.js

@@ -6,6 +6,7 @@ import SplitNode from '../utils/SplitNode.js';
 import SetNode from '../utils/SetNode.js';
 import FlipNode from '../utils/FlipNode.js';
 import ConstNode from '../core/ConstNode.js';
+import MemberNode from '../utils/MemberNode.js';
 import { getValueFromType, getValueType } from '../core/NodeUtils.js';
 
 /** @module TSLCore **/
@@ -112,6 +113,12 @@ const shaderNodeHandler = {
 
 				return nodeObject( new ArrayElementNode( nodeObj, new ConstNode( Number( prop ), 'uint' ) ) );
 
+			} else if ( /^get$/.test( prop ) === true ) {
+
+				// accessing properties
+
+				return ( value ) => nodeObject( new MemberNode( nodeObj, value ) );
+
 			}
 
 		}
@@ -261,6 +268,12 @@ class ShaderCallNodeInternal extends Node {
 
 	}
 
+	getMemberType( builder, name ) {
+
+		return this.getOutputNode( builder ).getMemberType( builder, name );
+
+	}
+
 	call( builder ) {
 
 		const { shaderNode, inputNodes } = this;

+ 68 - 0
src/nodes/utils/MemberNode.js

@@ -0,0 +1,68 @@
+import Node from '../core/Node.js';
+
+/**
+ * Base class for representing member access on an object-like
+ * node data structures.
+ *
+ * @augments Node
+ */
+class MemberNode extends Node {
+
+	static get type() {
+
+		return 'MemberNode';
+
+	}
+
+	/**
+	 * Constructs an array element node.
+	 *
+	 * @param {Node} node - The array-like node.
+	 * @param {String} property - The property name.
+	 */
+	constructor( node, property ) {
+
+		super();
+
+		/**
+		 * The array-like node.
+		 *
+		 * @type {Node}
+		 */
+		this.node = node;
+
+		/**
+		 * The property name.
+		 *
+		 * @type {Node}
+		 */
+		this.property = property;
+
+		/**
+		 * This flag can be used for type testing.
+		 *
+		 * @type {Boolean}
+		 * @readonly
+		 * @default true
+		 */
+		this.isMemberNode = true;
+
+	}
+
+	getNodeType( builder ) {
+
+		return this.node.getMemberType( builder, this.property );
+
+	}
+
+	generate( builder ) {
+
+		const propertyName = this.node.build( builder );
+
+		return propertyName + '.' + this.property;
+
+	}
+
+}
+
+export default MemberNode;

+ 14 - 0
src/nodes/utils/StorageArrayElementNode.js

@@ -61,6 +61,20 @@ class StorageArrayElementNode extends ArrayElementNode {
 
 	}
 
+	getMemberType( builder, name ) {
+
+		const structTypeNode = this.storageBufferNode.structTypeNode;
+
+		if ( structTypeNode ) {
+
+			return structTypeNode.getMemberType( builder, name );
+
+		}
+
+		return 'void';
+
+	}
+
 	setup( builder ) {
 
 		if ( builder.isAvailable( 'storageBuffer' ) === false ) {

+ 26 - 14
src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js

@@ -696,12 +696,10 @@ ${ flowData.code }
 	getStructMembers( struct ) {
 
 		const snippets = [];
-		const members = struct.getMemberTypes();
 
-		for ( let i = 0; i < members.length; i ++ ) {
+		for ( const member of struct.members ) {
 
-			const member = members[ i ];
-			snippets.push( `layout( location = ${i} ) out ${ member} m${i};` );
+			snippets.push( `\t${ member.type } ${ member.name };` );
 
 		}
 
@@ -720,25 +718,37 @@ ${ flowData.code }
 		const snippets = [];
 		const structs = this.structs[ shaderStage ];
 
-		if ( structs.length === 0 ) {
+		const outputSnippet = [];
 
-			return 'layout( location = 0 ) out vec4 fragColor;\n';
+		for ( const struct of structs ) {
 
-		}
+			if ( struct.output ) {
+
+				for ( const member of struct.members ) {
+
+					outputSnippet.push( `layout( location = ${ member.index } ) out ${ member.type } ${ member.name };` );
 
-		for ( let index = 0, length = structs.length; index < length; index ++ ) {
+				}
+
+			} else {
 
-			const struct = structs[ index ];
+				let snippet = 'struct ' + struct.name + ' {\n';
+				snippet += this.getStructMembers( struct );
+				snippet += '\n};\n';
+
+				snippets.push( snippet );
+
+			}
+
+		}
 
-			let snippet = '\n';
-			snippet += this.getStructMembers( struct );
-			snippet += '\n';
+		if ( outputSnippet.length === 0 ) {
 
-			snippets.push( snippet );
+			outputSnippet.push( 'layout( location = 0 ) out vec4 fragColor;' );
 
 		}
 
-		return snippets.join( '\n\n' );
+		return '\n' + outputSnippet.join( '\n' ) + '\n\n' + snippets.join( '\n' );
 
 	}
 
@@ -1156,6 +1166,7 @@ ${shaderData.varyings}
 // codes
 ${shaderData.codes}
 
+// structs
 ${shaderData.structs}
 
 void main() {
@@ -1245,6 +1256,7 @@ void main() {
 
 			this.vertexShader = this._getGLSLVertexCode( shadersData.vertex );
 			this.fragmentShader = this._getGLSLFragmentCode( shadersData.fragment );
+			console.log( this.fragmentShader );
 
 		} else {
 

+ 72 - 33
src/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -758,7 +758,13 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 			} else if ( type === 'buffer' || type === 'storageBuffer' || type === 'indirectStorageBuffer' ) {
 
-				return `NodeBuffer_${ node.id }.${name}`;
+				if ( this.isCustomStruct( node ) ) {
+
+					return name;
+
+				}
+
+				return name + '.value';
 
 			} else {
 
@@ -899,7 +905,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 				if ( ( shaderStage === 'fragment' || shaderStage === 'compute' ) && this.isUnfilterable( node.value ) === false && texture.store === false ) {
 
-					const sampler = new NodeSampler( `${uniformNode.name}_sampler`, uniformNode.node, group );
+					const sampler = new NodeSampler( `${ uniformNode.name }_sampler`, uniformNode.node, group );
 					sampler.setVisibility( gpuShaderStageLib[ shaderStage ] );
 
 					bindings.push( sampler, texture );
@@ -925,6 +931,8 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 				uniformGPU = buffer;
 
+				uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id;
+
 			} else {
 
 				const uniformsStage = this.uniformGroups[ shaderStage ] || ( this.uniformGroups[ shaderStage ] = {} );
@@ -1424,18 +1432,22 @@ ${ flowData.code }
 	getStructMembers( struct ) {
 
 		const snippets = [];
-		const members = struct.getMemberTypes();
 
-		for ( let i = 0; i < members.length; i ++ ) {
+		for ( const member of struct.members ) {
 
-			const member = members[ i ];
-			snippets.push( `\t@location( ${i} ) m${i} : ${ member }<f32>` );
+			const prefix = struct.output ? '@location( ' + member.index + ' ) ' : '';
 
-		}
+			let type = this.getType( member.type );
 
-		const builtins = this.getBuiltins( 'output' );
+			if ( member.atomic ) {
 
-		if ( builtins ) snippets.push( '\t' + builtins );
+				type = 'atomic< ' + type + ' >';
+
+			}
+
+			snippets.push( `\t${ prefix + member.name } : ${ type }` );
+
+		}
 
 		return snippets.join( ',\n' );
 
@@ -1449,26 +1461,29 @@ ${ flowData.code }
 	 */
 	getStructs( shaderStage ) {
 
-		const snippets = [];
+		let result = '';
+
 		const structs = this.structs[ shaderStage ];
 
-		for ( let index = 0, length = structs.length; index < length; index ++ ) {
+		if ( structs.length > 0 ) {
+
+			const snippets = [];
 
-			const struct = structs[ index ];
-			const name = struct.name;
+			for ( const struct of structs ) {
 
-			let snippet = `\struct ${ name } {\n`;
-			snippet += this.getStructMembers( struct );
-			snippet += '\n}';
+				let snippet = `struct ${ struct.name } {\n`;
+				snippet += this.getStructMembers( struct );
+				snippet += '\n};';
 
+				snippets.push( snippet );
 
-			snippets.push( snippet );
+			}
 
-			snippets.push( `\nvar<private> output : ${ name };\n\n` );
+			result = '\n' + snippets.join( '\n\n' ) + '\n';
 
 		}
 
-		return snippets.join( '\n\n' );
+		return result;
 
 	}
 
@@ -1581,6 +1596,12 @@ ${ flowData.code }
 
 	}
 
+	isCustomStruct( nodeUniform ) {
+
+		return nodeUniform.value.isStorageBufferAttribute && nodeUniform.node.structTypeNode !== null;
+
+	}
+
 	/**
 	 * Returns the uniforms of the given shader stage as a WGSL string.
 	 *
@@ -1662,7 +1683,7 @@ ${ flowData.code }
 
 					const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 );
 
-					textureType = `texture${multisampled}_2d<${ componentPrefix }32>`;
+					textureType = `texture${ multisampled }_2d<${ componentPrefix }32>`;
 
 				}
 
@@ -1671,15 +1692,23 @@ ${ flowData.code }
 			} else if ( uniform.type === 'buffer' || uniform.type === 'storageBuffer' || uniform.type === 'indirectStorageBuffer' ) {
 
 				const bufferNode = uniform.node;
-				const bufferType = this.getType( bufferNode.bufferType );
+				const bufferType = this.getType( bufferNode.getNodeType( this ) );
 				const bufferCount = bufferNode.bufferCount;
-
 				const bufferCountSnippet = bufferCount > 0 && uniform.type === 'buffer' ? ', ' + bufferCount : '';
-				const bufferTypeSnippet = bufferNode.isAtomic ? `atomic<${bufferType}>` : `${bufferType}`;
-				const bufferSnippet = `\t${ uniform.name } : array< ${ bufferTypeSnippet }${ bufferCountSnippet } >\n`;
 				const bufferAccessMode = bufferNode.isStorageBufferNode ? `storage, ${ this.getStorageAccess( bufferNode, shaderStage ) }` : 'uniform';
 
-				bufferSnippets.push( this._getWGSLStructBinding( 'NodeBuffer_' + bufferNode.id, bufferSnippet, bufferAccessMode, uniformIndexes.binding ++, uniformIndexes.group ) );
+				if ( this.isCustomStruct( uniform ) ) {
+
+					bufferSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var<${ bufferAccessMode }> ${ uniform.name } : ${ bufferType };` );
+
+				} else {
+
+					const bufferTypeSnippet = bufferNode.isAtomic ? `atomic<${ bufferType }>` : `${ bufferType }`;
+					const bufferSnippet = `\tvalue : array< ${ bufferTypeSnippet }${ bufferCountSnippet } >`;
+
+					bufferSnippets.push( this._getWGSLStructBinding( uniform.name, bufferSnippet, bufferAccessMode, uniformIndexes.binding ++, uniformIndexes.group ) );
+
+				}
 
 			} else {
 
@@ -1725,6 +1754,8 @@ ${ flowData.code }
 
 		for ( const shaderStage in shadersData ) {
 
+			this.shaderStage = shaderStage;
+
 			const stageData = shadersData[ shaderStage ];
 			stageData.uniforms = this.getUniforms( shaderStage );
 			stageData.attributes = this.getAttributes( shaderStage );
@@ -1773,7 +1804,8 @@ ${ flowData.code }
 
 						if ( isOutputStruct ) {
 
-							stageData.returnType = outputNode.nodeType;
+							stageData.returnType = outputNode.getNodeType( this );
+							stageData.structs += 'var<private> output : ' + stageData.returnType + ';';
 
 							flow += `return ${ flowSlotData.result };`;
 
@@ -1787,7 +1819,7 @@ ${ flowData.code }
 
 							stageData.returnType = 'OutputStruct';
 							stageData.structs += this._getWGSLStruct( 'OutputStruct', structSnippet );
-							stageData.structs += '\nvar<private> output : OutputStruct;\n\n';
+							stageData.structs += '\nvar<private> output : OutputStruct;';
 
 							flow += `output.color = ${ flowSlotData.result };\n\n\treturn output;`;
 
@@ -1801,9 +1833,10 @@ ${ flowData.code }
 
 			stageData.flow = flow;
 
-
 		}
 
+		this.shaderStage = null;
+
 		if ( this.material !== null ) {
 
 			this.vertexShader = this._getWGSLVertexCode( shadersData.vertex );
@@ -1941,6 +1974,9 @@ ${ flowData.code }
 // directives
 ${shaderData.directives}
 
+// structs
+${shaderData.structs}
+
 // uniforms
 ${shaderData.uniforms}
 
@@ -1980,12 +2016,12 @@ fn main( ${shaderData.attributes} ) -> VaryingsStruct {
 // global
 ${ diagnostics }
 
-// uniforms
-${shaderData.uniforms}
-
 // structs
 ${shaderData.structs}
 
+// uniforms
+${shaderData.uniforms}
+
 // codes
 ${shaderData.codes}
 
@@ -2023,6 +2059,9 @@ var<private> instanceIndex : u32;
 // locals
 ${shaderData.scopedArrays}
 
+// structs
+${shaderData.structs}
+
 // uniforms
 ${shaderData.uniforms}
 
@@ -2080,8 +2119,8 @@ ${vars}
 		const structSnippet = this._getWGSLStruct( structName, vars );
 
 		return `${structSnippet}
-@binding( ${binding} ) @group( ${group} )
-var<${access}> ${name} : ${structName};`;
+@binding( ${ binding } ) @group( ${ group } )
+var<${access}> ${ name } : ${ structName };`;
 
 	}
 

+ 1 - 0
test/e2e/puppeteer.js

@@ -126,6 +126,7 @@ const exceptionList = [
 	// Awaiting for WebGPU Backend support in Puppeteer
 	'webgpu_storage_buffer',
 	'webgpu_compute_sort_bitonic',
+	'webgpu_struct_drawindirect',
 
 	// WebGPURenderer: Unknown problem
 	'webgpu_backdrop_water',

粤ICP备19079148号