diff --git a/examples/files.json b/examples/files.json index a886b7144cc241..92abcf9cc7be38 100644 --- a/examples/files.json +++ b/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", diff --git a/examples/screenshots/webgpu_struct_drawindirect.jpg b/examples/screenshots/webgpu_struct_drawindirect.jpg new file mode 100644 index 00000000000000..58ceae7f0eb0a5 Binary files /dev/null and b/examples/screenshots/webgpu_struct_drawindirect.jpg differ diff --git a/examples/tags.json b/examples/tags.json index 1c508c2d1f4632..14b203880a8bb2 100644 --- a/examples/tags.json +++ b/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" ], diff --git a/examples/webgpu_compute_water.html b/examples/webgpu_compute_water.html index 849899d74355c9..21aff32bc94f7a 100644 --- a/examples/webgpu_compute_water.html +++ b/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 + velocity + unused = 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 ); diff --git a/examples/webgpu_struct_drawindirect.html b/examples/webgpu_struct_drawindirect.html new file mode 100644 index 00000000000000..56a572b4df6d4f --- /dev/null +++ b/examples/webgpu_struct_drawindirect.html @@ -0,0 +1,258 @@ + + + + three.js webgpu - struct drawIndirect + + + + + + +
+ three.js webgpu - struct drawIndirect
+
+ + + + + + + \ No newline at end of file diff --git a/src/Three.TSL.js b/src/Three.TSL.js index 800292da2b5587..064809e291855f 100644 --- a/src/Three.TSL.js +++ b/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; diff --git a/src/nodes/Nodes.js b/src/nodes/Nodes.js index e913d0e94b2397..c7bfbfe2604f6a 100644 --- a/src/nodes/Nodes.js +++ b/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'; diff --git a/src/nodes/TSL.js b/src/nodes/TSL.js index d4e8963e73dfa5..7de041d58ee4dc 100644 --- a/src/nodes/TSL.js +++ b/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'; diff --git a/src/nodes/accessors/Arrays.js b/src/nodes/accessors/Arrays.js index e516ca823aabbb..f235db08754c62 100644 --- a/src/nodes/accessors/Arrays.js +++ b/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 ); diff --git a/src/nodes/accessors/StorageBufferNode.js b/src/nodes/accessors/StorageBufferNode.js index fcc2c172b42de7..af1f8f44ca9087 100644 --- a/src/nodes/accessors/StorageBufferNode.js +++ b/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} */ diff --git a/src/nodes/core/Node.js b/src/nodes/core/Node.js index 5a2741e2e49694..647897b6a62baf 100644 --- a/src/nodes/core/Node.js +++ b/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. * diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index 9eceed8e0d8fed..0ec7cdc2054f21 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/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} [membersLayout] - The count. + * @param {Array?} [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} types - The output struct types. + * @param {Array} 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} 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. * diff --git a/src/nodes/core/OutputStructNode.js b/src/nodes/core/OutputStructNode.js index 30c7495293d341..e1212161c115e2 100644 --- a/src/nodes/core/OutputStructNode.js +++ b/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 ); diff --git a/src/nodes/core/StackNode.js b/src/nodes/core/StackNode.js index ea9a8e0d686093..bcddf3ba0b6e9e 100644 --- a/src/nodes/core/StackNode.js +++ b/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. * diff --git a/src/nodes/core/StructNode.js b/src/nodes/core/StructNode.js new file mode 100644 index 00000000000000..ad351c60ec3839 --- /dev/null +++ b/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; + +}; diff --git a/src/nodes/core/StructType.js b/src/nodes/core/StructType.js new file mode 100644 index 00000000000000..6f3f3f2bcceb14 --- /dev/null +++ b/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; diff --git a/src/nodes/core/StructTypeNode.js b/src/nodes/core/StructTypeNode.js index 3952f7f5c41bc2..930b9378949a5f 100644 --- a/src/nodes/core/StructTypeNode.js +++ b/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} 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} - */ - 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} The types. - */ - getMemberTypes() { + generate( builder ) { - return this.types; + return this.getNodeType( builder ); } diff --git a/src/nodes/tsl/TSLCore.js b/src/nodes/tsl/TSLCore.js index ac0961c123744e..94f02a851d1e01 100644 --- a/src/nodes/tsl/TSLCore.js +++ b/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; diff --git a/src/nodes/utils/MemberNode.js b/src/nodes/utils/MemberNode.js new file mode 100644 index 00000000000000..c826ff57b57c4a --- /dev/null +++ b/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; diff --git a/src/nodes/utils/StorageArrayElementNode.js b/src/nodes/utils/StorageArrayElementNode.js index ae2892549869cb..7cebcbf8b80f90 100644 --- a/src/nodes/utils/StorageArrayElementNode.js +++ b/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 ) { diff --git a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js index a425c61ef25f58..5124b4e63b8e87 100644 --- a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +++ b/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 { diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 0776054446fb97..e453b25011e0a8 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/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 }` ); + 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 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 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 output : OutputStruct;\n\n'; + stageData.structs += '\nvar 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 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 };`; } diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index 8586551d2084a5..ccf5b5b37289cb 100644 --- a/test/e2e/puppeteer.js +++ b/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',