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_};`; }