From 446cdaa9f6486b0c801a27a8d861d7e80a53e345 Mon Sep 17 00:00:00 2001 From: Drew Lazzeri Date: Thu, 18 Nov 2021 11:05:09 -0700 Subject: [PATCH 1/5] feat(sliceHelper): add helper for slicing imageMapper with clip planes To deprecate vtkInteractorStyleMPRSlice I add a helper using two clip planes instead. The included example is similar to vtkInteractorStyleMPRSlice example. re #1872 --- Examples/Volume/MPRSlice/index.js | 60 ++++++++++ .../Core/VolumeMapper/SliceHelper.js | 105 ++++++++++++++++++ Sources/Rendering/Core/VolumeMapper/index.js | 3 +- Sources/Rendering/Core/index.js | 1 + 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 Examples/Volume/MPRSlice/index.js create mode 100644 Sources/Rendering/Core/VolumeMapper/SliceHelper.js 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/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.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/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, From 079ab65e0f19ca38f9ae15421f51d8e5c69daf69 Mon Sep 17 00:00:00 2001 From: Drew Lazzeri Date: Thu, 18 Nov 2021 11:07:11 -0700 Subject: [PATCH 2/5] fix(InteractorStyleMPRSlice): deprecate InteractorStyleMPRSlice BREAKING CHANGE: InteractorStyleMPRSlice has been removed. vtkSliceHelper achieves the same goal with clip planes instead. fix #1872 --- Examples/Volume/VolumeOutline/index.js | 156 -------- .../Style/InteractorStyleMPRSlice/api.md | 31 -- .../example/controlPanel.html | 23 -- .../InteractorStyleMPRSlice/example/index.js | 88 ----- .../Style/InteractorStyleMPRSlice/index.js | 334 ------------------ Sources/Interaction/Style/index.js | 2 - 6 files changed, 634 deletions(-) delete mode 100644 Examples/Volume/VolumeOutline/index.js delete mode 100644 Sources/Interaction/Style/InteractorStyleMPRSlice/api.md delete mode 100644 Sources/Interaction/Style/InteractorStyleMPRSlice/example/controlPanel.html delete mode 100644 Sources/Interaction/Style/InteractorStyleMPRSlice/example/index.js delete mode 100644 Sources/Interaction/Style/InteractorStyleMPRSlice/index.js 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, }; From d4f8ca7e5107fc3ae071243c26400ac2e55efe85 Mon Sep 17 00:00:00 2001 From: Drew Lazzeri Date: Thu, 18 Nov 2021 13:29:47 -0700 Subject: [PATCH 3/5] docs(VolumeMapper): Add vtkSliceHelper types --- .../Rendering/Core/VolumeMapper/index.d.ts | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/Sources/Rendering/Core/VolumeMapper/index.d.ts b/Sources/Rendering/Core/VolumeMapper/index.d.ts index af36bcb614c..c68e255ddd0 100755 --- a/Sources/Rendering/Core/VolumeMapper/index.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/index.d.ts @@ -1,5 +1,5 @@ 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 +283,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 +343,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, +}; From a260c953adb014950e4bcde9830965aa41870a6f Mon Sep 17 00:00:00 2001 From: Julien Finet Date: Sat, 10 Sep 2022 12:51:29 +0200 Subject: [PATCH 4/5] docs(volumeproperty): add warning that gray transfer function is not supported --- Sources/Rendering/Core/VolumeProperty/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) 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 */ From cd364a847e5c94c8ff2e0afa9c0e589e77975415 Mon Sep 17 00:00:00 2001 From: Julien Finet Date: Sat, 10 Sep 2022 12:53:50 +0200 Subject: [PATCH 5/5] feat(reslicecursorwidget): add GPU rendering to ResliceCursorWidget Some features are still not yet supported: - [] display bug when no opaque actors (showDebugActors=false) are rendered. - [] volume mapper should write the plane in depth buffer. - [] volume mapper should render BEFORE the handles --- .../Rendering/Core/VolumeMapper/index.d.ts | 1 + .../example/controlPanel.html | 6 ++ .../ResliceCursorWidget/example/index.js | 90 ++++++++++++++++--- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/Sources/Rendering/Core/VolumeMapper/index.d.ts b/Sources/Rendering/Core/VolumeMapper/index.d.ts index c68e255ddd0..a0f9b70c4c2 100755 --- a/Sources/Rendering/Core/VolumeMapper/index.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/index.d.ts @@ -1,3 +1,4 @@ +import { vtkObject } from "../../../interfaces"; import vtkPiecewiseFunction from "../../../Common/DataModel/PiecewiseFunction"; import { Bounds, Range, Vector3 } from "../../../types"; import vtkAbstractMapper, { IAbstractMapperInitialValues } from "../AbstractMapper"; 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(); });