diff --git a/Examples/Volume/VolumePicker/controlPanel.html b/Examples/Volume/VolumePicker/controlPanel.html new file mode 100644 index 00000000000..2ef04a2b3b5 --- /dev/null +++ b/Examples/Volume/VolumePicker/controlPanel.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + +
Clip Plane 1
+ Position + + + Rotation + +
Clip Plane 2
+ Position + + + Rotation + +
diff --git a/Examples/Volume/VolumePicker/index.js b/Examples/Volume/VolumePicker/index.js new file mode 100644 index 00000000000..8bb08c81a5c --- /dev/null +++ b/Examples/Volume/VolumePicker/index.js @@ -0,0 +1,236 @@ +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 '@kitware/vtk.js/Rendering/Profiles/Volume'; + +// Force DataAccessHelper to have access to various data source +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; +import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; + +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; +import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; +import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import vtkMatrixBuilder from '@kitware/vtk.js/Common/Core/MatrixBuilder'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; + +import controlPanel from './controlPanel.html'; +import vtkCellPicker from '../../../Sources/Rendering/Core/CellPicker/index'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +fullScreenRenderer.addController(controlPanel); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- +// Server is not sending the .gz and with the compress header +// Need to fetch the true file name and uncompress it locally +// ---------------------------------------------------------------------------- + +const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); + +const actor = vtkVolume.newInstance(); +const mapper = vtkVolumeMapper.newInstance(); +// mapper.setSampleDistance(1.1); +actor.setMapper(mapper); + +const clipPlane1 = vtkPlane.newInstance(); +const clipPlane2 = vtkPlane.newInstance(); +let clipPlane1Position = 0; +let clipPlane2Position = 0; +let clipPlane1RotationAngle = 0; +let clipPlane2RotationAngle = 0; +const clipPlane1Normal = [-1, 1, 0]; +const clipPlane2Normal = [0, 0, 1]; +const rotationNormal = [0, 1, 0]; + +// create color and opacity transfer functions +const ctfun = vtkColorTransferFunction.newInstance(); +ctfun.addRGBPoint(0, 85 / 255.0, 0, 0); +ctfun.addRGBPoint(95, 1.0, 1.0, 1.0); +ctfun.addRGBPoint(225, 0.66, 0.66, 0.5); +ctfun.addRGBPoint(255, 0.3, 1.0, 0.5); +const ofun = vtkPiecewiseFunction.newInstance(); +ofun.addPoint(0.0, 0.0); +ofun.addPoint(255.0, 1.0); +actor.getProperty().setRGBTransferFunction(0, ctfun); +actor.getProperty().setScalarOpacity(0, ofun); +actor.getProperty().setScalarOpacityUnitDistance(0, 3.0); +actor.getProperty().setInterpolationTypeToLinear(); +actor.getProperty().setUseGradientOpacity(0, true); +actor.getProperty().setGradientOpacityMinimumValue(0, 2); +actor.getProperty().setGradientOpacityMinimumOpacity(0, 0.0); +actor.getProperty().setGradientOpacityMaximumValue(0, 20); +actor.getProperty().setGradientOpacityMaximumOpacity(0, 1.0); +actor.getProperty().setShade(true); +actor.getProperty().setAmbient(0.2); +actor.getProperty().setDiffuse(0.7); +actor.getProperty().setSpecular(0.3); +actor.getProperty().setSpecularPower(8.0); + +mapper.setInputConnection(reader.getOutputPort()); + +reader.setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`).then(() => { + reader.loadData().then(() => { + const data = reader.getOutputData(); + const extent = data.getExtent(); + const spacing = data.getSpacing(); + const sizeX = extent[1] * spacing[0]; + const sizeY = extent[3] * spacing[1]; + + clipPlane1Position = sizeX / 4; + clipPlane2Position = sizeY / 2; + const clipPlane1Origin = [ + clipPlane1Position * clipPlane1Normal[0], + clipPlane1Position * clipPlane1Normal[1], + clipPlane1Position * clipPlane1Normal[2], + ]; + const clipPlane2Origin = [ + clipPlane2Position * clipPlane2Normal[0], + clipPlane2Position * clipPlane2Normal[1], + clipPlane2Position * clipPlane2Normal[2], + ]; + + clipPlane1.setNormal(clipPlane1Normal); + clipPlane1.setOrigin(clipPlane1Origin); + clipPlane2.setNormal(clipPlane2Normal); + clipPlane2.setOrigin(clipPlane2Origin); + mapper.addClippingPlane(clipPlane1); + mapper.addClippingPlane(clipPlane2); + + renderer.addVolume(actor); + const interactor = renderWindow.getInteractor(); + interactor.setDesiredUpdateRate(15.0); + renderer.resetCamera(); + renderer.getActiveCamera().elevation(70); + renderWindow.render(); + + let el = document.querySelector('.plane1Position'); + el.setAttribute('min', -sizeX); + el.setAttribute('max', sizeX); + el.setAttribute('value', clipPlane1Position); + + el = document.querySelector('.plane2Position'); + el.setAttribute('min', -sizeY); + el.setAttribute('max', sizeY); + el.setAttribute('value', clipPlane2Position); + + el = document.querySelector('.plane1Rotation'); + el.setAttribute('min', 0); + el.setAttribute('max', 180); + el.setAttribute('value', clipPlane1RotationAngle); + + el = document.querySelector('.plane2Rotation'); + el.setAttribute('min', 0); + el.setAttribute('max', 180); + el.setAttribute('value', clipPlane2RotationAngle); + }); +}); + +document.querySelector('.plane1Position').addEventListener('input', (e) => { + clipPlane1Position = Number(e.target.value); + const clipPlane1Origin = [ + clipPlane1Position * clipPlane1Normal[0], + clipPlane1Position * clipPlane1Normal[1], + clipPlane1Position * clipPlane1Normal[2], + ]; + clipPlane1.setOrigin(clipPlane1Origin); + renderWindow.render(); +}); + +document.querySelector('.plane1Rotation').addEventListener('input', (e) => { + const changedDegree = Number(e.target.value) - clipPlane1RotationAngle; + clipPlane1RotationAngle = Number(e.target.value); + vtkMatrixBuilder + .buildFromDegree() + .rotate(changedDegree, rotationNormal) + .apply(clipPlane1Normal); + clipPlane1.setNormal(clipPlane1Normal); + renderWindow.render(); +}); + +document.querySelector('.plane2Position').addEventListener('input', (e) => { + clipPlane2Position = Number(e.target.value); + const clipPlane2Origin = [ + clipPlane2Position * clipPlane2Normal[0], + clipPlane2Position * clipPlane2Normal[1], + clipPlane2Position * clipPlane2Normal[2], + ]; + clipPlane2.setOrigin(clipPlane2Origin); + renderWindow.render(); +}); + +document.querySelector('.plane2Rotation').addEventListener('input', (e) => { + const changedDegree = Number(e.target.value) - clipPlane2RotationAngle; + clipPlane2RotationAngle = Number(e.target.value); + vtkMatrixBuilder + .buildFromDegree() + .rotate(changedDegree, rotationNormal) + .apply(clipPlane2Normal); + clipPlane2.setNormal(clipPlane2Normal); + renderWindow.render(); +}); + +const picker = vtkCellPicker.newInstance({ opacityThreshold: 0.0001 }); +picker.setPickFromList(1); +picker.setTolerance(0); +picker.initializePickList(); +picker.addPickList(actor); + +// Pick on mouse right click +renderWindow.getInteractor().onRightButtonPress((callData) => { + if (renderer !== callData.pokedRenderer) { + return; + } + + const pos = callData.position; + const point = [pos.x, pos.y, 0.0]; + console.log(`Pick at: ${point}`); + picker.pick(point, renderer); + + const pickedPoints = picker.getPickedPositions(); + for (let i = 0; i < pickedPoints.length; i++) { + const pickedPoint = pickedPoints[i]; + console.log(`Picked: ${pickedPoint}`); + const sphere = vtkSphereSource.newInstance(); + sphere.setCenter(pickedPoint); + sphere.setRadius(5); + const sphereMapper = vtkMapper.newInstance(); + sphereMapper.setInputData(sphere.getOutputData()); + const sphereActor = vtkActor.newInstance(); + sphereActor.setMapper(sphereMapper); + sphereActor.getProperty().setColor(0.0, 0.0, 1.0); + renderer.addActor(sphereActor); + } + renderWindow.render(); +}); + +// ----------------------------------------------------------- +// Make some variables global so that you can inspect and +// modify objects in your browser's developer console: +// ----------------------------------------------------------- + +global.source = reader; +global.mapper = mapper; +global.actor = actor; +global.ctfun = ctfun; +global.ofun = ofun; +global.renderer = renderer; +global.renderWindow = renderWindow; +global.clipPlane1 = clipPlane1; +global.clipPlane2 = clipPlane2; +global.picker = renderWindow.getInteractor().getPicker(); diff --git a/Sources/Rendering/Core/CellPicker/index.d.ts b/Sources/Rendering/Core/CellPicker/index.d.ts index 52ac5260e8d..78aa70395af 100755 --- a/Sources/Rendering/Core/CellPicker/index.d.ts +++ b/Sources/Rendering/Core/CellPicker/index.d.ts @@ -13,6 +13,7 @@ export interface ICellPickerInitialValues extends IPickerInitialValues { cellIJK?: number[]; pickNormal?: number[]; mapperNormal?: number[]; + opacityThreshold?:number; } export interface vtkCellPicker extends vtkPicker { @@ -42,6 +43,11 @@ export interface vtkCellPicker extends vtkPicker { */ getMapperNormalByReference(): number[]; + /** + * Get the opacity threshold for volume picking + */ + getOpacityThreshold(): number[]; + /** * Get the parametric coordinates of the picked cell. */ diff --git a/Sources/Rendering/Core/CellPicker/index.js b/Sources/Rendering/Core/CellPicker/index.js index 78d9e9fd2a0..1ac87b5a950 100644 --- a/Sources/Rendering/Core/CellPicker/index.js +++ b/Sources/Rendering/Core/CellPicker/index.js @@ -7,7 +7,8 @@ import vtkTriangle from 'vtk.js/Sources/Common/DataModel/Triangle'; import vtkQuad from 'vtk.js/Sources/Common/DataModel/Quad'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import { CellType } from 'vtk.js/Sources/Common/DataModel/CellTypes/Constants'; -import { vec3 } from 'gl-matrix'; +import { vec3, vec4, mat4 } from 'gl-matrix'; +import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder'; // ---------------------------------------------------------------------------- // Global methods @@ -197,7 +198,14 @@ function vtkCellPicker(publicAPI, model) { model.pCoords = pickData.pCoords; } } else if (mapper.isA('vtkVolumeMapper')) { - tMin = publicAPI.intersectVolumeWithLine(p1, p2, t1, t2, tol, actor); + tMin = publicAPI.intersectVolumeWithLine( + p1, + p2, + clipLine.t1, + clipLine.t2, + tol, + actor + ); } else if (mapper.isA('vtkMapper')) { tMin = publicAPI.intersectActorWithLine(p1, p2, t1, t2, tol, mapper); } @@ -255,20 +263,40 @@ function vtkCellPicker(publicAPI, model) { const dims = imageData.getDimensions(); const scalars = imageData.getPointData().getScalars().getData(); const extent = imageData.getExtent(); + const direction = imageData.getDirection(); + let imageTransform; + if (!vtkMath.isIdentity3x3(direction)) { + imageTransform = vtkMatrixBuilder + .buildFromRadian() + .translate(origin[0], origin[1], origin[2]) + .multiply3x3(direction) + .translate(-origin[0], -origin[1], -origin[2]) + .invert() + .getMatrix(); + } // calculate opacity table const numIComps = 1; const oWidth = 1024; const tmpTable = new Float32Array(oWidth); + const opacityArray = new Float32Array(oWidth); let ofun; let oRange; + const sampleDist = volume.getMapper().getSampleDistance(); for (let c = 0; c < numIComps; ++c) { ofun = volume.getProperty().getScalarOpacity(c); oRange = ofun.getRange(); ofun.getTable(oRange[0], oRange[1], oWidth, tmpTable, 1); + const opacityFactor = + sampleDist / volume.getProperty().getScalarOpacityUnitDistance(c); + + // adjust for sample distance etc + for (let i = 0; i < oWidth; ++i) { + opacityArray[i] = 1.0 - (1.0 - tmpTable[i]) ** opacityFactor; + } } - const scale = oWidth / (oRange[1] - oRange[0]); + const scale = oWidth / (oRange[1] - oRange[0] + 1); // Make a new p1 and p2 using the clipped t1 and t2 const q1 = [0, 0, 0]; @@ -292,36 +320,54 @@ function vtkCellPicker(publicAPI, model) { x1[i] = (p1[i] - origin[i]) / spacing[i]; x2[i] = (p2[i] - origin[i]) / spacing[i]; } - const x = [0, 0, 0]; + const x = [0, 0, 0, 0]; const xi = [0, 0, 0]; const pcoords = [0, 0, 0]; const sliceSize = dims[1] * dims[0]; const rowSize = dims[0]; const nSteps = 100; + let insideVolume; for (let t = t1; t < t2; t += 1 / nSteps) { // calculate the location of the point + insideVolume = true; for (let j = 0; j < 3; j++) { // "t" is the fractional distance between endpoints x1 and x2 x[j] = x1[j] * (1.0 - t) + x2[j] * t; + } + x[3] = 1.0; + if (imageTransform) { + vec4.transformMat4(x, x, imageTransform); + } - // Paranoia bounds check + for (let j = 0; j < 3; j++) { + // Bounds check if (x[j] < extent[2 * j]) { x[j] = extent[2 * j]; + insideVolume = false; } else if (x[j] > extent[2 * j + 1]) { x[j] = extent[2 * j + 1]; + insideVolume = false; } xi[j] = Math.floor(x[j]); pcoords[j] = x[j] - xi[j]; } - const index = xi[2] * sliceSize + xi[1] * rowSize + xi[0]; - const value = Math.floor((scalars[index] - oRange[0]) * scale); - const opacity = tmpTable[value]; - if (opacity > model.opacityThreshold) { - tMin = t; - break; + if (insideVolume) { + const index = xi[2] * sliceSize + xi[1] * rowSize + xi[0]; + let value = scalars[index]; + if (value < oRange[0]) { + value = oRange[0]; + } else if (value > oRange[1]) { + value = oRange[1]; + } + value = Math.floor((value - oRange[0]) * scale); + const opacity = tmpTable[value]; + if (opacity > model.opacityThreshold) { + tMin = t; + break; + } } }