diff --git a/Examples/Volume/MPRSlice/index.js b/Examples/Volume/MPRSlice/index.js new file mode 100644 index 00000000000..51eb0b57a0a --- /dev/null +++ b/Examples/Volume/MPRSlice/index.js @@ -0,0 +1,60 @@ +import 'vtk.js/Sources/favicon'; +/* eslint-disable */ + +import 'vtk.js/Sources/Rendering/Profiles/Volume'; + +// Force DataAccessHelper to have access to various data source +import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; +import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; +import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; + +import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader'; +import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; +import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; +import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + background: [0, 0, 0], +}); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +global.fullScreen = fullScreenRenderer; +global.renderWindow = renderWindow; + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); +const actor = vtkVolume.newInstance(); +const mapper = vtkVolumeMapper.newInstance(); +mapper.setSampleDistance(1.1); +actor.setMapper(mapper); +mapper.setInputConnection(reader.getOutputPort()); + +const mprSlice = vtkVolumeMapper.vtkSliceHelper.newInstance({ thickness: 3 }); +mprSlice.registerClipPlanesToMapper(mapper); + +reader.setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`).then(() => { + reader.loadData().then(() => { + const data = reader.getOutputData(); + renderer.addVolume(actor); + const [xMin, xMax, yMin, yMax, zMin, zMax] = data.getBounds(); + mprSlice.setOrigin((xMin + xMax) / 2, (yMin + yMax) / 2, (zMin + zMax) / 2); + + renderer.resetCamera(); + renderWindow.render(); + }); +}); + +// Set MPR slice to follow camera orientation +const camera = renderer.getActiveCamera(); +camera.onModified(() => { + const direction = camera.getDirectionOfProjection(); + mprSlice.setNormal(direction); +}); diff --git a/Examples/Volume/VolumeOutline/index.js b/Examples/Volume/VolumeOutline/index.js deleted file mode 100644 index 381c046bde3..00000000000 --- a/Examples/Volume/VolumeOutline/index.js +++ /dev/null @@ -1,156 +0,0 @@ -import 'vtk.js/Sources/favicon'; - -// Load the rendering pieces we want to use (for both WebGL and WebGPU) -import 'vtk.js/Sources/Rendering/Profiles/Volume'; - -// Force DataAccessHelper to have access to various data source -import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; -import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; -import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; - -import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; -import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader'; -import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; -import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; -import vtkInteractorStyleMPRSlice from 'vtk.js/Sources/Interaction/Style/InteractorStyleMPRSlice'; -import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData'; -import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; -import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; -import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; - -const fullScreenRenderWindow = vtkFullScreenRenderWindow.newInstance({ - background: [0.3, 0.3, 0.3], -}); -const renderWindow = fullScreenRenderWindow.getRenderWindow(); -const renderer = fullScreenRenderWindow.getRenderer(); - -const istyle = vtkInteractorStyleMPRSlice.newInstance(); -renderWindow.getInteractor().setInteractorStyle(istyle); - -global.fullScreen = fullScreenRenderWindow; -global.renderWindow = renderWindow; - -// ---------------------------------------------------------------------------- -// Volume rendering -// ---------------------------------------------------------------------------- - -const actor = vtkVolume.newInstance(); -const mapper = vtkVolumeMapper.newInstance(); -actor.setMapper(mapper); - -const ofun = vtkPiecewiseFunction.newInstance(); -ofun.addPoint(0, 0); -ofun.addPoint(1, 1.0); -actor.getProperty().setScalarOpacity(0, ofun); - -function createLabelPipeline(backgroundImageData) { - // Create a labelmap image the same dimensions as our background volume. - const labelMapData = vtkImageData.newInstance( - backgroundImageData.get('spacing', 'origin', 'direction') - ); - - labelMapData.computeTransforms(); - - const values = new Uint8Array(backgroundImageData.getNumberOfPoints()); - const dataArray = vtkDataArray.newInstance({ - numberOfComponents: 1, // labelmap with single component - values, - }); - labelMapData.getPointData().setScalars(dataArray); - - labelMapData.setDimensions(...backgroundImageData.getDimensions()); - labelMapData.setSpacing(...backgroundImageData.getSpacing()); - labelMapData.setOrigin(...backgroundImageData.getOrigin()); - labelMapData.setDirection(...backgroundImageData.getDirection()); - - const labelMap = { - actor: vtkVolume.newInstance(), - mapper: vtkVolumeMapper.newInstance(), - imageData: labelMapData, - cfun: vtkColorTransferFunction.newInstance(), - ofun: vtkPiecewiseFunction.newInstance(), - }; - - // Labelmap pipeline - labelMap.mapper.setInputData(labelMapData); - labelMap.actor.setMapper(labelMap.mapper); - - // Set up labelMap color and opacity mapping - labelMap.cfun.addRGBPoint(1, 1, 0, 0); // label "1" will be red - labelMap.cfun.addRGBPoint(2, 0, 1, 0); // label "2" will be green - labelMap.ofun.addPoint(0, 0); - labelMap.ofun.addPoint(1, 0.5, 0.5, 1.0); // Red will have an opacity of 0.2. - labelMap.ofun.addPoint(2, 0.5, 0.5, 1.0); // Green will have an opacity of 0.2. - labelMap.ofun.setClamping(false); - - labelMap.actor.getProperty().setRGBTransferFunction(0, labelMap.cfun); - labelMap.actor.getProperty().setScalarOpacity(0, labelMap.ofun); - labelMap.actor.getProperty().setInterpolationTypeToNearest(); - labelMap.actor.getProperty().setUseLabelOutline(true); - labelMap.actor.getProperty().setLabelOutlineThickness(3); - - return labelMap; -} - -function fillBlobForThreshold(imageData, backgroundImageData) { - const dims = imageData.getDimensions(); - const values = imageData.getPointData().getScalars().getData(); - - const backgroundValues = backgroundImageData - .getPointData() - .getScalars() - .getData(); - const size = dims[0] * dims[1] * dims[2]; - - // Head - const headThreshold = [324, 1524]; - for (let i = 0; i < size; i++) { - if ( - backgroundValues[i] >= headThreshold[0] && - backgroundValues[i] < headThreshold[1] - ) { - values[i] = 1; - } - } - - // Bone - const boneThreshold = [1200, 2324]; - for (let i = 0; i < size; i++) { - if ( - backgroundValues[i] >= boneThreshold[0] && - backgroundValues[i] < boneThreshold[1] - ) { - values[i] = 2; - } - } - - imageData.getPointData().getScalars().setData(values); -} - -const reader = vtkHttpDataSetReader.newInstance({ - fetchGzip: true, -}); -reader - .setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`, { loadData: true }) - .then(() => { - const data = reader.getOutputData(); - - mapper.setInputData(data); - - const labelMap = createLabelPipeline(data); - - const sourceDataRGBTransferFunction = actor - .getProperty() - .getRGBTransferFunction(0); - sourceDataRGBTransferFunction.setMappingRange(324, 2324); - - fillBlobForThreshold(labelMap.imageData, data); - - // Set interactor style volume mapper after mapper sets input data - istyle.setVolumeMapper(mapper); - - renderer.addVolume(actor); - renderer.addVolume(labelMap.actor); - renderer.getActiveCamera().setViewUp(1, 0, 0); - renderWindow.render(); - }); diff --git a/Sources/Interaction/Style/InteractorStyleMPRSlice/api.md b/Sources/Interaction/Style/InteractorStyleMPRSlice/api.md deleted file mode 100644 index a9864fb26a3..00000000000 --- a/Sources/Interaction/Style/InteractorStyleMPRSlice/api.md +++ /dev/null @@ -1,31 +0,0 @@ -## Introduction - -This interactor style performs MPR via the camera, a volume of the target image, and camera clipping planes. This does NOT manually compute image reslices. Since this interactor drives the camera, there should be no other usage of the renderer camera. - -This interactor style will trigger the `onModified` event whenever a camera property changes. - -This interactor inherits from `vtkInteractorStyleManipulator`. It utilizes the following manipulators internally: -- `vtkMouseCameraTrackballRotateManipulator` -- `vtkMouseCameraTrackballZoomManipulator` -- `vtkMouseCameraTrackballPanManipulator` -- `vtkMouseRangeManipulator` - -## See Also - -[vtkInteractorStyleManipulator](./Interactor_Style_InteractorStyleManipulator.html) - -### setSlice(slice) / getSlice() -> sliceNum - -Slice index should be provided in world space, along the slice normal. This effectively sets the camera focal point. - -### setSliceNormal(...normal) / getSliceNormal() -> normal[3] - -The slice normal should be provided in world space. This effectively sets the camera's direction of projection. - -### getSliceRange() -> [min, max] - -Retrieves the minimum and maximum possible values for the slice. Slice values are restricted based on bounding box and slice normal. - -### setVolumeMapper(mapper) / getVolumeMapper() - -Since this interactor style depends on the properties of an actual volume in the scene, it requires a valid volume mapper. diff --git a/Sources/Interaction/Style/InteractorStyleMPRSlice/example/controlPanel.html b/Sources/Interaction/Style/InteractorStyleMPRSlice/example/controlPanel.html deleted file mode 100644 index 9b0fbb9a543..00000000000 --- a/Sources/Interaction/Style/InteractorStyleMPRSlice/example/controlPanel.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - -
Slice Value - - -
-
Slice Normal -
-
Slice Range -
-
diff --git a/Sources/Interaction/Style/InteractorStyleMPRSlice/example/index.js b/Sources/Interaction/Style/InteractorStyleMPRSlice/example/index.js deleted file mode 100644 index 155386d6b5c..00000000000 --- a/Sources/Interaction/Style/InteractorStyleMPRSlice/example/index.js +++ /dev/null @@ -1,88 +0,0 @@ -import 'vtk.js/Sources/favicon'; - -// Load the rendering pieces we want to use (for both WebGL and WebGPU) -import 'vtk.js/Sources/Rendering/Profiles/Volume'; - -// use full HttpDataAccessHelper -import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; - -import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; -import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader'; -import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; -import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; -import vtkInteractorStyleMPRSlice from 'vtk.js/Sources/Interaction/Style/InteractorStyleMPRSlice'; - -import controlPanel from './controlPanel.html'; - -const fullScreenRenderWindow = vtkFullScreenRenderWindow.newInstance({ - background: [0, 0, 0], -}); -const renderWindow = fullScreenRenderWindow.getRenderWindow(); -const renderer = fullScreenRenderWindow.getRenderer(); -fullScreenRenderWindow.addController(controlPanel); - -const istyle = vtkInteractorStyleMPRSlice.newInstance(); -renderWindow.getInteractor().setInteractorStyle(istyle); - -global.fullScreen = fullScreenRenderWindow; -global.renderWindow = renderWindow; - -// ---------------------------------------------------------------------------- -// Volume rendering -// ---------------------------------------------------------------------------- - -const actor = vtkVolume.newInstance(); -const mapper = vtkVolumeMapper.newInstance(); -actor.setMapper(mapper); - -const reader = vtkHttpDataSetReader.newInstance({ - fetchGzip: true, -}); -reader - .setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`, { loadData: true }) - .then(() => { - const data = reader.getOutputData(); - - mapper.setInputData(data); - - // set interactor style volume mapper after mapper sets input data - istyle.setVolumeMapper(mapper); - istyle.setSliceNormal(0, 0, 1); - - const range = istyle.getSliceRange(); - istyle.setSlice((range[0] + range[1]) / 2); - - renderer.addVolume(actor); - renderWindow.render(); - }); - -// ---------------------------------------------------------------------------- -// UI -// ---------------------------------------------------------------------------- - -function updateUI() { - const range = istyle.getSliceRange(); - const slice = istyle.getSlice(); - const normal = istyle.getSliceNormal(); - - const sliceSlider = document.querySelector('.slice'); - sliceSlider.min = range[0]; - sliceSlider.max = range[1]; - sliceSlider.value = slice; - - function toFixed(n) { - return Number.parseFloat(n).toFixed(6); - } - - document.querySelector('.sliceText').innerText = toFixed(slice); - document.querySelector('.normal').innerText = normal.map(toFixed).join(', '); - document.querySelector('.range').innerText = range.map(toFixed).join(', '); -} - -document.querySelector('.slice').oninput = function onInput(ev) { - istyle.setSlice(Number.parseFloat(ev.target.value)); - renderWindow.render(); -}; - -istyle.onModified(updateUI); -updateUI(); diff --git a/Sources/Interaction/Style/InteractorStyleMPRSlice/index.js b/Sources/Interaction/Style/InteractorStyleMPRSlice/index.js deleted file mode 100644 index f6654c2a95f..00000000000 --- a/Sources/Interaction/Style/InteractorStyleMPRSlice/index.js +++ /dev/null @@ -1,334 +0,0 @@ -import macro from 'vtk.js/Sources/macros'; -import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; -import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder'; -import vtkInteractorStyleManipulator from 'vtk.js/Sources/Interaction/Style/InteractorStyleManipulator'; -import vtkMouseCameraTrackballRotateManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballRotateManipulator'; -import vtkMouseCameraTrackballPanManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulator'; -import vtkMouseCameraTrackballZoomManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballZoomManipulator'; -import vtkMouseRangeManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseRangeManipulator'; - -// ---------------------------------------------------------------------------- -// Global methods -// ---------------------------------------------------------------------------- - -function boundsToCorners(bounds) { - return [ - [bounds[0], bounds[2], bounds[4]], - [bounds[0], bounds[2], bounds[5]], - [bounds[0], bounds[3], bounds[4]], - [bounds[0], bounds[3], bounds[5]], - [bounds[1], bounds[2], bounds[4]], - [bounds[1], bounds[2], bounds[5]], - [bounds[1], bounds[3], bounds[4]], - [bounds[1], bounds[3], bounds[5]], - ]; -} - -// ---------------------------------------------------------------------------- - -function clamp(value, min, max) { - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; -} - -// ---------------------------------------------------------------------------- -// vtkInteractorStyleMPRSlice methods -// ---------------------------------------------------------------------------- - -function vtkInteractorStyleMPRSlice(publicAPI, model) { - // Set our className - model.classHierarchy.push('vtkInteractorStyleMPRSlice'); - - model.trackballManipulator = - vtkMouseCameraTrackballRotateManipulator.newInstance({ - button: 1, - }); - model.panManipulator = vtkMouseCameraTrackballPanManipulator.newInstance({ - button: 1, - shift: true, - }); - model.zoomManipulator = vtkMouseCameraTrackballZoomManipulator.newInstance({ - button: 3, - }); - model.scrollManipulator = vtkMouseRangeManipulator.newInstance({ - scrollEnabled: true, - dragEnabled: false, - }); - - // cache for sliceRange - const cache = { - sliceNormal: [0, 0, 0], - sliceRange: [0, 0], - }; - - let cameraSub = null; - - function updateScrollManipulator() { - const range = publicAPI.getSliceRange(); - model.scrollManipulator.removeScrollListener(); - model.scrollManipulator.setScrollListener( - range[0], - range[1], - 1, - publicAPI.getSlice, - publicAPI.setSlice - ); - } - - function setManipulators() { - publicAPI.removeAllMouseManipulators(); - publicAPI.addMouseManipulator(model.trackballManipulator); - publicAPI.addMouseManipulator(model.panManipulator); - publicAPI.addMouseManipulator(model.zoomManipulator); - publicAPI.addMouseManipulator(model.scrollManipulator); - updateScrollManipulator(); - } - - const superSetInteractor = publicAPI.setInteractor; - publicAPI.setInteractor = (interactor) => { - superSetInteractor(interactor); - - if (cameraSub) { - cameraSub.unsubscribe(); - cameraSub = null; - } - - if (interactor) { - const renderer = interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - - cameraSub = camera.onModified(() => { - updateScrollManipulator(); - publicAPI.modified(); - }); - } - }; - - publicAPI.handleMouseMove = macro.chain(publicAPI.handleMouseMove, () => { - const renderer = model._interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - const dist = camera.getDistance(); - camera.setClippingRange(dist, dist + 0.1); - }); - - const superSetVolumeMapper = publicAPI.setVolumeMapper; - publicAPI.setVolumeMapper = (mapper) => { - if (superSetVolumeMapper(mapper)) { - const renderer = model._interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - if (mapper) { - // prevent zoom manipulator from messing with our focal point - camera.setFreezeFocalPoint(true); - publicAPI.setSliceNormal(...publicAPI.getSliceNormal()); - } else { - camera.setFreezeFocalPoint(false); - } - } - }; - - publicAPI.getSlice = () => { - const renderer = model._interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - const sliceNormal = publicAPI.getSliceNormal(); - - // Get rotation matrix from normal to +X (since bounds is aligned to XYZ) - const transform = vtkMatrixBuilder - .buildFromDegree() - .identity() - .rotateFromDirections(sliceNormal, [1, 0, 0]); - - const fp = camera.getFocalPoint(); - transform.apply(fp); - return fp[0]; - }; - - publicAPI.setSlice = (slice) => { - const renderer = model._interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - - if (model.volumeMapper) { - const range = publicAPI.getSliceRange(); - const bounds = model.volumeMapper.getBounds(); - - const clampedSlice = clamp(slice, ...range); - const center = [ - (bounds[0] + bounds[1]) / 2.0, - (bounds[2] + bounds[3]) / 2.0, - (bounds[4] + bounds[5]) / 2.0, - ]; - - const distance = camera.getDistance(); - const dop = camera.getDirectionOfProjection(); - vtkMath.normalize(dop); - - const midPoint = (range[1] + range[0]) / 2.0; - const zeroPoint = [ - center[0] - dop[0] * midPoint, - center[1] - dop[1] * midPoint, - center[2] - dop[2] * midPoint, - ]; - const slicePoint = [ - zeroPoint[0] + dop[0] * clampedSlice, - zeroPoint[1] + dop[1] * clampedSlice, - zeroPoint[2] + dop[2] * clampedSlice, - ]; - - const newPos = [ - slicePoint[0] - dop[0] * distance, - slicePoint[1] - dop[1] * distance, - slicePoint[2] - dop[2] * distance, - ]; - - camera.setPosition(...newPos); - camera.setFocalPoint(...slicePoint); - } - }; - - publicAPI.getSliceRange = () => { - if (model.volumeMapper) { - const sliceNormal = publicAPI.getSliceNormal(); - - if ( - sliceNormal[0] === cache.sliceNormal[0] && - sliceNormal[1] === cache.sliceNormal[1] && - sliceNormal[2] === cache.sliceNormal[2] - ) { - return cache.sliceRange; - } - - const bounds = model.volumeMapper.getBounds(); - const points = boundsToCorners(bounds); - - // Get rotation matrix from normal to +X (since bounds is aligned to XYZ) - const transform = vtkMatrixBuilder - .buildFromDegree() - .identity() - .rotateFromDirections(sliceNormal, [1, 0, 0]); - - points.forEach((pt) => transform.apply(pt)); - - // range is now maximum X distance - let minX = Infinity; - let maxX = -Infinity; - for (let i = 0; i < 8; i++) { - const x = points[i][0]; - if (x > maxX) { - maxX = x; - } - if (x < minX) { - minX = x; - } - } - - cache.sliceNormal = sliceNormal; - cache.sliceRange = [minX, maxX]; - return cache.sliceRange; - } - return [0, 0]; - }; - - // Slice normal is just camera DOP - publicAPI.getSliceNormal = () => { - if (model.volumeMapper) { - const renderer = model._interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - return camera.getDirectionOfProjection(); - } - return [0, 0, 0]; - }; - - // in world space - publicAPI.setSliceNormal = (...normal) => { - const renderer = model._interactor.getCurrentRenderer(); - const camera = renderer.getActiveCamera(); - - vtkMath.normalize(normal); - - if (model.volumeMapper) { - const bounds = model.volumeMapper.getBounds(); - - // diagonal will be used as "width" of camera scene - const diagonal = Math.sqrt( - vtkMath.distance2BetweenPoints( - [bounds[0], bounds[2], bounds[4]], - [bounds[1], bounds[3], bounds[5]] - ) - ); - - // center will be used as initial focal point - const center = [ - (bounds[0] + bounds[1]) / 2.0, - (bounds[2] + bounds[3]) / 2.0, - (bounds[4] + bounds[5]) / 2.0, - ]; - - const angle = 90; - // distance from camera to focal point - const dist = diagonal / (2 * Math.tan((angle / 360) * Math.PI)); - - const cameraPos = [ - center[0] - normal[0] * dist, - center[1] - normal[1] * dist, - center[2] - normal[2] * dist, - ]; - - // set viewUp based on DOP rotation - const oldDop = camera.getDirectionOfProjection(); - const transform = vtkMatrixBuilder - .buildFromDegree() - .identity() - .rotateFromDirections(oldDop, normal); - - const viewUp = [0, 1, 0]; - transform.apply(viewUp); - - camera.setPosition(...cameraPos); - camera.setDistance(dist); - // should be set after pos and distance - camera.setDirectionOfProjection(...normal); - camera.setViewUp(...viewUp); - camera.setViewAngle(angle); - camera.setClippingRange(dist, dist + 0.1); - - publicAPI.setCenterOfRotation(center); - } - }; - - setManipulators(); -} - -// ---------------------------------------------------------------------------- -// Object factory -// ---------------------------------------------------------------------------- - -const DEFAULT_VALUES = {}; - -// ---------------------------------------------------------------------------- - -export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); - - // Inheritance - vtkInteractorStyleManipulator.extend(publicAPI, model, initialValues); - - macro.setGet(publicAPI, model, ['volumeMapper']); - - // Object specific methods - vtkInteractorStyleMPRSlice(publicAPI, model); -} - -// ---------------------------------------------------------------------------- - -export const newInstance = macro.newInstance( - extend, - 'vtkInteractorStyleMPRSlice' -); - -// ---------------------------------------------------------------------------- - -export default { newInstance, extend }; diff --git a/Sources/Interaction/Style/index.js b/Sources/Interaction/Style/index.js index 7b75f1441da..f3100fbbd99 100644 --- a/Sources/Interaction/Style/index.js +++ b/Sources/Interaction/Style/index.js @@ -1,13 +1,11 @@ import vtkInteractorStyleImage from './InteractorStyleImage'; import vtkInteractorStyleManipulator from './InteractorStyleManipulator'; -import vtkInteractorStyleMPRSlice from './InteractorStyleMPRSlice'; import vtkInteractorStyleRemoteMouse from './InteractorStyleRemoteMouse'; import vtkInteractorStyleTrackballCamera from './InteractorStyleTrackballCamera'; export default { vtkInteractorStyleImage, vtkInteractorStyleManipulator, - vtkInteractorStyleMPRSlice, vtkInteractorStyleRemoteMouse, vtkInteractorStyleTrackballCamera, }; diff --git a/Sources/Rendering/Core/VolumeMapper/SliceHelper.js b/Sources/Rendering/Core/VolumeMapper/SliceHelper.js new file mode 100644 index 00000000000..67ce0d88dce --- /dev/null +++ b/Sources/Rendering/Core/VolumeMapper/SliceHelper.js @@ -0,0 +1,105 @@ +import macro from 'vtk.js/Sources/macros'; +import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; +import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane'; + +function vtkSliceHelper(publicAPI, model) { + model.classHierarchy.push('vtkSliceHelper'); + + model._clipPlane1 = vtkPlane.newInstance(); + model._clipPlane2 = vtkPlane.newInstance(); + + const superClass = { ...publicAPI }; + + function update() { + const n1 = model._clipPlane1.getNormalByReference(); + n1[0] = model.normal[0]; + n1[1] = model.normal[1]; + n1[2] = model.normal[2]; + + const n2 = model._clipPlane2.getNormalByReference(); + n2[0] = -model.normal[0]; + n2[1] = -model.normal[1]; + n2[2] = -model.normal[2]; + + vtkMath.multiplyAccumulate( + model.origin, + model.normal, + -model.thickness / 2, + model._clipPlane1.getOriginByReference() + ); + + vtkMath.multiplyAccumulate( + model.origin, + model.normal, + model.thickness / 2, + model._clipPlane2.getOriginByReference() + ); + + model._clipPlane1.modified(); + model._clipPlane2.modified(); + } + + const subscription = publicAPI.onModified(update); + publicAPI.delete = () => { + superClass.delete(); + subscription.unsubscribe(); + }; + + publicAPI.registerClipPlanesToMapper = (mapper) => { + if (!mapper || !mapper.isA('vtkAbstractMapper')) { + return false; + } + + let changeDetected = mapper.addClippingPlane(model._clipPlane1); + changeDetected = + mapper.addClippingPlane(model._clipPlane2) || changeDetected; + + return changeDetected; + }; + + publicAPI.unregisterClipPlanesFromMapper = (mapper) => { + if (!mapper || !mapper.isA('vtkAbstractMapper')) { + return false; + } + + const planes = mapper.getClippingPlanes(); + let changeDetected = mapper.removeClippingPlane( + planes.indexOf(model._clipPlane1) + ); + changeDetected = + mapper.removeClippingPlane(planes.indexOf(model._clipPlane2)) || + changeDetected; + + return changeDetected; + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + thickness: 0, + origin: [0, 0, 0], + normal: [1, 0, 0], +}; + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Build VTK API + macro.obj(publicAPI, model); + macro.setGet(publicAPI, model, ['thickness']); + macro.setGetArray(publicAPI, model, ['origin', 'normal'], 3); + + // Object methods + vtkSliceHelper(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkSliceHelper'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Rendering/Core/VolumeMapper/index.d.ts b/Sources/Rendering/Core/VolumeMapper/index.d.ts index af36bcb614c..a0f9b70c4c2 100755 --- a/Sources/Rendering/Core/VolumeMapper/index.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/index.d.ts @@ -1,5 +1,6 @@ +import { vtkObject } from "../../../interfaces"; import vtkPiecewiseFunction from "../../../Common/DataModel/PiecewiseFunction"; -import { Bounds, Range } from "../../../types"; +import { Bounds, Range, Vector3 } from "../../../types"; import vtkAbstractMapper, { IAbstractMapperInitialValues } from "../AbstractMapper"; import { BlendMode, FilterMode } from "./Constants"; @@ -283,6 +284,57 @@ export function extend(publicAPI: object, model: object, initialValues?: IVolume */ export function newInstance(initialValues?: IVolumeMapperInitialValues): vtkVolumeMapper; +/** + * + */ +export interface ISliceHelperInitialValues { + thickness?: number; + origin?: Vector3; + normal?: Vector3; +} + +/** + * Helper class to perform MPR. + */ + + export interface vtkSliceHelper extends vtkObject { + /** + * Update the mapper from the slice helper parameters. + */ + update(): void; + + /** + * Get the distance between clip planes + */ + getThickness(): number; + + /** + * Get the origin of the MPR + */ + getOrigin(): Vector3; + + /** + * Get the orientation of the MPR + */ + getNormal(): Vector3; + + /** + * Set the distance between clip planes + */ + setThickness(thickness: number): boolean; + + /** + * Set the origin of the MPR + */ + setOrigin(origin: Vector3): boolean; + + /** + * Set the orientation of the MPR + */ + setNormal(normal: Vector3): boolean; + +} + /** * vtkVolumeMapper inherits from vtkMapper. * A volume mapper that performs ray casting on the GPU using fragment programs. @@ -292,5 +344,25 @@ export declare const vtkVolumeMapper: { extend: typeof extend; BlendMode: typeof BlendMode; FilterMode: typeof FilterMode; + vtkSliceHelper: vtkSliceHelper, }; export default vtkVolumeMapper; + +/** + * Method use to decorate a given object (publicAPI+model) with vtkVolumeMapper characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IVolumeMapperInitialValues} [initialValues] (default: {}) + */ + export function extendSliceHelper(publicAPI: object, model: object, initialValues?: IVolumeMapperInitialValues): void; + + /** + * Method use to create a new instance of vtkVolumeMapper + */ + export function newInstanceSliceHelper(initialValues?: IVolumeMapperInitialValues): vtkVolumeMapper; + +export declare const vtkSliceHelper: { + newInstance: typeof newInstanceSliceHelper, + extend: typeof extendSliceHelper, +}; diff --git a/Sources/Rendering/Core/VolumeMapper/index.js b/Sources/Rendering/Core/VolumeMapper/index.js index 13ff75cc177..41c5ab53d59 100644 --- a/Sources/Rendering/Core/VolumeMapper/index.js +++ b/Sources/Rendering/Core/VolumeMapper/index.js @@ -2,6 +2,7 @@ import macro from 'vtk.js/Sources/macros'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants'; import vtkAbstractMapper from 'vtk.js/Sources/Rendering/Core/AbstractMapper'; +import vtkSliceHelper from 'vtk.js/Sources/Rendering/Core/VolumeMapper/SliceHelper'; import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; const { BlendMode, FilterMode } = Constants; @@ -202,4 +203,4 @@ export const newInstance = macro.newInstance(extend, 'vtkVolumeMapper'); // ---------------------------------------------------------------------------- -export default { newInstance, extend, ...STATIC }; +export default { newInstance, extend, vtkSliceHelper, ...STATIC }; diff --git a/Sources/Rendering/Core/VolumeProperty/index.d.ts b/Sources/Rendering/Core/VolumeProperty/index.d.ts index 516a51ded4a..82a02949cea 100755 --- a/Sources/Rendering/Core/VolumeProperty/index.d.ts +++ b/Sources/Rendering/Core/VolumeProperty/index.d.ts @@ -81,6 +81,7 @@ export interface vtkVolumeProperty extends vtkObject { /** * Get the currently set gray transfer function. Create one if none set. + * NOT SUPPORTED as of now, use RGB transfer function instead. * @param {Number} index */ getGrayTransferFunction(index: number): vtkPiecewiseFunction; @@ -176,6 +177,7 @@ export interface vtkVolumeProperty extends vtkObject { /** * Set the color of a volume to a gray transfer function + * NOT SUPPORTED as of now, use RGB transfer function instead. * @param {Number} index * @param {vtkPiecewiseFunction} func */ diff --git a/Sources/Rendering/Core/index.js b/Sources/Rendering/Core/index.js index c69ef165ff7..fbf59bef00c 100644 --- a/Sources/Rendering/Core/index.js +++ b/Sources/Rendering/Core/index.js @@ -81,6 +81,7 @@ export default { vtkRenderWindowInteractor, vtkScalarBarActor, vtkSkybox, + vtkSliceHelper: vtkVolumeMapper.vtkSliceHelper, vtkSphereMapper, vtkStickMapper, vtkTexture, diff --git a/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/controlPanel.html b/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/controlPanel.html index dfd73aaa3f0..b87e18c6f8a 100644 --- a/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/controlPanel.html +++ b/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/controlPanel.html @@ -23,6 +23,12 @@ + + Render with GPU: + + + + Slab Mode : diff --git a/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/index.js b/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/index.js index 0b81d5ab60b..e0d21d4446b 100644 --- a/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/index.js +++ b/Sources/Widgets/Widgets3D/ResliceCursorWidget/example/index.js @@ -18,6 +18,9 @@ import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; import vtkOutlineFilter from 'vtk.js/Sources/Filters/General/OutlineFilter'; import vtkOrientationMarkerWidget from 'vtk.js/Sources/Interaction/Widgets/OrientationMarkerWidget'; import vtkResliceCursorWidget from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget'; +import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; +import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; +import { BlendMode } from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants'; import vtkWidgetManager from 'vtk.js/Sources/Widgets/Core/WidgetManager'; import vtkSphereSource from 'vtk.js/Sources/Filters/Sources/SphereSource'; @@ -53,6 +56,8 @@ widgetState.setSphereRadius(10 * window.devicePixelRatio); widgetState.setLineThickness(5); const showDebugActors = true; +const debugPlanes = false; +let renderWithGPU = true; // ---------------------------------------------------------------------------- // Define html structure @@ -142,7 +147,11 @@ for (let i = 0; i < 4; i++) { obj.interactor.bindEvents(element); obj.widgetManager.setRenderer(obj.renderer); if (i < 3) { - obj.interactor.setInteractorStyle(vtkInteractorStyleImage.newInstance()); + obj.interactor.setInteractorStyle( + debugPlanes + ? vtkInteractorStyleTrackballCamera.newInstance() + : vtkInteractorStyleImage.newInstance() + ); obj.widgetInstance = obj.widgetManager.addWidget(widget, xyzToViewType[i]); obj.widgetInstance.setScaleInPixels(true); obj.widgetInstance.setRotationHandlePosition(0.75); @@ -155,6 +164,7 @@ for (let i = 0; i < 4; i++) { ); } + // CPU reslice obj.reslice = vtkImageReslice.newInstance(); obj.reslice.setSlabMode(SlabMode.MEAN); obj.reslice.setSlabNumberOfSlices(1); @@ -165,6 +175,18 @@ for (let i = 0; i < 4; i++) { obj.resliceMapper.setInputConnection(obj.reslice.getOutputPort()); obj.resliceActor = vtkImageSlice.newInstance(); obj.resliceActor.setMapper(obj.resliceMapper); + + // GPU reslice + obj.volume = vtkVolume.newInstance(); + obj.volumeMapper = vtkVolumeMapper.newInstance(); + // obj.volumeMapper.setMaximumSamplesPerRay(1); + obj.volume.setMapper(obj.volumeMapper); + + obj.sliceHelper = vtkVolumeMapper.vtkSliceHelper.newInstance({ + thickness: 1, + }); + obj.sliceHelper.registerClipPlanesToMapper(obj.volumeMapper); + obj.sphereActors = []; obj.sphereSources = []; @@ -253,6 +275,7 @@ function updateReslice( viewType: '', reslice: null, actor: null, + sliceHelper: null, renderer: null, resetFocalPoint: false, // Reset the focal point to the center of the display image keepFocalPointPosition: false, // Defines if the focal point position is kepts (same display distance from reslice cursor center) @@ -267,9 +290,20 @@ function updateReslice( ); if (obj.modified) { // Get returned modified from setter to know if we have to render + // CPU interactionContext.actor.setUserMatrix( interactionContext.reslice.getResliceAxes() ); + // GPU + interactionContext.sliceHelper.setNormal( + interactionContext.reslice.getResliceAxes()[8], + interactionContext.reslice.getResliceAxes()[9], + interactionContext.reslice.getResliceAxes()[10] + ); + interactionContext.sliceHelper.setOrigin( + widget.getWidgetState().getCenter() + ); + interactionContext.sphereSources[0].setCenter(...obj.origin); interactionContext.sphereSources[1].setCenter(...obj.point1); interactionContext.sphereSources[2].setCenter(...obj.point2); @@ -301,8 +335,17 @@ reader.setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`).then(() => { view3D.renderer.addActor(outlineActor); viewAttributes.forEach((obj, i) => { - obj.reslice.setInputData(image); - obj.renderer.addActor(obj.resliceActor); + obj.reslice.setInputData(image); // CPU + obj.volumeMapper.setInputData(image); // GPU + + obj.renderer.addActor(obj.resliceActor); // CPU + obj.renderer.addVolume(obj.volume); // GPU + + obj.volume + .getProperty() + .getRGBTransferFunction(0) + .setRange(...image.getPointData().getScalars().getRange()); + view3D.renderer.addActor(obj.resliceActor); obj.sphereActors.forEach((actor) => { obj.renderer.addActor(actor); @@ -333,7 +376,8 @@ reader.setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`).then(() => { updateReslice({ viewType, reslice, - actor: obj.resliceActor, + actor: obj.resliceActor, // CPU + sliceHelper: obj.sliceHelper, // GPU renderer: obj.renderer, resetFocalPoint: false, keepFocalPointPosition, @@ -347,7 +391,8 @@ reader.setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`).then(() => { updateReslice({ viewType, reslice, - actor: obj.resliceActor, + actor: obj.resliceActor, // CPU + sliceHelper: obj.sliceHelper, // GPU renderer: obj.renderer, resetFocalPoint: true, // At first initilization, center the focal point to the image center keepFocalPointPosition: false, // Don't update the focal point as we already set it to the center of the image @@ -375,6 +420,7 @@ function updateViews() { viewType: xyzToViewType[i], reslice: obj.reslice, actor: obj.resliceActor, + sliceHelper: obj.sliceHelper, renderer: obj.renderer, resetFocalPoint: true, keepFocalPointPosition: false, @@ -411,6 +457,20 @@ checkboxScaleInPixels.addEventListener('change', (ev) => { }); }); +document.getElementById('renderWithGPU').addEventListener('change', (ev) => { + renderWithGPU = ev.target.checked; + viewAttributes.forEach((obj, i) => { + obj.resliceActor.setVisibility(!renderWithGPU); + obj.volume.setVisibility(renderWithGPU); + obj.interactor.render(); + }); +}); +viewAttributes.forEach((obj, i) => { + obj.resliceActor.setVisibility(!renderWithGPU); + obj.volume.setVisibility(renderWithGPU); + obj.interactor.render(); +}); + const optionSlabModeMin = document.getElementById('slabModeMin'); optionSlabModeMin.value = SlabMode.MIN; const optionSlabModeMax = document.getElementById('slabModeMax'); @@ -419,10 +479,16 @@ const optionSlabModeMean = document.getElementById('slabModeMean'); optionSlabModeMean.value = SlabMode.MEAN; const optionSlabModeSum = document.getElementById('slabModeSum'); optionSlabModeSum.value = SlabMode.SUM; -const selectSlabMode = document.getElementById('slabMode'); -selectSlabMode.addEventListener('change', (ev) => { +const slabModeToBlendMode = { + [SlabMode.MIN]: BlendMode.MINIMUM_INTENSITY_BLEND, + [SlabMode.MAX]: BlendMode.MAXIMUM_INTENSITY_BLEND, + [SlabMode.MEAN]: BlendMode.AVERAGE_INTENSITY_BLEND, + [SlabMode.SUM]: BlendMode.ADDITIVE_INTENSITY_BLEND, +}; +document.getElementById('slabMode').addEventListener('change', (ev) => { viewAttributes.forEach((obj) => { - obj.reslice.setSlabMode(Number(ev.target.value)); + obj.reslice.setSlabMode(Number(ev.target.value)); // CPU + obj.volumeMapper.setBlendMode(slabModeToBlendMode[ev.target.value]); // GPU }); updateViews(); }); @@ -432,7 +498,8 @@ sliderSlabNumberofSlices.addEventListener('change', (ev) => { const trSlabNumberValue = document.getElementById('slabNumberValue'); trSlabNumberValue.innerHTML = ev.target.value; viewAttributes.forEach((obj) => { - obj.reslice.setSlabNumberOfSlices(ev.target.value); + obj.reslice.setSlabNumberOfSlices(ev.target.value); // CPU + obj.sliceHelper.setThickness(Number(ev.target.value)); // GPU }); updateViews(); }); @@ -447,7 +514,10 @@ buttonReset.addEventListener('click', () => { const selectInterpolationMode = document.getElementById('selectInterpolation'); selectInterpolationMode.addEventListener('change', (ev) => { viewAttributes.forEach((obj) => { - obj.reslice.setInterpolationMode(Number(ev.target.selectedIndex)); + obj.reslice.setInterpolationMode(Number(ev.target.selectedIndex)); // CPU + obj.volume + .getProperty() + .setInterpolationType(Number(ev.target.selectedIndex)); // GPU }); updateViews(); });