diff --git a/Documentation/public/gallery/CutterMapper.jpg b/Documentation/public/gallery/CutterMapper.jpg new file mode 100644 index 00000000000..6a79ff7199a Binary files /dev/null and b/Documentation/public/gallery/CutterMapper.jpg differ diff --git a/Sources/Filters/Core/Cutter/example/index.js b/Sources/Filters/Core/Cutter/example/index.js index 8835e79d5c8..a2c5d2185c7 100644 --- a/Sources/Filters/Core/Cutter/example/index.js +++ b/Sources/Filters/Core/Cutter/example/index.js @@ -5,6 +5,7 @@ import '@kitware/vtk.js/Rendering/Profiles/Geometry'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkCutter from '@kitware/vtk.js/Filters/Core/Cutter'; +import vtkCutterMapper from '@kitware/vtk.js/Rendering/Core/CutterMapper'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import DataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper'; @@ -23,9 +24,7 @@ import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // Standard rendering code setup // ---------------------------------------------------------------------------- -const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ - background: [0, 0, 0], -}); +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); @@ -48,6 +47,17 @@ cutProperty.setLighting(false); cutProperty.setColor(0, 1, 0); renderer.addActor(cutActor); +const gpuCutMapper = vtkCutterMapper.newInstance({ + cutFunction: plane, + cutWidth: 2.0, +}); +const gpuCutActor = vtkActor.newInstance(); +gpuCutActor.setMapper(gpuCutMapper); +const gpuCutProperty = gpuCutActor.getProperty(); +gpuCutProperty.setLighting(false); +gpuCutProperty.setColor(1.0, 0.55, 0.15); +renderer.addActor(gpuCutActor); + const cubeMapper = vtkMapper.newInstance(); cubeMapper.setScalarVisibility(false); const cubeActor = vtkActor.newInstance(); @@ -63,6 +73,9 @@ renderer.addActor(cubeActor); // ----------------------------------------------------------- const state = { + showCPU: true, + showGPU: true, + gpuWidth: 2.0, originX: 0, originY: 0, originZ: 0, @@ -74,11 +87,38 @@ const state = { const updatePlaneFunction = () => { plane.setOrigin(state.originX, state.originY, state.originZ); plane.setNormal(state.normalX, state.normalY, state.normalZ); + cutActor.setVisibility(state.showCPU); + gpuCutActor.setVisibility(state.showGPU); + gpuCutMapper.setCutWidth(state.gpuWidth); renderWindow.render(); }; const gui = new GUI(); +gui + .add(state, 'showCPU') + .name('CPU vtkCutter') + .onChange((value) => { + state.showCPU = value; + updatePlaneFunction(); + }); + +gui + .add(state, 'showGPU') + .name('GPU vtkCutterMapper') + .onChange((value) => { + state.showGPU = value; + updatePlaneFunction(); + }); + +gui + .add(state, 'gpuWidth', 0.5, 5.0, 0.1) + .name('GPU vtkCutterMapper width') + .onChange((value) => { + state.gpuWidth = Number(value); + updatePlaneFunction(); + }); + const originFolder = gui.addFolder('Origin'); originFolder .add(state, 'originX', -6, 6, 0.01) @@ -144,6 +184,7 @@ HttpDataAccessHelper.fetchBinary( const source = sceneImporter.getScene()[0].source; cutter.setInputConnection(source.getOutputPort()); cubeMapper.setInputConnection(source.getOutputPort()); + gpuCutMapper.setInputConnection(source.getOutputPort()); renderer.resetCamera(); updatePlaneFunction(); }); diff --git a/Sources/Rendering/Core/CutterMapper/example/index.js b/Sources/Rendering/Core/CutterMapper/example/index.js new file mode 100644 index 00000000000..6ddd8a1dbfc --- /dev/null +++ b/Sources/Rendering/Core/CutterMapper/example/index.js @@ -0,0 +1,702 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; + +import GUI from 'lil-gui'; + +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkBox from '@kitware/vtk.js/Common/DataModel/Box'; +import vtkCone from '@kitware/vtk.js/Common/DataModel/Cone'; +import vtkCutterMapper from '@kitware/vtk.js/Rendering/Core/CutterMapper'; +import vtkCylinder from '@kitware/vtk.js/Common/DataModel/Cylinder'; +import vtkCylinderSource from '@kitware/vtk.js/Filters/Sources/CylinderSource'; +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; +import DataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper'; +import vtkHttpSceneLoader from '@kitware/vtk.js/IO/Core/HttpSceneLoader'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import vtkPlaneSource from '@kitware/vtk.js/Filters/Sources/PlaneSource'; +import vtkProperty from '@kitware/vtk.js/Rendering/Core/Property'; +import vtkSphere from '@kitware/vtk.js/Common/DataModel/Sphere'; +import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; +import vtkTransform from '@kitware/vtk.js/Common/Transform/Transform'; +import { IDENTITY } from '@kitware/vtk.js/Common/Core/Math/Constants'; +import vtkCubeSource from '@kitware/vtk.js/Filters/Sources/CubeSource'; +import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; + +// Force DataAccessHelper to have access to various data source +import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +const COLORS = { + cut: [0.0, 1.0, 0.0], + mesh: [0.72, 0.78, 0.86], + plane: [0.2, 0.75, 1.0], + sphere: [0.1, 0.55, 0.95], + box: [0.15, 0.85, 0.45], + cylinder: [1.0, 0.5, 0.15], + cone: [1.0, 0.82, 0.18], +}; + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const plane = vtkPlane.newInstance(); +const sphere = vtkSphere.newInstance(); +const box = vtkBox.newInstance(); +const cylinder = vtkCylinder.newInstance(); +const cone = vtkCone.newInstance(); +const coneTransform = vtkTransform.newInstance(); +cone.setTransform(coneTransform); + +const cutFunctions = { + plane, + sphere, + box, + cylinder, + cone, +}; + +const gpuCutMapper = vtkCutterMapper.newInstance({ + cutFunction: plane, + cutWidth: 0.5, +}); +const gpuCutActor = vtkActor.newInstance(); +gpuCutActor.setMapper(gpuCutMapper); +const gpuCutProperty = gpuCutActor.getProperty(); +gpuCutProperty.setLighting(false); +gpuCutProperty.setColor(...COLORS.cut); +renderer.addActor(gpuCutActor); + +const meshMapper = vtkMapper.newInstance(); +meshMapper.setScalarVisibility(false); +const meshActor = vtkActor.newInstance(); +meshActor.setMapper(meshMapper); +const meshProperty = meshActor.getProperty(); +meshProperty.setRepresentation(vtkProperty.Representation.WIREFRAME); +meshProperty.setLighting(false); +meshProperty.setColor(...COLORS.mesh); +meshProperty.setOpacity(0.1); +renderer.addActor(meshActor); + +const planeDebugSource = vtkPlaneSource.newInstance({ + xResolution: 1, + yResolution: 1, +}); +const planeDebugMapper = vtkMapper.newInstance(); +planeDebugMapper.setInputConnection(planeDebugSource.getOutputPort()); +const planeDebugActor = vtkActor.newInstance(); +planeDebugActor.setMapper(planeDebugMapper); +planeDebugActor.getProperty().setLighting(false); +planeDebugActor.getProperty().setColor(...COLORS.plane); +renderer.addActor(planeDebugActor); + +const sphereDebugSource = vtkSphereSource.newInstance({ + phiResolution: 48, + thetaResolution: 48, +}); +const sphereDebugMapper = vtkMapper.newInstance(); +sphereDebugMapper.setInputConnection(sphereDebugSource.getOutputPort()); +const sphereDebugActor = vtkActor.newInstance(); +sphereDebugActor.setMapper(sphereDebugMapper); +sphereDebugActor.getProperty().setLighting(false); +sphereDebugActor.getProperty().setColor(...COLORS.sphere); +renderer.addActor(sphereDebugActor); + +const boxDebugSource = vtkCubeSource.newInstance(); +const boxDebugMapper = vtkMapper.newInstance(); +boxDebugMapper.setInputConnection(boxDebugSource.getOutputPort()); +const boxDebugActor = vtkActor.newInstance(); +boxDebugActor.setMapper(boxDebugMapper); +boxDebugActor.getProperty().setLighting(false); +boxDebugActor.getProperty().setColor(...COLORS.box); +renderer.addActor(boxDebugActor); + +const cylinderDebugSource = vtkCylinderSource.newInstance({ + resolution: 64, + capping: false, +}); +const cylinderDebugMapper = vtkMapper.newInstance(); +cylinderDebugMapper.setInputConnection(cylinderDebugSource.getOutputPort()); +const cylinderDebugActor = vtkActor.newInstance(); +cylinderDebugActor.setMapper(cylinderDebugMapper); +cylinderDebugActor.getProperty().setLighting(false); +cylinderDebugActor.getProperty().setColor(...COLORS.cylinder); +renderer.addActor(cylinderDebugActor); + +const coneDebugSource = vtkConeSource.newInstance({ + resolution: 64, + capping: false, + direction: [1.0, 0.0, 0.0], +}); +const coneDebugMapper = vtkMapper.newInstance(); +coneDebugMapper.setInputConnection(coneDebugSource.getOutputPort()); +const coneDebugActor = vtkActor.newInstance(); +coneDebugActor.setMapper(coneDebugMapper); +coneDebugActor.getProperty().setLighting(false); +coneDebugActor.getProperty().setColor(...COLORS.cone); +renderer.addActor(coneDebugActor); + +const coneDebugSourceBack = vtkConeSource.newInstance({ + resolution: 64, + capping: false, + direction: [-1.0, 0.0, 0.0], +}); +const coneDebugMapperBack = vtkMapper.newInstance(); +coneDebugMapperBack.setInputConnection(coneDebugSourceBack.getOutputPort()); +const coneDebugActorBack = vtkActor.newInstance(); +coneDebugActorBack.setMapper(coneDebugMapperBack); +coneDebugActorBack.getProperty().setLighting(false); +coneDebugActorBack.getProperty().setColor(...COLORS.cone); +renderer.addActor(coneDebugActorBack); + +let modelBounds = [-1, 1, -1, 1, -1, 1]; +let modelLength = 2.5; +let modelCenter = [0.0, 0.0, 0.0]; +let modelHalfExtents = [1.0, 1.0, 1.0]; + +const state = { + showGPU: true, + gpuWidth: 0.5, + debugOpacity: 0.2, + cutType: 'plane', + planeOriginX: 0.0, + planeOriginY: 0.0, + planeOriginZ: 0.0, + planeNormalX: 1.0, + planeNormalY: 0.0, + planeNormalZ: 0.0, + sphereCenterX: 0.0, + sphereCenterY: 0.0, + sphereCenterZ: 0.0, + sphereRadius: 0.35, + boxCenterX: 0.0, + boxCenterY: 0.0, + boxCenterZ: 0.0, + boxSizeX: 0.6, + boxSizeY: 0.35, + boxSizeZ: 0.35, + cylinderCenterX: 0.0, + cylinderCenterY: 0.0, + cylinderCenterZ: 0.0, + cylinderAxisX: 1.0, + cylinderAxisY: 0.0, + cylinderAxisZ: 0.0, + cylinderRadius: 0.2, + coneCenterX: 0.0, + coneCenterY: 0.0, + coneCenterZ: 0.0, + coneAngle: 18.0, + coneRotateY: 0.0, + coneRotateZ: 0.0, +}; + +function normalizeVector3(x, y, z, fallback = [1.0, 0.0, 0.0]) { + const length = Math.hypot(x, y, z); + if (length <= 1e-6) { + return fallback; + } + return [x / length, y / length, z / length]; +} + +function cross(a, b) { + return [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ]; +} + +function getOffsetPosition(x, y, z) { + return [modelCenter[0] + x, modelCenter[1] + y, modelCenter[2] + z]; +} + +function updatePlane() { + const [nx, ny, nz] = normalizeVector3( + state.planeNormalX, + state.planeNormalY, + state.planeNormalZ + ); + const origin = getOffsetPosition( + state.planeOriginX, + state.planeOriginY, + state.planeOriginZ + ); + const normal = [nx, ny, nz]; + const tangentSeed = Math.abs(nz) < 0.9 ? [0.0, 0.0, 1.0] : [0.0, 1.0, 0.0]; + const tangent1 = normalizeVector3( + ...cross(normal, tangentSeed), + [1.0, 0.0, 0.0] + ); + const tangent2 = normalizeVector3( + ...cross(normal, tangent1), + [0.0, 1.0, 0.0] + ); + const halfSize = modelLength * 0.3; + plane.setOrigin(origin[0], origin[1], origin[2]); + plane.setNormal(nx, ny, nz); + planeDebugSource.setOrigin( + origin[0] - tangent1[0] * halfSize - tangent2[0] * halfSize, + origin[1] - tangent1[1] * halfSize - tangent2[1] * halfSize, + origin[2] - tangent1[2] * halfSize - tangent2[2] * halfSize + ); + planeDebugSource.setPoint1( + origin[0] + tangent1[0] * halfSize - tangent2[0] * halfSize, + origin[1] + tangent1[1] * halfSize - tangent2[1] * halfSize, + origin[2] + tangent1[2] * halfSize - tangent2[2] * halfSize + ); + planeDebugSource.setPoint2( + origin[0] - tangent1[0] * halfSize + tangent2[0] * halfSize, + origin[1] - tangent1[1] * halfSize + tangent2[1] * halfSize, + origin[2] - tangent1[2] * halfSize + tangent2[2] * halfSize + ); +} + +function updateSphere() { + const center = getOffsetPosition( + state.sphereCenterX, + state.sphereCenterY, + state.sphereCenterZ + ); + sphere.setCenter(center[0], center[1], center[2]); + sphere.setRadius(state.sphereRadius); + sphereDebugSource.setCenter(center[0], center[1], center[2]); + sphereDebugSource.setRadius(state.sphereRadius); +} + +function updateBox() { + const center = getOffsetPosition( + state.boxCenterX, + state.boxCenterY, + state.boxCenterZ + ); + const halfSizeX = state.boxSizeX / 2; + const halfSizeY = state.boxSizeY / 2; + const halfSizeZ = state.boxSizeZ / 2; + box.setBounds([ + center[0] - halfSizeX, + center[0] + halfSizeX, + center[1] - halfSizeY, + center[1] + halfSizeY, + center[2] - halfSizeZ, + center[2] + halfSizeZ, + ]); + boxDebugSource.setBounds([ + center[0] - halfSizeX, + center[0] + halfSizeX, + center[1] - halfSizeY, + center[1] + halfSizeY, + center[2] - halfSizeZ, + center[2] + halfSizeZ, + ]); +} + +function updateCylinder() { + const [axisX, axisY, axisZ] = normalizeVector3( + state.cylinderAxisX, + state.cylinderAxisY, + state.cylinderAxisZ + ); + const center = getOffsetPosition( + state.cylinderCenterX, + state.cylinderCenterY, + state.cylinderCenterZ + ); + cylinder.setCenter(center[0], center[1], center[2]); + cylinder.setAxis(axisX, axisY, axisZ); + cylinder.setRadius(state.cylinderRadius); + + cylinderDebugSource.setCenter(center[0], center[1], center[2]); + cylinderDebugSource.setRadius(state.cylinderRadius); + cylinderDebugSource.setHeight(modelLength * 1.2); + cylinderDebugSource.setDirection(axisX, axisY, axisZ); +} + +function updateCone() { + cone.setAngle(state.coneAngle); + const coneCenterX = modelCenter[0] + state.coneCenterX; + const coneCenterY = modelCenter[1] + state.coneCenterY; + const coneCenterZ = modelCenter[2] + state.coneCenterZ; + coneTransform.setMatrix(IDENTITY); + coneTransform.translate(-coneCenterX, -coneCenterY, -coneCenterZ); + coneTransform.rotateY(-state.coneRotateY); + coneTransform.rotateZ(-state.coneRotateZ); + + const coneHeight = modelLength * 0.7; + // vtkCone is an infinite double cone with its apex at the local origin. + // Each debug vtkConeSource spans from that apex to a base located one + // coneHeight away along the axis, so the base radius must use the full + // apex-to-base distance. + const coneRadius = coneHeight * Math.tan((state.coneAngle * Math.PI) / 180.0); + const [dirX, dirY, dirZ] = normalizeVector3( + Math.cos((state.coneRotateY * Math.PI) / 180.0) * + Math.cos((state.coneRotateZ * Math.PI) / 180.0), + Math.sin((state.coneRotateZ * Math.PI) / 180.0), + Math.sin((state.coneRotateY * Math.PI) / 180.0) * + Math.cos((state.coneRotateZ * Math.PI) / 180.0) + ); + const halfHeight = coneHeight / 2.0; + coneDebugSource.setHeight(coneHeight); + coneDebugSource.setRadius(coneRadius); + coneDebugSource.setCenter( + coneCenterX - dirX * halfHeight, + coneCenterY - dirY * halfHeight, + coneCenterZ - dirZ * halfHeight + ); + coneDebugSource.setDirection(dirX, dirY, dirZ); + coneDebugSourceBack.setHeight(coneHeight); + coneDebugSourceBack.setRadius(coneRadius); + coneDebugSourceBack.setCenter( + coneCenterX + dirX * halfHeight, + coneCenterY + dirY * halfHeight, + coneCenterZ + dirZ * halfHeight + ); + coneDebugSourceBack.setDirection(-dirX, -dirY, -dirZ); +} + +function updateDebugActors() { + planeDebugActor.setVisibility(state.cutType === 'plane' ? 1 : 0); + sphereDebugActor.setVisibility(state.cutType === 'sphere' ? 1 : 0); + boxDebugActor.setVisibility(state.cutType === 'box' ? 1 : 0); + cylinderDebugActor.setVisibility(state.cutType === 'cylinder' ? 1 : 0); + coneDebugActor.setVisibility(state.cutType === 'cone' ? 1 : 0); + coneDebugActorBack.setVisibility(state.cutType === 'cone' ? 1 : 0); + + [ + planeDebugActor, + sphereDebugActor, + boxDebugActor, + cylinderDebugActor, + coneDebugActor, + coneDebugActorBack, + ].forEach((actor) => { + const property = actor.getProperty(); + property.setOpacity(state.debugOpacity); + property.setRepresentation(vtkProperty.Representation.SURFACE); + }); +} + +function updateCutFunctions() { + updatePlane(); + updateSphere(); + updateBox(); + updateCylinder(); + updateCone(); +} + +function updateScene() { + updateCutFunctions(); + + const cutFunction = cutFunctions[state.cutType]; + gpuCutMapper.setCutFunction(cutFunction); + gpuCutMapper.setCutWidth(state.gpuWidth); + + gpuCutActor.setVisibility(state.showGPU); + updateDebugActors(); + renderWindow.render(); +} + +const gui = new GUI(); +const typeFolders = {}; +const positionControllers = []; +let sphereRadiusController = null; +let cylinderRadiusController = null; +let boxSizeXController = null; +let boxSizeYController = null; +let boxSizeZController = null; + +function addPositionController(folder, key, axisIndex, label) { + const controller = folder + .add( + state, + key, + -modelHalfExtents[axisIndex], + modelHalfExtents[axisIndex], + 0.01 + ) + .name(label) + .onChange((value) => { + state[key] = Number(value); + updateScene(); + }); + positionControllers.push({ controller, axisIndex }); + return controller; +} + +function updatePositionControllerRanges() { + positionControllers.forEach(({ controller, axisIndex }) => { + controller.min(-modelHalfExtents[axisIndex]); + controller.max(modelHalfExtents[axisIndex]); + controller.updateDisplay(); + }); +} + +function setFolderVisibility(folder, visible) { + if (folder?.domElement) { + folder.domElement.style.display = visible ? '' : 'none'; + } +} + +function updateVisibleTypeSettings() { + Object.entries(typeFolders).forEach(([type, folder]) => { + setFolderVisibility(folder, type === state.cutType); + }); +} + +gui + .add(state, 'cutType', Object.keys(cutFunctions)) + .name('Cut type') + .onChange((value) => { + state.cutType = value; + updateVisibleTypeSettings(); + updateScene(); + }); + +gui + .add(state, 'gpuWidth', 0.5, 5.0, 0.1) + .name('Cut width') + .onChange((value) => { + state.gpuWidth = Number(value); + updateScene(); + }); + +gui + .add(state, 'showGPU') + .name('Cut rendering') + .onChange((value) => { + state.showGPU = value; + updateScene(); + }); + +gui + .add(state, 'debugOpacity', 0.0, 1.0, 0.01) + .name('Opacity') + .onChange((value) => { + state.debugOpacity = Number(value); + updateScene(); + }); + +const planeFolder = gui.addFolder('Plane'); +typeFolders.plane = planeFolder; +addPositionController(planeFolder, 'planeOriginX', 0, 'Offset X'); +addPositionController(planeFolder, 'planeOriginY', 1, 'Offset Y'); +addPositionController(planeFolder, 'planeOriginZ', 2, 'Offset Z'); +planeFolder + .add(state, 'planeNormalX', -1.0, 1.0, 0.01) + .name('Normal X') + .onChange((value) => { + state.planeNormalX = Number(value); + updateScene(); + }); +planeFolder + .add(state, 'planeNormalY', -1.0, 1.0, 0.01) + .name('Normal Y') + .onChange((value) => { + state.planeNormalY = Number(value); + updateScene(); + }); +planeFolder + .add(state, 'planeNormalZ', -1.0, 1.0, 0.01) + .name('Normal Z') + .onChange((value) => { + state.planeNormalZ = Number(value); + updateScene(); + }); + +const sphereFolder = gui.addFolder('Sphere'); +typeFolders.sphere = sphereFolder; +addPositionController(sphereFolder, 'sphereCenterX', 0, 'Offset X'); +addPositionController(sphereFolder, 'sphereCenterY', 1, 'Offset Y'); +addPositionController(sphereFolder, 'sphereCenterZ', 2, 'Offset Z'); +sphereRadiusController = sphereFolder + .add(state, 'sphereRadius', 0.05, 1.0, 0.01) + .name('Radius'); +sphereRadiusController.onChange((value) => { + state.sphereRadius = Number(value); + updateScene(); +}); + +const boxFolder = gui.addFolder('Box'); +typeFolders.box = boxFolder; +addPositionController(boxFolder, 'boxCenterX', 0, 'Offset X'); +addPositionController(boxFolder, 'boxCenterY', 1, 'Offset Y'); +addPositionController(boxFolder, 'boxCenterZ', 2, 'Offset Z'); +boxSizeXController = boxFolder + .add(state, 'boxSizeX', 0.05, 1.5, 0.01) + .name('Size X'); +boxSizeXController.onChange((value) => { + state.boxSizeX = Number(value); + updateScene(); +}); +boxSizeYController = boxFolder + .add(state, 'boxSizeY', 0.05, 1.5, 0.01) + .name('Size Y'); +boxSizeYController.onChange((value) => { + state.boxSizeY = Number(value); + updateScene(); +}); +boxSizeZController = boxFolder + .add(state, 'boxSizeZ', 0.05, 1.5, 0.01) + .name('Size Z'); +boxSizeZController.onChange((value) => { + state.boxSizeZ = Number(value); + updateScene(); +}); + +const cylinderFolder = gui.addFolder('Cylinder'); +typeFolders.cylinder = cylinderFolder; +addPositionController(cylinderFolder, 'cylinderCenterX', 0, 'Offset X'); +addPositionController(cylinderFolder, 'cylinderCenterY', 1, 'Offset Y'); +addPositionController(cylinderFolder, 'cylinderCenterZ', 2, 'Offset Z'); +cylinderFolder + .add(state, 'cylinderAxisX', -1.0, 1.0, 0.01) + .name('Axis X') + .onChange((value) => { + state.cylinderAxisX = Number(value); + updateScene(); + }); +cylinderFolder + .add(state, 'cylinderAxisY', -1.0, 1.0, 0.01) + .name('Axis Y') + .onChange((value) => { + state.cylinderAxisY = Number(value); + updateScene(); + }); +cylinderFolder + .add(state, 'cylinderAxisZ', -1.0, 1.0, 0.01) + .name('Axis Z') + .onChange((value) => { + state.cylinderAxisZ = Number(value); + updateScene(); + }); +cylinderRadiusController = cylinderFolder + .add(state, 'cylinderRadius', 0.05, 1.0, 0.01) + .name('Radius'); +cylinderRadiusController.onChange((value) => { + state.cylinderRadius = Number(value); + updateScene(); +}); + +const coneFolder = gui.addFolder('Cone'); +typeFolders.cone = coneFolder; +addPositionController(coneFolder, 'coneCenterX', 0, 'Offset X'); +addPositionController(coneFolder, 'coneCenterY', 1, 'Offset Y'); +addPositionController(coneFolder, 'coneCenterZ', 2, 'Offset Z'); +coneFolder + .add(state, 'coneAngle', 5.0, 60.0, 0.5) + .name('Angle') + .onChange((value) => { + state.coneAngle = Number(value); + updateScene(); + }); +coneFolder + .add(state, 'coneRotateY', -180.0, 180.0, 1.0) + .name('Rotate Y') + .onChange((value) => { + state.coneRotateY = Number(value); + updateScene(); + }); +coneFolder + .add(state, 'coneRotateZ', -180.0, 180.0, 1.0) + .name('Rotate Z') + .onChange((value) => { + state.coneRotateZ = Number(value); + updateScene(); + }); + +updateVisibleTypeSettings(); + +HttpDataAccessHelper.fetchBinary( + `${__BASE_PATH__}/data/StanfordDragon.vtkjs`, + {} +).then((zipContent) => { + const dataAccessHelper = DataAccessHelper.get('zip', { + zipContent, + callback: () => { + const sceneImporter = vtkHttpSceneLoader.newInstance({ + renderer, + dataAccessHelper, + }); + sceneImporter.setUrl('index.json'); + sceneImporter.onReady(() => { + sceneImporter.getScene()[0].actor.setVisibility(false); + + const source = sceneImporter.getScene()[0].source; + meshMapper.setInputConnection(source.getOutputPort()); + gpuCutMapper.setInputConnection(source.getOutputPort()); + const inputData = source.getOutputData(); + if (inputData?.getBounds) { + modelBounds = inputData.getBounds(); + const dx = modelBounds[1] - modelBounds[0]; + const dy = modelBounds[3] - modelBounds[2]; + const dz = modelBounds[5] - modelBounds[4]; + modelLength = Math.hypot(dx, dy, dz); + const centerX = (modelBounds[0] + modelBounds[1]) / 2; + const centerY = (modelBounds[2] + modelBounds[3]) / 2; + const centerZ = (modelBounds[4] + modelBounds[5]) / 2; + modelCenter = [centerX, centerY, centerZ]; + modelHalfExtents = [ + Math.max(dx / 2, 0.01), + Math.max(dy / 2, 0.01), + Math.max(dz / 2, 0.01), + ]; + const maxSpan = Math.max(dx, dy, dz); + + state.planeOriginX = 0.0; + state.planeOriginY = 0.0; + state.planeOriginZ = 0.0; + state.sphereCenterX = 0.0; + state.sphereCenterY = 0.0; + state.sphereCenterZ = 0.0; + state.sphereRadius = maxSpan * 0.22; + if (sphereRadiusController) { + sphereRadiusController.min(maxSpan * 0.02); + sphereRadiusController.max(maxSpan * 0.6); + sphereRadiusController.updateDisplay(); + } + state.boxCenterX = 0.0; + state.boxCenterY = 0.0; + state.boxCenterZ = 0.0; + state.boxSizeX = maxSpan * 0.42; + state.boxSizeY = maxSpan * 0.42; + state.boxSizeZ = maxSpan * 0.42; + [boxSizeXController, boxSizeYController, boxSizeZController].forEach( + (controller) => { + if (controller) { + controller.min(maxSpan * 0.02); + controller.max(maxSpan * 0.8); + controller.updateDisplay(); + } + } + ); + state.cylinderCenterX = 0.0; + state.cylinderCenterY = 0.0; + state.cylinderCenterZ = 0.0; + state.cylinderRadius = maxSpan * 0.18; + if (cylinderRadiusController) { + cylinderRadiusController.min(maxSpan * 0.02); + cylinderRadiusController.max(maxSpan * 0.5); + cylinderRadiusController.updateDisplay(); + } + state.coneCenterX = 0.0; + state.coneCenterY = 0.0; + state.coneCenterZ = 0.0; + state.coneAngle = 28.0; + updatePositionControllerRanges(); + } + renderer.resetCamera(); + updateScene(); + }); + }, + }); +}); diff --git a/Sources/Rendering/Core/CutterMapper/index.d.ts b/Sources/Rendering/Core/CutterMapper/index.d.ts new file mode 100644 index 00000000000..7af56454fc4 --- /dev/null +++ b/Sources/Rendering/Core/CutterMapper/index.d.ts @@ -0,0 +1,98 @@ +import vtkImplicitFunction from '../../../Common/DataModel/ImplicitFunction'; +import vtkMapper, { IMapperInitialValues } from '../Mapper'; +import { Nullable } from '../../../types'; + +export interface ICutterMapperInitialValues extends IMapperInitialValues { + /** Implicit function evaluated by the GPU cut shader. */ + cutFunction?: Nullable; + /** Offset applied to the implicit function isosurface. */ + cutValue?: number; + /** Screen-space thickness multiplier for the displayed cut contour. */ + cutWidth?: number; +} + +export interface vtkCutterMapper extends vtkMapper { + /** + * Returns the current implicit cut function. + */ + getCutFunction(): Nullable; + + /** + * Returns the scalar offset applied to the implicit function. + * + * For a plane this acts as an additional displacement along the plane normal. + */ + getCutValue(): number; + + /** + * Returns the screen-space cut width multiplier used by the shader. + */ + getCutWidth(): number; + + /** + * Sets the implicit function used to generate the cut on the GPU. + * + * Supported implicit functions are currently plane, sphere, box, cylinder, + * and cone. + */ + setCutFunction(cutFunction: Nullable): boolean; + + /** + * Sets the scalar offset applied to the implicit function. + * + * For a plane, changing the plane origin only changes the cut when the + * displacement has a component along the plane normal. Moving the origin + * parallel to the plane describes the same plane in 3D, so the cut does not + * move. `cutValue` can also be used to offset the plane along its normal. + */ + setCutValue(cutValue: number): boolean; + + /** + * Sets the screen-space cut width multiplier used by the shader. + */ + setCutWidth(cutWidth: number): boolean; + + /** + * Returns the VTK.js class name of the supported implicit function currently in + * use, or `null` when the cut function is missing or unsupported. + */ + getSupportedImplicitFunctionName(): Nullable; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkCutterMapper characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {ICutterMapperInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: ICutterMapperInitialValues +): void; + +/** + * Method used to create a new instance of vtkCutterMapper + * @param {ICutterMapperInitialValues} initialValues for pre-setting some of its content + */ +export function newInstance( + initialValues?: ICutterMapperInitialValues +): vtkCutterMapper; + +/** + * vtkCutterMapper renders the zero-isosurface of a supported implicit + * function on the GPU. Supported implicit functions are currently plane, + * sphere, box, cylinder, and cone. + * + * - For a plane, only displacement along the plane normal changes the cut. + * Moving the plane origin parallel to the plane still describes the same + * plane in 3D, so the cut does not move. + * - For a sphere, box, cylinder, or cone, translating the center changes the + * position of the implicit function, so the cut follows that translation. + */ +export declare const vtkCutterMapper: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkCutterMapper; diff --git a/Sources/Rendering/Core/CutterMapper/index.js b/Sources/Rendering/Core/CutterMapper/index.js new file mode 100644 index 00000000000..15ceb20536e --- /dev/null +++ b/Sources/Rendering/Core/CutterMapper/index.js @@ -0,0 +1,76 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; + +const SUPPORTED_IMPLICIT_FUNCTIONS = [ + 'vtkPlane', + 'vtkSphere', + 'vtkBox', + 'vtkCylinder', + 'vtkCone', +]; + +// ---------------------------------------------------------------------------- +// vtkCutterMapper methods +// ---------------------------------------------------------------------------- + +function vtkCutterMapper(publicAPI, model) { + model.classHierarchy.push('vtkCutterMapper'); + + const superClass = { ...publicAPI }; + + publicAPI.getMTime = () => { + let mTime = superClass.getMTime(); + if (model.cutFunction) { + mTime = Math.max(mTime, model.cutFunction.getMTime()); + const transform = model.cutFunction.getTransform?.(); + if (transform?.getMTime) { + mTime = Math.max(mTime, transform.getMTime()); + } + } + return mTime; + }; + + publicAPI.getSupportedImplicitFunctionName = () => { + const cutFunction = model.cutFunction; + if (!cutFunction || !cutFunction.isA) { + return null; + } + + return ( + SUPPORTED_IMPLICIT_FUNCTIONS.find((className) => + cutFunction.isA(className) + ) || null + ); + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + cutFunction: null, + cutValue: 0.0, + cutWidth: 1.5, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + vtkMapper.extend(publicAPI, model, initialValues); + publicAPI.setScalarVisibility(false); + + macro.setGet(publicAPI, model, ['cutFunction', 'cutValue', 'cutWidth']); + + vtkCutterMapper(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkCutterMapper'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Rendering/Core/index.js b/Sources/Rendering/Core/index.js index efd6baf6dbe..ecdb09bd718 100644 --- a/Sources/Rendering/Core/index.js +++ b/Sources/Rendering/Core/index.js @@ -13,6 +13,7 @@ import vtkCamera from './Camera'; import vtkCellPicker from './CellPicker'; import vtkColorTransferFunction from './ColorTransferFunction'; import vtkCoordinate from './Coordinate'; +import vtkCutterMapper from './CutterMapper'; import vtkCubeAxesActor from './CubeAxesActor'; import vtkFollower from './Follower'; import vtkGlyph3DMapper from './Glyph3DMapper'; @@ -61,6 +62,7 @@ export default { vtkCellPicker, vtkColorTransferFunction: { vtkColorMaps, ...vtkColorTransferFunction }, vtkCoordinate, + vtkCutterMapper, vtkCubeAxesActor, vtkFollower, vtkGlyph3DMapper, diff --git a/Sources/Rendering/OpenGL/CutterMapper/index.js b/Sources/Rendering/OpenGL/CutterMapper/index.js new file mode 100644 index 00000000000..ac91d973e65 --- /dev/null +++ b/Sources/Rendering/OpenGL/CutterMapper/index.js @@ -0,0 +1,358 @@ +import { mat4 } from 'gl-matrix'; + +import macro from 'vtk.js/Sources/macros'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; +import vtkShaderProgram from 'vtk.js/Sources/Rendering/OpenGL/ShaderProgram'; +import vtkOpenGLPolyDataMapper from 'vtk.js/Sources/Rendering/OpenGL/PolyDataMapper'; +import { registerOverride } from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; + +import { IDENTITY } from 'vtk.js/Sources/Common/Core/Math/Constants'; + +const { vtkErrorMacro } = macro; + +// ---------------------------------------------------------------------------- +// vtkOpenGLCutterMapper methods +// ---------------------------------------------------------------------------- + +function vtkOpenGLCutterMapper(publicAPI, model) { + model.classHierarchy.push('vtkOpenGLCutterMapper'); + + const superClass = { ...publicAPI }; + + publicAPI.replaceShaderClip = (shaders, ren, actor) => { + let VSSource = shaders.Vertex; + let FSSource = shaders.Fragment; + const functionName = model.renderable.getSupportedImplicitFunctionName(); + + if (!functionName) { + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + return; + } + + if (model.renderable.getNumberOfClippingPlanes()) { + const numClipPlanes = model.renderable.getNumberOfClippingPlanes(); + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Dec', [ + 'uniform int numClipPlanes;', + `uniform vec4 clipPlanes[${numClipPlanes}];`, + `varying float clipDistancesVSOutput[${numClipPlanes}];`, + '//VTK::Clip::Dec', + ]).result; + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Impl', [ + `for (int planeNum = 0; planeNum < ${numClipPlanes}; planeNum++)`, + ' {', + ' if (planeNum >= numClipPlanes)', + ' {', + ' break;', + ' }', + ' clipDistancesVSOutput[planeNum] = dot(clipPlanes[planeNum], vertexMC);', + ' }', + '//VTK::Clip::Impl', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Dec', [ + 'uniform int numClipPlanes;', + `varying float clipDistancesVSOutput[${numClipPlanes}];`, + '//VTK::Clip::Dec', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Impl', [ + `for (int planeNum = 0; planeNum < ${numClipPlanes}; planeNum++)`, + ' {', + ' if (planeNum >= numClipPlanes)', + ' {', + ' break;', + ' }', + ' if (clipDistancesVSOutput[planeNum] < 0.0) discard;', + ' }', + '//VTK::Clip::Impl', + ]).result; + } + + let cutDistanceDec = []; + let cutDistanceImpl = []; + + switch (functionName) { + case 'vtkPlane': + cutDistanceDec = [ + 'uniform vec3 cutPlaneOrigin;', + 'uniform vec3 cutPlaneNormal;', + 'uniform float cutValue;', + ]; + cutDistanceImpl = [ + 'cutDistanceVSOutput = dot(cutPlaneNormal, cutFunctionPoint - cutPlaneOrigin) - cutValue;', + ]; + break; + case 'vtkSphere': + cutDistanceDec = [ + 'uniform vec3 cutSphereCenter;', + 'uniform vec3 cutSphereRadius;', + 'uniform int cutSphereUsesAxisRadii;', + ]; + cutDistanceImpl = [ + 'vec3 cutSphereDelta = cutFunctionPoint - cutSphereCenter;', + 'if (cutSphereUsesAxisRadii == 1) {', + ' vec3 cutSphereNormalizedDelta = cutSphereDelta / cutSphereRadius;', + ' cutDistanceVSOutput = dot(cutSphereNormalizedDelta, cutSphereNormalizedDelta) - 1.0;', + '} else {', + ' cutDistanceVSOutput = dot(cutSphereDelta, cutSphereDelta) - cutSphereRadius.x * cutSphereRadius.x;', + '}', + ]; + break; + case 'vtkBox': + cutDistanceDec = [ + 'uniform vec3 cutBoxMinPoint;', + 'uniform vec3 cutBoxMaxPoint;', + ]; + cutDistanceImpl = [ + 'float cutBoxMinDistance = -1.0e20;', + 'float cutBoxDistance = 0.0;', + 'bool cutBoxInside = true;', + 'for (int i = 0; i < 3; ++i) {', + ' float cutBoxLength = cutBoxMaxPoint[i] - cutBoxMinPoint[i];', + ' float cutBoxDist = 0.0;', + ' if (cutBoxLength != 0.0) {', + ' float cutBoxT = (cutFunctionPoint[i] - cutBoxMinPoint[i]) / cutBoxLength;', + ' if (cutBoxT < 0.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = cutBoxMinPoint[i] - cutFunctionPoint[i];', + ' } else if (cutBoxT > 1.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = cutFunctionPoint[i] - cutBoxMaxPoint[i];', + ' } else if (cutBoxT <= 0.5) {', + ' cutBoxDist = cutBoxMinPoint[i] - cutFunctionPoint[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' } else {', + ' cutBoxDist = cutFunctionPoint[i] - cutBoxMaxPoint[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' }', + ' } else {', + ' cutBoxDist = abs(cutFunctionPoint[i] - cutBoxMinPoint[i]);', + ' if (cutBoxDist > 0.0) {', + ' cutBoxInside = false;', + ' }', + ' }', + ' if (cutBoxDist > 0.0) {', + ' cutBoxDistance += cutBoxDist * cutBoxDist;', + ' }', + '}', + 'cutDistanceVSOutput = cutBoxInside ? cutBoxMinDistance : sqrt(cutBoxDistance);', + ]; + break; + case 'vtkCylinder': + cutDistanceDec = [ + 'uniform vec3 cutCylinderCenter;', + 'uniform vec3 cutCylinderAxis;', + 'uniform float cutCylinderRadius;', + ]; + cutDistanceImpl = [ + 'vec3 cutCylinderDelta = cutFunctionPoint - cutCylinderCenter;', + 'float cutCylinderProjection = dot(cutCylinderAxis, cutCylinderDelta);', + 'cutDistanceVSOutput = dot(cutCylinderDelta, cutCylinderDelta) - cutCylinderProjection * cutCylinderProjection - cutCylinderRadius * cutCylinderRadius;', + ]; + break; + case 'vtkCone': + cutDistanceDec = ['uniform float cutConeTanThetaSquared;']; + cutDistanceImpl = [ + 'cutDistanceVSOutput = cutFunctionPoint.y * cutFunctionPoint.y + cutFunctionPoint.z * cutFunctionPoint.z - cutFunctionPoint.x * cutFunctionPoint.x * cutConeTanThetaSquared;', + ]; + break; + default: + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + break; + } + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Dec', [ + 'uniform mat4 cutFunctionMatrix;', + 'varying float cutDistanceVSOutput;', + ...cutDistanceDec, + ]).result; + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Impl', [ + 'vec3 cutFunctionPoint = (cutFunctionMatrix * vertexMC).xyz;', + ...cutDistanceImpl, + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Dec', [ + 'varying float cutDistanceVSOutput;', + 'uniform float cutWidth;', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Impl', [ + 'if (abs(cutDistanceVSOutput) > max(fwidth(cutDistanceVSOutput) * cutWidth, 1e-6)) discard;', + ]).result; + + shaders.Vertex = VSSource; + shaders.Fragment = FSSource; + }; + + publicAPI.replaceShaderValues = (shaders, ren, actor) => { + superClass.replaceShaderValues(shaders, ren, actor); + + if (model.renderable.getSupportedImplicitFunctionName()) { + shaders.Fragment = vtkShaderProgram.substitute( + shaders.Fragment, + '//VTK::UniformFlow::Impl', + [ + ' float cutterDistanceDx = dFdx(cutDistanceVSOutput);', + ' float cutterDistanceDy = dFdy(cutDistanceVSOutput);', + ' float cutterDistanceGradient = length(vec2(cutterDistanceDx, cutterDistanceDy));', + ' if (cutterDistanceGradient <= 0.0 && abs(cutDistanceVSOutput) > 1e-6) discard;', + ] + ).result; + } + }; + + publicAPI.setMapperShaderParameters = (cellBO, ren, actor) => { + superClass.setMapperShaderParameters(cellBO, ren, actor); + + const cutFunction = model.renderable.getCutFunction(); + const functionName = model.renderable.getSupportedImplicitFunctionName(); + if (!cutFunction || !functionName) { + return; + } + + const shiftScaleEnabled = cellBO.getCABO().getCoordShiftAndScaleEnabled(); + const inverseShiftScaleMatrix = shiftScaleEnabled + ? cellBO.getCABO().getInverseShiftAndScaleMatrix() + : null; + const cutFunctionTransform = cutFunction.getTransform?.(); + const transformMatrix = cutFunctionTransform?.getMatrix + ? Array.from(cutFunctionTransform.getMatrix()) + : IDENTITY; + const cutFunctionMatrix = inverseShiftScaleMatrix + ? mat4.multiply(model.tmpMat4, transformMatrix, inverseShiftScaleMatrix) + : transformMatrix; + + const program = cellBO.getProgram(); + if (program.isUniformUsed('cutFunctionMatrix')) { + program.setUniformMatrix('cutFunctionMatrix', cutFunctionMatrix); + } + + switch (functionName) { + case 'vtkPlane': + if (program.isUniformUsed('cutPlaneOrigin')) { + program.setUniform3fArray('cutPlaneOrigin', cutFunction.getOrigin()); + } + if (program.isUniformUsed('cutPlaneNormal')) { + program.setUniform3fArray('cutPlaneNormal', cutFunction.getNormal()); + } + if (program.isUniformUsed('cutValue')) { + program.setUniformf('cutValue', model.renderable.getCutValue()); + } + break; + case 'vtkSphere': { + const radius = cutFunction.getRadius(); + if (program.isUniformUsed('cutSphereCenter')) { + program.setUniform3fArray('cutSphereCenter', cutFunction.getCenter()); + } + if (program.isUniformUsed('cutSphereRadius')) { + const radiusArray = model.tmpVec3A; + if (Array.isArray(radius)) { + radiusArray[0] = radius[0]; + radiusArray[1] = radius[1]; + radiusArray[2] = radius[2]; + } else { + radiusArray[0] = radius; + radiusArray[1] = radius; + radiusArray[2] = radius; + } + program.setUniform3fArray('cutSphereRadius', radiusArray); + } + if (program.isUniformUsed('cutSphereUsesAxisRadii')) { + program.setUniformi( + 'cutSphereUsesAxisRadii', + Array.isArray(radius) ? 1 : 0 + ); + } + break; + } + case 'vtkBox': { + const bounds = cutFunction.getBounds(); + if (program.isUniformUsed('cutBoxMinPoint')) { + const minPoint = model.tmpVec3A; + minPoint[0] = bounds[0]; + minPoint[1] = bounds[2]; + minPoint[2] = bounds[4]; + program.setUniform3fArray('cutBoxMinPoint', minPoint); + } + if (program.isUniformUsed('cutBoxMaxPoint')) { + const maxPoint = model.tmpVec3B; + maxPoint[0] = bounds[1]; + maxPoint[1] = bounds[3]; + maxPoint[2] = bounds[5]; + program.setUniform3fArray('cutBoxMaxPoint', maxPoint); + } + break; + } + case 'vtkCylinder': + if (program.isUniformUsed('cutCylinderCenter')) { + program.setUniform3fArray( + 'cutCylinderCenter', + cutFunction.getCenter() + ); + } + if (program.isUniformUsed('cutCylinderAxis')) { + program.setUniform3fArray('cutCylinderAxis', cutFunction.getAxis()); + } + if (program.isUniformUsed('cutCylinderRadius')) { + program.setUniformf('cutCylinderRadius', cutFunction.getRadius()); + } + break; + case 'vtkCone': + if (program.isUniformUsed('cutConeTanThetaSquared')) { + const tanTheta = Math.tan( + vtkMath.radiansFromDegrees(cutFunction.getAngle()) + ); + program.setUniformf('cutConeTanThetaSquared', tanTheta * tanTheta); + } + break; + default: + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + break; + } + + if (program.isUniformUsed('cutWidth')) { + program.setUniformf('cutWidth', model.renderable.getCutWidth()); + } + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + tmpVec3A: null, + tmpVec3B: null, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + vtkOpenGLPolyDataMapper.extend(publicAPI, model, initialValues); + + model.tmpVec3A = [0.0, 0.0, 0.0]; + model.tmpVec3B = [0.0, 0.0, 0.0]; + + vtkOpenGLCutterMapper(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkOpenGLCutterMapper'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; + +// Register ourself to OpenGL backend if imported +registerOverride('vtkCutterMapper', newInstance); diff --git a/Sources/Rendering/OpenGL/Profiles/Geometry.js b/Sources/Rendering/OpenGL/Profiles/Geometry.js index 85dcd3cc213..53700d3e4e2 100644 --- a/Sources/Rendering/OpenGL/Profiles/Geometry.js +++ b/Sources/Rendering/OpenGL/Profiles/Geometry.js @@ -6,6 +6,7 @@ import 'vtk.js/Sources/Rendering/OpenGL/Renderer'; import 'vtk.js/Sources/Rendering/OpenGL/Actor'; import 'vtk.js/Sources/Rendering/OpenGL/Actor2D'; import 'vtk.js/Sources/Rendering/OpenGL/CubeAxesActor'; +import 'vtk.js/Sources/Rendering/OpenGL/CutterMapper'; import 'vtk.js/Sources/Rendering/OpenGL/PolyDataMapper'; import 'vtk.js/Sources/Rendering/OpenGL/PolyDataMapper2D'; import 'vtk.js/Sources/Rendering/OpenGL/ScalarBarActor'; diff --git a/Sources/Rendering/OpenGL/index.js b/Sources/Rendering/OpenGL/index.js index 64e083073c8..67063257801 100644 --- a/Sources/Rendering/OpenGL/index.js +++ b/Sources/Rendering/OpenGL/index.js @@ -5,6 +5,7 @@ import vtkCamera from './Camera'; import vtkCellArrayBufferObject from './CellArrayBufferObject'; import vtkConvolution2DPass from './Convolution2DPass'; import './CubeAxesActor'; +import vtkCutterMapper from './CutterMapper'; import vtkForwardPass from './ForwardPass'; import vtkFramebuffer from './Framebuffer'; import vtkGlyph3DMapper from './Glyph3DMapper'; @@ -38,6 +39,7 @@ export default { vtkBufferObject, vtkCamera, vtkCellArrayBufferObject, + vtkCutterMapper, vtkConvolution2DPass, vtkForwardPass, vtkFramebuffer, diff --git a/Sources/Rendering/WebGPU/CutterMapper/helpers.js b/Sources/Rendering/WebGPU/CutterMapper/helpers.js new file mode 100644 index 00000000000..12f34fcc01c --- /dev/null +++ b/Sources/Rendering/WebGPU/CutterMapper/helpers.js @@ -0,0 +1,31 @@ +export function vec3ToVec4(out, x, y = 0.0) { + out[0] = x[0]; + out[1] = x[1]; + out[2] = x[2]; + out[3] = y; + return out; +} + +export function shiftVec3ToVec4(out, x, shift) { + out[0] = x[0] + shift[0]; + out[1] = x[1] + shift[1]; + out[2] = x[2] + shift[2]; + out[3] = 0.0; + return out; +} + +export function boundsToMinPoint(out, bounds, shift) { + out[0] = bounds[0] + shift[0]; + out[1] = bounds[2] + shift[1]; + out[2] = bounds[4] + shift[2]; + out[3] = 0.0; + return out; +} + +export function boundsToMaxPoint(out, bounds, shift) { + out[0] = bounds[1] + shift[0]; + out[1] = bounds[3] + shift[1]; + out[2] = bounds[5] + shift[2]; + out[3] = 0.0; + return out; +} diff --git a/Sources/Rendering/WebGPU/CutterMapper/index.js b/Sources/Rendering/WebGPU/CutterMapper/index.js new file mode 100644 index 00000000000..62b968b2bb2 --- /dev/null +++ b/Sources/Rendering/WebGPU/CutterMapper/index.js @@ -0,0 +1,375 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; +import vtkWebGPUCellArrayMapper from 'vtk.js/Sources/Rendering/WebGPU/CellArrayMapper'; +import vtkWebGPUPolyDataMapper from 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper'; +import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache'; + +import { registerOverride } from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory'; + +import { + vec3ToVec4, + shiftVec3ToVec4, + boundsToMinPoint, + boundsToMaxPoint, +} from 'vtk.js/Sources/Rendering/WebGPU/CutterMapper/helpers'; + +const { vtkErrorMacro } = macro; + +function vtkWebGPUCutterCellArrayMapper(publicAPI, model) { + model.classHierarchy.push('vtkWebGPUCutterCellArrayMapper'); + + publicAPI.replaceShaderPosition = (hash, pipeline, vertexInput) => { + const functionName = model.renderable.getSupportedImplicitFunctionName(); + if (!functionName) { + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + return; + } + + const vDesc = pipeline.getShaderDescription('vertex'); + vDesc.addBuiltinOutput('vec4', '@builtin(position) Position'); + if (!vDesc.hasOutput('vertexVC')) { + vDesc.addOutput('vec4', 'vertexVC'); + } + vDesc.addOutput('f32', 'cutDistanceVS'); + + let positionImpl = []; + if (model.useRendererMatrix) { + positionImpl = [ + ' var pCoord: vec4 = rendererUBO.SCPCMatrix*mapperUBO.BCSCMatrix*vertexBC;', + ' output.vertexVC = rendererUBO.SCVCMatrix * mapperUBO.BCSCMatrix * vec4(vertexBC.xyz, 1.0);', + ]; + } else { + positionImpl = [ + ' var pCoord: vec4 = mapperUBO.BCSCMatrix*vertexBC;', + ' pCoord.x = 2.0* pCoord.x / rendererUBO.viewportSize.x - 1.0;', + ' pCoord.y = 2.0* pCoord.y / rendererUBO.viewportSize.y - 1.0;', + ' pCoord.z = 0.5 - 0.5 * pCoord.z;', + ' output.vertexVC = vec4(0.0);', + ]; + } + + let cutDistanceImpl = [' output.cutDistanceVS = 1.0;']; + switch (functionName) { + case 'vtkPlane': + cutDistanceImpl = [ + ' output.cutDistanceVS = dot(mapperUBO.CutPlaneNormal.xyz, vertexBC.xyz - mapperUBO.CutPlaneOrigin.xyz) - mapperUBO.CutValue;', + ]; + break; + case 'vtkSphere': + cutDistanceImpl = [ + ' let cutSphereDelta: vec3 = vertexBC.xyz - mapperUBO.CutSphereCenter.xyz;', + ' if (mapperUBO.CutSphereUsesAxisRadii == 1u) {', + ' let cutSphereNormalizedDelta: vec3 = cutSphereDelta / mapperUBO.CutSphereRadius.xyz;', + ' output.cutDistanceVS = dot(cutSphereNormalizedDelta, cutSphereNormalizedDelta) - 1.0;', + ' } else {', + ' output.cutDistanceVS = dot(cutSphereDelta, cutSphereDelta) - mapperUBO.CutSphereRadius.x * mapperUBO.CutSphereRadius.x;', + ' }', + ]; + break; + case 'vtkBox': + cutDistanceImpl = [ + ' var cutBoxMinDistance: f32 = -1.0e20;', + ' var cutBoxDistance: f32 = 0.0;', + ' var cutBoxInside: bool = true;', + ' for (var i: i32 = 0; i < 3; i++) {', + ' let cutBoxLength: f32 = mapperUBO.CutBoxMaxPoint[i] - mapperUBO.CutBoxMinPoint[i];', + ' var cutBoxDist: f32 = 0.0;', + ' if (cutBoxLength != 0.0) {', + ' let cutBoxT: f32 = (vertexBC[i] - mapperUBO.CutBoxMinPoint[i]) / cutBoxLength;', + ' if (cutBoxT < 0.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = mapperUBO.CutBoxMinPoint[i] - vertexBC[i];', + ' } else if (cutBoxT > 1.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = vertexBC[i] - mapperUBO.CutBoxMaxPoint[i];', + ' } else if (cutBoxT <= 0.5) {', + ' cutBoxDist = mapperUBO.CutBoxMinPoint[i] - vertexBC[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' } else {', + ' cutBoxDist = vertexBC[i] - mapperUBO.CutBoxMaxPoint[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' }', + ' } else {', + ' cutBoxDist = abs(vertexBC[i] - mapperUBO.CutBoxMinPoint[i]);', + ' if (cutBoxDist > 0.0) {', + ' cutBoxInside = false;', + ' }', + ' }', + ' if (cutBoxDist > 0.0) {', + ' cutBoxDistance += cutBoxDist * cutBoxDist;', + ' }', + ' }', + ' output.cutDistanceVS = select(sqrt(cutBoxDistance), cutBoxMinDistance, cutBoxInside);', + ]; + break; + case 'vtkCylinder': + cutDistanceImpl = [ + ' let cutCylinderDelta: vec3 = vertexBC.xyz - mapperUBO.CutCylinderCenter.xyz;', + ' let cutCylinderProjection: f32 = dot(mapperUBO.CutCylinderAxis.xyz, cutCylinderDelta);', + ' output.cutDistanceVS = dot(cutCylinderDelta, cutCylinderDelta) - cutCylinderProjection * cutCylinderProjection - mapperUBO.CutCylinderRadius * mapperUBO.CutCylinderRadius;', + ]; + break; + case 'vtkCone': + cutDistanceImpl = [ + ' let cutConeDelta: vec3 = vertexBC.xyz - mapperUBO.CutConeApex.xyz;', + ' let cutConeAxial: f32 = dot(mapperUBO.CutConeAxis.xyz, cutConeDelta);', + ' let cutConeRadial2: f32 = dot(cutConeDelta, cutConeDelta) - cutConeAxial * cutConeAxial;', + ' output.cutDistanceVS = cutConeRadial2 - cutConeAxial * cutConeAxial * mapperUBO.CutConeTanThetaSquared;', + ]; + break; + default: + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + break; + } + + let code = vDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ...positionImpl, + ...cutDistanceImpl, + '//VTK::Position::Impl', + ]).result; + + if (model.forceZValue) { + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + model.useRendererMatrix + ? 'pCoord = vec4(pCoord.xyz/pCoord.w, 1.0);' + : ' pCoord = vec4(pCoord.xyz/pCoord.w, 1.0);', + ' pCoord.z = mapperUBO.ZValue;', + '//VTK::Position::Impl', + ]).result; + } + + if (publicAPI.haveWideLines()) { + vDesc.addBuiltinInput('u32', '@builtin(instance_index) instanceIndex'); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ' var tmpPos: vec4 = pCoord;', + ' var numSteps: f32 = ceil(mapperUBO.LineWidth - 1.0);', + ' var offset: f32 = (mapperUBO.LineWidth - 1.0) * (f32(input.instanceIndex / 2u) - numSteps/2.0) / numSteps;', + ' var tmpPos2: vec3 = tmpPos.xyz / tmpPos.w;', + ' tmpPos2.x = tmpPos2.x + 2.0 * (f32(input.instanceIndex) % 2.0) * offset / rendererUBO.viewportSize.x;', + ' tmpPos2.y = tmpPos2.y + 2.0 * (f32(input.instanceIndex + 1u) % 2.0) * offset / rendererUBO.viewportSize.y;', + ' tmpPos2.z = min(1.0, tmpPos2.z + 0.00001);', + ' pCoord = vec4(tmpPos2.xyz * tmpPos.w, tmpPos.w);', + '//VTK::Position::Impl', + ]).result; + } + + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ' output.Position = pCoord;', + ]).result; + vDesc.setCode(code); + + const fDesc = pipeline.getShaderDescription('fragment'); + code = fDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ' if (abs(input.cutDistanceVS) > max(fwidth(input.cutDistanceVS) * mapperUBO.CutWidth, 1.0e-6)) { discard; }', + '//VTK::Position::Impl', + ]).result; + code = vtkWebGPUShaderCache.substitute(code, '//VTK::RenderEncoder::Impl', [ + ' let cutterDistanceDx: f32 = dpdx(input.cutDistanceVS);', + ' let cutterDistanceDy: f32 = dpdy(input.cutDistanceVS);', + ' let cutterDistanceGradient: f32 = length(vec2(cutterDistanceDx, cutterDistanceDy));', + ' if (cutterDistanceGradient <= 0.0 && abs(input.cutDistanceVS) > 1.0e-6) { discard; }', + '//VTK::RenderEncoder::Impl', + ]).result; + fDesc.setCode(code); + }; + model.shaderReplacements.set( + 'replaceShaderPosition', + publicAPI.replaceShaderPosition + ); + + const superComputePipelineHash = publicAPI.computePipelineHash; + publicAPI.computePipelineHash = () => { + superComputePipelineHash(); + const functionName = + model.renderable.getSupportedImplicitFunctionName() || 'none'; + model.pipelineHash += `cf${functionName}Params`; + }; + + const superUpdateUBO = publicAPI.updateUBO; + publicAPI.updateUBO = () => { + superUpdateUBO(); + + const renderable = model.renderable; + const cutFunction = renderable.getCutFunction(); + const functionName = renderable.getSupportedImplicitFunctionName(); + if (!functionName || !cutFunction) { + return; + } + + const bufferShift = model.WebGPUActor.getBufferShift(model.WebGPURenderer); + model.UBO.setValue('CutWidth', vtkMath.max(renderable.getCutWidth(), 0.0)); + + switch (functionName) { + case 'vtkPlane': + model.UBO.setArray( + 'CutPlaneOrigin', + shiftVec3ToVec4( + model.cutPlaneOrigin, + cutFunction.getOrigin(), + bufferShift + ) + ); + model.UBO.setArray( + 'CutPlaneNormal', + vec3ToVec4(model.cutPlaneNormal, cutFunction.getNormal()) + ); + model.UBO.setValue('CutValue', renderable.getCutValue()); + break; + case 'vtkSphere': { + const radius = cutFunction.getRadius(); + const usesAxisRadii = Array.isArray(radius); + model.UBO.setArray( + 'CutSphereCenter', + shiftVec3ToVec4( + model.cutSphereCenter, + cutFunction.getCenter(), + bufferShift + ) + ); + if (usesAxisRadii) { + model.cutSphereRadius[0] = radius[0]; + model.cutSphereRadius[1] = radius[1]; + model.cutSphereRadius[2] = radius[2]; + } else { + model.cutSphereRadius[0] = radius; + model.cutSphereRadius[1] = radius; + model.cutSphereRadius[2] = radius; + } + model.cutSphereRadius[3] = 0.0; + model.UBO.setArray('CutSphereRadius', model.cutSphereRadius); + model.UBO.setValue('CutSphereUsesAxisRadii', usesAxisRadii ? 1 : 0); + break; + } + case 'vtkBox': { + const bounds = cutFunction.getBounds(); + model.UBO.setArray( + 'CutBoxMinPoint', + boundsToMinPoint(model.cutBoxMinPoint, bounds, bufferShift) + ); + model.UBO.setArray( + 'CutBoxMaxPoint', + boundsToMaxPoint(model.cutBoxMaxPoint, bounds, bufferShift) + ); + break; + } + case 'vtkCylinder': + model.UBO.setArray( + 'CutCylinderCenter', + shiftVec3ToVec4( + model.cutCylinderCenter, + cutFunction.getCenter(), + bufferShift + ) + ); + model.UBO.setArray( + 'CutCylinderAxis', + vec3ToVec4(model.cutCylinderAxis, cutFunction.getAxis()) + ); + model.UBO.setValue('CutCylinderRadius', cutFunction.getRadius()); + break; + case 'vtkCone': { + const apex = model.cutConeApex; + const axis = model.cutConeAxis; + const axisPoint = model.cutConeAxisPoint; + const inverseTransform = cutFunction.getTransform?.()?.getInverse?.(); + + apex[0] = 0.0; + apex[1] = 0.0; + apex[2] = 0.0; + axis[0] = 1.0; + axis[1] = 0.0; + axis[2] = 0.0; + axisPoint[0] = 1.0; + axisPoint[1] = 0.0; + axisPoint[2] = 0.0; + + if (inverseTransform?.transformPoint) { + inverseTransform.transformPoint(apex, apex); + inverseTransform.transformPoint(axisPoint, axisPoint); + vtkMath.subtract(axisPoint, apex, axis); + if (vtkMath.norm(axis) > 0.0) { + vtkMath.normalize(axis); + } + } + model.UBO.setArray( + 'CutConeApex', + shiftVec3ToVec4(model.cutConeApex4, apex, bufferShift) + ); + model.UBO.setArray('CutConeAxis', vec3ToVec4(model.cutConeAxis4, axis)); + const tanTheta = Math.tan( + vtkMath.radiansFromDegrees(cutFunction.getAngle()) + ); + model.UBO.setValue('CutConeTanThetaSquared', tanTheta * tanTheta); + break; + } + default: + break; + } + + model.UBO.sendIfNeeded(model.WebGPURenderWindow.getDevice()); + }; +} + +function extendCellArray(publicAPI, model, initialValues = {}) { + vtkWebGPUCellArrayMapper.extend(publicAPI, model, initialValues); + + model.UBO.addEntry('CutPlaneOrigin', 'vec4'); + model.UBO.addEntry('CutPlaneNormal', 'vec4'); + model.UBO.addEntry('CutSphereCenter', 'vec4'); + model.UBO.addEntry('CutSphereRadius', 'vec4'); + model.UBO.addEntry('CutBoxMinPoint', 'vec4'); + model.UBO.addEntry('CutBoxMaxPoint', 'vec4'); + model.UBO.addEntry('CutCylinderCenter', 'vec4'); + model.UBO.addEntry('CutCylinderAxis', 'vec4'); + model.UBO.addEntry('CutConeApex', 'vec4'); + model.UBO.addEntry('CutConeAxis', 'vec4'); + model.UBO.addEntry('CutValue', 'f32'); + model.UBO.addEntry('CutWidth', 'f32'); + model.UBO.addEntry('CutCylinderRadius', 'f32'); + model.UBO.addEntry('CutConeTanThetaSquared', 'f32'); + model.UBO.addEntry('CutSphereUsesAxisRadii', 'u32'); + + model.cutPlaneOrigin = [0.0, 0.0, 0.0, 0.0]; + model.cutPlaneNormal = [0.0, 0.0, 0.0, 0.0]; + model.cutSphereCenter = [0.0, 0.0, 0.0, 0.0]; + model.cutSphereRadius = [0.0, 0.0, 0.0, 0.0]; + model.cutBoxMinPoint = [0.0, 0.0, 0.0, 0.0]; + model.cutBoxMaxPoint = [0.0, 0.0, 0.0, 0.0]; + model.cutCylinderCenter = [0.0, 0.0, 0.0, 0.0]; + model.cutCylinderAxis = [0.0, 0.0, 0.0, 0.0]; + model.cutConeApex = [0.0, 0.0, 0.0]; + model.cutConeAxis = [1.0, 0.0, 0.0]; + model.cutConeAxisPoint = [1.0, 0.0, 0.0]; + model.cutConeApex4 = [0.0, 0.0, 0.0, 0.0]; + model.cutConeAxis4 = [1.0, 0.0, 0.0, 0.0]; + + vtkWebGPUCutterCellArrayMapper(publicAPI, model); +} + +const newCellArrayInstance = macro.newInstance( + extendCellArray, + 'vtkWebGPUCutterCellArrayMapper' +); + +function vtkWebGPUCutterMapper(publicAPI, model) { + model.classHierarchy.push('vtkWebGPUCutterMapper'); + + publicAPI.createCellArrayMapper = () => newCellArrayInstance(); +} + +export function extend(publicAPI, model, initialValues = {}) { + vtkWebGPUPolyDataMapper.extend(publicAPI, model, initialValues); + vtkWebGPUCutterMapper(publicAPI, model); +} + +export const newInstance = macro.newInstance(extend, 'vtkWebGPUCutterMapper'); + +export default { newInstance, extend }; + +registerOverride('vtkCutterMapper', newInstance); diff --git a/Sources/Rendering/WebGPU/Profiles/All.js b/Sources/Rendering/WebGPU/Profiles/All.js index d1030b1ab26..89b149cc804 100644 --- a/Sources/Rendering/WebGPU/Profiles/All.js +++ b/Sources/Rendering/WebGPU/Profiles/All.js @@ -6,6 +6,7 @@ import 'vtk.js/Sources/Rendering/WebGPU/Renderer'; import 'vtk.js/Sources/Rendering/WebGPU/Actor'; import 'vtk.js/Sources/Rendering/WebGPU/Actor2D'; import 'vtk.js/Sources/Rendering/WebGPU/CubeAxesActor'; +import 'vtk.js/Sources/Rendering/WebGPU/CutterMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper2D'; // import 'vtk.js/Sources/Rendering/WebGPU/Skybox'; diff --git a/Sources/Rendering/WebGPU/Profiles/Geometry.js b/Sources/Rendering/WebGPU/Profiles/Geometry.js index d4e596651e4..0baaac28dc4 100644 --- a/Sources/Rendering/WebGPU/Profiles/Geometry.js +++ b/Sources/Rendering/WebGPU/Profiles/Geometry.js @@ -6,6 +6,7 @@ import 'vtk.js/Sources/Rendering/WebGPU/Renderer'; import 'vtk.js/Sources/Rendering/WebGPU/Actor'; import 'vtk.js/Sources/Rendering/WebGPU/Actor2D'; import 'vtk.js/Sources/Rendering/WebGPU/CubeAxesActor'; +import 'vtk.js/Sources/Rendering/WebGPU/CutterMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper2D'; // import 'vtk.js/Sources/Rendering/WebGPU/Skybox'; diff --git a/Sources/Rendering/WebGPU/index.js b/Sources/Rendering/WebGPU/index.js index e511771c11b..2300797a68b 100644 --- a/Sources/Rendering/WebGPU/index.js +++ b/Sources/Rendering/WebGPU/index.js @@ -3,6 +3,7 @@ import './Actor'; import './Actor2D'; import './Camera'; import './CubeAxesActor'; +import './CutterMapper'; import './ForwardPass'; import './Glyph3DMapper'; import './HardwareSelector';