Skip to content

Commit 52ef796

Browse files
committed
feat(WebGPU): add attribute id support to hardware selection
1 parent c192f04 commit 52ef796

5 files changed

Lines changed: 168 additions & 18 deletions

File tree

Sources/Rendering/WebGPU/BufferManager/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,13 @@ function vtkWebGPUBufferManager(publicAPI, model) {
317317
buffer.setStrideInBytes(
318318
vtkWebGPUTypes.getByteStrideFromBufferFormat(req.format)
319319
);
320-
buffer.setArrayInformation([{ offset: 0, format: req.format }]);
320+
buffer.setArrayInformation([
321+
{
322+
offset: 0,
323+
format: req.format,
324+
interpolation: req.interpolation,
325+
},
326+
]);
321327
}
322328

323329
buffer.setSourceTime(req.time);

Sources/Rendering/WebGPU/CellArrayMapper/index.js

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,26 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
425425
}
426426
};
427427

428+
publicAPI.isEdgePrimitive = () =>
429+
model.primitiveType === PrimitiveTypes.TriangleEdges ||
430+
model.primitiveType === PrimitiveTypes.TriangleStripEdges;
431+
432+
publicAPI.shouldSkipPass = () =>
433+
publicAPI.isEdgePrimitive() && (model.depthOnlyPass || model.selectionPass);
434+
428435
// Renders myself
429436
publicAPI.renderForPass = (renderEncoder, depthOnly = false) => {
430437
model.depthOnlyPass = depthOnly;
438+
model.selectionPass = renderEncoder?.getPipelineHash?.() === 'sel';
439+
if (publicAPI.shouldSkipPass()) {
440+
model.depthOnlyPass = false;
441+
model.selectionPass = false;
442+
return;
443+
}
431444
publicAPI.prepareToDraw(renderEncoder);
432445
model.renderEncoder.registerDrawCallback(model.pipeline, publicAPI.draw);
433446
model.depthOnlyPass = false;
447+
model.selectionPass = false;
434448
};
435449

436450
publicAPI.translucentPass = (prepass) => {
@@ -458,8 +472,10 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
458472
const ppty = actor.getProperty();
459473
const clippingPlanesMTime = model.renderable.getClippingPlanesMTime();
460474
const backfaceProperty = actor.getBackfaceProperty?.() ?? ppty;
475+
const selector = model.WebGPURenderer?.getSelector?.();
461476
const utime = model.UBO.getSendTime();
462477
if (
478+
!selector &&
463479
publicAPI.getMTime() <= utime &&
464480
ppty.getMTime() <= utime &&
465481
backfaceProperty.getMTime() <= utime &&
@@ -573,7 +589,13 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
573589
'Opacity',
574590
edgeLikeRepresentation ? ppty.getEdgeOpacity() : ppty.getOpacity()
575591
);
576-
model.UBO.setValue('PropID', model.WebGPUActor.getPropID());
592+
const runtimePropID = model.WebGPUActor.getPropID();
593+
model.UBO.setValue(
594+
'PropID',
595+
selector?.getPropIDForSelection
596+
? selector.getPropIDForSelection(runtimePropID, actor) + 1
597+
: runtimePropID
598+
);
577599
const cp = publicAPI.getCoincidentParameters();
578600
model.UBO.setValue('CoincidentFactor', cp.factor);
579601
model.UBO.setValue('CoincidentOffset', cp.offset);
@@ -1210,12 +1232,40 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
12101232

12111233
publicAPI.replaceShaderSelect = (hash, pipeline, vertexInput) => {
12121234
if (hash.includes('sel')) {
1235+
const selectBuffer = vertexInput.getBuffer('selectId');
1236+
if (selectBuffer) {
1237+
const vDesc = pipeline.getShaderDescription('vertex');
1238+
vDesc.addOutput(
1239+
'u32',
1240+
'attributeID',
1241+
selectBuffer.getArrayInformation()[0].interpolation
1242+
);
1243+
vDesc.addOutput(
1244+
'u32',
1245+
'compositeID',
1246+
selectBuffer.getArrayInformation()[0].interpolation
1247+
);
1248+
let code = vDesc.getCode();
1249+
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [
1250+
' output.compositeID = 1u;',
1251+
' output.attributeID = selectId + 1u;',
1252+
]).result;
1253+
vDesc.setCode(code);
1254+
}
1255+
12131256
const fDesc = pipeline.getShaderDescription('fragment');
12141257
let code = fDesc.getCode();
1215-
// by default there are no composites, so just 0
1216-
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [
1217-
' var compositeID: u32 = 0u;',
1218-
]).result;
1258+
const selectImpl = selectBuffer
1259+
? [
1260+
' var compositeID: u32 = input.compositeID;',
1261+
' var attributeID: u32 = input.attributeID;',
1262+
]
1263+
: [' var compositeID: u32 = 0u;', ' var attributeID: u32 = 0u;'];
1264+
code = vtkWebGPUShaderCache.substitute(
1265+
code,
1266+
'//VTK::Select::Impl',
1267+
selectImpl
1268+
).result;
12191269
fDesc.setCode(code);
12201270
}
12211271
};
@@ -1451,6 +1501,43 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
14511501
} else {
14521502
vertexInput.removeBufferIfPresent('tcoord');
14531503
}
1504+
1505+
// --- Selection IDs ---
1506+
const selector = model.WebGPURenderer?.getSelector?.();
1507+
if (selector && !edges && indexBuffer) {
1508+
let selectIds = null;
1509+
if (
1510+
selector.getFieldAssociation() ===
1511+
FieldAssociations.FIELD_ASSOCIATION_POINTS
1512+
) {
1513+
selectIds = indexBuffer.getFlatIdToPointId();
1514+
} else if (
1515+
selector.getFieldAssociation() ===
1516+
FieldAssociations.FIELD_ASSOCIATION_CELLS
1517+
) {
1518+
selectIds = indexBuffer.getFlatIdToCellId();
1519+
}
1520+
1521+
if (selectIds) {
1522+
vertexInput.addBuffer(
1523+
device.getBufferManager().getBuffer({
1524+
hash: `sel${selector.getFieldAssociation()}I${indexBuffer.getMTime()}`,
1525+
usage: BufferUsage.RawVertex,
1526+
format: 'uint32',
1527+
interpolation: 'flat',
1528+
nativeArray:
1529+
selectIds instanceof Uint32Array
1530+
? selectIds
1531+
: Uint32Array.from(selectIds),
1532+
}),
1533+
['selectId']
1534+
);
1535+
} else {
1536+
vertexInput.removeBufferIfPresent('selectId');
1537+
}
1538+
} else {
1539+
vertexInput.removeBufferIfPresent('selectId');
1540+
}
14541541
};
14551542

14561543
publicAPI.updateTextures = () => {
@@ -1573,6 +1660,9 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
15731660
if (model.depthOnlyPass) {
15741661
pipelineHash += 'd';
15751662
}
1663+
if (model.selectionPass) {
1664+
pipelineHash += 's';
1665+
}
15761666

15771667
if (
15781668
model.primitiveType === PrimitiveTypes.TriangleEdges ||
@@ -1651,6 +1741,7 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
16511741

16521742
const DEFAULT_VALUES = {
16531743
depthOnlyPass: false,
1744+
selectionPass: false,
16541745
is2D: false,
16551746
cellArray: null,
16561747
currentInput: null,

Sources/Rendering/WebGPU/Glyph3DMapper/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,20 @@ function vtkWebGPUGlyph3DCellArrayMapper(publicAPI, model) {
8686
publicAPI.replaceShaderSelect = (hash, pipeline, vertexInput) => {
8787
if (hash.includes('sel')) {
8888
const vDesc = pipeline.getShaderDescription('vertex');
89+
vDesc.addOutput('u32', 'attributeID', 'flat');
8990
vDesc.addOutput('u32', 'compositeID', 'flat');
9091
let code = vDesc.getCode();
9192
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [
92-
' output.compositeID = input.instanceIndex;',
93+
' output.compositeID = input.instanceIndex + 1u;',
94+
' output.attributeID = input.instanceIndex + 1u;',
9395
]).result;
9496
vDesc.setCode(code);
9597

9698
const fDesc = pipeline.getShaderDescription('fragment');
9799
code = fDesc.getCode();
98100
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [
99101
'var compositeID: u32 = input.compositeID;',
102+
'var attributeID: u32 = input.attributeID;',
100103
]).result;
101104
fDesc.setCode(code);
102105
}

Sources/Rendering/WebGPU/HardwareSelectionPass/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,22 @@ function vtkWebGPUHardwareSelectionPass(publicAPI, model) {
8787
const fDesc = pipeline.getShaderDescription('fragment');
8888
fDesc.addOutput('vec4<u32>', 'outColor');
8989
let code = fDesc.getCode();
90+
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Select::Impl', [
91+
' var compositeID: u32 = 0u;',
92+
' var attributeID: u32 = compositeID;',
93+
]).result;
9094
code = vtkWebGPUShaderCache.substitute(
9195
code,
9296
'//VTK::RenderEncoder::Impl',
93-
['output.outColor = vec4<u32>(mapperUBO.PropID, compositeID, 0u, 0u);']
97+
[
98+
// Selection buffer layout: [propID, compositeID, attributeID, unused]
99+
// propID and compositeID are already encoded with a +1 offset by the
100+
// mappers (0 = no data). attributeID is offset here (+1u) so that
101+
// a buffer value of 0 unambiguously means "no attribute data written",
102+
// since attributeID === 0 is a valid attribute index. The reader
103+
// (HardwareSelector) subtracts 1 when decoding.
104+
'output.outColor = vec4<u32>(mapperUBO.PropID, compositeID, attributeID + 1u, 0u);',
105+
]
94106
).result;
95107
fDesc.setCode(code);
96108
});

Sources/Rendering/WebGPU/HardwareSelector/index.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@ function getPixelInformationWithData(
4646
0
4747
);
4848

49-
if (actorid <= 0) {
49+
if (actorid <= 0 || actorid - 1 >= (buffdata.props?.length ?? 0)) {
5050
// the pixel did not hit any actor.
5151
return null;
5252
}
5353

5454
const info = {};
5555

56-
info.propID = actorid;
56+
info.propID = actorid - 1;
57+
info.prop = buffdata.props?.[info.propID];
5758

5859
let compositeID = convert(
5960
inDisplayPosition[0],
@@ -64,7 +65,21 @@ function getPixelInformationWithData(
6465
if (compositeID < 0 || compositeID > 0xffffff) {
6566
compositeID = 0;
6667
}
67-
info.compositeID = compositeID;
68+
info.compositeID = compositeID - 1;
69+
// attributeID is stored with a +1 offset in the selection buffer so that
70+
// 0 means "no data" while 0 remains a valid decoded attribute index.
71+
// A buffer value < 1 means no attribute was written, fall back to compositeID.
72+
// After fallback, subtract 1 to recover the original 0 based index.
73+
let attributeID = convert(
74+
inDisplayPosition[0],
75+
inDisplayPosition[1],
76+
buffdata,
77+
2
78+
);
79+
if (attributeID < 1) {
80+
attributeID = compositeID;
81+
}
82+
info.attributeID = attributeID - 1;
6883

6984
if (buffdata.captureZValues) {
7085
const offset =
@@ -179,9 +194,9 @@ function convertSelection(fieldassociation, dataMap, buffdata) {
179194
vtkErrorMacro('Unknown field association');
180195
}
181196
child.getProperties().propID = value.info.propID;
182-
const wprop = buffdata.webGPURenderer.getPropFromID(value.info.propID);
183-
child.getProperties().prop = wprop.getRenderable();
197+
child.getProperties().prop = value.info.prop;
184198
child.getProperties().compositeID = value.info.compositeID;
199+
child.getProperties().attributeID = value.info.attributeID;
185200
child.getProperties().pixelCount = value.pixelCount;
186201
if (buffdata.captureZValues) {
187202
child.getProperties().displayPosition = [
@@ -260,6 +275,17 @@ function vtkWebGPUHardwareSelector(publicAPI, model) {
260275
// Set our className
261276
model.classHierarchy.push('vtkWebGPUHardwareSelector');
262277

278+
publicAPI.getPropIDForSelection = (runtimePropID, prop = null) => {
279+
if (model._selectionPropMap.has(runtimePropID)) {
280+
return model._selectionPropMap.get(runtimePropID);
281+
}
282+
283+
const selectionPropID = model._selectionProps.length;
284+
model._selectionPropMap.set(runtimePropID, selectionPropID);
285+
model._selectionProps.push(prop);
286+
return selectionPropID;
287+
};
288+
263289
//----------------------------------------------------------------------------
264290
publicAPI.endSelection = () => {
265291
model.WebGPURenderer.setSelector(null);
@@ -296,12 +322,19 @@ function vtkWebGPUHardwareSelector(publicAPI, model) {
296322
// Initialize renderer for selection.
297323
// change the renderer's background to black, which will indicate a miss
298324
const originalSuppress = webGPURenderer.getSuppressClear();
325+
const originalSelector = webGPURenderer.getSelector();
326+
model._selectionPropMap.clear();
327+
model._selectionProps = [];
299328
webGPURenderer.setSuppressClear(true);
300-
301-
model._selectionPass.traverse(model._WebGPURenderWindow, webGPURenderer);
302-
303-
// restore original background
304-
webGPURenderer.setSuppressClear(originalSuppress);
329+
webGPURenderer.setSelector(publicAPI);
330+
331+
try {
332+
model._selectionPass.traverse(model._WebGPURenderWindow, webGPURenderer);
333+
} finally {
334+
// restore original renderer state
335+
webGPURenderer.setSelector(originalSelector);
336+
webGPURenderer.setSuppressClear(originalSuppress);
337+
}
305338

306339
const device = model._WebGPURenderWindow.getDevice();
307340
const texture = model._selectionPass.getColorTexture();
@@ -315,6 +348,7 @@ function vtkWebGPUHardwareSelector(publicAPI, model) {
315348
area: [0, 0, texture.getWidth() - 1, texture.getHeight() - 1],
316349
captureZValues: model.captureZValues,
317350
fieldAssociation: model.fieldAssociation,
351+
props: [...model._selectionProps],
318352
renderer,
319353
webGPURenderer,
320354
webGPURenderWindow: model._WebGPURenderWindow,
@@ -419,6 +453,8 @@ function vtkWebGPUHardwareSelector(publicAPI, model) {
419453

420454
const DEFAULT_VALUES = {
421455
// WebGPURenderWindow: null,
456+
_selectionPropMap: null,
457+
_selectionProps: null,
422458
};
423459

424460
// ----------------------------------------------------------------------------
@@ -430,6 +466,8 @@ export function extend(publicAPI, model, initialValues = {}) {
430466
vtkHardwareSelector.extend(publicAPI, model, initialValues);
431467

432468
model._selectionPass = vtkWebGPUHardwareSelectionPass.newInstance();
469+
model._selectionPropMap = new Map();
470+
model._selectionProps = [];
433471

434472
macro.setGet(publicAPI, model, ['_WebGPURenderWindow']);
435473
macro.moveToProtected(publicAPI, model, ['WebGPURenderWindow']);

0 commit comments

Comments
 (0)