Skip to content

Commit cfabfc4

Browse files
committed
feat(WebGPU): add clipping plane support for polydata, volume and image
Implement WebGPU clipping planes in CellArrayMapper, VolumePassFSQ and ImageMapper.
1 parent 920f0ce commit cfabfc4

6 files changed

Lines changed: 345 additions & 50 deletions

File tree

Sources/Rendering/Core/AbstractMapper/index.d.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { mat4 } from 'gl-matrix';
12
import { vtkAlgorithm, vtkObject } from '../../../interfaces';
23
import vtkPlane from '../../../Common/DataModel/Plane';
3-
import { mat4 } from 'gl-matrix';
4+
import { Vector4 } from '../../../types';
45

56
/**
67
*
@@ -31,6 +32,12 @@ export interface vtkAbstractMapper extends vtkAbstractMapperBase {
3132
*/
3233
getClippingPlanes(): vtkPlane[];
3334

35+
/**
36+
* Get the modified time of the clipping planes list.
37+
* @return {Number} The modified time.
38+
*/
39+
getClippingPlanesMTime(): number;
40+
3441
/**
3542
* Remove all clipping planes.
3643
* @return true if there were planes, false otherwise.
@@ -50,9 +57,25 @@ export interface vtkAbstractMapper extends vtkAbstractMapperBase {
5057
*/
5158
setClippingPlanes(planes: vtkPlane[]): void;
5259

60+
/**
61+
* Get the ith clipping plane transformed from world coordinates into the
62+
* target coordinate system defined by the provided world-to-coordinates
63+
* matrix.
64+
* @param {mat4} worldToCoords
65+
* @param {Number} i
66+
* @param {Number[]} [hnormal]
67+
*/
68+
getClippingPlaneInCoords(
69+
worldToCoords: mat4,
70+
i: number,
71+
hnormal?: Vector4 | Float64Array
72+
): Vector4 | Float64Array | undefined;
73+
5374
/**
5475
* Get the ith clipping plane as a homogeneous plane equation.
5576
* Use getNumberOfClippingPlanes() to get the number of planes.
77+
* This API expects a coordinates-to-world matrix and preserves the legacy
78+
* behavior used by existing data-coordinate callers.
5679
* @param {mat4} propMatrix
5780
* @param {Number} i
5881
* @param {Number[]} hnormal

Sources/Rendering/Core/AbstractMapper/index.js

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
1+
import { mat4, vec4 } from 'gl-matrix';
12
import macro from 'vtk.js/Sources/macros';
23

4+
const { vtkErrorMacro } = macro;
5+
36
// ----------------------------------------------------------------------------
47
// vtkAbstractMapper methods
58
// ----------------------------------------------------------------------------
69

10+
const tmpClipMatrix = new Float64Array(16);
11+
const tmpClipWorldPlane = new Float64Array(4);
12+
13+
function getClipPlaneEquation(plane, out) {
14+
const normal = plane.getNormalByReference();
15+
const origin = plane.getOriginByReference();
16+
17+
out[0] = normal[0];
18+
out[1] = normal[1];
19+
out[2] = normal[2];
20+
out[3] = -(
21+
normal[0] * origin[0] +
22+
normal[1] * origin[1] +
23+
normal[2] * origin[2]
24+
);
25+
26+
return out;
27+
}
28+
729
function vtkAbstractMapper(publicAPI, model) {
830
model.classHierarchy.push('vtkAbstractMapper');
931
publicAPI.update = () => {
@@ -45,6 +67,14 @@ function vtkAbstractMapper(publicAPI, model) {
4567

4668
publicAPI.getClippingPlanes = () => model.clippingPlanes;
4769

70+
publicAPI.getClippingPlanesMTime = () => {
71+
let mtime = 0;
72+
for (let i = 0; i < model.clippingPlanes.length; i++) {
73+
mtime = Math.max(mtime, model.clippingPlanes[i].getMTime());
74+
}
75+
return mtime;
76+
};
77+
4878
publicAPI.setClippingPlanes = (planes) => {
4979
if (!planes) {
5080
return;
@@ -59,34 +89,32 @@ function vtkAbstractMapper(publicAPI, model) {
5989
}
6090
};
6191

62-
publicAPI.getClippingPlaneInDataCoords = (propMatrix, i, hnormal) => {
92+
publicAPI.getClippingPlaneInCoords = (worldToCoords, i, hnormal) => {
93+
if (i < 0 || i >= model.clippingPlanes?.length) {
94+
vtkErrorMacro(`Clipping plane index ${i} is out of range.`);
95+
return undefined;
96+
}
97+
const outHNormal = hnormal || new Float64Array(4);
98+
getClipPlaneEquation(model.clippingPlanes[i], tmpClipWorldPlane);
99+
mat4.invert(tmpClipMatrix, worldToCoords);
100+
mat4.transpose(tmpClipMatrix, tmpClipMatrix);
101+
vec4.transformMat4(outHNormal, tmpClipWorldPlane, tmpClipMatrix);
102+
return outHNormal;
103+
};
104+
105+
publicAPI.getClippingPlaneInDataCoords = (coordsToWorld, i, hnormal) => {
63106
const clipPlanes = model.clippingPlanes;
64-
const mat = propMatrix;
65107

66108
if (clipPlanes) {
67109
const n = clipPlanes.length;
68110
if (i >= 0 && i < n) {
69-
// Get the plane
70-
const plane = clipPlanes[i];
71-
const normal = plane.getNormal();
72-
const origin = plane.getOrigin();
73-
74-
// Compute the plane equation
75-
const v1 = normal[0];
76-
const v2 = normal[1];
77-
const v3 = normal[2];
78-
const v4 = -(v1 * origin[0] + v2 * origin[1] + v3 * origin[2]);
79-
80-
// Transform normal from world to data coords
81-
hnormal[0] = v1 * mat[0] + v2 * mat[4] + v3 * mat[8] + v4 * mat[12];
82-
hnormal[1] = v1 * mat[1] + v2 * mat[5] + v3 * mat[9] + v4 * mat[13];
83-
hnormal[2] = v1 * mat[2] + v2 * mat[6] + v3 * mat[10] + v4 * mat[14];
84-
hnormal[3] = v1 * mat[3] + v2 * mat[7] + v3 * mat[11] + v4 * mat[15];
111+
getClipPlaneEquation(clipPlanes[i], tmpClipWorldPlane);
112+
vec4.transformMat4(hnormal, tmpClipWorldPlane, coordsToWorld);
85113

86114
return;
87115
}
88116
}
89-
macro.vtkErrorMacro(`Clipping plane index ${i} is out of range.`);
117+
vtkErrorMacro(`Clipping plane index ${i} is out of range.`);
90118
};
91119
}
92120

Sources/Rendering/WebGPU/CellArrayMapper/index.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache';
1111
import vtkWebGPUUniformBuffer from 'vtk.js/Sources/Rendering/WebGPU/UniformBuffer';
1212
import vtkWebGPUSimpleMapper from 'vtk.js/Sources/Rendering/WebGPU/SimpleMapper';
1313
import vtkWebGPUTypes from 'vtk.js/Sources/Rendering/WebGPU/Types';
14+
import {
15+
addClipPlaneEntries,
16+
getClippingPlaneEquationsInCoords,
17+
getClipPlaneShaderChecks,
18+
MAX_CLIPPING_PLANES,
19+
} from 'vtk.js/Sources/Rendering/WebGPU/Helpers/ClippingPlanes';
1420

1521
const { BufferUsage, PrimitiveTypes } = vtkWebGPUBufferManager;
1622
const { Representation } = vtkProperty;
1723
const { ScalarMode } = vtkMapper;
1824
const { CoordinateSystem } = vtkProp;
1925
const { DisplayLocation } = vtkProperty2D;
20-
2126
const vtkWebGPUPolyDataVS = `
2227
//VTK::Renderer::Dec
2328
@@ -380,6 +385,8 @@ fn main(
380385
}
381386
`;
382387

388+
const tmp2Mat4 = new Float64Array(16);
389+
383390
function isEdges(hash) {
384391
// edge pipelines have "edge" in them
385392
return hash.indexOf('edge') >= 0;
@@ -432,13 +439,15 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
432439
publicAPI.updateUBO = () => {
433440
const actor = model.WebGPUActor.getRenderable();
434441
const ppty = actor.getProperty();
442+
const clippingPlanesMTime = model.renderable.getClippingPlanesMTime();
435443
const backfaceProperty = actor.getBackfaceProperty?.() ?? ppty;
436444
const utime = model.UBO.getSendTime();
437445
if (
438446
publicAPI.getMTime() <= utime &&
439447
ppty.getMTime() <= utime &&
440448
backfaceProperty.getMTime() <= utime &&
441-
model.renderable.getMTime() <= utime
449+
model.renderable.getMTime() <= utime &&
450+
clippingPlanesMTime <= utime
442451
) {
443452
return;
444453
}
@@ -541,6 +550,28 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
541550
model.UBO.setValue('LineWidth', ppty.getLineWidth());
542551
model.UBO.setValue('Opacity', ppty.getOpacity());
543552
model.UBO.setValue('PropID', model.WebGPUActor.getPropID());
553+
model.UBO.setValue('NumClipPlanes', 0);
554+
555+
if (!model.is2D && model.useRendererMatrix) {
556+
const center = model.WebGPURenderer.getStabilizedCenterByReference();
557+
mat4.fromTranslation(tmp2Mat4, [
558+
-center[0],
559+
-center[1],
560+
-center[2],
561+
]);
562+
const numClipPlanes = getClippingPlaneEquationsInCoords(
563+
model.renderable,
564+
tmp2Mat4,
565+
model.clipPlanes
566+
);
567+
model.UBO.setValue('NumClipPlanes', numClipPlanes);
568+
569+
if (numClipPlanes > 0) {
570+
for (let i = 0; i < numClipPlanes; i++) {
571+
model.UBO.setArray(`ClipPlane${i}`, model.clipPlanes[i]);
572+
}
573+
}
574+
}
544575

545576
// Only send if needed
546577
model.UBO.sendIfNeeded(model.WebGPURenderWindow.getDevice());
@@ -586,9 +617,11 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
586617
const vDesc = pipeline.getShaderDescription('vertex');
587618
vDesc.addBuiltinOutput('vec4<f32>', '@builtin(position) Position');
588619
if (!vDesc.hasOutput('vertexVC')) vDesc.addOutput('vec4<f32>', 'vertexVC');
620+
if (!vDesc.hasOutput('vertexSC')) vDesc.addOutput('vec4<f32>', 'vertexSC');
589621
let code = vDesc.getCode();
590622
if (model.useRendererMatrix) {
591623
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [
624+
' output.vertexSC = mapperUBO.BCSCMatrix * vec4<f32>(vertexBC.xyz, 1.0);',
592625
' var pCoord: vec4<f32> = rendererUBO.SCPCMatrix*mapperUBO.BCSCMatrix*vertexBC;',
593626
' output.vertexVC = rendererUBO.SCVCMatrix * mapperUBO.BCSCMatrix * vec4<f32>(vertexBC.xyz, 1.0);',
594627
'//VTK::Position::Impl',
@@ -635,6 +668,19 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
635668
]).result;
636669

637670
vDesc.setCode(code);
671+
672+
const fDesc = pipeline.getShaderDescription('fragment');
673+
code = fDesc.getCode();
674+
const clipPlaneChecks = getClipPlaneShaderChecks({
675+
countName: 'mapperUBO.NumClipPlanes',
676+
planePrefix: 'mapperUBO.ClipPlane',
677+
positionName: 'input.vertexSC',
678+
});
679+
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [
680+
...clipPlaneChecks,
681+
'//VTK::Position::Impl',
682+
]).result;
683+
fDesc.setCode(code);
638684
};
639685
model.shaderReplacements.set(
640686
'replaceShaderPosition',
@@ -1451,7 +1497,6 @@ export function extend(publicAPI, model, initiaLalues = {}) {
14511497
model.vertexShaderTemplate = vtkWebGPUPolyDataVS;
14521498

14531499
model._tmpMat3 = mat3.identity(new Float64Array(9));
1454-
model._tmpMat4 = mat4.identity(new Float64Array(16));
14551500

14561501
// UBO
14571502
model.UBO = vtkWebGPUUniformBuffer.newInstance({ label: 'mapperUBO' });
@@ -1491,6 +1536,8 @@ export function extend(publicAPI, model, initiaLalues = {}) {
14911536
model.UBO.addEntry('ClipNear', 'f32');
14921537
model.UBO.addEntry('ClipFar', 'f32');
14931538
model.UBO.addEntry('Time', 'u32');
1539+
addClipPlaneEntries(model.UBO, 'ClipPlane');
1540+
model.UBO.addEntry('NumClipPlanes', 'u32');
14941541

14951542
// Build VTK API
14961543
macro.setGet(publicAPI, model, [
@@ -1503,6 +1550,9 @@ export function extend(publicAPI, model, initiaLalues = {}) {
15031550
]);
15041551

15051552
model.textures = [];
1553+
model.clipPlanes = Array.from({ length: MAX_CLIPPING_PLANES }, () => [
1554+
0.0, 0.0, 0.0, 0.0,
1555+
]);
15061556

15071557
// Object methods
15081558
vtkWebGPUCellArrayMapper(publicAPI, model);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export const MAX_CLIPPING_PLANES = 6;
2+
3+
export function addClipPlaneEntries(buffer, prefix) {
4+
for (let i = 0; i < MAX_CLIPPING_PLANES; i++) {
5+
buffer.addEntry(`${prefix}${i}`, 'vec4<f32>');
6+
}
7+
}
8+
9+
export function getClipPlaneShaderChecks({
10+
countName,
11+
planePrefix,
12+
positionName,
13+
returnValue = 'discard',
14+
}) {
15+
const checks = [];
16+
for (let i = 0; i < MAX_CLIPPING_PLANES; i++) {
17+
checks.push(
18+
` if (${countName} > ${i}u && dot(${planePrefix}${i}, ${positionName}) < 0.0) { ${returnValue}; }`
19+
);
20+
}
21+
return checks;
22+
}
23+
24+
export function getClippingPlaneEquationsInCoords(
25+
mapper,
26+
worldToCoords,
27+
outPlanes
28+
) {
29+
const count = mapper.getClippingPlanes().length;
30+
for (let i = 0; i < count; i++) {
31+
mapper.getClippingPlaneInCoords(worldToCoords, i, outPlanes[i]);
32+
}
33+
return count;
34+
}

0 commit comments

Comments
 (0)