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 @@
+
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;
+ }
}
}