From e17e2addcbe27db1fcf43f5fa43bd5222823c255 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Wed, 22 Apr 2026 14:56:02 +0200 Subject: [PATCH 1/4] feat(WebGPU): support scalars at cells in CellArrayMapper --- .../Rendering/WebGPU/BufferManager/index.js | 23 +++++++- .../Rendering/WebGPU/CellArrayMapper/index.js | 56 ++++++++++++++++--- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/Sources/Rendering/WebGPU/BufferManager/index.js b/Sources/Rendering/WebGPU/BufferManager/index.js index 6a459cc2f6b..20bb7f092fc 100644 --- a/Sources/Rendering/WebGPU/BufferManager/index.js +++ b/Sources/Rendering/WebGPU/BufferManager/index.js @@ -103,8 +103,10 @@ function packArray(indexBuffer, inArrayData, numComp, outputType, options) { // pick the right function based on point versus cell data let flatIdMap = indexBuffer.getFlatIdToPointId(); + let flatIdOffset = 0; if (options.cellData) { flatIdMap = indexBuffer.getFlatIdToCellId(); + flatIdOffset = options.cellOffset || 0; } // add data based on number of components @@ -141,7 +143,7 @@ function packArray(indexBuffer, inArrayData, numComp, outputType, options) { // for each entry in the flat array process it for (let index = 0; index < flatSize; index++) { - const inArrayId = numComp * flatIdMap[index]; + const inArrayId = numComp * (flatIdMap[index] - flatIdOffset); addAPoint(inArrayId); } @@ -298,6 +300,7 @@ function vtkWebGPUBufferManager(publicAPI, model) { const normals = generateNormals(req.cells, req.dataArray); const result = packArray(req.indexBuffer, normals, 4, arrayType, { cellData: true, + cellOffset: req.cellOffset, }); buffer.createAndWrite(result.nativeArray, gpuUsage); buffer.setStrideInBytes( @@ -346,6 +349,24 @@ function vtkWebGPUBufferManager(publicAPI, model) { return publicAPI.getBuffer(buffRequest); }; + publicAPI.getBufferForCellArray = ( + dataArray, + indexBuffer, + cellOffset = 0 + ) => { + const format = _getFormatForDataArray(dataArray); + const buffRequest = { + hash: `cell${dataArray.getMTime()}I${indexBuffer.getMTime()}O${cellOffset}${format}`, + usage: BufferUsage.PointArray, + format, + dataArray, + indexBuffer, + cellData: true, + cellOffset, + }; + return publicAPI.getBuffer(buffRequest); + }; + publicAPI.getFullScreenQuadBuffer = () => { if (model.fullScreenQuadBuffer) { return model.fullScreenQuadBuffer; diff --git a/Sources/Rendering/WebGPU/CellArrayMapper/index.js b/Sources/Rendering/WebGPU/CellArrayMapper/index.js index 938adf7ce03..7540d209290 100644 --- a/Sources/Rendering/WebGPU/CellArrayMapper/index.js +++ b/Sources/Rendering/WebGPU/CellArrayMapper/index.js @@ -1004,6 +1004,28 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { return; } + // Check if using texture based coloring (color coordinates from mapper) + const useTextureColoring = + (model.renderable.getAreScalarsMappedFromCells() || + model.renderable.getInterpolateScalarsBeforeMapping?.()) && + model.renderable.getColorCoordinates() && + vertexInput.hasAttribute('tcoord') && + model.colorTexture; + + if (useTextureColoring) { + // Use texture sampling for colors (cell scalars or interpolated point scalars) + const fDesc = pipeline.getShaderDescription('fragment'); + let code = fDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Color::Impl', [ + 'var texColor = textureSample(DiffuseTexture, DiffuseTextureSampler, input.tcoordVS);', + 'diffuseColor = vec4(texColor.rgb, 1.0);', + 'ambientColor = vec4(texColor.rgb, 1.0);', + 'opacity = opacity * texColor.a;', + ]).result; + fDesc.setCode(code); + return; + } + // If there's no vertex color buffer return the shader as is const colorBuffer = vertexInput.getBuffer('colorVI'); if (!colorBuffer) return; @@ -1041,11 +1063,13 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { const vDesc = pipeline.getShaderDescription('vertex'); const tcoords = vertexInput.getBuffer('tcoord'); + const arrayInfo = tcoords.getArrayInformation()[0]; const numComp = vtkWebGPUTypes.getNumberOfComponentsFromBufferFormat( - tcoords.getArrayInformation()[0].format + arrayInfo.format ); + const interpolation = arrayInfo.interpolation; let code = vDesc.getCode(); - vDesc.addOutput(`vec${numComp}`, 'tcoordVS'); + vDesc.addOutput(`vec${numComp}`, 'tcoordVS', interpolation); code = vtkWebGPUShaderCache.substitute(code, '//VTK::TCoord::Impl', [ ' output.tcoordVS = tcoord;', ]).result; @@ -1241,12 +1265,15 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { let indexBuffer = null; if (cells) { indexBuffer = device.getBufferManager().getBuffer({ - hash: `R${representation}P${primType}${cells.getMTime()}`, + hash: `R${representation}P${primType}O${ + model.cellOffset + }${cells.getMTime()}`, usage: BufferUsage.Index, cells, numberOfPoints: points.getNumberOfPoints(), primitiveType: primType, representation, + cellOffset: model.cellOffset, }); vertexInput.setIndexBuffer(indexBuffer); } else { @@ -1321,6 +1348,7 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { buffRequest.dataArray = points; buffRequest.cells = cells; buffRequest.usage = BufferUsage.NormalsFromPoints; + buffRequest.cellOffset = model.cellOffset; vertexInput.addBuffer( device.getBufferManager().getBuffer(buffRequest), ['normalMC'] @@ -1350,11 +1378,13 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { device.getBufferManager().getBuffer({ usage: BufferUsage.PointArray, format: 'unorm8x4', - hash: `${haveCellScalars}${c.getMTime()}I${indexBuffer.getMTime()}unorm8x4`, + hash: `${haveCellScalars}${c.getMTime()}I${indexBuffer.getMTime()}O${ + model.cellOffset + }unorm8x4`, dataArray: c, indexBuffer, cellData: haveCellScalars, - cellOffset: 0, + cellOffset: model.cellOffset, }), ['colorVI'] ); @@ -1365,20 +1395,30 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { // --- Texture Coordinates --- let tcoords = null; + let useCellTCoords = false; if ( (model.renderable.getAreScalarsMappedFromCells() || model.renderable.getInterpolateScalarsBeforeMapping?.()) && model.renderable.getColorCoordinates() ) { tcoords = model.renderable.getColorCoordinates(); + useCellTCoords = model.renderable.getAreScalarsMappedFromCells(); } else { tcoords = pd.getPointData().getTCoords(); } if (tcoords && !edges) { vertexInput.addBuffer( - device - .getBufferManager() - .getBufferForPointArray(tcoords, vertexInput.getIndexBuffer()), + useCellTCoords + ? device + .getBufferManager() + .getBufferForCellArray( + tcoords, + vertexInput.getIndexBuffer(), + model.cellOffset + ) + : device + .getBufferManager() + .getBufferForPointArray(tcoords, vertexInput.getIndexBuffer()), ['tcoord'] ); } else { From 07198a227878ede741a62868e3fffd1f21131ea0 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Wed, 22 Apr 2026 15:10:36 +0200 Subject: [PATCH 2/4] feat(WebGPU): sample ORM/RM textures once --- Sources/Rendering/WebGPU/CellArrayMapper/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Rendering/WebGPU/CellArrayMapper/index.js b/Sources/Rendering/WebGPU/CellArrayMapper/index.js index 7540d209290..9cfc20a95d6 100644 --- a/Sources/Rendering/WebGPU/CellArrayMapper/index.js +++ b/Sources/Rendering/WebGPU/CellArrayMapper/index.js @@ -1119,16 +1119,18 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { if (ormTexture?.getImageLoaded()) { if (checkDims(ormTexture)) { usedTextures.push( - '_ambientOcclusionMap = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS).rrra;', - '_roughnessMap = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS).ggga;', - '_metallicMap = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS).bbba;' + 'let ormSample = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS);', + '_ambientOcclusionMap = ormSample.rrra;', + '_roughnessMap = ormSample.ggga;', + '_metallicMap = ormSample.bbba;' ); } } else if (rmTexture?.getImageLoaded()) { if (checkDims(rmTexture)) { usedTextures.push( - '_roughnessMap = textureSample(RMTexture, RMTextureSampler, input.tcoordVS).ggga;', - '_metallicMap = textureSample(RMTexture, RMTextureSampler, input.tcoordVS).bbba;' + 'let rmSample = textureSample(RMTexture, RMTextureSampler, input.tcoordVS);', + '_roughnessMap = rmSample.ggga;', + '_metallicMap = rmSample.bbba;' ); } } else { From c192f04e33c498a5419e1b717a36726d037f6050 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Wed, 22 Apr 2026 15:47:40 +0200 Subject: [PATCH 3/4] feat(WebGPU): Depth only / zbuffer pass support in CellArrayMapper --- .../Rendering/WebGPU/CellArrayMapper/index.js | 37 +++++++++++++++++-- .../Rendering/WebGPU/RenderEncoder/index.js | 9 +++-- Sources/Rendering/WebGPU/Renderer/index.js | 13 +++++++ .../WebGPU/ShaderDescription/index.js | 5 ++- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/Sources/Rendering/WebGPU/CellArrayMapper/index.js b/Sources/Rendering/WebGPU/CellArrayMapper/index.js index 9cfc20a95d6..0debea61070 100644 --- a/Sources/Rendering/WebGPU/CellArrayMapper/index.js +++ b/Sources/Rendering/WebGPU/CellArrayMapper/index.js @@ -426,20 +426,33 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { }; // Renders myself + publicAPI.renderForPass = (renderEncoder, depthOnly = false) => { + model.depthOnlyPass = depthOnly; + publicAPI.prepareToDraw(renderEncoder); + model.renderEncoder.registerDrawCallback(model.pipeline, publicAPI.draw); + model.depthOnlyPass = false; + }; + publicAPI.translucentPass = (prepass) => { if (prepass) { - publicAPI.prepareToDraw(model.WebGPURenderer.getRenderEncoder()); - model.renderEncoder.registerDrawCallback(model.pipeline, publicAPI.draw); + publicAPI.renderForPass(model.WebGPURenderer.getRenderEncoder()); } }; publicAPI.opaquePass = (prepass) => { if (prepass) { - publicAPI.prepareToDraw(model.WebGPURenderer.getRenderEncoder()); - model.renderEncoder.registerDrawCallback(model.pipeline, publicAPI.draw); + publicAPI.renderForPass(model.WebGPURenderer.getRenderEncoder()); } }; + publicAPI.zBufferPass = (prepass) => { + if (prepass) { + publicAPI.renderForPass(model.WebGPURenderer.getRenderEncoder(), true); + } + }; + + publicAPI.opaqueZBufferPass = (prepass) => publicAPI.zBufferPass(prepass); + publicAPI.updateUBO = () => { const actor = model.WebGPUActor.getRenderable(); const ppty = actor.getProperty(); @@ -621,6 +634,18 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { primitive: { cullMode: publicAPI.getCullMode(), }, + ...(model.depthOnlyPass + ? { + fragment: { + targets: [ + { + format: 'rgba16float', + writeMask: 0, + }, + ], + }, + } + : {}), }); publicAPI.getCoincidentParameters = () => { @@ -1545,6 +1570,9 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { let pipelineHash = `pd${model.useRendererMatrix ? 'r' : ''}${ model.forceZValue ? 'z' : '' }`; + if (model.depthOnlyPass) { + pipelineHash += 'd'; + } if ( model.primitiveType === PrimitiveTypes.TriangleEdges || @@ -1622,6 +1650,7 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { + depthOnlyPass: false, is2D: false, cellArray: null, currentInput: null, diff --git a/Sources/Rendering/WebGPU/RenderEncoder/index.js b/Sources/Rendering/WebGPU/RenderEncoder/index.js index 92f47619d3d..4a0faa2e29c 100644 --- a/Sources/Rendering/WebGPU/RenderEncoder/index.js +++ b/Sources/Rendering/WebGPU/RenderEncoder/index.js @@ -50,19 +50,20 @@ function vtkWebGPURenderEncoder(publicAPI, model) { } model.handle.setPipeline(pl.getHandle()); const pd = pl.getPipelineDescription(); + const fragmentTargets = pd.fragment?.targets; // check attachment state - if (model.colorTextureViews.length !== pd.fragment.targets.length) { + if (model.colorTextureViews.length !== fragmentTargets.length) { console.log( - `mismatched attachment counts on pipeline ${pd.fragment.targets.length} while encoder has ${model.colorTextureViews.length}` + `mismatched attachment counts on pipeline ${fragmentTargets.length} while encoder has ${model.colorTextureViews.length}` ); console.trace(); } else { for (let i = 0; i < model.colorTextureViews.length; i++) { const fmt = model.colorTextureViews[i].getTexture()?.getFormat(); - if (fmt && fmt !== pd.fragment.targets[i].format) { + if (fmt && fmt !== fragmentTargets[i].format) { console.log( - `mismatched attachments for attachment ${i} on pipeline ${pd.fragment.targets[i].format} while encoder has ${fmt}` + `mismatched attachments for attachment ${i} on pipeline ${fragmentTargets[i].format} while encoder has ${fmt}` ); console.trace(); } diff --git a/Sources/Rendering/WebGPU/Renderer/index.js b/Sources/Rendering/WebGPU/Renderer/index.js index a04ad8bdb19..8a55c49cf56 100644 --- a/Sources/Rendering/WebGPU/Renderer/index.js +++ b/Sources/Rendering/WebGPU/Renderer/index.js @@ -356,6 +356,19 @@ function vtkWebGPURenderer(publicAPI, model) { } }; + publicAPI.zBufferPass = (prepass) => { + if (prepass) { + model.renderEncoder.begin(model._parent.getCommandEncoder()); + publicAPI.updateUBO(); + publicAPI.updateSSBO(); + } else { + publicAPI.scissorAndViewport(model.renderEncoder); + model.renderEncoder.end(); + } + }; + + publicAPI.opaqueZBufferPass = (prepass) => publicAPI.zBufferPass(prepass); + publicAPI.clear = () => { if (model.renderable.getTransparent() || model.suppressClear) { return; diff --git a/Sources/Rendering/WebGPU/ShaderDescription/index.js b/Sources/Rendering/WebGPU/ShaderDescription/index.js index 35ae61e1632..c5116b9ee4e 100644 --- a/Sources/Rendering/WebGPU/ShaderDescription/index.js +++ b/Sources/Rendering/WebGPU/ShaderDescription/index.js @@ -77,7 +77,10 @@ function vtkWebGPUShaderDescription(publicAPI, model) { ).result; } - if (model.outputNames.length + model.builtinOutputNames.length) { + if ( + model.outputNames.length + model.builtinOutputNames.length || + model.code.includes('//VTK::IOStructs::Output') + ) { const outputStruct = [`struct ${model.type}Output\n{`]; for (let i = 0; i < model.outputNames.length; i++) { if (model.outputInterpolations[i] !== undefined) { From 1f677acd88eae28e6a5c62e060625f0fd26f3382 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Wed, 22 Apr 2026 15:58:46 +0200 Subject: [PATCH 4/4] feat(WebGPU): add attribute id support to hardware selection --- .../Rendering/WebGPU/BufferManager/index.js | 8 +- .../Rendering/WebGPU/CellArrayMapper/index.js | 101 +++++++++++++++++- .../Rendering/WebGPU/Glyph3DMapper/index.js | 5 +- .../WebGPU/HardwareSelectionPass/index.js | 14 ++- .../WebGPU/HardwareSelector/index.js | 58 ++++++++-- 5 files changed, 168 insertions(+), 18 deletions(-) diff --git a/Sources/Rendering/WebGPU/BufferManager/index.js b/Sources/Rendering/WebGPU/BufferManager/index.js index 20bb7f092fc..433e4b8ddf6 100644 --- a/Sources/Rendering/WebGPU/BufferManager/index.js +++ b/Sources/Rendering/WebGPU/BufferManager/index.js @@ -317,7 +317,13 @@ function vtkWebGPUBufferManager(publicAPI, model) { buffer.setStrideInBytes( vtkWebGPUTypes.getByteStrideFromBufferFormat(req.format) ); - buffer.setArrayInformation([{ offset: 0, format: req.format }]); + buffer.setArrayInformation([ + { + offset: 0, + format: req.format, + interpolation: req.interpolation, + }, + ]); } buffer.setSourceTime(req.time); diff --git a/Sources/Rendering/WebGPU/CellArrayMapper/index.js b/Sources/Rendering/WebGPU/CellArrayMapper/index.js index 0debea61070..5c4af2d74e7 100644 --- a/Sources/Rendering/WebGPU/CellArrayMapper/index.js +++ b/Sources/Rendering/WebGPU/CellArrayMapper/index.js @@ -425,12 +425,26 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { } }; + publicAPI.isEdgePrimitive = () => + model.primitiveType === PrimitiveTypes.TriangleEdges || + model.primitiveType === PrimitiveTypes.TriangleStripEdges; + + publicAPI.shouldSkipPass = () => + publicAPI.isEdgePrimitive() && (model.depthOnlyPass || model.selectionPass); + // Renders myself publicAPI.renderForPass = (renderEncoder, depthOnly = false) => { model.depthOnlyPass = depthOnly; + model.selectionPass = renderEncoder?.getPipelineHash?.() === 'sel'; + if (publicAPI.shouldSkipPass()) { + model.depthOnlyPass = false; + model.selectionPass = false; + return; + } publicAPI.prepareToDraw(renderEncoder); model.renderEncoder.registerDrawCallback(model.pipeline, publicAPI.draw); model.depthOnlyPass = false; + model.selectionPass = false; }; publicAPI.translucentPass = (prepass) => { @@ -458,8 +472,10 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { const ppty = actor.getProperty(); const clippingPlanesMTime = model.renderable.getClippingPlanesMTime(); const backfaceProperty = actor.getBackfaceProperty?.() ?? ppty; + const selector = model.WebGPURenderer?.getSelector?.(); const utime = model.UBO.getSendTime(); if ( + !selector && publicAPI.getMTime() <= utime && ppty.getMTime() <= utime && backfaceProperty.getMTime() <= utime && @@ -573,7 +589,13 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { 'Opacity', edgeLikeRepresentation ? ppty.getEdgeOpacity() : ppty.getOpacity() ); - model.UBO.setValue('PropID', model.WebGPUActor.getPropID()); + const runtimePropID = model.WebGPUActor.getPropID(); + model.UBO.setValue( + 'PropID', + selector?.getPropIDForSelection + ? selector.getPropIDForSelection(runtimePropID, actor) + 1 + : runtimePropID + ); const cp = publicAPI.getCoincidentParameters(); model.UBO.setValue('CoincidentFactor', cp.factor); model.UBO.setValue('CoincidentOffset', cp.offset); @@ -1210,12 +1232,40 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { publicAPI.replaceShaderSelect = (hash, pipeline, vertexInput) => { if (hash.includes('sel')) { + const selectBuffer = vertexInput.getBuffer('selectId'); + if (selectBuffer) { + const vDesc = pipeline.getShaderDescription('vertex'); + vDesc.addOutput( + 'u32', + 'attributeID', + selectBuffer.getArrayInformation()[0].interpolation + ); + vDesc.addOutput( + 'u32', + 'compositeID', + selectBuffer.getArrayInformation()[0].interpolation + ); + let code = vDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [ + ' output.compositeID = 1u;', + ' output.attributeID = selectId + 1u;', + ]).result; + vDesc.setCode(code); + } + const fDesc = pipeline.getShaderDescription('fragment'); let code = fDesc.getCode(); - // by default there are no composites, so just 0 - code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [ - ' var compositeID: u32 = 0u;', - ]).result; + const selectImpl = selectBuffer + ? [ + ' var compositeID: u32 = input.compositeID;', + ' var attributeID: u32 = input.attributeID;', + ] + : [' var compositeID: u32 = 0u;', ' var attributeID: u32 = 0u;']; + code = vtkWebGPUShaderCache.substitute( + code, + '//VTK::Select::Impl', + selectImpl + ).result; fDesc.setCode(code); } }; @@ -1451,6 +1501,43 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { } else { vertexInput.removeBufferIfPresent('tcoord'); } + + // --- Selection IDs --- + const selector = model.WebGPURenderer?.getSelector?.(); + if (selector && !edges && indexBuffer) { + let selectIds = null; + if ( + selector.getFieldAssociation() === + FieldAssociations.FIELD_ASSOCIATION_POINTS + ) { + selectIds = indexBuffer.getFlatIdToPointId(); + } else if ( + selector.getFieldAssociation() === + FieldAssociations.FIELD_ASSOCIATION_CELLS + ) { + selectIds = indexBuffer.getFlatIdToCellId(); + } + + if (selectIds) { + vertexInput.addBuffer( + device.getBufferManager().getBuffer({ + hash: `sel${selector.getFieldAssociation()}I${indexBuffer.getMTime()}`, + usage: BufferUsage.RawVertex, + format: 'uint32', + interpolation: 'flat', + nativeArray: + selectIds instanceof Uint32Array + ? selectIds + : Uint32Array.from(selectIds), + }), + ['selectId'] + ); + } else { + vertexInput.removeBufferIfPresent('selectId'); + } + } else { + vertexInput.removeBufferIfPresent('selectId'); + } }; publicAPI.updateTextures = () => { @@ -1573,6 +1660,9 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { if (model.depthOnlyPass) { pipelineHash += 'd'; } + if (model.selectionPass) { + pipelineHash += 's'; + } if ( model.primitiveType === PrimitiveTypes.TriangleEdges || @@ -1651,6 +1741,7 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) { const DEFAULT_VALUES = { depthOnlyPass: false, + selectionPass: false, is2D: false, cellArray: null, currentInput: null, diff --git a/Sources/Rendering/WebGPU/Glyph3DMapper/index.js b/Sources/Rendering/WebGPU/Glyph3DMapper/index.js index 6bc72ccd9a1..ca1db10a1c8 100644 --- a/Sources/Rendering/WebGPU/Glyph3DMapper/index.js +++ b/Sources/Rendering/WebGPU/Glyph3DMapper/index.js @@ -86,10 +86,12 @@ function vtkWebGPUGlyph3DCellArrayMapper(publicAPI, model) { publicAPI.replaceShaderSelect = (hash, pipeline, vertexInput) => { if (hash.includes('sel')) { const vDesc = pipeline.getShaderDescription('vertex'); + vDesc.addOutput('u32', 'attributeID', 'flat'); vDesc.addOutput('u32', 'compositeID', 'flat'); let code = vDesc.getCode(); code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [ - ' output.compositeID = input.instanceIndex;', + ' output.compositeID = input.instanceIndex + 1u;', + ' output.attributeID = input.instanceIndex + 1u;', ]).result; vDesc.setCode(code); @@ -97,6 +99,7 @@ function vtkWebGPUGlyph3DCellArrayMapper(publicAPI, model) { code = fDesc.getCode(); code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [ 'var compositeID: u32 = input.compositeID;', + 'var attributeID: u32 = input.attributeID;', ]).result; fDesc.setCode(code); } diff --git a/Sources/Rendering/WebGPU/HardwareSelectionPass/index.js b/Sources/Rendering/WebGPU/HardwareSelectionPass/index.js index 2cd6949d3a1..ca77c70addb 100644 --- a/Sources/Rendering/WebGPU/HardwareSelectionPass/index.js +++ b/Sources/Rendering/WebGPU/HardwareSelectionPass/index.js @@ -87,10 +87,22 @@ function vtkWebGPUHardwareSelectionPass(publicAPI, model) { const fDesc = pipeline.getShaderDescription('fragment'); fDesc.addOutput('vec4', 'outColor'); let code = fDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [ + ' var compositeID: u32 = 0u;', + ' var attributeID: u32 = compositeID;', + ]).result; code = vtkWebGPUShaderCache.substitute( code, '//VTK::RenderEncoder::Impl', - ['output.outColor = vec4(mapperUBO.PropID, compositeID, 0u, 0u);'] + [ + // Selection buffer layout: [propID, compositeID, attributeID, unused] + // propID and compositeID are already encoded with a +1 offset by the + // mappers (0 = no data). attributeID is offset here (+1u) so that + // a buffer value of 0 unambiguously means "no attribute data written", + // since attributeID === 0 is a valid attribute index. The reader + // (HardwareSelector) subtracts 1 when decoding. + 'output.outColor = vec4(mapperUBO.PropID, compositeID, attributeID + 1u, 0u);', + ] ).result; fDesc.setCode(code); }); diff --git a/Sources/Rendering/WebGPU/HardwareSelector/index.js b/Sources/Rendering/WebGPU/HardwareSelector/index.js index f89e45ae422..dea272af127 100644 --- a/Sources/Rendering/WebGPU/HardwareSelector/index.js +++ b/Sources/Rendering/WebGPU/HardwareSelector/index.js @@ -46,14 +46,15 @@ function getPixelInformationWithData( 0 ); - if (actorid <= 0) { + if (actorid <= 0 || actorid - 1 >= (buffdata.props?.length ?? 0)) { // the pixel did not hit any actor. return null; } const info = {}; - info.propID = actorid; + info.propID = actorid - 1; + info.prop = buffdata.props?.[info.propID]; let compositeID = convert( inDisplayPosition[0], @@ -64,7 +65,21 @@ function getPixelInformationWithData( if (compositeID < 0 || compositeID > 0xffffff) { compositeID = 0; } - info.compositeID = compositeID; + info.compositeID = compositeID - 1; + // attributeID is stored with a +1 offset in the selection buffer so that + // 0 means "no data" while 0 remains a valid decoded attribute index. + // A buffer value < 1 means no attribute was written, fall back to compositeID. + // After fallback, subtract 1 to recover the original 0 based index. + let attributeID = convert( + inDisplayPosition[0], + inDisplayPosition[1], + buffdata, + 2 + ); + if (attributeID < 1) { + attributeID = compositeID; + } + info.attributeID = attributeID - 1; if (buffdata.captureZValues) { const offset = @@ -179,9 +194,9 @@ function convertSelection(fieldassociation, dataMap, buffdata) { vtkErrorMacro('Unknown field association'); } child.getProperties().propID = value.info.propID; - const wprop = buffdata.webGPURenderer.getPropFromID(value.info.propID); - child.getProperties().prop = wprop.getRenderable(); + child.getProperties().prop = value.info.prop; child.getProperties().compositeID = value.info.compositeID; + child.getProperties().attributeID = value.info.attributeID; child.getProperties().pixelCount = value.pixelCount; if (buffdata.captureZValues) { child.getProperties().displayPosition = [ @@ -260,6 +275,17 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { // Set our className model.classHierarchy.push('vtkWebGPUHardwareSelector'); + publicAPI.getPropIDForSelection = (runtimePropID, prop = null) => { + if (model._selectionPropMap.has(runtimePropID)) { + return model._selectionPropMap.get(runtimePropID); + } + + const selectionPropID = model._selectionProps.length; + model._selectionPropMap.set(runtimePropID, selectionPropID); + model._selectionProps.push(prop); + return selectionPropID; + }; + //---------------------------------------------------------------------------- publicAPI.endSelection = () => { model.WebGPURenderer.setSelector(null); @@ -296,12 +322,19 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { // Initialize renderer for selection. // change the renderer's background to black, which will indicate a miss const originalSuppress = webGPURenderer.getSuppressClear(); + const originalSelector = webGPURenderer.getSelector(); + model._selectionPropMap.clear(); + model._selectionProps = []; webGPURenderer.setSuppressClear(true); - - model._selectionPass.traverse(model._WebGPURenderWindow, webGPURenderer); - - // restore original background - webGPURenderer.setSuppressClear(originalSuppress); + webGPURenderer.setSelector(publicAPI); + + try { + model._selectionPass.traverse(model._WebGPURenderWindow, webGPURenderer); + } finally { + // restore original renderer state + webGPURenderer.setSelector(originalSelector); + webGPURenderer.setSuppressClear(originalSuppress); + } const device = model._WebGPURenderWindow.getDevice(); const texture = model._selectionPass.getColorTexture(); @@ -315,6 +348,7 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { area: [0, 0, texture.getWidth() - 1, texture.getHeight() - 1], captureZValues: model.captureZValues, fieldAssociation: model.fieldAssociation, + props: [...model._selectionProps], renderer, webGPURenderer, webGPURenderWindow: model._WebGPURenderWindow, @@ -419,6 +453,8 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { const DEFAULT_VALUES = { // WebGPURenderWindow: null, + _selectionPropMap: null, + _selectionProps: null, }; // ---------------------------------------------------------------------------- @@ -430,6 +466,8 @@ export function extend(publicAPI, model, initialValues = {}) { vtkHardwareSelector.extend(publicAPI, model, initialValues); model._selectionPass = vtkWebGPUHardwareSelectionPass.newInstance(); + model._selectionPropMap = new Map(); + model._selectionProps = []; macro.setGet(publicAPI, model, ['_WebGPURenderWindow']); macro.moveToProtected(publicAPI, model, ['WebGPURenderWindow']);