diff --git a/examples/files.json b/examples/files.json
index e70614edb858ee..6d22e10bc3b768 100644
--- a/examples/files.json
+++ b/examples/files.json
@@ -421,6 +421,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..11d35b11b1bebd
Binary files /dev/null and b/examples/screenshots/webgpu_struct_drawIndirect.jpg differ
diff --git a/examples/webgpu_struct_drawIndirect.html b/examples/webgpu_struct_drawIndirect.html
new file mode 100644
index 00000000000000..b9e19b94d82d13
--- /dev/null
+++ b/examples/webgpu_struct_drawIndirect.html
@@ -0,0 +1,267 @@
+
+
+
+ three.js webgpu - gltf loader
+
+
+
+
+
+
+
+
three.js webgpu - struct drawIndirect
+
+
+
+
+
+
+
+
diff --git a/src/nodes/TSL.js b/src/nodes/TSL.js
index ded6a8ef5bcc8d..f8a0f9522b1057 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/StructTypeNode.js';
export * from './core/UniformGroupNode.js';
export * from './core/UniformNode.js';
export * from './core/VaryingNode.js';
diff --git a/src/nodes/accessors/StorageBufferNode.js b/src/nodes/accessors/StorageBufferNode.js
index 8cfe16237d59af..1689ccc738077c 100644
--- a/src/nodes/accessors/StorageBufferNode.js
+++ b/src/nodes/accessors/StorageBufferNode.js
@@ -22,6 +22,7 @@ class StorageBufferNode extends BufferNode {
this.isAtomic = false;
this.bufferObject = false;
+ this.bufferStruct = false;
this.bufferCount = bufferCount;
this._attribute = null;
@@ -84,6 +85,14 @@ class StorageBufferNode extends BufferNode {
}
+ setBufferStruct( value ) {
+
+ this.bufferStruct = value;
+
+ return this;
+
+ }
+
setAccess( value ) {
this.access = value;
@@ -166,4 +175,5 @@ export default StorageBufferNode;
// Read-Write Storage
export const storage = ( value, type, count ) => nodeObject( new StorageBufferNode( value, type, count ) );
+export const storageStruct = ( value, type, count ) => nodeObject( new StorageBufferNode( value, type, count ).setBufferStruct( true ) );
export const storageObject = ( value, type, count ) => nodeObject( new StorageBufferNode( value, type, count ).setBufferObject( true ) );
diff --git a/src/nodes/core/StructTypeNode.js b/src/nodes/core/StructTypeNode.js
index 288ea9535096aa..e309ac38f23589 100644
--- a/src/nodes/core/StructTypeNode.js
+++ b/src/nodes/core/StructTypeNode.js
@@ -26,3 +26,19 @@ class StructTypeNode extends Node {
}
export default StructTypeNode;
+
+export const struct = ( members ) => {
+
+ return Object.entries( members ).map( ( [ name, value ] ) => {
+
+ if ( typeof value === 'string' ) {
+
+ return { name, type: value, isAtomic: false };
+
+ }
+
+ return { name, type: value.type, isAtomic: value.atomic || false };
+
+ } );
+
+};
diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
index f3b71446153dd0..5da257a01ffdd4 100644
--- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
+++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
@@ -964,6 +964,83 @@ ${ flowData.code }
}
+ getTypeFromCustomStruct( shaderStage ) {
+
+ const uniforms = this.uniforms[ shaderStage ];
+
+ const bufferStructMap = new Map();
+
+ uniforms.forEach( ( uniform ) => {
+
+ const { name, node } = uniform;
+ const hasBufferStruct = node.bufferStruct === true;
+ bufferStructMap.set( name, hasBufferStruct );
+
+ } );
+
+ return bufferStructMap;
+
+ }
+
+ getMembersFromCustomStruct( members ) {
+
+ const structMembers = members.map( ( { name, type, isAtomic } ) => {
+
+ let finalType = wgslTypeLib[ type ];
+
+ if ( ! finalType ) {
+
+ console.warn( `Unrecognized type: ${type}` );
+ finalType = 'vec4';
+
+ }
+
+ return `${name}: ${isAtomic ? `atomic<${finalType}>` : finalType}`;
+
+ } );
+
+ return `\t${structMembers.join( ',\n\t' )}`;
+
+ }
+
+ getCustomStructNameFromShader( source ) {
+
+ //This declarationRegexp can be adopted as a new standard in a separate PR and would no longer be necessary separately here.
+ //The difference is that the shader return type is optional
+
+ const declarationRegexp = /^[fn]*\s*([a-z_0-9]+)?\s*\(([\s\S]*?)\)\s*(?:->\s*([a-z_0-9]+(?:<[\s\S]+?>)?))?/i;
+ const parameterRegex = /([a-z_0-9]+)\s*:\s*(ptr<\s*([a-z_0-9]+),\s*(?:array<([a-z_0-9<>]+)>|([a-z_0-9]+))[^>]*>|[a-z_0-9<>,]+)/ig;
+
+ const results = [];
+
+ source = source.trim();
+
+ const declaration = source.match( declarationRegexp );
+
+ if ( declaration !== null && declaration.length === 4 ) {
+
+ let paramMatch;
+
+ while ( ( paramMatch = parameterRegex.exec( declaration[ 2 ] ) ) !== null ) {
+
+ const name = paramMatch[ 1 ];
+
+ const structName = paramMatch[ 4 ] || paramMatch[ 5 ]; //4 array, 5 single
+
+ if ( structName && ! Object.values( wgslTypeLib ).includes( structName ) ) {
+
+ results.push( { name, structName } );
+
+ }
+
+ }
+
+ }
+
+ return results;
+
+ }
+
getStructMembers( struct ) {
const snippets = [];
@@ -1171,12 +1248,20 @@ ${ flowData.code }
const bufferType = this.getType( bufferNode.bufferType );
const bufferCount = bufferNode.bufferCount;
+ let isArray = false;
+
+ if ( bufferNode.value && bufferNode.value.array && bufferNode.value.itemSize ) {
+
+ isArray = bufferType && bufferNode.value.array.length > bufferNode.value.itemSize;
+
+ }
+
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 bufferSnippet = bufferNode.bufferStruct ? this.getMembersFromCustomStruct( bufferType ) : `\t${ uniform.name } : array< ${ bufferTypeSnippet }${ bufferCountSnippet } >\n`;
const bufferAccessMode = bufferNode.isStorageBufferNode ? `storage, ${ this.getStorageAccess( bufferNode ) }` : 'uniform';
- bufferSnippets.push( this._getWGSLStructBinding( 'NodeBuffer_' + bufferNode.id, bufferSnippet, bufferAccessMode, uniformIndexes.binding ++, uniformIndexes.group ) );
+ bufferSnippets.push( this._getWGSLStructBinding( bufferNode.bufferStruct, isArray, 'NodeBuffer_' + bufferNode.id, bufferSnippet, bufferAccessMode, uniformIndexes.binding ++, uniformIndexes.group ) );
} else {
@@ -1199,7 +1284,7 @@ ${ flowData.code }
const group = uniformGroups[ name ];
- structSnippets.push( this._getWGSLStructBinding( name, group.snippets.join( ',\n' ), 'uniform', group.index, group.id ) );
+ structSnippets.push( this._getWGSLStructBinding( false, false, name, group.snippets.join( ',\n' ), 'uniform', group.index, group.id ) );
}
@@ -1228,9 +1313,69 @@ ${ flowData.code }
stageData.codes = this.getCodes( shaderStage );
stageData.directives = this.getDirectives( shaderStage );
stageData.scopedArrays = this.getScopedArrays( shaderStage );
+ stageData.isBufferStruct = this.getTypeFromCustomStruct( shaderStage );
+ stageData.customStructNames = this.getCustomStructNameFromShader( stageData.codes );
//
+ const reduceFlow = ( flow ) => {
+
+ return flow.replace( /&(\w+)\.(\w+)/g, ( match, bufferName, uniformName ) =>
+
+ stageData.isBufferStruct.get( uniformName ) === true ? `&${bufferName}` : match
+
+ );
+
+ };
+
+ const extractPointerNames = ( source ) => {
+
+ const match = source.match( /\(([^)]+)\)/ );
+ if ( ! match ) return [];
+
+ const content = match[ 1 ];
+
+ return content
+ .split( /\s*,\s*/ )
+ .map( part => part.trim() )
+ .filter( part => part.includes( '&' ) )
+ .map( part => part.replace( /&/g, '' ) )
+ .filter( part => ! part.includes( '.' ) );
+
+ };
+
+ const createStructNameMapping = ( nodeBuffers, structs ) => {
+
+ const resultMap = new Map();
+
+ for ( let i = 0; i < nodeBuffers.length; i ++ ) {
+
+ const bufferName = nodeBuffers[ i ];
+ const struct = structs[ i ];
+
+ resultMap.set( bufferName, struct.structName );
+
+ }
+
+ return resultMap;
+
+ };
+
+ const replaceStructNamesInUniforms = ( shaderCode, map ) => {
+
+ for ( const [ key, value ] of map.entries() ) {
+
+ const regex = new RegExp( `\\b${key}Struct\\b`, 'g' );
+ shaderCode = shaderCode.replace( regex, value );
+
+ }
+
+ return shaderCode;
+
+ };
+
+
+ let pointerNames, structnameMapping;
let flow = '// code\n\n';
flow += this.flowCode[ shaderStage ];
@@ -1255,6 +1400,13 @@ ${ flowData.code }
flow += `${ flowSlotData.code }\n\t`;
+
+
+ flow = reduceFlow( flow );
+ pointerNames = extractPointerNames( flow );
+ structnameMapping = createStructNameMapping( pointerNames, stageData.customStructNames );
+ stageData.uniforms = replaceStructNamesInUniforms( stageData.uniforms, structnameMapping );
+
if ( node === mainNode && shaderStage !== 'compute' ) {
flow += '// result\n\n\t';
@@ -1289,6 +1441,11 @@ ${ flowData.code }
}
+ flow = reduceFlow( flow );
+ pointerNames = extractPointerNames( flow );
+ structnameMapping = createStructNameMapping( pointerNames, stageData.customStructNames );
+ stageData.uniforms = replaceStructNamesInUniforms( stageData.uniforms, structnameMapping );
+
}
}
@@ -1493,14 +1650,15 @@ ${vars}
}
- _getWGSLStructBinding( name, vars, access, binding = 0, group = 0 ) {
+ _getWGSLStructBinding( isBufferStruct, isArray, name, vars, access, binding = 0, group = 0 ) {
const structName = name + 'Struct';
const structSnippet = this._getWGSLStruct( structName, vars );
+ const structName_ = isBufferStruct ? ( isArray ? `array<${structName}>` : structName ) : structName;
return `${structSnippet}
@binding( ${binding} ) @group( ${group} )
-var<${access}> ${name} : ${structName};`;
+var<${access}> ${name} : ${structName_};`;
}