diff --git a/Examples/Rendering/ManyRenderWindows/index.js b/Examples/Rendering/ManyRenderWindows/index.js index c2fcd8a697d..6bc0978887c 100644 --- a/Examples/Rendering/ManyRenderWindows/index.js +++ b/Examples/Rendering/ManyRenderWindows/index.js @@ -44,25 +44,24 @@ function createVolumeActor(imageData) { // Create and setup the mapper const mapper = vtkVolumeMapper.newInstance(); mapper.setSampleDistance(0.7); - mapper.setVolumetricScatteringBlending(0); - mapper.setLocalAmbientOcclusion(0); - mapper.setLAOKernelSize(10); - mapper.setLAOKernelRadius(5); - mapper.setComputeNormalFromOpacity(true); mapper.setInputData(imageData); // Create and setup the actor const actor = vtkVolume.newInstance(); - actor - .getProperty() - .setRGBTransferFunction(0, getRandomColorTransferFunction()); - actor.getProperty().setScalarOpacity(0, sharedOpacityFunction); - actor.getProperty().setInterpolationTypeToLinear(); - actor.getProperty().setShade(true); - actor.getProperty().setAmbient(0.3); - actor.getProperty().setDiffuse(0.8); - actor.getProperty().setSpecular(1); - actor.getProperty().setSpecularPower(8); + const actorProperty = actor.getProperty(); + actorProperty.setComputeNormalFromOpacity(true); + actorProperty.setLAOKernelRadius(5); + actorProperty.setLAOKernelSize(10); + actorProperty.setLocalAmbientOcclusion(0); + actorProperty.setVolumetricScatteringBlending(0); + actorProperty.setRGBTransferFunction(0, getRandomColorTransferFunction()); + actorProperty.setScalarOpacity(0, sharedOpacityFunction); + actorProperty.setInterpolationTypeToLinear(); + actorProperty.setShade(true); + actorProperty.setAmbient(0.3); + actorProperty.setDiffuse(0.8); + actorProperty.setSpecular(1); + actorProperty.setSpecularPower(8); actor.setMapper(mapper); return actor; diff --git a/Examples/Rendering/QuadView/index.js b/Examples/Rendering/QuadView/index.js index 8813a35052f..7eb6fbf3204 100644 --- a/Examples/Rendering/QuadView/index.js +++ b/Examples/Rendering/QuadView/index.js @@ -158,28 +158,27 @@ function createVolumeView(renderer, source) { .reduce((a, b) => a + b, 0) ); mapper.setSampleDistance(sampleDistance / 2.5); - mapper.setComputeNormalFromOpacity(false); - mapper.setGlobalIlluminationReach(0.0); - mapper.setVolumetricScatteringBlending(0.5); - mapper.setVolumeShadowSamplingDistFactor(5.0); // Set volume properties const volProp = vtkVolumeProperty.newInstance(); + volProp.setComputeNormalFromOpacity(false); + volProp.setGlobalIlluminationReach(0.0); + volProp.setVolumeShadowSamplingDistFactor(5.0); + volProp.setVolumetricScatteringBlending(0.5); volProp.setInterpolationTypeToLinear(); - volume - .getProperty() - .setScalarOpacityUnitDistance( - 0, - vtkBoundingBox.getDiagonalLength(source.getBounds()) / - Math.max(...source.getDimensions()) - ); + volProp.setScalarOpacityUnitDistance( + 0, + vtkBoundingBox.getDiagonalLength(source.getBounds()) / + Math.max(...source.getDimensions()) + ); volProp.setGradientOpacityMinimumValue(0, 0); const dataArray = source.getPointData().getScalars() || source.getPointData().getArrays()[0]; const dataRange = dataArray.getRange(); - volume - .getProperty() - .setGradientOpacityMaximumValue(0, (dataRange[1] - dataRange[0]) * 0.05); + volProp.setGradientOpacityMaximumValue( + 0, + (dataRange[1] - dataRange[0]) * 0.05 + ); volProp.setShade(true); volProp.setUseGradientOpacity(0, false); volProp.setGradientOpacityMinimumOpacity(0, 0.0); diff --git a/Examples/Volume/VolumeMapperBlendModes/index.js b/Examples/Volume/VolumeMapperBlendModes/index.js index 4f6262edf21..881e4b0282a 100644 --- a/Examples/Volume/VolumeMapperBlendModes/index.js +++ b/Examples/Volume/VolumeMapperBlendModes/index.js @@ -40,11 +40,12 @@ const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); const initialSampleDistance = 1.3; const actor = vtkVolume.newInstance(); +const actorProperty = actor.getProperty(); const mapper = vtkVolumeMapper.newInstance(); mapper.setSampleDistance(initialSampleDistance); // use half float at the cost of precision to save memory -mapper.setPreferSizeOverAccuracy(true); +actorProperty.setPreferSizeOverAccuracy(true); actor.setMapper(mapper); @@ -125,16 +126,16 @@ function updateSampleDistance(event) { } function updateScalarMin(event) { - mapper.setIpScalarRange( + actorProperty.setIpScalarRange( event.target.valueAsNumber, - mapper.getIpScalarRange()[1] + actorProperty.getIpScalarRange()[1] ); renderWindow.render(); } function updateScalarMax(event) { - mapper.setIpScalarRange( - mapper.getIpScalarRange()[0], + actorProperty.setIpScalarRange( + actorProperty.getIpScalarRange()[0], event.target.valueAsNumber ); renderWindow.render(); @@ -146,7 +147,7 @@ function updateBlendMode(event) { const radonScalars = document.querySelectorAll('.radonScalar'); mapper.setBlendMode(currentBlendMode); - mapper.setIpScalarRange(0.0, 1.0); + actorProperty.setIpScalarRange(0.0, 1.0); // if average or additive blend mode for (let i = 0; i < ipScalarEls.length; i += 1) { diff --git a/Examples/Volume/VolumeMapperLightAndShadow/index.js b/Examples/Volume/VolumeMapperLightAndShadow/index.js index 370fd98b86b..3f0ad59d17c 100644 --- a/Examples/Volume/VolumeMapperLightAndShadow/index.js +++ b/Examples/Volume/VolumeMapperLightAndShadow/index.js @@ -2,7 +2,6 @@ import '@kitware/vtk.js/favicon'; import '@kitware/vtk.js/Rendering/Profiles/Volume'; import '@kitware/vtk.js/Rendering/Profiles/Geometry'; -import macro from '@kitware/vtk.js/macros'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; @@ -14,6 +13,7 @@ import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpD import vtkVolumeController from '@kitware/vtk.js/Interaction/UI/VolumeController'; import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import vtkFPSMonitor from '@kitware/vtk.js/Interaction/UI/FPSMonitor'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; @@ -31,19 +31,6 @@ const fpsMonitor = vtkFPSMonitor.newInstance(); const progressContainer = document.createElement('div'); myContainer.appendChild(progressContainer); -const progressCallback = (progressEvent) => { - if (progressEvent.lengthComputable) { - const percent = Math.floor( - (100 * progressEvent.loaded) / progressEvent.total - ); - progressContainer.innerHTML = `Loading ${percent}%`; - } else { - progressContainer.innerHTML = macro.formatBytesToProperUnit( - progressEvent.loaded - ); - } -}; - // ---------------------------------------------------------------------------- // Main function to set up and render volume // ---------------------------------------------------------------------------- @@ -94,10 +81,23 @@ function createVolumeShadowViewer(rootContainer, fileContents) { const source = vtiReader.getOutputData(0); const actor = vtkVolume.newInstance(); + const actorProperty = actor.getProperty(0); const mapper = vtkVolumeMapper.newInstance(); actor.setMapper(mapper); - mapper.setInputData(source); + mapper.addInputData(source); + + for (let i = 0; i < 3; ++i) { + const otherImageData = vtkImageData.newInstance(); + otherImageData.setPointData(source.getPointData()); + otherImageData.setDimensions(...source.getDimensions()); + otherImageData.setSpacing(...source.getSpacing()); + otherImageData.setOrigin(...source.getOrigin()); + otherImageData.setDirection(...source.getDirection()); + otherImageData.setOrigin(...[120 * (i + 1), 0, 0]); + mapper.addInputData(otherImageData); + actor.setProperty(actorProperty, 1 + i); + } // Add one positional light const bounds = actor.getBounds(); @@ -124,44 +124,42 @@ function createVolumeShadowViewer(rootContainer, fileContents) { .reduce((a, b) => a + b, 0) ); mapper.setSampleDistance(sampleDistance / 2.5); - mapper.setComputeNormalFromOpacity(false); - mapper.setGlobalIlluminationReach(0.0); - mapper.setVolumetricScatteringBlending(0.0); mapper.setVolumeShadowSamplingDistFactor(5.0); // Add transfer function const lookupTable = vtkColorTransferFunction.newInstance(); const piecewiseFunction = vtkPiecewiseFunction.newInstance(); - actor.getProperty().setRGBTransferFunction(0, lookupTable); - actor.getProperty().setScalarOpacity(0, piecewiseFunction); + actorProperty.setRGBTransferFunction(0, lookupTable); + actorProperty.setScalarOpacity(0, piecewiseFunction); // Set actor properties - actor.getProperty().setInterpolationTypeToLinear(); - actor - .getProperty() - .setScalarOpacityUnitDistance( - 0, - vtkBoundingBox.getDiagonalLength(source.getBounds()) / - Math.max(...source.getDimensions()) - ); - actor.getProperty().setGradientOpacityMinimumValue(0, 0); + actorProperty.setComputeNormalFromOpacity(false); + actorProperty.setGlobalIlluminationReach(0.0); + actorProperty.setVolumetricScatteringBlending(0.0); + actorProperty.setInterpolationTypeToLinear(); + actorProperty.setScalarOpacityUnitDistance( + 0, + vtkBoundingBox.getDiagonalLength(source.getBounds()) / + Math.max(...source.getDimensions()) + ); + actorProperty.setGradientOpacityMinimumValue(0, 0); const dataArray = source.getPointData().getScalars() || source.getPointData().getArrays()[0]; const dataRange = dataArray.getRange(); - actor - .getProperty() - .setGradientOpacityMaximumValue(0, (dataRange[1] - dataRange[0]) * 0.05); - actor.getProperty().setShade(true); - actor.getProperty().setUseGradientOpacity(0, false); - actor.getProperty().setGradientOpacityMinimumOpacity(0, 0.0); - actor.getProperty().setGradientOpacityMaximumOpacity(0, 1.0); - actor.getProperty().setAmbient(0.0); - actor.getProperty().setDiffuse(2.0); - actor.getProperty().setSpecular(0.0); - actor.getProperty().setSpecularPower(0.0); - actor.getProperty().setUseLabelOutline(false); - actor.getProperty().setLabelOutlineThickness(2); - renderer.addActor(actor); + actorProperty.setGradientOpacityMaximumValue( + 0, + (dataRange[1] - dataRange[0]) * 0.05 + ); + actorProperty.setShade(true); + actorProperty.setUseGradientOpacity(0, false); + actorProperty.setGradientOpacityMinimumOpacity(0, 0.0); + actorProperty.setGradientOpacityMaximumOpacity(0, 1.0); + actorProperty.setAmbient(0.0); + actorProperty.setDiffuse(2.0); + actorProperty.setSpecular(0.0); + actorProperty.setSpecularPower(0.0); + actorProperty.setUseLabelOutline(false); + actorProperty.setLabelOutlineThickness(2); // Control UI for sample distance, transfer function, and shadow on/off const controllerWidget = vtkVolumeController.newInstance({ @@ -191,12 +189,12 @@ function createVolumeShadowViewer(rootContainer, fileContents) { // Add sliders to tune volume shadow effect function updateVSB(e) { const vsb = Number(e.target.value); - mapper.setVolumetricScatteringBlending(vsb); + actorProperty.setVolumetricScatteringBlending(vsb); renderWindow.render(); } function updateGlobalReach(e) { const gir = Number(e.target.value); - mapper.setGlobalIlluminationReach(gir); + actorProperty.setGlobalIlluminationReach(gir); renderWindow.render(); } function updateSD(e) { @@ -206,7 +204,7 @@ function createVolumeShadowViewer(rootContainer, fileContents) { } function updateAT(e) { const at = Number(e.target.value); - mapper.setAnisotropy(at); + actorProperty.setAnisotropy(at); renderWindow.render(); } const el = document.querySelector('.volumeBlending'); @@ -235,7 +233,7 @@ function createVolumeShadowViewer(rootContainer, fileContents) { const buttonID = document.querySelector('.text2'); function toggleDensityNormal() { isDensity = !isDensity; - mapper.setComputeNormalFromOpacity(isDensity); + actorProperty.setComputeNormalFromOpacity(isDensity); buttonID.innerText = `(${isDensity ? 'on' : 'off'})`; renderWindow.render(); } @@ -257,6 +255,9 @@ function createVolumeShadowViewer(rootContainer, fileContents) { renderer.addActor(actorSphere); } + // Add the volume actor here to avoid compiling the shader twice + renderer.addActor(actor); + // Camera and first render renderer.resetCamera(); renderWindow.render(); @@ -279,11 +280,7 @@ function createVolumeShadowViewer(rootContainer, fileContents) { // Read volume and render // ---------------------------------------------------------------------------- HttpDataAccessHelper.fetchBinary( - 'https://data.kitware.com/api/v1/item/59de9dc98d777f31ac641dc1/download', - { - progressCallback, - } + `${__BASE_PATH__}/data/volume/head-binary.vti` ).then((binary) => { - myContainer.removeChild(progressContainer); createVolumeShadowViewer(myContainer, binary); }); diff --git a/Examples/Volume/WebXRChestCTBlendedCVR/index.js b/Examples/Volume/WebXRChestCTBlendedCVR/index.js index 38c144b2eba..611a1c1c255 100644 --- a/Examples/Volume/WebXRChestCTBlendedCVR/index.js +++ b/Examples/Volume/WebXRChestCTBlendedCVR/index.js @@ -107,9 +107,9 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { actor.getProperty().setAmbient(0.2); actor.getProperty().setDiffuse(1.3); actor.getProperty().setSpecular(0.0); - mapper.setGlobalIlluminationReach(0.1); - mapper.setVolumetricScatteringBlending(0.5); - mapper.setVolumeShadowSamplingDistFactor(1.0); + actor.getProperty().setGlobalIlluminationReach(0.1); + actor.getProperty().setVolumetricScatteringBlending(0.5); + actor.getProperty().setVolumeShadowSamplingDistFactor(1.0); mapper.setAutoAdjustSampleDistances(false); // Set up rendering diff --git a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js index b85b9bb5f54..59596c13407 100644 --- a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js +++ b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js @@ -139,9 +139,9 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { actor.getProperty().setAmbient(0.0); actor.getProperty().setDiffuse(2.0); actor.getProperty().setSpecular(0.0); - mapper.setGlobalIlluminationReach(1.0); - mapper.setVolumetricScatteringBlending(1.0); - mapper.setVolumeShadowSamplingDistFactor(1.0); + actor.getProperty().setGlobalIlluminationReach(1.0); + actor.getProperty().setVolumetricScatteringBlending(1.0); + actor.getProperty().setVolumeShadowSamplingDistFactor(1.0); mapper.setAutoAdjustSampleDistances(false); // Set up rendering diff --git a/Examples/Volume/WebXRHeadGradientCVR/index.js b/Examples/Volume/WebXRHeadGradientCVR/index.js index 5698a566abf..15549f9628d 100644 --- a/Examples/Volume/WebXRHeadGradientCVR/index.js +++ b/Examples/Volume/WebXRHeadGradientCVR/index.js @@ -136,9 +136,9 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { // CVR actor.getProperty().setShade(true); - mapper.setGlobalIlluminationReach(0.0); - mapper.setVolumetricScatteringBlending(0.0); - mapper.setVolumeShadowSamplingDistFactor(1.0); + actor.getProperty().setGlobalIlluminationReach(0.0); + actor.getProperty().setVolumetricScatteringBlending(0.0); + actor.getProperty().setVolumeShadowSamplingDistFactor(1.0); mapper.setAutoAdjustSampleDistances(false); // Set up rendering diff --git a/Sources/Rendering/Core/Actor/index.d.ts b/Sources/Rendering/Core/Actor/index.d.ts index 50f529928d1..54a4e68cf74 100755 --- a/Sources/Rendering/Core/Actor/index.d.ts +++ b/Sources/Rendering/Core/Actor/index.d.ts @@ -36,12 +36,6 @@ export interface vtkActor extends vtkProp3D { */ getBackfaceProperty(): vtkProperty; - /** - * Get the bounds for this mapper as [xmin, xmax, ymin, ymax,zmin, zmax]. - * @return {Bounds} The bounds for the mapper. - */ - getBounds(): Bounds; - /** * Check whether the opaque is forced or not. */ diff --git a/Sources/Rendering/Core/Actor/index.js b/Sources/Rendering/Core/Actor/index.js index 43f769187f6..4fe57492029 100644 --- a/Sources/Rendering/Core/Actor/index.js +++ b/Sources/Rendering/Core/Actor/index.js @@ -1,11 +1,7 @@ -import { vec3, mat4 } from 'gl-matrix'; import macro from 'vtk.js/Sources/macros'; -import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkProp3D from 'vtk.js/Sources/Rendering/Core/Prop3D'; import vtkProperty from 'vtk.js/Sources/Rendering/Core/Property'; -const { vtkDebugMacro } = macro; - // ---------------------------------------------------------------------------- // vtkActor methods // ---------------------------------------------------------------------------- @@ -66,66 +62,6 @@ function vtkActor(publicAPI, model) { return model.property; }; - publicAPI.getBounds = () => { - if (model.mapper === null) { - return model.bounds; - } - - // Check for the special case when the mapper's bounds are unknown - const bds = model.mapper.getBounds(); - if (!bds || bds.length !== 6) { - return bds; - } - - // Check for the special case when the actor is empty. - if (bds[0] > bds[1]) { - model.mapperBounds = bds.concat(); // copy the mapper's bounds - model.bounds = [1, -1, 1, -1, 1, -1]; - model.boundsMTime.modified(); - return bds; - } - - // Check if we have cached values for these bounds - we cache the - // values returned by model.mapper.getBounds() and we store the time - // of caching. If the values returned this time are different, or - // the modified time of this class is newer than the cached time, - // then we need to rebuild. - if ( - !model.mapperBounds || - bds[0] !== model.mapperBounds[0] || - bds[1] !== model.mapperBounds[1] || - bds[2] !== model.mapperBounds[2] || - bds[3] !== model.mapperBounds[3] || - bds[4] !== model.mapperBounds[4] || - bds[5] !== model.mapperBounds[5] || - publicAPI.getMTime() > model.boundsMTime.getMTime() - ) { - vtkDebugMacro('Recomputing bounds...'); - model.mapperBounds = bds.concat(); // copy the mapper's bounds - const bbox = []; - vtkBoundingBox.getCorners(bds, bbox); - - publicAPI.computeMatrix(); - const tmp4 = new Float64Array(16); - mat4.transpose(tmp4, model.matrix); - bbox.forEach((pt) => vec3.transformMat4(pt, pt, tmp4)); - - /* eslint-disable no-multi-assign */ - model.bounds[0] = model.bounds[2] = model.bounds[4] = Number.MAX_VALUE; - model.bounds[1] = model.bounds[3] = model.bounds[5] = -Number.MAX_VALUE; - /* eslint-enable no-multi-assign */ - - model.bounds = model.bounds.map((d, i) => - i % 2 === 0 - ? bbox.reduce((a, b) => (a > b[i / 2] ? b[i / 2] : a), d) - : bbox.reduce((a, b) => (a < b[(i - 1) / 2] ? b[(i - 1) / 2] : a), d) - ); - - model.boundsMTime.modified(); - } - return model.bounds; - }; - publicAPI.getMTime = () => { let mt = superClass.getMTime(); if (model.property !== null) { @@ -177,8 +113,6 @@ const DEFAULT_VALUES = { forceOpaque: false, forceTranslucent: false, - - bounds: [1, -1, 1, -1, 1, -1], }; // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/Core/ImageSlice/index.d.ts b/Sources/Rendering/Core/ImageSlice/index.d.ts index 52f5962bd9f..b25417fd536 100755 --- a/Sources/Rendering/Core/ImageSlice/index.d.ts +++ b/Sources/Rendering/Core/ImageSlice/index.d.ts @@ -17,18 +17,6 @@ export interface vtkImageSlice extends vtkProp3D { */ getActors(): any; - /** - * Get the bounds for this mapper as [xmin, xmax, ymin, ymax,zmin, zmax]. - * @return {Bounds} The bounds for the mapper. - */ - getBounds(): Bounds; - - /** - * Get the bounds for this mapper as [xmin, xmax, ymin, ymax,zmin, zmax]. - * @return {Bounds} The bounds for the mapper. - */ - getBoundsByReference(): Bounds; - /** * Get the bounds for a given slice as [xmin, xmax, ymin, ymax,zmin, zmax]. * @param {Number} slice The slice index. If undefined, the current slice is considered. diff --git a/Sources/Rendering/Core/ImageSlice/index.js b/Sources/Rendering/Core/ImageSlice/index.js index 3cdc5ca3136..5c61d2ccc74 100644 --- a/Sources/Rendering/Core/ImageSlice/index.js +++ b/Sources/Rendering/Core/ImageSlice/index.js @@ -4,8 +4,6 @@ import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkProp3D from 'vtk.js/Sources/Rendering/Core/Prop3D'; import vtkImageProperty from 'vtk.js/Sources/Rendering/Core/ImageProperty'; -const { vtkDebugMacro } = macro; - // ---------------------------------------------------------------------------- // vtkImageSlice methods // ---------------------------------------------------------------------------- @@ -54,52 +52,6 @@ function vtkImageSlice(publicAPI, model) { return model.property; }; - publicAPI.getBounds = () => { - if (model.mapper === null) { - return model.bounds; - } - - // Check for the special case when the mapper's bounds are unknown - const bds = model.mapper.getBounds(); - if (!bds || bds.length !== 6) { - return bds; - } - - // Check for the special case when the actor is empty. - if (bds[0] > bds[1]) { - model.mapperBounds = bds.concat(); // copy the mapper's bounds - model.bounds = [1, -1, 1, -1, 1, -1]; - model.boundsMTime.modified(); - return bds; - } - - // Check if we have cached values for these bounds - we cache the - // values returned by model.mapper.getBounds() and we store the time - // of caching. If the values returned this time are different, or - // the modified time of this class is newer than the cached time, - // then we need to rebuild. - const zip = (rows) => rows[0].map((_, c) => rows.map((row) => row[c])); - if ( - !model.mapperBounds || - !zip([bds, model.mapperBounds]).reduce( - (a, b) => a && b[0] === b[1], - true - ) || - publicAPI.getMTime() > model.boundsMTime.getMTime() - ) { - vtkDebugMacro('Recomputing bounds...'); - model.mapperBounds = bds.map((x) => x); - - publicAPI.computeMatrix(); - const tmp4 = new Float64Array(16); - mat4.transpose(tmp4, model.matrix); - - vtkBoundingBox.transformBounds(bds, tmp4, model.bounds); - model.boundsMTime.modified(); - } - return model.bounds; - }; - publicAPI.getBoundsForSlice = (slice, thickness) => { // Check for the special case when the mapper's bounds are unknown const bds = model.mapper.getBoundsForSlice(slice, thickness); @@ -178,11 +130,8 @@ function vtkImageSlice(publicAPI, model) { const DEFAULT_VALUES = { mapper: null, property: null, - forceOpaque: false, forceTranslucent: false, - - bounds: [...vtkBoundingBox.INIT_BOUNDS], }; // ---------------------------------------------------------------------------- @@ -201,8 +150,6 @@ export function extend(publicAPI, model, initialValues = {}) { macro.set(publicAPI, model, ['property']); macro.setGet(publicAPI, model, ['mapper', 'forceOpaque', 'forceTranslucent']); - macro.getArray(publicAPI, model, ['bounds'], 6); - // Object methods vtkImageSlice(publicAPI, model); } diff --git a/Sources/Rendering/Core/Prop3D/index.d.ts b/Sources/Rendering/Core/Prop3D/index.d.ts index 19e680f6fec..12fa4849f00 100755 --- a/Sources/Rendering/Core/Prop3D/index.d.ts +++ b/Sources/Rendering/Core/Prop3D/index.d.ts @@ -19,11 +19,17 @@ export interface vtkProp3D extends vtkProp { addPosition(deltaXYZ: number[]): void; /** - * Get the bounds as [xmin, xmax, ymin, ymax, zmin, zmax]. - * @return {Bounds} The bounds for the mapper. + * Get the bounds of this actor as [xmin, xmax, ymin, ymax, zmin, zmax]. + * They are the bounds of the underlying mapper, transformed using the actor's matrix. + * @return {Bounds} The bounds for the actor. */ getBounds(): Bounds; + /** + * Same as getBounds() but the returned array is not copied, so it should not be written to. + */ + getBoundsByReference(): Bounds; + /** * Check if there was a modification or transformation. * @default null diff --git a/Sources/Rendering/Core/Prop3D/index.js b/Sources/Rendering/Core/Prop3D/index.js index aaecd376e0a..2b5e62f3a55 100644 --- a/Sources/Rendering/Core/Prop3D/index.js +++ b/Sources/Rendering/Core/Prop3D/index.js @@ -161,6 +161,62 @@ function vtkProp3D(publicAPI, model) { } }; + publicAPI.getBoundsByReference = () => { + if (model.mapper === null) { + return model.bounds; + } + + // Check for the special case when the mapper's bounds are unknown + const bds = model.mapper.getBounds(); + if (!bds || bds.length !== 6) { + return bds; + } + + // Check for the special case when the actor is empty. + if (bds[0] > bds[1]) { + // No need to copy bds, a new array is created when calling getBounds() + model.mapperBounds = bds; + model.bounds = [...vtkBoundingBox.INIT_BOUNDS]; + model.boundsMTime.modified(); + return bds; + } + + // Check if we have cached values for these bounds - we cache the + // values returned by model.mapper.getBounds() and we store the time + // of caching. If the values returned this time are different, or + // the modified time of this class is newer than the cached time, + // then we need to rebuild. + if ( + !model.mapperBounds || + !bds.every((_, i) => bds[i] === model.mapperBounds[i]) || + publicAPI.getMTime() > model.boundsMTime.getMTime() + ) { + macro.vtkDebugMacro('Recomputing bounds...'); + // No need to copy bds, a new array is created when calling getBounds() + model.mapperBounds = bds; + + // Compute actor bounds from matrix and mapper bounds + publicAPI.computeMatrix(); + const transposedMatrix = new Float64Array(16); + mat4.transpose(transposedMatrix, model.matrix); + vtkBoundingBox.transformBounds(bds, transposedMatrix, model.bounds); + + model.boundsMTime.modified(); + } + + return model.bounds; + }; + + publicAPI.getBounds = () => { + const bounds = publicAPI.getBoundsByReference(); + // Handle case when bounds are not iterable (for example null or undefined) + try { + return [...bounds]; + } catch { + return bounds; + } + }; + publicAPI.getCenter = () => vtkBoundingBox.getCenter(model.bounds); publicAPI.getLength = () => vtkBoundingBox.getLength(model.bounds); publicAPI.getXRange = () => vtkBoundingBox.getXRange(model.bounds); @@ -186,7 +242,7 @@ const DEFAULT_VALUES = { orientation: [0, 0, 0], rotation: null, scale: [1, 1, 1], - bounds: [1, -1, 1, -1, 1, -1], + bounds: [...vtkBoundingBox.INIT_BOUNDS], userMatrix: null, userMatrixMTime: null, @@ -208,7 +264,7 @@ export function extend(publicAPI, model, initialValues = {}) { macro.obj(model.matrixMTime); // Build VTK API - macro.get(publicAPI, model, ['bounds', 'isIdentity']); + macro.get(publicAPI, model, ['isIdentity']); macro.getArray(publicAPI, model, ['orientation']); macro.setGetArray(publicAPI, model, ['origin', 'position', 'scale'], 3); diff --git a/Sources/Rendering/Core/Volume/index.d.ts b/Sources/Rendering/Core/Volume/index.d.ts index 58fc94684e6..3430d43daec 100755 --- a/Sources/Rendering/Core/Volume/index.d.ts +++ b/Sources/Rendering/Core/Volume/index.d.ts @@ -30,20 +30,15 @@ export interface vtkVolume extends vtkProp3D { getVolumes(): vtkVolume[]; /** - * Get the volume property + * Get the volume property for the specified mapper input port, which defaults to 0 */ - getProperty(): vtkVolumeProperty; + getProperty(mapperInputPort = 0): vtkVolumeProperty; /** - * Get the bounds for this mapper as [xmin, xmax, ymin, ymax,zmin, zmax]. - * @return {Bounds} The bounds for the mapper. + * Get the volume properties array + * Each element of the array corresponds to a mapper input port */ - getBounds(): Bounds; - - /** - * Get the bounds as [xmin, xmax, ymin, ymax, zmin, zmax]. - */ - getBoundsByReference(): Bounds; + getProperties(): vtkVolumeProperty[]; /** * Get the `Modified Time` which is a monotonic increasing integer @@ -76,10 +71,17 @@ export interface vtkVolume extends vtkProp3D { setMapper(mapper: vtkVolumeMapper): boolean; /** - * Set the volume property + * Set the volume property for the specified mapper input port, which defaults to 0 * @param {vtkVolumeProperty} property */ - setProperty(property: vtkVolumeProperty): boolean; + setProperty(property: vtkVolumeProperty, mapperInputPort = 0): boolean; + + /** + * Set the volume properties array + * Each element of the array corresponds to a mapper input port + * @param {vtkVolumeProperty[]} properties + */ + setProperties(properties: vtkVolumeProperty[]): boolean; } /** diff --git a/Sources/Rendering/Core/Volume/index.js b/Sources/Rendering/Core/Volume/index.js index 420322b02b3..a442d4e0cd3 100644 --- a/Sources/Rendering/Core/Volume/index.js +++ b/Sources/Rendering/Core/Volume/index.js @@ -1,11 +1,7 @@ -import { vec3, mat4 } from 'gl-matrix'; import macro from 'vtk.js/Sources/macros'; -import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkProp3D from 'vtk.js/Sources/Rendering/Core/Prop3D'; import vtkVolumeProperty from 'vtk.js/Sources/Rendering/Core/VolumeProperty'; -const { vtkDebugMacro } = macro; - // ---------------------------------------------------------------------------- // vtkVolume methods // ---------------------------------------------------------------------------- @@ -14,79 +10,33 @@ function vtkVolume(publicAPI, model) { // Set our className model.classHierarchy.push('vtkVolume'); - publicAPI.getVolumes = () => publicAPI; + publicAPI.getVolumes = () => [publicAPI]; publicAPI.makeProperty = vtkVolumeProperty.newInstance; - publicAPI.getProperty = () => { - if (model.property === null) { - model.property = publicAPI.makeProperty(); + publicAPI.getProperty = (mapperInputPort = 0) => { + if (model.properties[mapperInputPort] == null) { + model.properties[mapperInputPort] = publicAPI.makeProperty(); } - return model.property; + return model.properties[mapperInputPort]; }; - publicAPI.getBounds = () => { - if (model.mapper === null) { - return model.bounds; - } - - // Check for the special case when the mapper's bounds are unknown - const bds = model.mapper.getBounds(); - if (!bds || bds.length !== 6) { - return bds; + publicAPI.setProperty = (property, mapperInputPort = 0) => { + if (model.properties[mapperInputPort] === property) { + return false; } - - // Check for the special case when the actor is empty. - if (bds[0] > bds[1]) { - model.mapperBounds = bds.concat(); // copy the mapper's bounds - model.bounds = [1, -1, 1, -1, 1, -1]; - model.boundsMTime.modified(); - return bds; - } - - // Check if we have cached values for these bounds - we cache the - // values returned by model.mapper.getBounds() and we store the time - // of caching. If the values returned this time are different, or - // the modified time of this class is newer than the cached time, - // then we need to rebuild. - const zip = (rows) => rows[0].map((_, c) => rows.map((row) => row[c])); - if ( - !model.mapperBounds || - !zip([bds, model.mapperBounds]).reduce( - (a, b) => a && b[0] === b[1], - true - ) || - publicAPI.getMTime() > model.boundsMTime.getMTime() - ) { - vtkDebugMacro('Recomputing bounds...'); - model.mapperBounds = bds.map((x) => x); - const bbox = []; - vtkBoundingBox.getCorners(bds, bbox); - publicAPI.computeMatrix(); - const tmp4 = new Float64Array(16); - mat4.transpose(tmp4, model.matrix); - bbox.forEach((pt) => vec3.transformMat4(pt, pt, tmp4)); - - /* eslint-disable no-multi-assign */ - model.bounds[0] = model.bounds[2] = model.bounds[4] = Number.MAX_VALUE; - model.bounds[1] = model.bounds[3] = model.bounds[5] = -Number.MAX_VALUE; - /* eslint-enable no-multi-assign */ - model.bounds = model.bounds.map((d, i) => - i % 2 === 0 - ? bbox.reduce((a, b) => (a > b[i / 2] ? b[i / 2] : a), d) - : bbox.reduce((a, b) => (a < b[(i - 1) / 2] ? b[(i - 1) / 2] : a), d) - ); - model.boundsMTime.modified(); - } - return model.bounds; + model.properties[mapperInputPort] = property; + return true; }; publicAPI.getMTime = () => { let mt = model.mtime; - if (model.property !== null) { - const time = model.property.getMTime(); - mt = time > mt ? time : mt; - } + model.properties.forEach((property) => { + if (property !== null) { + const time = property.getMTime(); + mt = time > mt ? time : mt; + } + }); return mt; }; @@ -112,8 +62,7 @@ function vtkVolume(publicAPI, model) { const DEFAULT_VALUES = { mapper: null, - property: null, - bounds: [1, -1, 1, -1, 1, -1], + properties: [], }; // ---------------------------------------------------------------------------- @@ -129,9 +78,7 @@ export function extend(publicAPI, model, initialValues = {}) { macro.obj(model.boundsMTime); // Build VTK API - macro.set(publicAPI, model, ['property']); - macro.setGet(publicAPI, model, ['mapper']); - macro.getArray(publicAPI, model, ['bounds'], 6); + macro.setGet(publicAPI, model, ['mapper', 'properties']); // Object methods vtkVolume(publicAPI, model); diff --git a/Sources/Rendering/Core/VolumeMapper/Constants.d.ts b/Sources/Rendering/Core/VolumeMapper/Constants.d.ts index c462ce021c9..568702ed087 100644 --- a/Sources/Rendering/Core/VolumeMapper/Constants.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/Constants.d.ts @@ -1,21 +1,8 @@ -export declare enum BlendMode { - COMPOSITE_BLEND = 0, - MAXIMUM_INTENSITY_BLEND = 1, - MINIMUM_INTENSITY_BLEND = 2, - AVERAGE_INTENSITY_BLEND = 3, - ADDITIVE_INTENSITY_BLEND = 4, - RADON_TRANSFORM_BLEND = 5, - LABELMAP_EDGE_PROJECTION_BLEND = 6, -} +// Don't use the constants from this file -export declare enum FilterMode { - OFF = 0, - NORMALIZED = 1, - RAW = 2, -} - -declare const _default: { - BlendMode: typeof BlendMode; - FilterMode: typeof FilterMode; -}; -export default _default; +// Prefer constants from volume property: +export { + default, + BlendMode, + FilterMode, +} from '../VolumeProperty/Constants'; diff --git a/Sources/Rendering/Core/VolumeMapper/Constants.js b/Sources/Rendering/Core/VolumeMapper/Constants.js index 50641c6f419..e253a48af36 100644 --- a/Sources/Rendering/Core/VolumeMapper/Constants.js +++ b/Sources/Rendering/Core/VolumeMapper/Constants.js @@ -1,20 +1,13 @@ -export const BlendMode = { - COMPOSITE_BLEND: 0, - MAXIMUM_INTENSITY_BLEND: 1, - MINIMUM_INTENSITY_BLEND: 2, - AVERAGE_INTENSITY_BLEND: 3, - ADDITIVE_INTENSITY_BLEND: 4, - RADON_TRANSFORM_BLEND: 5, - LABELMAP_EDGE_PROJECTION_BLEND: 6, -}; +// Don't use the constants from this file -export const FilterMode = { - OFF: 0, - NORMALIZED: 1, - RAW: 2, -}; +// Prefer constants from volume property: +import Constants, { + BlendMode as OriginalBlendMode, + FilterMode as OriginalFilterMode, +} from 'vtk.js/Sources/Rendering/Core/VolumeProperty/Constants'; -export default { - BlendMode, - FilterMode, -}; +export const BlendMode = OriginalBlendMode; + +export const FilterMode = OriginalFilterMode; + +export default Constants; diff --git a/Sources/Rendering/Core/VolumeMapper/example/index.js b/Sources/Rendering/Core/VolumeMapper/example/index.js index 1cc2dcf7a0e..853173db204 100644 --- a/Sources/Rendering/Core/VolumeMapper/example/index.js +++ b/Sources/Rendering/Core/VolumeMapper/example/index.js @@ -54,11 +54,6 @@ const forceNearestElem = document.getElementById('forceNearest'); const actor = vtkVolume.newInstance(); const mapper = vtkVolumeMapper.newInstance(); mapper.setSampleDistance(0.7); -mapper.setVolumetricScatteringBlending(0); -mapper.setLocalAmbientOcclusion(0); -mapper.setLAOKernelSize(10); -mapper.setLAOKernelRadius(5); -mapper.setComputeNormalFromOpacity(true); actor.setMapper(mapper); // create color and opacity transfer functions @@ -70,6 +65,11 @@ ctfun.addRGBPoint(255, 0.3, 0.3, 0.5); const ofun = vtkPiecewiseFunction.newInstance(); ofun.addPoint(0.0, 0.1); ofun.addPoint(255.0, 1.0); +actor.getProperty().setComputeNormalFromOpacity(true); +actor.getProperty().setLAOKernelRadius(5); +actor.getProperty().setLAOKernelSize(10); +actor.getProperty().setLocalAmbientOcclusion(0); +actor.getProperty().setVolumetricScatteringBlending(0); actor.getProperty().setRGBTransferFunction(0, ctfun); actor.getProperty().setScalarOpacity(0, ofun); actor.getProperty().setInterpolationTypeToLinear(); @@ -145,14 +145,6 @@ if (light.getPositional()) { renderer.addActor(lca); } -{ - const optionElem = document.createElement('option'); - optionElem.label = 'Default'; - optionElem.value = ''; - presetSelectElem.appendChild(optionElem); - presetSelectElem.value = optionElem.value; -} - Object.keys(ColorMixPreset).forEach((key) => { if (key === 'CUSTOM') { // Don't enable custom mode @@ -167,7 +159,7 @@ Object.keys(ColorMixPreset).forEach((key) => { }); const setColorMixPreset = (presetKey) => { - const preset = presetKey ? ColorMixPreset[presetKey] : null; + const preset = ColorMixPreset[presetKey]; actor.getProperty().setColorMixPreset(preset); presetSelectElem.value = presetKey; }; @@ -195,7 +187,7 @@ updateForceNearestElem(1); volumeSelectElem.addEventListener('change', () => { const { comp, data } = volumeOptions[volumeSelectElem.value]; if (comp === 1) { - setColorMixPreset(''); + setColorMixPreset('DEFAULT'); presetSelectElem.style.display = 'none'; } else { presetSelectElem.style.display = 'block'; @@ -301,7 +293,7 @@ const button = document.querySelector('.text'); const lao = document.querySelector('.lao'); lao.addEventListener('click', (e) => { isLAO = !isLAO; - mapper.setLocalAmbientOcclusion(isLAO); + actor.getProperty().setLocalAmbientOcclusion(isLAO); button.innerText = `(${isLAO ? 'on' : 'off'})`; renderWindow.render(); }); @@ -311,7 +303,7 @@ vs.addEventListener('input', (e) => { const b = (0.1 * Number(e.target.value)).toPrecision(1); const sbutton = document.querySelector('.stext'); sbutton.innerText = `(${b > 0 ? b : 'off'})`; - mapper.setVolumetricScatteringBlending(b); + actor.getProperty().setVolumetricScatteringBlending(b); renderWindow.render(); }); diff --git a/Sources/Rendering/Core/VolumeMapper/index.d.ts b/Sources/Rendering/Core/VolumeMapper/index.d.ts index f527a2f427f..41727af45e3 100755 --- a/Sources/Rendering/Core/VolumeMapper/index.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/index.d.ts @@ -10,20 +10,12 @@ import { BlendMode, FilterMode } from './Constants'; */ export interface IVolumeMapperInitialValues extends IAbstractMapper3DInitialValues { - anisotropy?: number; autoAdjustSampleDistances?: boolean; - averageIPScalarRange?: Range; blendMode?: BlendMode; bounds?: Bounds; - computeNormalFromOpacity?: boolean; - getVolumeShadowSamplingDistFactor?: number; - globalIlluminationReach?: number; - imageSampleDistance?: number; - localAmbientOcclusion?: boolean; maximumSamplesPerRay?: number; sampleDistance?: number; - LAOKernelRadius?: number; - LAOKernelSize?: number; + volumeShadowSamplingDistFactor?: number; } export interface vtkVolumeMapper extends vtkAbstractMapper3D { @@ -84,71 +76,6 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D { */ getInteractionSampleDistanceFactor(): number; - /** - * - */ - getAverageIPScalarRange(): Range; - - /** - * - */ - getAverageIPScalarRangeByReference(): Range; - - /** - * Get the blending coefficient that interpolates between surface and volume rendering - * @default 0.0 - */ - getVolumetricScatteringBlending(): number; - - /** - * Get the global illumination reach of volume shadow - * @default 0.0 - */ - getGlobalIlluminationReach(): number; - - /** - * Get the multipler for volume shadow sampling distance - * @default 5.0 - */ - getVolumeShadowSamplingDistFactor(): number; - - /** - * Get anisotropy of volume shadow scatter - * @default 0.0 - */ - getAnisotropy(): number; - - /** - * Get local ambient occlusion flag - * @default false - */ - getLocalAmbientOcclusion(): boolean; - - /** - * Get kernel size for local ambient occlusion - * @default 15 - */ - getLAOKernelSize(): number; - - /** - * Get kernel radius for local ambient occlusion - * @default 7 - */ - getLAOKernelRadius(): number; - - /** - * - * @param x - * @param y - */ - setAverageIPScalarRange(x: number, y: number): boolean; - - /** - * - * @param {Range} averageIPScalarRange - */ - setAverageIPScalarRangeFrom(averageIPScalarRange: Range): boolean; - /** * Set blend mode to COMPOSITE_BLEND * @param {BlendMode} blendMode @@ -218,69 +145,6 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D { interactionSampleDistanceFactor: number ): boolean; - /** - * Set the normal computation to be dependent on the transfer function. - * By default, the mapper relies on the scalar gradient for computing normals at sample locations - * for lighting calculations. This is an approximation and can lead to inaccurate results. - * When enabled, this property makes the mapper compute normals based on the accumulated opacity - * at sample locations. This can generate a more accurate representation of edge structures in the - * data but adds an overhead and drops frame rate. - * @param computeNormalFromOpacity - */ - setComputeNormalFromOpacity(computeNormalFromOpacity: boolean): boolean; - - /** - * Set the blending coefficient that determines the interpolation between surface and volume rendering. - * Default value of 0.0 means shadow effect is computed with phong model. - * Value of 1.0 means shadow is created by volume occlusion. - * @param volumeScatterBlendCoef - */ - setVolumetricScatteringBlending(volumeScatterBlendCoef: number): void; - - /** - * Set the global illumination reach of volume shadow. This function is only effective when volumeScatterBlendCoef is greater than 0. - * Default value of 0.0 means only the neighboring voxel is considered when creating global shadow. - * Value of 1.0 means the shadow ray traverses through the entire volume. - * @param globalIlluminationReach - */ - setGlobalIlluminationReach(globalIlluminationReach: number): void; - - /** - * Set the multipler for volume shadow sampling distance. This function is only effective when volumeScatterBlendCoef is greater than 0. - * For VSSampleDistanceFactor >= 1.0, volume shadow sampling distance = VSSampleDistanceFactor * SampleDistance. - * @param VSSampleDistanceFactor - */ - setVolumeShadowSamplingDistFactor(VSSampleDistanceFactor: number): void; - - /** - * Set anisotropy of volume shadow scatter. This function is only effective when volumeScatterBlendCoef is greater than 0. - * Default value of 0.0 means light scatters uniformly in all directions. - * Value of -1.0 means light scatters backward, value of 1.0 means light scatters forward. - * @param anisotropy - */ - setAnisotropy(anisotropy: number): void; - - /** - * Set whether to turn on local ambient occlusion (LAO). LAO is only effective if shading is on and volumeScatterBlendCoef is set to 0. - * LAO effect is added to ambient lighting, so the ambient component of the actor needs to be great than 0. - * @param localAmbientOcclusion - */ - setLocalAmbientOcclusion(localAmbientOcclusion: boolean): void; - - /** - * Set kernel size for local ambient occlusion. It specifies the number of rays that are randomly sampled in the hemisphere. - * Value is clipped between 1 and 32. - * @param LAOKernelSize - */ - setLAOKernelSize(LAOKernelSize: number): void; - - /** - * Set kernel radius for local ambient occlusion. It specifies the number of samples that are considered on each random ray. - * Value must be greater than or equal to 1. - * @param LAOKernelRadius - */ - setLAOKernelRadius(LAOKernelRadius: number): void; - /** * */ diff --git a/Sources/Rendering/Core/VolumeMapper/index.js b/Sources/Rendering/Core/VolumeMapper/index.js index 6f120ca2e87..b82e37bc976 100644 --- a/Sources/Rendering/Core/VolumeMapper/index.js +++ b/Sources/Rendering/Core/VolumeMapper/index.js @@ -1,10 +1,10 @@ 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 vtkAbstractMapper3D from 'vtk.js/Sources/Rendering/Core/AbstractMapper3D'; +import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; -const { BlendMode, FilterMode } = Constants; +const { BlendMode } = Constants; function createRadonTransferFunction( firstAbsorbentMaterialHounsfieldValue, @@ -33,6 +33,36 @@ function createRadonTransferFunction( return ofun; } +const methodNamesMovedToVolumeProperties = [ + 'getAnisotropy', + 'getComputeNormalFromOpacity', + 'getFilterMode', + 'getFilterModeAsString', + 'getGlobalIlluminationReach', + 'getIpScalarRange', + 'getIpScalarRangeByReference', + 'getLAOKernelRadius', + 'getLAOKernelSize', + 'getLocalAmbientOcclusion', + 'getPreferSizeOverAccuracy', + 'getVolumetricScatteringBlending', + 'setAnisotropy', + 'setAverageIPScalarRange', + 'setComputeNormalFromOpacity', + 'setFilterMode', + 'setFilterModeToNormalized', + 'setFilterModeToOff', + 'setFilterModeToRaw', + 'setGlobalIlluminationReach', + 'setIpScalarRange', + 'setIpScalarRangeFrom', + 'setLAOKernelRadius', + 'setLAOKernelSize', + 'setLocalAmbientOcclusion', + 'setPreferSizeOverAccuracy', + 'setVolumetricScatteringBlending', +]; + // ---------------------------------------------------------------------------- // Static API // ---------------------------------------------------------------------------- @@ -52,22 +82,19 @@ function vtkVolumeMapper(publicAPI, model) { const superClass = { ...publicAPI }; publicAPI.getBounds = () => { - const input = publicAPI.getInputData(); - if (!input) { - model.bounds = vtkMath.createUninitializedBounds(); - } else { - if (!model.static) { - publicAPI.update(); + model.bounds = [...vtkBoundingBox.INIT_BOUNDS]; + if (!model.static) { + publicAPI.update(); + } + for (let inputIndex = 0; inputIndex < model.numberOfInputs; inputIndex++) { + const input = publicAPI.getInputData(inputIndex); + if (input) { + vtkBoundingBox.addBounds(model.bounds, input.getBounds()); } - model.bounds = input.getBounds(); } return model.bounds; }; - publicAPI.update = () => { - publicAPI.getInputData(); - }; - publicAPI.setBlendModeToComposite = () => { publicAPI.setBlendMode(BlendMode.COMPOSITE_BLEND); }; @@ -95,54 +122,29 @@ function vtkVolumeMapper(publicAPI, model) { publicAPI.getBlendModeAsString = () => macro.enumToString(BlendMode, model.blendMode); - publicAPI.setAverageIPScalarRange = (min, max) => { - console.warn('setAverageIPScalarRange is deprecated use setIpScalarRange'); - publicAPI.setIpScalarRange(min, max); - }; - - publicAPI.getFilterModeAsString = () => - macro.enumToString(FilterMode, model.filterMode); - - publicAPI.setFilterModeToOff = () => { - publicAPI.setFilterMode(FilterMode.OFF); - }; - - publicAPI.setFilterModeToNormalized = () => { - publicAPI.setFilterMode(FilterMode.NORMALIZED); - }; - - publicAPI.setFilterModeToRaw = () => { - publicAPI.setFilterMode(FilterMode.RAW); - }; - - publicAPI.setGlobalIlluminationReach = (gl) => - superClass.setGlobalIlluminationReach(vtkMath.clampValue(gl, 0.0, 1.0)); - - publicAPI.setVolumetricScatteringBlending = (vsb) => - superClass.setVolumetricScatteringBlending( - vtkMath.clampValue(vsb, 0.0, 1.0) - ); - publicAPI.setVolumeShadowSamplingDistFactor = (vsdf) => superClass.setVolumeShadowSamplingDistFactor(vsdf >= 1.0 ? vsdf : 1.0); - publicAPI.setAnisotropy = (at) => - superClass.setAnisotropy(vtkMath.clampValue(at, -0.99, 0.99)); - - publicAPI.setLAOKernelSize = (ks) => - superClass.setLAOKernelSize(vtkMath.floor(vtkMath.clampValue(ks, 1, 32))); - - publicAPI.setLAOKernelRadius = (kr) => - superClass.setLAOKernelRadius(kr >= 1 ? kr : 1); + // Instead of a "undefined is not a function" error, give more context and advice for these widely used methods + methodNamesMovedToVolumeProperties.forEach((removedMethodName) => { + const removedMethod = () => { + throw new Error( + `The method "volumeMapper.${removedMethodName}()" doesn't exist anymore. ` + + `It is a rendering property that has been moved to the volume property. ` + + `Replace your code with:\n` + + `volumeActor.getProperty().${removedMethodName}()\n` + ); + }; + publicAPI[removedMethodName] = removedMethod; + }); } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- -// TODO: what values to use for averageIPScalarRange to get GLSL to use max / min values like [-Math.inf, Math.inf]? const DEFAULT_VALUES = { - bounds: [1, -1, 1, -1, 1, -1], + bounds: [...vtkBoundingBox.INIT_BOUNDS], sampleDistance: 1.0, imageSampleDistance: 1.0, maximumSamplesPerRay: 1000, @@ -150,19 +152,7 @@ const DEFAULT_VALUES = { initialInteractionScale: 1.0, interactionSampleDistanceFactor: 1.0, blendMode: BlendMode.COMPOSITE_BLEND, - ipScalarRange: [-1000000.0, 1000000.0], - filterMode: FilterMode.OFF, // ignored by WebGL so no behavior change - preferSizeOverAccuracy: false, // Whether to use halfFloat representation of float, when it is inaccurate - computeNormalFromOpacity: false, - // volume shadow parameters - volumetricScatteringBlending: 0.0, - globalIlluminationReach: 0.0, volumeShadowSamplingDistFactor: 5.0, - anisotropy: 0.0, - // local ambient occlusion - localAmbientOcclusion: false, - LAOKernelSize: 15, - LAOKernelRadius: 7, }; // ---------------------------------------------------------------------------- @@ -180,20 +170,9 @@ export function extend(publicAPI, model, initialValues = {}) { 'initialInteractionScale', 'interactionSampleDistanceFactor', 'blendMode', - 'filterMode', - 'preferSizeOverAccuracy', - 'computeNormalFromOpacity', - 'volumetricScatteringBlending', - 'globalIlluminationReach', 'volumeShadowSamplingDistFactor', - 'anisotropy', - 'localAmbientOcclusion', - 'LAOKernelSize', - 'LAOKernelRadius', ]); - macro.setGetArray(publicAPI, model, ['ipScalarRange'], 2); - macro.event(publicAPI, model, 'lightingActivated'); // Object methods diff --git a/Sources/Rendering/Core/VolumeProperty/Constants.d.ts b/Sources/Rendering/Core/VolumeProperty/Constants.d.ts index 0465485cb42..e04b51d6637 100644 --- a/Sources/Rendering/Core/VolumeProperty/Constants.d.ts +++ b/Sources/Rendering/Core/VolumeProperty/Constants.d.ts @@ -10,9 +10,7 @@ export declare enum OpacityMode { } export declare enum ColorMixPreset { - // Add a `//VTK::CustomColorMix` tag to the Fragment shader - // See usage in file `testColorMix` and in function `setColorMixPreset` - CUSTOM = 0, + DEFAULT = 0, // Two components preset // Out color: sum of colors weighted by opacity @@ -23,11 +21,33 @@ export declare enum ColorMixPreset { // Out color: color of the first component, colorized by second component with an intensity that is the second component's opacity // Out opacity: opacity of the first component COLORIZE = 2, + + // Add a `//VTK::CustomColorMix` tag to the Fragment shader + // See usage in file `testColorMix` and in function `setColorMixPreset` + CUSTOM = 3, +} + +export declare enum BlendMode { + COMPOSITE_BLEND = 0, + MAXIMUM_INTENSITY_BLEND = 1, + MINIMUM_INTENSITY_BLEND = 2, + AVERAGE_INTENSITY_BLEND = 3, + ADDITIVE_INTENSITY_BLEND = 4, + RADON_TRANSFORM_BLEND = 5, + LABELMAP_EDGE_PROJECTION_BLEND = 6, +} + +export declare enum FilterMode { + OFF = 0, + NORMALIZED = 1, + RAW = 2, } declare const _default: { InterpolationType: typeof InterpolationType; OpacityMode: typeof OpacityMode; ColorMixPreset: typeof ColorMixPreset; + BlendMode: typeof BlendMode; + FilterMode: typeof FilterMode; }; export default _default; diff --git a/Sources/Rendering/Core/VolumeProperty/Constants.js b/Sources/Rendering/Core/VolumeProperty/Constants.js index d673977c429..0a66cc8d789 100644 --- a/Sources/Rendering/Core/VolumeProperty/Constants.js +++ b/Sources/Rendering/Core/VolumeProperty/Constants.js @@ -10,13 +10,32 @@ export const OpacityMode = { }; export const ColorMixPreset = { - CUSTOM: 0, + DEFAULT: 0, ADDITIVE: 1, COLORIZE: 2, + CUSTOM: 3, +}; + +export const BlendMode = { + COMPOSITE_BLEND: 0, + MAXIMUM_INTENSITY_BLEND: 1, + MINIMUM_INTENSITY_BLEND: 2, + AVERAGE_INTENSITY_BLEND: 3, + ADDITIVE_INTENSITY_BLEND: 4, + RADON_TRANSFORM_BLEND: 5, + LABELMAP_EDGE_PROJECTION_BLEND: 6, +}; + +export const FilterMode = { + OFF: 0, + NORMALIZED: 1, + RAW: 2, }; export default { InterpolationType, OpacityMode, ColorMixPreset, + BlendMode, + FilterMode, }; diff --git a/Sources/Rendering/Core/VolumeProperty/index.d.ts b/Sources/Rendering/Core/VolumeProperty/index.d.ts index 12731d92987..18888b4c84d 100755 --- a/Sources/Rendering/Core/VolumeProperty/index.d.ts +++ b/Sources/Rendering/Core/VolumeProperty/index.d.ts @@ -13,6 +13,7 @@ export interface IVolumePropertyInitialValues { specularPower?: number; useLabelOutline?: boolean; labelOutlineThickness?: number | number[]; + colorMixPreset?: ColorMixPreset; } export interface vtkVolumeProperty extends vtkObject { @@ -71,7 +72,7 @@ export interface vtkVolumeProperty extends vtkObject { /** * */ - getColorMixPreset(): Nullable; + getColorMixPreset(): ColorMixPreset; /** * @@ -194,7 +195,7 @@ export interface vtkVolumeProperty extends vtkObject { /** * Set the color mix code to a preset value - * Set to null to use no preset + * Defaults to ColorMixPreset.DEFAULT * See the test `testColorMix` for an example on how to use this preset. * * If set to `CUSTOM`, a tag `//VTK::CustomColorMix` is made available to the @@ -202,9 +203,9 @@ export interface vtkVolumeProperty extends vtkObject { * will be used to mix the colors from each component. * Each component is available as a rgba vec4: `comp0`, `comp1`... * There are other useful functions or variable available. To find them, - * see `//VTK::CustomComponentsColorMix::Impl` tag in `vtkVolumeFS.glsl`. + * see `//VTK::CustomColorMix` tag in `vtkVolumeFS.glsl`. */ - setColorMixPreset(preset: Nullable): boolean; + setColorMixPreset(preset: ColorMixPreset): boolean; /** * Does the data have independent components, or do some define color only? @@ -370,6 +371,141 @@ export interface vtkVolumeProperty extends vtkObject { * Get the interpolation type for sampling a volume as a string. */ getInterpolationTypeAsString(): string; + + /** + * + */ + getAverageIPScalarRange(): Range; + + /** + * + */ + getAverageIPScalarRangeByReference(): Range; + + /** + * Get the blending coefficient that interpolates between surface and volume rendering + * @default 0.0 + */ + getVolumetricScatteringBlending(): number; + + /** + * Get the global illumination reach of volume shadow + * @default 0.0 + */ + getGlobalIlluminationReach(): number; + + /** + * Get the multipler for volume shadow sampling distance + * @default 5.0 + */ + getVolumeShadowSamplingDistFactor(): number; + + /** + * Get anisotropy of volume shadow scatter + * @default 0.0 + */ + getAnisotropy(): number; + + /** + * Get local ambient occlusion flag + * @default false + */ + getLocalAmbientOcclusion(): boolean; + + /** + * Get kernel size for local ambient occlusion + * @default 15 + */ + getLAOKernelSize(): number; + + /** + * Get kernel radius for local ambient occlusion + * @default 7 + */ + getLAOKernelRadius(): number; + + /** + * + * @param x + * @param y + */ + setAverageIPScalarRange(x: number, y: number): boolean; + + /** + * + * @param {Range} averageIPScalarRange + */ + setAverageIPScalarRangeFrom(averageIPScalarRange: Range): boolean; + + /** + * Set the normal computation to be dependent on the transfer function. + * By default, the mapper relies on the scalar gradient for computing normals at sample locations + * for lighting calculations. This is an approximation and can lead to inaccurate results. + * When enabled, this property makes the mapper compute normals based on the accumulated opacity + * at sample locations. This can generate a more accurate representation of edge structures in the + * data but adds an overhead and drops frame rate. + * @param computeNormalFromOpacity + */ + setComputeNormalFromOpacity(computeNormalFromOpacity: boolean): boolean; + + /** + * Set the blending coefficient that determines the interpolation between surface and volume rendering. + * Default value of 0.0 means shadow effect is computed with phong model. + * Value of 1.0 means shadow is created by volume occlusion. + * @param volumeScatterBlendCoef + */ + setVolumetricScatteringBlending(volumeScatterBlendCoef: number): void; + + /** + * Set the global illumination reach of volume shadow. This function is only effective when volumeScatterBlendCoef is greater than 0. + * Default value of 0.0 means only the neighboring voxel is considered when creating global shadow. + * Value of 1.0 means the shadow ray traverses through the entire volume. + * @param globalIlluminationReach + */ + setGlobalIlluminationReach(globalIlluminationReach: number): void; + + /** + * Set the multipler for volume shadow sampling distance. This function is only effective when volumeScatterBlendCoef is greater than 0. + * For VSSampleDistanceFactor >= 1.0, volume shadow sampling distance = VSSampleDistanceFactor * SampleDistance. + * @param VSSampleDistanceFactor + */ + setVolumeShadowSamplingDistFactor(VSSampleDistanceFactor: number): void; + + /** + * Set the multipler for volume shadow sampling distance. This function is only effective when volumeScatterBlendCoef is greater than 0. + * For VSSampleDistanceFactor >= 1.0, volume shadow sampling distance = VSSampleDistanceFactor * SampleDistance. + * @param VSSampleDistanceFactor + */ + setVolumeShadowSamplingDistFactor(VSSampleDistanceFactor: number): void; + + /** + * Set anisotropy of volume shadow scatter. This function is only effective when volumeScatterBlendCoef is greater than 0. + * Default value of 0.0 means light scatters uniformly in all directions. + * Value of -1.0 means light scatters backward, value of 1.0 means light scatters forward. + * @param anisotropy + */ + setAnisotropy(anisotropy: number): void; + + /** + * Set whether to turn on local ambient occlusion (LAO). LAO is only effective if shading is on and volumeScatterBlendCoef is set to 0. + * LAO effect is added to ambient lighting, so the ambient component of the actor needs to be great than 0. + * @param localAmbientOcclusion + */ + setLocalAmbientOcclusion(localAmbientOcclusion: boolean): void; + + /** + * Set kernel size for local ambient occlusion. It specifies the number of rays that are randomly sampled in the hemisphere. + * Value is clipped between 1 and 32. + * @param LAOKernelSize + */ + setLAOKernelSize(LAOKernelSize: number): void; + + /** + * Set kernel radius for local ambient occlusion. It specifies the number of samples that are considered on each random ray. + * Value must be greater than or equal to 1. + * @param LAOKernelRadius + */ + setLAOKernelRadius(LAOKernelRadius: number): void; } /** diff --git a/Sources/Rendering/Core/VolumeProperty/index.js b/Sources/Rendering/Core/VolumeProperty/index.js index f8c178a65f9..5574212cf01 100644 --- a/Sources/Rendering/Core/VolumeProperty/index.js +++ b/Sources/Rendering/Core/VolumeProperty/index.js @@ -1,9 +1,11 @@ import macro from 'vtk.js/Sources/macros'; +import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeProperty/Constants'; -const { InterpolationType, OpacityMode } = Constants; +const { InterpolationType, OpacityMode, FilterMode, ColorMixPreset } = + Constants; const { vtkErrorMacro } = macro; const VTK_MAX_VRCOMP = 4; @@ -16,6 +18,8 @@ function vtkVolumeProperty(publicAPI, model) { // Set our className model.classHierarchy.push('vtkVolumeProperty'); + const superClass = { ...publicAPI }; + publicAPI.getMTime = () => { let mTime = model.mtime; let time; @@ -239,6 +243,43 @@ function vtkVolumeProperty(publicAPI, model) { const cap = macro.capitalize(val); publicAPI[`get${cap}`] = (index) => model.componentData[index][`${val}`]; }); + + publicAPI.setAverageIPScalarRange = (min, max) => { + console.warn('setAverageIPScalarRange is deprecated use setIpScalarRange'); + publicAPI.setIpScalarRange(min, max); + }; + + publicAPI.getFilterModeAsString = () => + macro.enumToString(FilterMode, model.filterMode); + + publicAPI.setFilterModeToOff = () => { + publicAPI.setFilterMode(FilterMode.OFF); + }; + + publicAPI.setFilterModeToNormalized = () => { + publicAPI.setFilterMode(FilterMode.NORMALIZED); + }; + + publicAPI.setFilterModeToRaw = () => { + publicAPI.setFilterMode(FilterMode.RAW); + }; + + publicAPI.setGlobalIlluminationReach = (gl) => + superClass.setGlobalIlluminationReach(vtkMath.clampValue(gl, 0.0, 1.0)); + + publicAPI.setVolumetricScatteringBlending = (vsb) => + superClass.setVolumetricScatteringBlending( + vtkMath.clampValue(vsb, 0.0, 1.0) + ); + + publicAPI.setAnisotropy = (at) => + superClass.setAnisotropy(vtkMath.clampValue(at, -0.99, 0.99)); + + publicAPI.setLAOKernelSize = (ks) => + superClass.setLAOKernelSize(vtkMath.floor(vtkMath.clampValue(ks, 1, 32))); + + publicAPI.setLAOKernelRadius = (kr) => + superClass.setLAOKernelRadius(kr >= 1 ? kr : 1); } // ---------------------------------------------------------------------------- @@ -246,7 +287,7 @@ function vtkVolumeProperty(publicAPI, model) { // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { - colorMixPreset: null, + colorMixPreset: ColorMixPreset.DEFAULT, independentComponents: true, interpolationType: InterpolationType.FAST_LINEAR, shade: false, @@ -257,6 +298,20 @@ const DEFAULT_VALUES = { useLabelOutline: false, labelOutlineThickness: [1], labelOutlineOpacity: 1.0, + + // Properties moved from volume mapper + ipScalarRange: [-1000000.0, 1000000.0], + filterMode: FilterMode.OFF, // ignored by WebGL so no behavior change + preferSizeOverAccuracy: false, // Whether to use halfFloat representation of float, when it is inaccurate + computeNormalFromOpacity: false, + // volume shadow parameters + volumetricScatteringBlending: 0.0, + globalIlluminationReach: 0.0, + anisotropy: 0.0, + // local ambient occlusion + localAmbientOcclusion: false, + LAOKernelSize: 15, + LAOKernelRadius: 7, }; // ---------------------------------------------------------------------------- @@ -301,8 +356,21 @@ export function extend(publicAPI, model, initialValues = {}) { 'specularPower', 'useLabelOutline', 'labelOutlineOpacity', + // Properties moved from volume mapper + 'filterMode', + 'preferSizeOverAccuracy', + 'computeNormalFromOpacity', + 'volumetricScatteringBlending', + 'globalIlluminationReach', + 'anisotropy', + 'localAmbientOcclusion', + 'LAOKernelSize', + 'LAOKernelRadius', ]); + // Property moved from volume mapper + macro.setGetArray(publicAPI, model, ['ipScalarRange'], 2); + macro.setGetArray(publicAPI, model, ['labelOutlineThickness']); // Object methods diff --git a/Sources/Rendering/OpenGL/ImageMapper/index.js b/Sources/Rendering/OpenGL/ImageMapper/index.js index 3169b54071c..be583c1045b 100644 --- a/Sources/Rendering/OpenGL/ImageMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageMapper/index.js @@ -4,7 +4,6 @@ import * as macro from 'vtk.js/Sources/macros'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; import vtkHelper from 'vtk.js/Sources/Rendering/OpenGL/Helper'; -import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkOpenGLTexture from 'vtk.js/Sources/Rendering/OpenGL/Texture'; import vtkShaderProgram from 'vtk.js/Sources/Rendering/OpenGL/ShaderProgram'; import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode'; @@ -890,14 +889,6 @@ function vtkOpenGLImageMapper(publicAPI, model) { publicAPI.renderPieceFinish(ren, actor); }; - publicAPI.computeBounds = (ren, actor) => { - if (!publicAPI.getInput()) { - vtkMath.uninitializeBounds(model.bounds); - return; - } - model.bounds = publicAPI.getInput().getBounds(); - }; - publicAPI.updateBufferObjects = (ren, actor) => { // Rebuild buffers if needed if (publicAPI.getNeedToRebuildBufferObjects(ren, actor)) { diff --git a/Sources/Rendering/OpenGL/PolyDataMapper/index.js b/Sources/Rendering/OpenGL/PolyDataMapper/index.js index 32e388a2927..d4dbd29ae1a 100755 --- a/Sources/Rendering/OpenGL/PolyDataMapper/index.js +++ b/Sources/Rendering/OpenGL/PolyDataMapper/index.js @@ -1748,14 +1748,6 @@ function vtkOpenGLPolyDataMapper(publicAPI, model) { publicAPI.renderPieceFinish(ren, actor); }; - publicAPI.computeBounds = (ren, actor) => { - if (!publicAPI.getInput()) { - vtkMath.uninitializeBounds(model.bounds); - return; - } - model.bounds = publicAPI.getInput().getBounds(); - }; - publicAPI.updateBufferObjects = (ren, actor) => { // Rebuild buffers if needed if (publicAPI.getNeedToRebuildBufferObjects(ren, actor)) { diff --git a/Sources/Rendering/OpenGL/VolumeMapper/index.js b/Sources/Rendering/OpenGL/VolumeMapper/index.js index 1662031302c..853bea6838c 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/index.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/index.js @@ -1,11 +1,10 @@ import * as macro from 'vtk.js/Sources/macros'; import DeepEqual from 'fast-deep-equal'; import { vec3, mat3, mat4 } from 'gl-matrix'; -// import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; +import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; import vtkHelper from 'vtk.js/Sources/Rendering/OpenGL/Helper'; -import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkOpenGLFramebuffer from 'vtk.js/Sources/Rendering/OpenGL/Framebuffer'; import vtkOpenGLTexture from 'vtk.js/Sources/Rendering/OpenGL/Texture'; import vtkReplacementShaderMapper from 'vtk.js/Sources/Rendering/OpenGL/ReplacementShaderMapper'; @@ -40,81 +39,14 @@ const { vtkWarningMacro, vtkErrorMacro } = macro; // helper methods // ---------------------------------------------------------------------------- -function getColorCodeFromPreset(colorMixPreset) { - switch (colorMixPreset) { - case ColorMixPreset.CUSTOM: - return '//VTK::CustomColorMix'; - case ColorMixPreset.ADDITIVE: - return ` - // compute normals - mat4 normalMat = computeMat4Normal(posIS, tValue, tstep); - #if (vtkLightComplexity > 0) && defined(vtkComputeNormalFromOpacity) - vec3 scalarInterp0[2]; - vec4 normalLight0 = computeNormalForDensity(posIS, tstep, scalarInterp0, 0); - scalarInterp0[0] = scalarInterp0[0] * oscale0 + oshift0; - scalarInterp0[1] = scalarInterp0[1] * oscale0 + oshift0; - normalLight0 = computeDensityNormal(scalarInterp0, height0, 1.0); - - vec3 scalarInterp1[2]; - vec4 normalLight1 = computeNormalForDensity(posIS, tstep, scalarInterp1, 1); - scalarInterp1[0] = scalarInterp1[0] * oscale1 + oshift1; - scalarInterp1[1] = scalarInterp1[1] * oscale1 + oshift1; - normalLight1 = computeDensityNormal(scalarInterp1, height1, 1.0); - #else - vec4 normalLight0 = normalMat[0]; - vec4 normalLight1 = normalMat[1]; - #endif - - // compute opacities - float opacity0 = pwfValue0; - float opacity1 = pwfValue1; - #ifdef vtkGradientOpacityOn - float gof0 = computeGradientOpacityFactor(normalMat[0].a, goscale0, goshift0, gomin0, gomax0); - opacity0 *= gof0; - float gof1 = computeGradientOpacityFactor(normalMat[1].a, goscale1, goshift1, gomin1, gomax1); - opacity1 *= gof1; - #endif - float opacitySum = opacity0 + opacity1; - if (opacitySum <= 0.0) { - return vec4(0.0); - } - - // mix the colors and opacities - tColor0 = applyAllLightning(tColor0, opacity0, posIS, normalLight0); - tColor1 = applyAllLightning(tColor1, opacity1, posIS, normalLight1); - vec3 mixedColor = (opacity0 * tColor0 + opacity1 * tColor1) / opacitySum; - return vec4(mixedColor, min(1.0, opacitySum)); -`; - case ColorMixPreset.COLORIZE: - return ` - // compute normals - mat4 normalMat = computeMat4Normal(posIS, tValue, tstep); - #if (vtkLightComplexity > 0) && defined(vtkComputeNormalFromOpacity) - vec3 scalarInterp0[2]; - vec4 normalLight0 = computeNormalForDensity(posIS, tstep, scalarInterp0, 0); - scalarInterp0[0] = scalarInterp0[0] * oscale0 + oshift0; - scalarInterp0[1] = scalarInterp0[1] * oscale0 + oshift0; - normalLight0 = computeDensityNormal(scalarInterp0, height0, 1.0); - #else - vec4 normalLight0 = normalMat[0]; - #endif - - // compute opacities - float opacity0 = pwfValue0; - #ifdef vtkGradientOpacityOn - float gof0 = computeGradientOpacityFactor(normalMat[0].a, goscale0, goshift0, gomin0, gomax0); - opacity0 *= gof0; - #endif - - // mix the colors and opacities - vec3 color = tColor0 * mix(vec3(1.0), tColor1, pwfValue1); - color = applyAllLightning(color, opacity0, posIS, normalLight0); - return vec4(color, opacity0); -`; - default: - return null; - } -} +// Some matrices to avoid reallocations when we need them +const preAllocatedMatrices = { + idxToView: mat4.identity(new Float64Array(16)), + idxNormalMatrix: mat3.identity(new Float64Array(9)), + modelToView: mat4.identity(new Float64Array(16)), + projectionToView: mat4.identity(new Float64Array(16)), + projectionToWorld: mat4.identity(new Float64Array(16)), +}; // ---------------------------------------------------------------------------- // vtkOpenGLVolumeMapper methods @@ -124,17 +56,69 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { // Set our className model.classHierarchy.push('vtkOpenGLVolumeMapper'); - function unregisterGraphicsResources(renderWindow) { - [ - model._scalars, - model._scalarOpacityFunc, - model._colorTransferFunc, - model._labelOutlineThicknessArray, - ].forEach((coreObject) => - renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI) + function useIndependentComponents(actorProperty, numComp) { + const iComps = actorProperty.getIndependentComponents(); + const colorMixPreset = actorProperty.getColorMixPreset(); + return (iComps && numComp >= 2) || !!colorMixPreset; + } + + function isLabelmapOutlineRequired(actorProperty) { + return ( + actorProperty.getUseLabelOutline() || + model.renderable.getBlendMode() === + BlendMode.LABELMAP_EDGE_PROJECTION_BLEND ); } + // Associate a reference counter to each graphics resource + const graphicsResourceReferenceCount = new Map(); + + function decreaseGraphicsResourceCount(openGLRenderWindow, coreObject) { + if (!coreObject) { + return; + } + const oldCount = graphicsResourceReferenceCount.get(coreObject) ?? 0; + const newCount = oldCount - 1; + if (newCount <= 0) { + openGLRenderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI); + graphicsResourceReferenceCount.delete(coreObject); + } else { + graphicsResourceReferenceCount.set(coreObject, newCount); + } + } + + function increaseGraphicsResourceCount(openGLRenderWindow, coreObject) { + if (!coreObject) { + return; + } + const oldCount = graphicsResourceReferenceCount.get(coreObject) ?? 0; + const newCount = oldCount + 1; + graphicsResourceReferenceCount.set(coreObject, newCount); + if (oldCount <= 0) { + openGLRenderWindow.registerGraphicsResourceUser(coreObject, publicAPI); + } + } + + function replaceGraphicsResource( + openGLRenderWindow, + oldResourceCoreObject, + newResourceCoreObject + ) { + if (oldResourceCoreObject === newResourceCoreObject) { + return; + } + decreaseGraphicsResourceCount(openGLRenderWindow, oldResourceCoreObject); + increaseGraphicsResourceCount(openGLRenderWindow, newResourceCoreObject); + } + + function unregisterGraphicsResources(renderWindow) { + graphicsResourceReferenceCount + .keys() + .forEach((coreObject) => + renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI) + ); + } + publicAPI.buildPass = () => { model.zBufferTexture = null; }; @@ -191,187 +175,37 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { shaders.Geometry = ''; }; - publicAPI.useIndependentComponents = (actorProperty) => { - const iComps = actorProperty.getIndependentComponents(); - const image = model.currentInput; - const numComp = image - ?.getPointData() - ?.getScalars() - ?.getNumberOfComponents(); - const colorMixPreset = actorProperty.getColorMixPreset(); - return (iComps && numComp >= 2) || !!colorMixPreset; - }; - publicAPI.replaceShaderValues = (shaders, ren, actor) => { - const actorProps = actor.getProperty(); let FSSource = shaders.Fragment; - // define some values in the shader - const iType = actorProps.getInterpolationType(); - if (iType === InterpolationType.LINEAR) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::TrilinearOn', - '#define vtkTrilinearOn' - ).result; - } - - const vtkImageLabelOutline = publicAPI.isLabelmapOutlineRequired(actor); - if (vtkImageLabelOutline === true) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::ImageLabelOutlineOn', - '#define vtkImageLabelOutlineOn' - ).result; - } - - const LabelEdgeProjection = - model.renderable.getBlendMode() === - BlendMode.LABELMAP_EDGE_PROJECTION_BLEND; - - if (LabelEdgeProjection) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::LabelEdgeProjectionOn', - '#define vtkLabelEdgeProjectionOn' - ).result; - } - - const numComp = model.scalarTexture.getComponents(); FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::NumComponents', - `#define vtkNumComponents ${numComp}` + '//VTK::EnabledColorFunctions', + model.previousState.usedColorForValueFunctionIds.map( + (functionId) => `#define EnableColorForValueFunctionId${functionId}` + ) ).result; - const useIndependentComps = publicAPI.useIndependentComponents(actorProps); - if (useIndependentComps) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::IndependentComponentsOn', - '#define UseIndependentComponents' - ).result; + const enabledLightings = []; + if (model.previousState.surfaceLightingEnabled) { + enabledLightings.push('Surface'); } - - // Define any proportional components - const proportionalComponents = []; - const forceNearestComponents = []; - for (let nc = 0; nc < numComp; nc++) { - if (actorProps.getOpacityMode(nc) === OpacityMode.PROPORTIONAL) { - proportionalComponents.push(`#define vtkComponent${nc}Proportional`); - } - if (actorProps.getForceNearestInterpolation(nc)) { - forceNearestComponents.push(`#define vtkComponent${nc}ForceNearest`); - } + if (model.previousState.volumeLightingEnabled) { + enabledLightings.push('Volume'); } - - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::vtkProportionalComponents', - proportionalComponents.join('\n') - ).result; - - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::vtkForceNearestComponents', - forceNearestComponents.join('\n') - ).result; - - const colorMixPreset = actorProps.getColorMixPreset(); - const colorMixCode = getColorCodeFromPreset(colorMixPreset); - if (colorMixCode) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::CustomComponentsColorMixOn', - '#define vtkCustomComponentsColorMix' - ).result; - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::CustomComponentsColorMix::Impl', - colorMixCode - ).result; - } - - // WebGL only supports loops over constants - // and does not support while loops so we - // have to hard code how many steps/samples to take - // We do a break so most systems will gracefully - // early terminate, but it is always possible - // a system will execute every step regardless - const ext = model.currentInput.getSpatialExtent(); - const spc = model.currentInput.getSpacing(); - const vsize = new Float64Array(3); - vec3.set( - vsize, - (ext[1] - ext[0]) * spc[0], - (ext[3] - ext[2]) * spc[1], - (ext[5] - ext[4]) * spc[2] - ); - - const maxSamples = - vec3.length(vsize) / publicAPI.getCurrentSampleDistance(ren); - FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::MaximumSamplesValue', - `${Math.ceil(maxSamples)}` + '//VTK::EnabledLightings', + enabledLightings.map( + (lightingType) => `#define Enable${lightingType}Lighting` + ) ).result; - // set light complexity - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::LightComplexity', - `#define vtkLightComplexity ${model.lightComplexity}` - ).result; - - // set shadow blending flag - if (model.lightComplexity > 0) { - if (model.renderable.getVolumetricScatteringBlending() > 0.0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::VolumeShadowOn', - `#define VolumeShadowOn` - ).result; - } - if (model.renderable.getVolumetricScatteringBlending() < 1.0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::SurfaceShadowOn', - `#define SurfaceShadowOn` - ).result; - } - if ( - model.renderable.getLocalAmbientOcclusion() && - actorProps.getAmbient() > 0.0 - ) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::localAmbientOcclusionOn', - `#define localAmbientOcclusionOn` - ).result; - } - } - - // if using gradient opacity define that - const numIComps = useIndependentComps ? numComp : 1; - model.gopacity = false; - for (let nc = 0; !model.gopacity && nc < numIComps; ++nc) { - model.gopacity ||= actorProps.getUseGradientOpacity(nc); - } - if (model.gopacity) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::GradientOpacityOn', - '#define vtkGradientOpacityOn' - ).result; - } - - // set normal from density - if (model.renderable.getComputeNormalFromOpacity()) { + if (model.previousState.forceNearestInterpolationEnabled) { FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::vtkComputeNormalFromOpacity', - `#define vtkComputeNormalFromOpacity` + '//VTK::EnableForceNearestInterpolation', + '#define EnableForceNearestInterpolation' ).result; } @@ -390,7 +224,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { 'zdepth = -2.0 * camFar * camNear / (zdepth*(camFar-camNear)-(camFar+camNear)) - camNear;}', 'else {', 'zdepth = (zdepth + 1.0) * 0.5 * (camFar - camNear);}\n', - 'zdepth = -zdepth/rayDir.z;', + 'zdepth = -zdepth/rayDirVC.z;', 'dists.y = min(zdepth,dists.y);', ]).result; } @@ -402,95 +236,69 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { `${model.renderable.getBlendMode()}` ).result; - shaders.Fragment = FSSource; - - publicAPI.replaceShaderLight(shaders, ren, actor); - publicAPI.replaceShaderClippingPlane(shaders, ren, actor); - }; + const numberOfVolumes = model.currentValidInputs.length; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::NumberOfVolumes', + `${numberOfVolumes}` + ).result; - publicAPI.replaceShaderLight = (shaders, ren, actor) => { - if (model.lightComplexity === 0) { - return; + // Also replace the sampling functions, because arrays of sampler can only be indexed using constants + const fetchVolumeCases = []; + const sampleVolumeCases = []; + const sampleColorCases = []; + const sampleOpacityCases = []; + for (let volumeIndex = 0; volumeIndex < numberOfVolumes; ++volumeIndex) { + fetchVolumeCases.push(` + case ${volumeIndex}: + return texelFetch(volumeTexture[${volumeIndex}], pos, 0);`); + sampleVolumeCases.push(` + case ${volumeIndex}: + return texture(volumeTexture[${volumeIndex}], pos);`); + sampleColorCases.push(` + case ${volumeIndex}: + return texture2D(colorTexture[${volumeIndex}], pos);`); + sampleOpacityCases.push(` + case ${volumeIndex}: + return texture2D(opacityTexture[${volumeIndex}], pos);`); } - let FSSource = shaders.Fragment; - // check for shadow maps - not implemented yet, skip - // const shadowFactor = ''; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::fetchVolumeTexture', + fetchVolumeCases + ).result; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::sampleVolumeTexture', + sampleVolumeCases + ).result; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::sampleColorTexture', + sampleColorCases + ).result; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::sampleOpacityTexture', + sampleOpacityCases + ).result; - // to-do: single out the case when complexity = 1 + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::NumberOfLights', + `${model.numberOfLights}` + ).result; - // only account for lights that are switched on - let lightNum = 0; - ren.getLights().forEach((light) => { - if (light.getSwitch()) { - lightNum += 1; - } - }); FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::Light::Dec', - [ - `uniform int lightNum;`, - `uniform bool twoSidedLighting;`, - `uniform vec3 lightColor[${lightNum}];`, - `uniform vec3 lightDirectionVC[${lightNum}]; // normalized`, - `uniform vec3 lightHalfAngleVC[${lightNum}];`, - '//VTK::Light::Dec', - ], - false + '//VTK::MaxLaoKernelSize', + `${model.maxLaoKernelSize}` ).result; - // support any number of lights - if (model.lightComplexity === 3) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::Light::Dec', - [ - `uniform vec3 lightPositionVC[${lightNum}];`, - `uniform vec3 lightAttenuation[${lightNum}];`, - `uniform float lightConeAngle[${lightNum}];`, - `uniform float lightExponent[${lightNum}];`, - `uniform int lightPositional[${lightNum}];`, - ], - false - ).result; - } - if (model.renderable.getVolumetricScatteringBlending() > 0.0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::VolumeShadow::Dec', - [ - `uniform float volumetricScatteringBlending;`, - `uniform float giReach;`, - `uniform float volumeShadowSamplingDistFactor;`, - `uniform float anisotropy;`, - `uniform float anisotropy2;`, - ], - false - ).result; - } - if ( - model.renderable.getLocalAmbientOcclusion() && - actor.getProperty().getAmbient() > 0.0 - ) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::LAO::Dec', - [ - `uniform int kernelRadius;`, - `uniform vec2 kernelSample[${model.renderable.getLAOKernelRadius()}];`, - `uniform int kernelSize;`, - ], - false - ).result; - } shaders.Fragment = FSSource; - }; - publicAPI.replaceShaderClippingPlane = (shaders, ren, actor) => { - let FSSource = shaders.Fragment; - - if (model.renderable.getClippingPlanes().length > 0) { - const clipPlaneSize = model.renderable.getClippingPlanes().length; + const numberOfClippingPlanes = model.renderable.getClippingPlanes().length; + if (numberOfClippingPlanes > 0) { FSSource = vtkShaderProgram.substitute( FSSource, '//VTK::ClipPlane::Dec', @@ -509,7 +317,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { FSSource, '//VTK::ClipPlane::Impl', [ - `for(int i = 0; i < ${clipPlaneSize}; i++) {`, + `for(int i = 0; i < ${numberOfClippingPlanes}; i++) {`, ' float rayDirRatio = dot(rayDir, vClipPlaneNormals[i]);', ' float equationResult = dot(vertexVCVSOutput, vClipPlaneNormals[i]) + vClipPlaneDistances[i];', ' if (rayDirRatio == 0.0)', @@ -530,86 +338,105 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { shaders.Fragment = FSSource; }; - const recomputeLightComplexity = (actor, lights) => { - // do we need lighting? - let lightComplexity = 0; - if ( - actor.getProperty().getShade() && - model.renderable.getBlendMode() === BlendMode.COMPOSITE_BLEND - ) { - // consider the lighting complexity to determine which case applies - // simple headlight, Light Kit, the whole feature set of VTK - lightComplexity = 0; - model.numberOfLights = 0; - - lights.forEach((light) => { - const status = light.getSwitch(); - if (status > 0) { - model.numberOfLights++; - if (lightComplexity === 0) { - lightComplexity = 1; - } - } - + publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => { + // These are all the variables that fully determine the behavior of replaceShaderValues + // and the exact content of the shader + // See replaceShaderValues method + const hasZBufferTexture = !!model.zBufferTexture; + const numberOfValidInputs = model.currentValidInputs.length; + const numberOfLights = model.numberOfLights; + const maxLaoKernelSize = model.maxLaoKernelSize; + const numberOfClippingPlanes = model.renderable.getClippingPlanes().length; + // These are from the buildShader function in vtkReplacementShaderMapper + const mapperShaderReplacements = + model.renderable.getViewSpecificProperties().OpenGL?.ShaderReplacements; + const renderPassShaderReplcaements = + model.currentRenderPass?.getShaderReplacement(); + const blendMode = model.renderable.getBlendMode(); + + // This enables optimizing out some function which avoids huge shader compilation time + // The result of this computation is used in getColorForValue in the fragment shader + const volumeProperties = actor.getProperties(); + model.colorForValueFunctionIds = model.currentValidInputs.map( + ({ imageData, inputIndex }) => { + const volumeProperty = volumeProperties[inputIndex]; + // If labeloutline and not the edge labelmap, since in the edge labelmap blend + // we need the underlying data to sample through if ( - lightComplexity === 1 && - (model.numberOfLights > 1 || - light.getIntensity() !== 1.0 || - !light.lightTypeIsHeadLight()) + blendMode !== BlendMode.LABELMAP_EDGE_PROJECTION_BLEND && + isLabelmapOutlineRequired(volumeProperty) ) { - lightComplexity = 2; + return 5; } - if (lightComplexity < 3 && light.getPositional()) { - lightComplexity = 3; + const scalars = imageData.getPointData()?.getScalars(); + const numberOfComponents = scalars.getNumberOfComponents(); + const useIndependentComps = useIndependentComponents( + volumeProperty, + numberOfComponents + ); + if (useIndependentComps) { + switch (volumeProperty.getColorMixPreset()) { + case ColorMixPreset.ADDITIVE: + return 1; + case ColorMixPreset.COLORIZE: + return 2; + case ColorMixPreset.CUSTOM: + return 3; + default: // ColorMixPreset.DEFAULT + return 4; + } } - }); - } - if (lightComplexity !== model.lightComplexity) { - model.lightComplexity = lightComplexity; - publicAPI.modified(); - } - }; - - publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => { - const actorProps = actor.getProperty(); - - recomputeLightComplexity(actor, ren.getLights()); + return 0; + } + ); - const numComp = model.scalarTexture.getComponents(); - const opacityModes = []; - const forceNearestInterps = []; - for (let nc = 0; nc < numComp; nc++) { - opacityModes.push(actorProps.getOpacityMode(nc)); - forceNearestInterps.push(actorProps.getForceNearestInterpolation(nc)); + // Get all the functions that are used, to check if the shader needs to be recompiled + const usedFunctionsSet = new Set(model.colorForValueFunctionIds); + // Custom mix could use a fallback on default mix + if (usedFunctionsSet.has(3)) { + usedFunctionsSet.add(4); } + usedFunctionsSet.values(); + const usedColorForValueFunctionIds = [...usedFunctionsSet.values()].sort(); - const ext = model.currentInput.getSpatialExtent(); - const spc = model.currentInput.getSpacing(); - const vsize = new Float64Array(3); - vec3.set( - vsize, - (ext[1] - ext[0]) * spc[0], - (ext[3] - ext[2]) * spc[1], - (ext[5] - ext[4]) * spc[2] + // Get which types of lighting are enabled + const surfaceLightingEnabled = model.currentValidInputs.some( + ({ inputIndex }) => + volumeProperties[inputIndex].getVolumetricScatteringBlending() < 1.0 + ); + const volumeLightingEnabled = model.currentValidInputs.some( + ({ inputIndex }) => + volumeProperties[inputIndex].getVolumetricScatteringBlending() > 0.0 ); - const maxSamples = - vec3.length(vsize) / publicAPI.getCurrentSampleDistance(ren); - - const hasZBufferTexture = !!model.zBufferTexture; + // Is any volume using ForceNearestInterpolation + const forceNearestInterpolationEnabled = model.currentValidInputs.some( + ({ imageData, inputIndex }) => { + const volumeProperty = volumeProperties[inputIndex]; + const scalars = imageData.getPointData()?.getScalars(); + const numberOfComponents = scalars?.getNumberOfComponents() || 0; + for (let compIdx = 0; compIdx < numberOfComponents; ++compIdx) { + if (volumeProperty.getForceNearestInterpolation(compIdx)) { + return true; + } + } + return false; + } + ); - const state = { - iComps: actorProps.getIndependentComponents(), - colorMixPreset: actorProps.getColorMixPreset(), - interpolationType: actorProps.getInterpolationType(), - useLabelOutline: publicAPI.isLabelmapOutlineRequired(actor), - numComp, - maxSamples, - useGradientOpacity: actorProps.getUseGradientOpacity(0), - blendMode: model.renderable.getBlendMode(), + const currentState = { + blendMode, + numberOfLights, + numberOfValidInputs, hasZBufferTexture, - opacityModes, - forceNearestInterps, + maxLaoKernelSize, + numberOfClippingPlanes, + mapperShaderReplacements, + renderPassShaderReplcaements, + usedColorForValueFunctionIds, + surfaceLightingEnabled, + volumeLightingEnabled, + forceNearestInterpolationEnabled, }; // We need to rebuild the shader if one of these variables has changed, @@ -617,12 +444,10 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { // We also need to rebuild if the shader source time is outdated. if ( cellBO.getProgram()?.getHandle() === 0 || - cellBO.getShaderSourceTime().getMTime() < publicAPI.getMTime() || - cellBO.getShaderSourceTime().getMTime() < model.renderable.getMTime() || !model.previousState || - !DeepEqual(model.previousState, state) + !DeepEqual(model.previousState, currentState) ) { - model.previousState = state; + model.previousState = currentState; return true; } return false; @@ -697,47 +522,65 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { cellBO.getAttributeUpdateTime().modified(); } - program.setUniformi('texture1', model.scalarTexture.getTextureUnit()); + const sampleDistance = publicAPI.getCurrentSampleDistance(ren); + program.setUniformf('sampleDistance', sampleDistance); + + const volumeShadowSampleDistance = + sampleDistance * model.renderable.getVolumeShadowSamplingDistFactor(); program.setUniformf( - 'sampleDistance', - publicAPI.getCurrentSampleDistance(ren) + 'volumeShadowSampleDistance', + volumeShadowSampleDistance ); - const volInfo = model.scalarTexture.getVolumeInfo(); - const ipScalarRange = model.renderable.getIpScalarRange(); + const volumeProperties = actor.getProperties(); + model.currentValidInputs.forEach( + ({ imageData, inputIndex }, shaderIndex) => { + const volumeProperty = volumeProperties[inputIndex]; + const scalarTexture = model.scalarTextures[shaderIndex]; + const uniformPrefix = `volumes[${shaderIndex}]`; - // In some situations, we might not have computed the scale and offset - // for the data range, or it might not be needed. - if (volInfo?.dataComputedScale?.length) { - const minVals = []; - const maxVals = []; - for (let i = 0; i < 4; i++) { - // convert iprange from 0-1 into data range values - minVals[i] = - ipScalarRange[0] * volInfo.dataComputedScale[i] + - volInfo.dataComputedOffset[i]; - maxVals[i] = - ipScalarRange[1] * volInfo.dataComputedScale[i] + - volInfo.dataComputedOffset[i]; - // convert data ranges into texture values - minVals[i] = (minVals[i] - volInfo.offset[i]) / volInfo.scale[i]; - maxVals[i] = (maxVals[i] - volInfo.offset[i]) / volInfo.scale[i]; + program.setUniformi( + `volumeTexture[${shaderIndex}]`, + scalarTexture.getTextureUnit() + ); + + const volInfo = scalarTexture.getVolumeInfo(); + const ipScalarRange = volumeProperty.getIpScalarRange(); + + // In some situations, we might not have computed the scale and offset + // for the data range, or it might not be needed. + if (volInfo?.dataComputedScale?.length) { + const minVals = []; + const maxVals = []; + for (let i = 0; i < 4; i++) { + // convert iprange from 0-1 into data range values + minVals[i] = + ipScalarRange[0] * volInfo.dataComputedScale[i] + + volInfo.dataComputedOffset[i]; + maxVals[i] = + ipScalarRange[1] * volInfo.dataComputedScale[i] + + volInfo.dataComputedOffset[i]; + // convert data ranges into texture values + minVals[i] = (minVals[i] - volInfo.offset[i]) / volInfo.scale[i]; + maxVals[i] = (maxVals[i] - volInfo.offset[i]) / volInfo.scale[i]; + } + program.setUniform4f( + `${uniformPrefix}.ipScalarRangeMin`, + minVals[0], + minVals[1], + minVals[2], + minVals[3] + ); + program.setUniform4f( + `${uniformPrefix}.ipScalarRangeMax`, + maxVals[0], + maxVals[1], + maxVals[2], + maxVals[3] + ); + } } - program.setUniform4f( - 'ipScalarRangeMin', - minVals[0], - minVals[1], - minVals[2], - minVals[3] - ); - program.setUniform4f( - 'ipScalarRangeMax', - maxVals[0], - maxVals[1], - maxVals[2], - maxVals[3] - ); - } + ); // if we have a zbuffer texture then set it if (model.zBufferTexture !== null) { @@ -754,429 +597,551 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { }; publicAPI.setCameraShaderParameters = (cellBO, ren, actor) => { - // // [WMVP]C == {world, model, view, projection} coordinates - // // E.g., WCPC == world to projection coordinate transformation + // These matrices are not cached for their content, but only to avoid reallocations + const { + idxToView, + idxNormalMatrix: idxToViewNormalMatrix, + modelToView, + projectionToView, + projectionToWorld, + } = preAllocatedMatrices; + + // [WMVP]C == {world, model, view, projection} coordinates + // E.g., WCPC == world to projection coordinate transformation const keyMats = model.openGLCamera.getKeyMatrices(ren); const actMats = model.openGLVolume.getKeyMatrices(); - mat4.multiply(model.modelToView, keyMats.wcvc, actMats.mcwc); + mat4.multiply(modelToView, keyMats.wcvc, actMats.mcwc); const program = cellBO.getProgram(); - const cam = model.openGLCamera.getRenderable(); - const crange = cam.getClippingRange(); - program.setUniformf('camThick', crange[1] - crange[0]); - program.setUniformf('camNear', crange[0]); - program.setUniformf('camFar', crange[1]); - - const bounds = model.currentInput.getBounds(); - const dims = model.currentInput.getDimensions(); - - // compute the viewport bounds of the volume - // we will only render those fragments. - const pos = new Float64Array(3); - const dir = new Float64Array(3); - let dcxmin = 1.0; - let dcxmax = -1.0; - let dcymin = 1.0; - let dcymax = -1.0; - - for (let i = 0; i < 8; ++i) { - vec3.set( - pos, - bounds[i % 2], - bounds[2 + (Math.floor(i / 2) % 2)], - bounds[4 + Math.floor(i / 4)] - ); - vec3.transformMat4(pos, pos, model.modelToView); - if (!cam.getParallelProjection()) { - vec3.normalize(dir, pos); - - // now find the projection of this point onto a - // nearZ distance plane. Since the camera is at 0,0,0 - // in VC the ray is just t*pos and - // t is -nearZ/dir.z - // intersection becomes pos.x/pos.z - const t = -crange[0] / pos[2]; - vec3.scale(pos, dir, t); + const camera = model.openGLCamera.getRenderable(); + const useParallelProjection = camera.getParallelProjection(); + const clippingRange = camera.getClippingRange(); + program.setUniformf('camThick', clippingRange[1] - clippingRange[0]); + program.setUniformf('camNear', clippingRange[0]); + program.setUniformf('camFar', clippingRange[1]); + program.setUniformi('cameraParallel', useParallelProjection); + + // Compute the viewport bounds of the volume + // We will only render those fragments + // First, merge all bounds to get a fusion of all bounds in model coordinates + const boundsMC = model.currentValidInputs.reduce( + (bounds, { imageData }) => + vtkBoundingBox.addBounds(bounds, imageData.getBounds()), + [...vtkBoundingBox.INIT_BOUNDS] + ); + const cornersMC = vtkBoundingBox.getCorners(boundsMC, []); + const cornersDC = cornersMC.map((corner) => { + // Convert to view coordinates + vec3.transformMat4(corner, corner, modelToView); + + if (!useParallelProjection) { + // Now find the projection of this point onto a + // nearZ distance plane. Since pos is in view coordinates, + // scale it until pos.z == nearZ + const newScale = -clippingRange[0] / (corner[2] * vec3.length(corner)); + vec3.scale(corner, corner, newScale); } - // now convert to DC - vec3.transformMat4(pos, pos, keyMats.vcpc); - dcxmin = Math.min(pos[0], dcxmin); - dcxmax = Math.max(pos[0], dcxmax); - dcymin = Math.min(pos[1], dcymin); - dcymax = Math.max(pos[1], dcymax); + // Now convert to display coordinates + vec3.transformMat4(corner, corner, keyMats.vcpc); + + return corner; + }); + const boundsDC = vtkBoundingBox.addPoints( + [...vtkBoundingBox.INIT_BOUNDS], + cornersDC + ); + program.setUniformf('dcxmin', boundsDC[0]); + program.setUniformf('dcxmax', boundsDC[1]); + program.setUniformf('dcymin', boundsDC[2]); + program.setUniformf('dcymax', boundsDC[3]); + + const maximumRayLength = vtkBoundingBox.getDiagonalLength(boundsMC); + const maximumNumberOfSamples = Math.ceil( + maximumRayLength / publicAPI.getCurrentSampleDistance(ren) + ); + program.setUniformi('maximumNumberOfSamples', maximumNumberOfSamples); + if (maximumNumberOfSamples > model.renderable.getMaximumSamplesPerRay()) { + vtkWarningMacro( + `The number of steps required ${maximumNumberOfSamples} is larger than the ` + + `specified maximum number of steps ${model.renderable.getMaximumSamplesPerRay()}.\n` + + 'Please either change the volumeMapper sampleDistance or its maximum number of samples.' + ); } - program.setUniformf('dcxmin', dcxmin); - program.setUniformf('dcxmax', dcxmax); - program.setUniformf('dcymin', dcymin); - program.setUniformf('dcymax', dcymax); + const size = publicAPI.getRenderTargetSize(); + program.setUniformf('vpWidth', size[0]); + program.setUniformf('vpHeight', size[1]); + const offset = publicAPI.getRenderTargetOffset(); + program.setUniformf('vpOffsetX', offset[0] / size[0]); + program.setUniformf('vpOffsetY', offset[1] / size[1]); - if (program.isUniformUsed('cameraParallel')) { - program.setUniformi('cameraParallel', cam.getParallelProjection()); - } + mat4.invert(projectionToView, keyMats.vcpc); + program.setUniformMatrix('PCVCMatrix', projectionToView); - const ext = model.currentInput.getSpatialExtent(); - const spc = model.currentInput.getSpacing(); - const vsize = new Float64Array(3); - vec3.set( - vsize, - (ext[1] - ext[0]) * spc[0], - (ext[3] - ext[2]) * spc[1], - (ext[5] - ext[4]) * spc[2] - ); - program.setUniform3f('vSpacing', spc[0], spc[1], spc[2]); + program.setUniformi('twoSidedLighting', ren.getTwoSidedLighting()); - vec3.set(pos, ext[0], ext[2], ext[4]); - model.currentInput.indexToWorldVec3(pos, pos); + const kernelSample = new Array(2 * model.maxLaoKernelSize); + for (let i = 0; i < model.maxLaoKernelSize; i++) { + kernelSample[i * 2] = Math.random(); + kernelSample[i * 2 + 1] = Math.random(); + } + program.setUniform2fv('kernelSample', kernelSample); - vec3.transformMat4(pos, pos, model.modelToView); - program.setUniform3f('vOriginVC', pos[0], pos[1], pos[2]); + // Handle lighting values + if (model.numberOfLights > 0) { + let lightIndex = 0; + ren.getLights().forEach((light) => { + if (light.getSwitch() > 0) { + const lightPrefix = `lights[${lightIndex}]`; + + // Merge color and intensity + const color = light.getColor(); + const intensity = light.getIntensity(); + const scaledColor = vec3.scale([], color, intensity); + program.setUniform3fv(`${lightPrefix}.color`, scaledColor); + + // Position in view coordinates + const position = light.getTransformedPosition(); + vec3.transformMat4(position, position, modelToView); + program.setUniform3fv(`${lightPrefix}.positionVC`, position); + + // Convert lightDirection in view coordinates and normalize it + const direction = [...light.getDirection()]; + vec3.transformMat3(direction, direction, keyMats.normalMatrix); + vec3.normalize(direction, direction); + program.setUniform3fv(`${lightPrefix}.directionVC`, direction); + + // Camera direction of projection is (0, 0, -1.0) in view coordinates + const halfAngle = [ + -0.5 * direction[0], + -0.5 * direction[1], + -0.5 * (direction[2] - 1.0), + ]; + program.setUniform3fv(`${lightPrefix}.halfAngleVC`, halfAngle); + + // Attenuation + const attenuation = light.getAttenuationValues(); + program.setUniform3fv(`${lightPrefix}.attenuation`, attenuation); - // apply the image directions - const i2wmat4 = model.currentInput.getIndexToWorld(); - mat4.multiply(model.idxToView, model.modelToView, i2wmat4); + // Exponent + const exponent = light.getExponent(); + program.setUniformf(`${lightPrefix}.exponent`, exponent); - mat3.multiply( - model.idxNormalMatrix, - keyMats.normalMatrix, - actMats.normalMatrix - ); - mat3.multiply( - model.idxNormalMatrix, - model.idxNormalMatrix, - model.currentInput.getDirectionByReference() - ); + // Cone angle + const coneAngle = light.getConeAngle(); + program.setUniformf(`${lightPrefix}.coneAngle`, coneAngle); - const maxSamples = - vec3.length(vsize) / publicAPI.getCurrentSampleDistance(ren); - if (maxSamples > model.renderable.getMaximumSamplesPerRay()) { - vtkWarningMacro(`The number of steps required ${Math.ceil( - maxSamples - )} is larger than the - specified maximum number of steps ${model.renderable.getMaximumSamplesPerRay()}. - Please either change the - volumeMapper sampleDistance or its maximum number of samples.`); - } + // Positional flag + const isPositional = light.getPositional(); + program.setUniformi(`${lightPrefix}.isPositional`, isPositional); - const vctoijk = new Float64Array(3); - - vec3.set(vctoijk, 1.0, 1.0, 1.0); - vec3.divide(vctoijk, vctoijk, vsize); - program.setUniform3f('vVCToIJK', vctoijk[0], vctoijk[1], vctoijk[2]); - program.setUniform3i('volumeDimensions', dims[0], dims[1], dims[2]); - program.setUniform3f('volumeSpacings', spc[0], spc[1], spc[2]); - - if (!model._openGLRenderWindow.getWebgl2()) { - const volInfo = model.scalarTexture.getVolumeInfo(); - program.setUniformf('texWidth', model.scalarTexture.getWidth()); - program.setUniformf('texHeight', model.scalarTexture.getHeight()); - program.setUniformi('xreps', volInfo.xreps); - program.setUniformi('xstride', volInfo.xstride); - program.setUniformi('ystride', volInfo.ystride); + lightIndex++; + } + }); } - // map normals through normal matrix - // then use a point on the plane to compute the distance - const normal = new Float64Array(3); - const pos2 = new Float64Array(3); - for (let i = 0; i < 6; ++i) { - switch (i) { - case 1: - vec3.set(normal, -1.0, 0.0, 0.0); - vec3.set(pos2, ext[0], ext[2], ext[4]); - break; - case 2: - vec3.set(normal, 0.0, 1.0, 0.0); - vec3.set(pos2, ext[1], ext[3], ext[5]); - break; - case 3: - vec3.set(normal, 0.0, -1.0, 0.0); - vec3.set(pos2, ext[0], ext[2], ext[4]); - break; - case 4: - vec3.set(normal, 0.0, 0.0, 1.0); - vec3.set(pos2, ext[1], ext[3], ext[5]); - break; - case 5: - vec3.set(normal, 0.0, 0.0, -1.0); - vec3.set(pos2, ext[0], ext[2], ext[4]); - break; - case 0: - default: - vec3.set(normal, 1.0, 0.0, 0.0); - vec3.set(pos2, ext[1], ext[3], ext[5]); - break; - } - vec3.transformMat3(normal, normal, model.idxNormalMatrix); - vec3.transformMat4(pos2, pos2, model.idxToView); - const dist = -1.0 * vec3.dot(pos2, normal); - - // we have the plane in view coordinates - // specify the planes in view coordinates - program.setUniform3f(`vPlaneNormal${i}`, normal[0], normal[1], normal[2]); - program.setUniformf(`vPlaneDistance${i}`, dist); - } + // Set uniforms per volume + const volumeProperties = actor.getProperties(); + model.currentValidInputs.forEach( + ({ imageData, inputIndex }, shaderIndex) => { + const volumeProperty = volumeProperties[inputIndex]; + const uniformPrefix = `volumes[${shaderIndex}]`; + + const spatialExtent = imageData.getSpatialExtent(); + const spacing = imageData.getSpacing(); + const dimensions = imageData.getDimensions(); + const idxToModel = imageData.getIndexToWorld(); + const worldToIndex = imageData.getWorldToIndex(); + const imageDirection = imageData.getDirectionByReference(); + + // idxToView is equivalent to applying idxToModel then modelToView + mat4.multiply(idxToView, modelToView, idxToModel); + + // Set size uniform + const sizeVC = vec3.multiply(new Float64Array(3), dimensions, spacing); + program.setUniform3fv(`${uniformPrefix}.size`, sizeVC); + + const diagonalLength = vec3.length(sizeVC); + program.setUniformf(`${uniformPrefix}.diagonalLength`, diagonalLength); + + // Set vctoijk uniform + const inverseSize = vec3.inverse(new Float64Array(3), sizeVC); + program.setUniform3fv(`${uniformPrefix}.inverseSize`, inverseSize); + + // Set spacing uniform + program.setUniform3fv(`${uniformPrefix}.spacing`, spacing); + const inverseSpacing = vec3.inverse([], spacing); + program.setUniform3fv( + `${uniformPrefix}.inverseSpacing`, + inverseSpacing + ); + + // Set dimensions uniform + program.setUniform3iv(`${uniformPrefix}.dimensions`, dimensions); - if (publicAPI.isLabelmapOutlineRequired(actor)) { - const image = model.currentInput; - const worldToIndex = image.getWorldToIndex(); + // Set inverse dimensions uniform + program.setUniform3fv( + `${uniformPrefix}.inverseDimensions`, + vec3.inverse([], dimensions) + ); - program.setUniformMatrix('vWCtoIDX', worldToIndex); + // Set originVC uniform + const spacialExtentMinIC = vec3.fromValues( + spatialExtent[0], + spatialExtent[2], + spatialExtent[4] + ); + const originVC = vec3.transformMat4( + new Float64Array(3), + spacialExtentMinIC, + idxToView + ); + program.setUniform3fv(`${uniformPrefix}.originVC`, originVC); - const camera = ren.getActiveCamera(); - const [cRange0, cRange1] = camera.getClippingRange(); - const distance = camera.getDistance(); + // Set world to index + program.setUniformMatrix(`${uniformPrefix}.worldToIndex`, worldToIndex); - // set the clipping range to be model.distance and model.distance + 0.1 - // since we use the in the keyMats.wcpc (world to projection) matrix - // the projection matrix calculation relies on the clipping range to be - // set correctly. This is done inside the interactorStyleMPRSlice which - // limits use cases where the interactor style is not used. + // map normals through normal matrix + // then use a point on the plane to compute the distance + mat3.multiply( + idxToViewNormalMatrix, + keyMats.normalMatrix, + actMats.normalMatrix + ); + mat3.multiply( + idxToViewNormalMatrix, + idxToViewNormalMatrix, + imageDirection + ); + program.setUniformMatrix3x3( + `${uniformPrefix}.ISVCNormalMatrix`, + idxToViewNormalMatrix + ); + program.setUniformMatrix3x3( + `${uniformPrefix}.VCISNormalMatrix`, + mat3.transpose([], idxToViewNormalMatrix) + ); - camera.setClippingRange(distance, distance + 0.1); - const labelOutlineKeyMats = model.openGLCamera.getKeyMatrices(ren); + if (isLabelmapOutlineRequired(volumeProperty)) { + const distance = camera.getDistance(); - // Get the projection coordinate to world coordinate transformation matrix. - mat4.invert(model.projectionToWorld, labelOutlineKeyMats.wcpc); + // set the clipping range to be model.distance and model.distance + 0.1 + // since we use the in the keyMats.wcpc (world to projection) matrix + // the projection matrix calculation relies on the clipping range to be + // set correctly. This is done inside the interactorStyleMPRSlice which + // limits use cases where the interactor style is not used. - // reset the clipping range since the keyMats are cached - camera.setClippingRange(cRange0, cRange1); + camera.setClippingRange(distance, distance + 0.1); + const labelOutlineKeyMats = model.openGLCamera.getKeyMatrices(ren); - // to re compute the matrices for the current camera and cache them - model.openGLCamera.getKeyMatrices(ren); + // Get the projection coordinate to world coordinate transformation matrix. + mat4.invert(projectionToWorld, labelOutlineKeyMats.wcpc); - program.setUniformMatrix('PCWCMatrix', model.projectionToWorld); + // reset the clipping range since the keyMats are cached + camera.setClippingRange(clippingRange[0], clippingRange[1]); - const size = publicAPI.getRenderTargetSize(); + // to re compute the matrices for the current camera and cache them + model.openGLCamera.getKeyMatrices(ren); - program.setUniformf('vpWidth', size[0]); - program.setUniformf('vpHeight', size[1]); + program.setUniformMatrix( + `${uniformPrefix}.PCWCMatrix`, + projectionToWorld + ); + } - const offset = publicAPI.getRenderTargetOffset(); - program.setUniformf('vpOffsetX', offset[0] / size[0]); - program.setUniformf('vpOffsetY', offset[1] / size[1]); - } + if (volumeProperty.getVolumetricScatteringBlending() > 0.0) { + program.setUniformf( + `${uniformPrefix}.globalIlluminationReach`, + volumeProperty.getGlobalIlluminationReach() + ); + program.setUniformf( + `${uniformPrefix}.volumetricScatteringBlending`, + volumeProperty.getVolumetricScatteringBlending() + ); + program.setUniformf( + `${uniformPrefix}.anisotropy`, + volumeProperty.getAnisotropy() + ); + program.setUniformf( + `${uniformPrefix}.anisotropySquared`, + volumeProperty.getAnisotropy() ** 2.0 + ); + } - mat4.invert(model.projectionToView, keyMats.vcpc); - program.setUniformMatrix('PCVCMatrix', model.projectionToView); + if ( + volumeProperty.getLocalAmbientOcclusion() && + volumeProperty.getAmbient() > 0.0 + ) { + const kernelSize = volumeProperty.getLAOKernelSize(); + program.setUniformi(`${uniformPrefix}.kernelSize`, kernelSize); - // handle lighting values - if (model.lightComplexity === 0) { - return; - } - let lightNum = 0; - const lightColor = []; - const lightDir = []; - const halfAngle = []; - ren.getLights().forEach((light) => { - const status = light.getSwitch(); - if (status > 0) { - const dColor = light.getColor(); - const intensity = light.getIntensity(); - lightColor[0 + lightNum * 3] = dColor[0] * intensity; - lightColor[1 + lightNum * 3] = dColor[1] * intensity; - lightColor[2 + lightNum * 3] = dColor[2] * intensity; - const ldir = light.getDirection(); - vec3.set(normal, ldir[0], ldir[1], ldir[2]); - vec3.transformMat3(normal, normal, keyMats.normalMatrix); // in view coordinat - vec3.normalize(normal, normal); - lightDir[0 + lightNum * 3] = normal[0]; - lightDir[1 + lightNum * 3] = normal[1]; - lightDir[2 + lightNum * 3] = normal[2]; - // camera DOP is 0,0,-1.0 in VC - halfAngle[0 + lightNum * 3] = -0.5 * normal[0]; - halfAngle[1 + lightNum * 3] = -0.5 * normal[1]; - halfAngle[2 + lightNum * 3] = -0.5 * (normal[2] - 1.0); - lightNum++; - } - }); - program.setUniformi('twoSidedLighting', ren.getTwoSidedLighting()); - program.setUniformi('lightNum', lightNum); - program.setUniform3fv('lightColor', lightColor); - program.setUniform3fv('lightDirectionVC', lightDir); - program.setUniform3fv('lightHalfAngleVC', halfAngle); - - if (model.lightComplexity === 3) { - lightNum = 0; - const lightPositionVC = []; - const lightAttenuation = []; - const lightConeAngle = []; - const lightExponent = []; - const lightPositional = []; - ren.getLights().forEach((light) => { - const status = light.getSwitch(); - if (status > 0) { - const attenuation = light.getAttenuationValues(); - lightAttenuation[0 + lightNum * 3] = attenuation[0]; - lightAttenuation[1 + lightNum * 3] = attenuation[1]; - lightAttenuation[2 + lightNum * 3] = attenuation[2]; - lightExponent[lightNum] = light.getExponent(); - lightConeAngle[lightNum] = light.getConeAngle(); - lightPositional[lightNum] = light.getPositional(); - const lp = light.getTransformedPosition(); - vec3.transformMat4(lp, lp, model.modelToView); - lightPositionVC[0 + lightNum * 3] = lp[0]; - lightPositionVC[1 + lightNum * 3] = lp[1]; - lightPositionVC[2 + lightNum * 3] = lp[2]; - lightNum += 1; + const kernelRadius = volumeProperty.getLAOKernelRadius(); + program.setUniformi(`${uniformPrefix}.kernelRadius`, kernelRadius); + } else { + program.setUniformi(`${uniformPrefix}.kernelSize`, 0); } - }); - program.setUniform3fv('lightPositionVC', lightPositionVC); - program.setUniform3fv('lightAttenuation', lightAttenuation); - program.setUniformfv('lightConeAngle', lightConeAngle); - program.setUniformfv('lightExponent', lightExponent); - program.setUniformiv('lightPositional', lightPositional); - } - if (model.renderable.getVolumetricScatteringBlending() > 0.0) { - program.setUniformf( - 'giReach', - model.renderable.getGlobalIlluminationReach() - ); - program.setUniformf( - 'volumetricScatteringBlending', - model.renderable.getVolumetricScatteringBlending() - ); - program.setUniformf( - 'volumeShadowSamplingDistFactor', - model.renderable.getVolumeShadowSamplingDistFactor() - ); - program.setUniformf('anisotropy', model.renderable.getAnisotropy()); - program.setUniformf( - 'anisotropy2', - model.renderable.getAnisotropy() ** 2.0 - ); - } - if ( - model.renderable.getLocalAmbientOcclusion() && - actor.getProperty().getAmbient() > 0.0 - ) { - const ks = model.renderable.getLAOKernelSize(); - program.setUniformi('kernelSize', ks); - const kernelSample = []; - for (let i = 0; i < ks; i++) { - kernelSample[i * 2] = Math.random() * 0.5; - kernelSample[i * 2 + 1] = Math.random() * 0.5; } - program.setUniform2fv('kernelSample', kernelSample); - program.setUniformi( - 'kernelRadius', - model.renderable.getLAOKernelRadius() - ); - } + ); }; publicAPI.setPropertyShaderParameters = (cellBO, ren, actor) => { const program = cellBO.getProgram(); - program.setUniformi('ctexture', model.colorTexture.getTextureUnit()); - program.setUniformi('otexture', model.opacityTexture.getTextureUnit()); program.setUniformi('jtexture', model.jitterTexture.getTextureUnit()); + + const volumeProperties = actor.getProperties(); + + // There is only one label outline thickness texture program.setUniformi( - 'ttexture', + `labelOutlineThicknessTexture`, model.labelOutlineThicknessTexture.getTextureUnit() ); - const volInfo = model.scalarTexture.getVolumeInfo(); - const vprop = actor.getProperty(); - - // set the component mix when independent - const numComp = model.scalarTexture.getComponents(); - const useIndependentComps = publicAPI.useIndependentComponents(vprop); - if (useIndependentComps) { - for (let i = 0; i < numComp; i++) { - program.setUniformf( - `mix${i}`, - actor.getProperty().getComponentWeight(i) + model.currentValidInputs.forEach( + ({ imageData, inputIndex }, shaderIndex) => { + const volumeProperty = volumeProperties[inputIndex]; + const uniformPrefix = `volumes[${shaderIndex}]`; + + const scalarTexture = model.scalarTextures[shaderIndex]; + const opacityTexture = model.opacityTextures[shaderIndex]; + const colorTexture = model.colorTextures[shaderIndex]; + + program.setUniformi( + `colorTexture[${shaderIndex}]`, + colorTexture.getTextureUnit() + ); + program.setUniformi( + `opacityTexture[${shaderIndex}]`, + opacityTexture.getTextureUnit() ); - } - } - // three levels of shift scale combined into one - // for performance in the fragment shader - for (let i = 0; i < numComp; i++) { - const target = useIndependentComps ? i : 0; - const sscale = volInfo.scale[i]; - const ofun = vprop.getScalarOpacity(target); - const oRange = ofun.getRange(); - const oscale = sscale / (oRange[1] - oRange[0]); - const oshift = (volInfo.offset[i] - oRange[0]) / (oRange[1] - oRange[0]); - program.setUniformf(`oshift${i}`, oshift); - program.setUniformf(`oscale${i}`, oscale); - - const cfun = vprop.getRGBTransferFunction(target); - const cRange = cfun.getRange(); - const cshift = (volInfo.offset[i] - cRange[0]) / (cRange[1] - cRange[0]); - const cScale = sscale / (cRange[1] - cRange[0]); - program.setUniformf(`cshift${i}`, cshift); - program.setUniformf(`cscale${i}`, cScale); - } + const volInfo = scalarTexture.getVolumeInfo(); - if (model.gopacity) { - if (useIndependentComps) { - for (let nc = 0; nc < numComp; ++nc) { - const sscale = volInfo.scale[nc]; - const useGO = vprop.getUseGradientOpacity(nc); - if (useGO) { - const gomin = vprop.getGradientOpacityMinimumOpacity(nc); - const gomax = vprop.getGradientOpacityMaximumOpacity(nc); - program.setUniformf(`gomin${nc}`, gomin); - program.setUniformf(`gomax${nc}`, gomax); - const goRange = [ - vprop.getGradientOpacityMinimumValue(nc), - vprop.getGradientOpacityMaximumValue(nc), - ]; - program.setUniformf( - `goscale${nc}`, - (sscale * (gomax - gomin)) / (goRange[1] - goRange[0]) - ); - program.setUniformf( - `goshift${nc}`, - (-goRange[0] * (gomax - gomin)) / (goRange[1] - goRange[0]) + - gomin - ); - } else { - program.setUniformf(`gomin${nc}`, 1.0); - program.setUniformf(`gomax${nc}`, 1.0); - program.setUniformf(`goscale${nc}`, 0.0); - program.setUniformf(`goshift${nc}`, 1.0); + // set the component mix when independent + const scalars = imageData.getPointData()?.getScalars(); + const numberOfComponents = scalars.getNumberOfComponents(); + const useIndependentComps = useIndependentComponents( + volumeProperty, + numberOfComponents + ); + program.setUniformi( + `${uniformPrefix}.useIndependentComponents`, + useIndependentComps + ); + program.setUniformi( + `${uniformPrefix}.numberOfComponents`, + numberOfComponents + ); + if (useIndependentComps) { + const independentComponentMix = new Float32Array(4); + for (let i = 0; i < numberOfComponents; i++) { + independentComponentMix[i] = volumeProperty.getComponentWeight(i); } + program.setUniform4fv( + `${uniformPrefix}.independentComponentMix`, + independentComponentMix + ); + const transferFunctionsSampleHeight = new Float32Array(4); + const pixelHeight = 1 / numberOfComponents; + for (let i = 0; i < numberOfComponents; ++i) { + transferFunctionsSampleHeight[i] = (i + 0.5) * pixelHeight; + } + program.setUniform4fv( + `${uniformPrefix}.transferFunctionsSampleHeight`, + transferFunctionsSampleHeight + ); } - } else { - const sscale = volInfo.scale[numComp - 1]; - const gomin = vprop.getGradientOpacityMinimumOpacity(0); - const gomax = vprop.getGradientOpacityMaximumOpacity(0); - program.setUniformf('gomin0', gomin); - program.setUniformf('gomax0', gomax); - const goRange = [ - vprop.getGradientOpacityMinimumValue(0), - vprop.getGradientOpacityMaximumValue(0), - ]; - program.setUniformf( - 'goscale0', - (sscale * (gomax - gomin)) / (goRange[1] - goRange[0]) + + const proportionalComponents = [0, 0, 0, 0]; + const forceNearestComponents = [0, 0, 0, 0]; + for (let nc = 0; nc < numberOfComponents; nc++) { + proportionalComponents[nc] = + volumeProperty.getOpacityMode(nc) === OpacityMode.PROPORTIONAL + ? 1 + : 0; + forceNearestComponents[nc] = + volumeProperty.getForceNearestInterpolation(nc) ? 1 : 0; + } + program.setUniform4i( + `${uniformPrefix}.isComponentProportional`, + proportionalComponents ); - program.setUniformf( - 'goshift0', - (-goRange[0] * (gomax - gomin)) / (goRange[1] - goRange[0]) + gomin + program.setUniform4i( + `${uniformPrefix}.isComponentNearestInterpolationForced`, + forceNearestComponents ); - } - } - const vtkImageLabelOutline = publicAPI.isLabelmapOutlineRequired(actor); - if (vtkImageLabelOutline === true) { - const labelOutlineOpacity = actor.getProperty().getLabelOutlineOpacity(); - program.setUniformf('outlineOpacity', labelOutlineOpacity); - } + const colorForValueFunctionId = + model.colorForValueFunctionIds[shaderIndex]; + program.setUniformi( + `${uniformPrefix}.colorForValueFunctionId`, + colorForValueFunctionId + ); - if (model.lightComplexity > 0) { - program.setUniformf('vAmbient', vprop.getAmbient()); - program.setUniformf('vDiffuse', vprop.getDiffuse()); - program.setUniformf('vSpecular', vprop.getSpecular()); - program.setUniformf('vSpecularPower', vprop.getSpecularPower()); - } + const computeNormalFromOpacity = + volumeProperty.getComputeNormalFromOpacity(); + program.setUniformi( + `${uniformPrefix}.computeNormalFromOpacity`, + computeNormalFromOpacity + ); + + // three levels of shift scale combined into one + // for performance in the fragment shader + const colorTextureScale = new Float32Array(4); + const colorTextureShift = new Float32Array(4); + const opacityTextureScale = new Float32Array(4); + const opacityTextureShift = new Float32Array(4); + for (let i = 0; i < numberOfComponents; i++) { + const target = useIndependentComps ? i : 0; + const sscale = volInfo.scale[i]; + + // Color + const colorFunction = volumeProperty.getRGBTransferFunction(target); + const colorRange = colorFunction.getRange(); + colorTextureScale[i] = sscale / (colorRange[1] - colorRange[0]); + colorTextureShift[i] = + (volInfo.offset[i] - colorRange[0]) / + (colorRange[1] - colorRange[0]); + + // Opacity + const opacityFunction = volumeProperty.getScalarOpacity(target); + const opacityRange = opacityFunction.getRange(); + opacityTextureScale[i] = sscale / (opacityRange[1] - opacityRange[0]); + opacityTextureShift[i] = + (volInfo.offset[i] - opacityRange[0]) / + (opacityRange[1] - opacityRange[0]); + } + program.setUniform4fv( + `${uniformPrefix}.colorTextureScale`, + colorTextureScale + ); + program.setUniform4fv( + `${uniformPrefix}.colorTextureShift`, + colorTextureShift + ); + program.setUniform4fv( + `${uniformPrefix}.opacityTextureScale`, + opacityTextureScale + ); + program.setUniform4fv( + `${uniformPrefix}.opacityTextureShift`, + opacityTextureShift + ); + + const numberOfIndependantComponents = useIndependentComps + ? numberOfComponents + : 1; + let isGradientOpacityEnabled = false; + for (let i = 0; i < numberOfIndependantComponents; ++i) { + if (volumeProperty.getUseGradientOpacity(i)) { + isGradientOpacityEnabled = true; + break; + } + } + program.setUniformi( + `${uniformPrefix}.isGradientOpacityEnabled`, + isGradientOpacityEnabled + ); + + if (isGradientOpacityEnabled) { + const gradientOpacityScale = new Array(4); + const gradientOpacityShift = new Array(4); + const gradientOpacityMin = new Array(4); + const gradientOpacityMax = new Array(4); + if (useIndependentComps) { + for (let nc = 0; nc < numberOfComponents; ++nc) { + const sscale = volInfo.scale[nc]; + const useGO = volumeProperty.getUseGradientOpacity(nc); + if (useGO) { + const goOpacityRange = [ + volumeProperty.getGradientOpacityMinimumOpacity(nc), + volumeProperty.getGradientOpacityMaximumOpacity(nc), + ]; + const goValueRange = [ + volumeProperty.getGradientOpacityMinimumValue(nc), + volumeProperty.getGradientOpacityMaximumValue(nc), + ]; + gradientOpacityMin[nc] = goOpacityRange[0]; + gradientOpacityMax[nc] = goOpacityRange[1]; + gradientOpacityScale[nc] = + (sscale * (goOpacityRange[1] - goOpacityRange[0])) / + (goValueRange[1] - goValueRange[0]); + gradientOpacityShift[nc] = + (-goValueRange[0] * (goOpacityRange[1] - goOpacityRange[0])) / + (goValueRange[1] - goValueRange[0]) + + goOpacityRange[0]; + } else { + gradientOpacityMin[nc] = 1; + gradientOpacityMax[nc] = 1; + gradientOpacityScale[nc] = 0; + gradientOpacityShift[nc] = 1; + } + } + } else { + const sscale = volInfo.scale[numberOfComponents - 1]; + const goOpacityRange = [ + volumeProperty.getGradientOpacityMinimumOpacity(0), + volumeProperty.getGradientOpacityMaximumOpacity(0), + ]; + const goValueRange = [ + volumeProperty.getGradientOpacityMinimumValue(0), + volumeProperty.getGradientOpacityMaximumValue(0), + ]; + gradientOpacityMin[0] = goOpacityRange[0]; + gradientOpacityMax[0] = goOpacityRange[1]; + gradientOpacityScale[0] = + (sscale * (goOpacityRange[1] - goOpacityRange[0])) / + (goValueRange[1] - goValueRange[0]); + gradientOpacityShift[0] = + (-goValueRange[0] * (goOpacityRange[1] - goOpacityRange[0])) / + (goValueRange[1] - goValueRange[0]) + + goOpacityRange[0]; + } + program.setUniform4f( + `${uniformPrefix}.gradientOpacityScale`, + gradientOpacityScale + ); + program.setUniform4f( + `${uniformPrefix}.gradientOpacityShift`, + gradientOpacityShift + ); + program.setUniform4f( + `${uniformPrefix}.gradientOpacityMin`, + gradientOpacityMin + ); + program.setUniform4f( + `${uniformPrefix}.gradientOpacityMax`, + gradientOpacityMax + ); + } + + const outlineOpacity = volumeProperty.getLabelOutlineOpacity(); + program.setUniformf(`${uniformPrefix}.outlineOpacity`, outlineOpacity); + + if (model.numberOfLights > 0) { + program.setUniformf( + `${uniformPrefix}.ambient`, + volumeProperty.getAmbient() + ); + program.setUniformf( + `${uniformPrefix}.diffuse`, + volumeProperty.getDiffuse() + ); + program.setUniformf( + `${uniformPrefix}.specular`, + volumeProperty.getSpecular() + ); + const specularPower = volumeProperty.getSpecularPower(); + program.setUniformf( + `${uniformPrefix}.specularPower`, + specularPower === 0 ? 1.0 : specularPower + ); + } + } + ); }; publicAPI.getClippingPlaneShaderParameters = (cellBO, ren, actor) => { @@ -1344,14 +1309,19 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { publicAPI.updateBufferObjects(ren, actor); // set interpolation on the texture based on property setting - const iType = actor.getProperty().getInterpolationType(); - if (iType === InterpolationType.NEAREST) { - model.scalarTexture.setMinificationFilter(Filter.NEAREST); - model.scalarTexture.setMagnificationFilter(Filter.NEAREST); - } else { - model.scalarTexture.setMinificationFilter(Filter.LINEAR); - model.scalarTexture.setMagnificationFilter(Filter.LINEAR); - } + const volumeProperties = actor.getProperties(); + model.currentValidInputs.forEach(({ inputIndex }) => { + const volumeProperty = volumeProperties[inputIndex]; + const interpolationType = volumeProperty.getInterpolationType(); + const scalarTexture = model.scalarTextures[inputIndex]; + if (interpolationType === InterpolationType.NEAREST) { + scalarTexture.setMinificationFilter(Filter.NEAREST); + scalarTexture.setMagnificationFilter(Filter.NEAREST); + } else { + scalarTexture.setMinificationFilter(Filter.LINEAR); + scalarTexture.setMagnificationFilter(Filter.LINEAR); + } + }); // if we have a zbuffer texture then activate it if (model.zBufferTexture !== null) { @@ -1363,26 +1333,22 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const gl = model.context; // render the texture - model.scalarTexture.activate(); - model.opacityTexture.activate(); - model.labelOutlineThicknessTexture.activate(); - model.colorTexture.activate(); - model.jitterTexture.activate(); + const allTextures = [ + ...model.scalarTextures, + ...model.colorTextures, + ...model.opacityTextures, + model.labelOutlineThicknessTexture, + model.jitterTexture, + ]; + allTextures.forEach((texture) => texture.activate()); publicAPI.updateShaders(model.tris, ren, actor); // First we do the triangles, update the shader, set uniforms, etc. - // for (let i = 0; i < 11; ++i) { - // gl.drawArrays(gl.TRIANGLES, 66 * i, 66); - // } gl.drawArrays(gl.TRIANGLES, 0, model.tris.getCABO().getElementCount()); model.tris.getVAO().release(); - model.scalarTexture.deactivate(); - model.colorTexture.deactivate(); - model.opacityTexture.deactivate(); - model.labelOutlineThicknessTexture.deactivate(); - model.jitterTexture.deactivate(); + allTextures.forEach((texture) => texture.deactivate()); }; publicAPI.renderPieceFinish = (ren, actor) => { @@ -1478,12 +1444,49 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { publicAPI.renderPiece = (ren, actor) => { publicAPI.invokeEvent({ type: 'StartEvent' }); + + // Get the valid image data inputs model.renderable.update(); - model.currentInput = model.renderable.getInputData(); + const numberOfInputs = model.renderable.getNumberOfInputPorts(); + model.currentValidInputs = []; + for (let inputIndex = 0; inputIndex < numberOfInputs; ++inputIndex) { + const imageData = model.renderable.getInputData(inputIndex); + if (imageData && !imageData.isDeleted()) { + model.currentValidInputs.push({ imageData, inputIndex }); + } + } + + // Get the number of lights + let newNumberOfLights = 0; + ren.getLights().forEach((light) => { + if (light.getSwitch() > 0) { + newNumberOfLights++; + } + }); + if (newNumberOfLights !== model.numberOfLights) { + model.numberOfLights = newNumberOfLights; + publicAPI.modified(); + } + + // Get the max kernel size from volume properties that use LAO and + // that are linked to a valid input imageData + model.maxLaoKernelSize = 0; + const volumeProperties = actor.getProperties(); + model.currentValidInputs.forEach(({ inputIndex }) => { + const volumeProperty = volumeProperties[inputIndex]; + const kernelSize = volumeProperty.getLAOKernelSize(); + if ( + kernelSize > model.maxLaoKernelSize && + volumeProperty.getLocalAmbientOcclusion() && + volumeProperty.getAmbient() > 0.0 + ) { + model.maxLaoKernelSize = kernelSize; + } + }); + publicAPI.invokeEvent({ type: 'EndEvent' }); - if (!model.currentInput) { - vtkErrorMacro('No input!'); + if (model.currentValidInputs.length === 0) { return; } @@ -1492,14 +1495,6 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { publicAPI.renderPieceFinish(ren, actor); }; - publicAPI.computeBounds = (ren, actor) => { - if (!publicAPI.getInput()) { - vtkMath.uninitializeBounds(model.Bounds); - return; - } - model.bounds = publicAPI.getInput().getBounds(); - }; - publicAPI.updateBufferObjects = (ren, actor) => { // Rebuild buffers if needed if (publicAPI.getNeedToRebuildBufferObjects(ren, actor)) { @@ -1508,13 +1503,13 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { }; publicAPI.getNeedToRebuildBufferObjects = (ren, actor) => { - // first do a coarse check if ( model.VBOBuildTime.getMTime() < publicAPI.getMTime() || model.VBOBuildTime.getMTime() < actor.getMTime() || model.VBOBuildTime.getMTime() < model.renderable.getMTime() || - model.VBOBuildTime.getMTime() < actor.getProperty().getMTime() || - model.VBOBuildTime.getMTime() < model.currentInput.getMTime() || + model.currentValidInputs.some( + ({ imageData }) => model.VBOBuildTime.getMTime() < imageData.getMTime() + ) || !model.scalarTexture?.getHandle() || !model.colorTexture?.getHandle() || !model.labelOutlineThicknessTexture?.getHandle() @@ -1525,315 +1520,241 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { }; publicAPI.buildBufferObjects = (ren, actor) => { - const image = model.currentInput; - if (!image) { - return; - } - - const scalars = image.getPointData() && image.getPointData().getScalars(); - if (!scalars) { - return; - } - - const vprop = actor.getProperty(); - - if (!model.jitterTexture.getHandle()) { - const oTable = new Uint8Array(32 * 32); - for (let i = 0; i < 32 * 32; ++i) { - oTable[i] = 255.0 * Math.random(); - } - model.jitterTexture.setMinificationFilter(Filter.LINEAR); - model.jitterTexture.setMagnificationFilter(Filter.LINEAR); - model.jitterTexture.create2DFromRaw( - 32, - 32, - 1, - VtkDataTypes.UNSIGNED_CHAR, - oTable - ); - } - - const numComp = scalars.getNumberOfComponents(); - const useIndependentComps = publicAPI.useIndependentComponents(vprop); - const numIComps = useIndependentComps ? numComp : 1; - - const scalarOpacityFunc = vprop.getScalarOpacity(); - const opTex = - model._openGLRenderWindow.getGraphicsResourceForObject(scalarOpacityFunc); - let toString = getTransferFunctionHash( - scalarOpacityFunc, - useIndependentComps, - numIComps - ); - const reBuildOp = !opTex?.oglObject || opTex.hash !== toString; - if (reBuildOp) { - model.opacityTexture = vtkOpenGLTexture.newInstance(); - model.opacityTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - // rebuild opacity tfun? - const oWidth = 1024; - const oSize = oWidth * 2 * numIComps; - const ofTable = new Float32Array(oSize); - const tmpTable = new Float32Array(oWidth); - - for (let c = 0; c < numIComps; ++c) { - const ofun = vprop.getScalarOpacity(c); - const opacityFactor = - publicAPI.getCurrentSampleDistance(ren) / - vprop.getScalarOpacityUnitDistance(c); - - const oRange = ofun.getRange(); - ofun.getTable(oRange[0], oRange[1], oWidth, tmpTable, 1); - // adjust for sample distance etc - for (let i = 0; i < oWidth; ++i) { - ofTable[c * oWidth * 2 + i] = - 1.0 - (1.0 - tmpTable[i]) ** opacityFactor; - ofTable[c * oWidth * 2 + i + oWidth] = ofTable[c * oWidth * 2 + i]; + const volumeProperties = actor.getProperties(); + model.currentValidInputs.forEach( + ({ imageData, inputIndex }, shaderIndex) => { + const volumeProperty = volumeProperties[inputIndex]; + const scalars = imageData.getPointData()?.getScalars(); + if (!volumeProperty || !scalars) { + return; } - } - model.opacityTexture.resetFormatAndType(); - model.opacityTexture.setMinificationFilter(Filter.LINEAR); - model.opacityTexture.setMagnificationFilter(Filter.LINEAR); - - // use float texture where possible because we really need the resolution - // for this table. Errors in low values of opacity accumulate to - // visible artifacts. High values of opacity quickly terminate without - // artifacts. - if ( - model._openGLRenderWindow.getWebgl2() || - (model.context.getExtension('OES_texture_float') && - model.context.getExtension('OES_texture_float_linear')) - ) { - model.opacityTexture.create2DFromRaw( - oWidth, - 2 * numIComps, - 1, - VtkDataTypes.FLOAT, - ofTable - ); - } else { - const oTable = new Uint8ClampedArray(oSize); - for (let i = 0; i < oSize; ++i) { - oTable[i] = 255.0 * ofTable[i]; + if (!model.jitterTexture.getHandle()) { + const jitterArray = new Float32Array(32 * 32); + for (let i = 0; i < 32 * 32; ++i) { + jitterArray[i] = Math.random(); + } + model.jitterTexture.setMinificationFilter(Filter.NEAREST); + model.jitterTexture.setMagnificationFilter(Filter.NEAREST); + model.jitterTexture.create2DFromRaw( + 32, + 32, + 1, + VtkDataTypes.FLOAT, + jitterArray + ); } - model.opacityTexture.create2DFromRaw( - oWidth, - 2 * numIComps, - 1, - VtkDataTypes.UNSIGNED_CHAR, - oTable + + const numberOfComponents = scalars.getNumberOfComponents(); + const useIndependentComps = useIndependentComponents( + volumeProperty, + numberOfComponents ); - } - if (scalarOpacityFunc) { - model._openGLRenderWindow.setGraphicsResourceForObject( + const numIComps = useIndependentComps ? numberOfComponents : 1; + + const scalarOpacityFunc = volumeProperty.getScalarOpacity(); + const opTex = + model._openGLRenderWindow.getGraphicsResourceForObject( + scalarOpacityFunc + ); + const opacityFuncHash = getTransferFunctionHash( scalarOpacityFunc, - model.opacityTexture, - toString + useIndependentComps, + numIComps ); - if (scalarOpacityFunc !== model._scalarOpacityFunc) { - model._openGLRenderWindow.registerGraphicsResourceUser( - scalarOpacityFunc, - publicAPI - ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._scalarOpacityFunc, - publicAPI - ); - } - model._scalarOpacityFunc = scalarOpacityFunc; - } - } else { - model.opacityTexture = opTex.oglObject; - } + const reBuildOp = !opTex?.oglObject || opTex.hash !== opacityFuncHash; + if (reBuildOp) { + const newOpacityTexture = vtkOpenGLTexture.newInstance(); + newOpacityTexture.setOpenGLRenderWindow(model._openGLRenderWindow); + // rebuild opacity tfun? + const oWidth = 1024; + const oSize = oWidth * 2 * numIComps; + const ofTable = new Float32Array(oSize); + const tmpTable = new Float32Array(oWidth); + + for (let c = 0; c < numIComps; ++c) { + const ofun = volumeProperty.getScalarOpacity(c); + const opacityFactor = + publicAPI.getCurrentSampleDistance(ren) / + volumeProperty.getScalarOpacityUnitDistance(c); + + const oRange = ofun.getRange(); + ofun.getTable(oRange[0], oRange[1], oWidth, tmpTable, 1); + // adjust for sample distance etc + for (let i = 0; i < oWidth; ++i) { + ofTable[c * oWidth * 2 + i] = + 1.0 - (1.0 - tmpTable[i]) ** opacityFactor; + ofTable[c * oWidth * 2 + i + oWidth] = + ofTable[c * oWidth * 2 + i]; + } + } - // rebuild color tfun? - const colorTransferFunc = vprop.getRGBTransferFunction(); - toString = getTransferFunctionHash( - colorTransferFunc, - useIndependentComps, - numIComps - ); - const cTex = - model._openGLRenderWindow.getGraphicsResourceForObject(colorTransferFunc); - const reBuildC = !cTex?.oglObject?.getHandle() || cTex?.hash !== toString; - if (reBuildC) { - model.colorTexture = vtkOpenGLTexture.newInstance(); - model.colorTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - const cWidth = 1024; - const cSize = cWidth * 2 * numIComps * 3; - const cTable = new Uint8ClampedArray(cSize); - const tmpTable = new Float32Array(cWidth * 3); - - for (let c = 0; c < numIComps; ++c) { - const cfun = vprop.getRGBTransferFunction(c); - const cRange = cfun.getRange(); - cfun.getTable(cRange[0], cRange[1], cWidth, tmpTable, 1); - for (let i = 0; i < cWidth * 3; ++i) { - cTable[c * cWidth * 6 + i] = 255.0 * tmpTable[i]; - cTable[c * cWidth * 6 + i + cWidth * 3] = 255.0 * tmpTable[i]; + newOpacityTexture.resetFormatAndType(); + newOpacityTexture.setMinificationFilter(Filter.LINEAR); + newOpacityTexture.setMagnificationFilter(Filter.LINEAR); + + // use float texture where possible because we really need the resolution + // for this table. Errors in low values of opacity accumulate to + // visible artifacts. High values of opacity quickly terminate without + // artifacts. + if ( + model._openGLRenderWindow.getWebgl2() || + (model.context.getExtension('OES_texture_float') && + model.context.getExtension('OES_texture_float_linear')) + ) { + newOpacityTexture.create2DFromRaw( + oWidth, + 2 * numIComps, + 1, + VtkDataTypes.FLOAT, + ofTable + ); + } else { + const oTable = new Uint8ClampedArray(oSize); + for (let i = 0; i < oSize; ++i) { + oTable[i] = 255.0 * ofTable[i]; + } + newOpacityTexture.create2DFromRaw( + oWidth, + 2 * numIComps, + 1, + VtkDataTypes.UNSIGNED_CHAR, + oTable + ); + } + if (scalarOpacityFunc) { + model._openGLRenderWindow.setGraphicsResourceForObject( + scalarOpacityFunc, + newOpacityTexture, + opacityFuncHash + ); + } + model.opacityTextures[shaderIndex] = newOpacityTexture; + } else { + model.opacityTextures[shaderIndex] = opTex.oglObject; } - } - - model.colorTexture.resetFormatAndType(); - model.colorTexture.setMinificationFilter(Filter.LINEAR); - model.colorTexture.setMagnificationFilter(Filter.LINEAR); + replaceGraphicsResource( + model._openGLRenderWindow, + model._opacityTexturesCore[shaderIndex], + scalarOpacityFunc + ); + model._opacityTexturesCore[shaderIndex] = scalarOpacityFunc; - model.colorTexture.create2DFromRaw( - cWidth, - 2 * numIComps, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); - if (colorTransferFunc) { - model._openGLRenderWindow.setGraphicsResourceForObject( + // rebuild color tfun? + const colorTransferFunc = volumeProperty.getRGBTransferFunction(); + const colorFuncHash = getTransferFunctionHash( colorTransferFunc, - model.colorTexture, - toString + useIndependentComps, + numIComps ); - if (colorTransferFunc !== model._colorTransferFunc) { - model._openGLRenderWindow.registerGraphicsResourceUser( - colorTransferFunc, - publicAPI + const cTex = + model._openGLRenderWindow.getGraphicsResourceForObject( + colorTransferFunc ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._colorTransferFunc, - publicAPI + const reBuildC = + !cTex?.oglObject?.getHandle() || cTex?.hash !== colorFuncHash; + if (reBuildC) { + const newColorTexture = vtkOpenGLTexture.newInstance(); + newColorTexture.setOpenGLRenderWindow(model._openGLRenderWindow); + const cWidth = 1024; + const cSize = cWidth * 2 * numIComps * 3; + const cTable = new Uint8ClampedArray(cSize); + const tmpTable = new Float32Array(cWidth * 3); + + for (let c = 0; c < numIComps; ++c) { + const cfun = volumeProperty.getRGBTransferFunction(c); + const cRange = cfun.getRange(); + cfun.getTable(cRange[0], cRange[1], cWidth, tmpTable, 1); + for (let i = 0; i < cWidth * 3; ++i) { + cTable[c * cWidth * 6 + i] = 255.0 * tmpTable[i]; + cTable[c * cWidth * 6 + i + cWidth * 3] = 255.0 * tmpTable[i]; + } + } + + newColorTexture.resetFormatAndType(); + newColorTexture.setMinificationFilter(Filter.LINEAR); + newColorTexture.setMagnificationFilter(Filter.LINEAR); + + newColorTexture.create2DFromRaw( + cWidth, + 2 * numIComps, + 3, + VtkDataTypes.UNSIGNED_CHAR, + cTable ); + if (colorTransferFunc) { + model._openGLRenderWindow.setGraphicsResourceForObject( + colorTransferFunc, + newColorTexture, + colorFuncHash + ); + } + model.colorTextures[shaderIndex] = newColorTexture; + } else { + model.colorTextures[shaderIndex] = cTex.oglObject; } - model._colorTransferFunc = colorTransferFunc; - } - } else { - model.colorTexture = cTex.oglObject; - } - - publicAPI.updateLabelOutlineThicknessTexture(actor); - - const tex = model._openGLRenderWindow.getGraphicsResourceForObject(scalars); - // rebuild the scalarTexture if the data has changed - toString = getImageDataHash(image, scalars); - const reBuildTex = !tex?.oglObject?.getHandle() || tex?.hash !== toString; - if (reBuildTex) { - model.scalarTexture = vtkOpenGLTexture.newInstance(); - model.scalarTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - // Build the textures - const dims = image.getDimensions(); - // Use norm16 for scalar texture if the extension is available - model.scalarTexture.setOglNorm16Ext( - model.context.getExtension('EXT_texture_norm16') - ); - model.scalarTexture.resetFormatAndType(); - model.scalarTexture.create3DFilterableFromDataArray( - dims[0], - dims[1], - dims[2], - scalars, - model.renderable.getPreferSizeOverAccuracy() - ); - if (scalars) { - model._openGLRenderWindow.setGraphicsResourceForObject( - scalars, - model.scalarTexture, - toString + replaceGraphicsResource( + model._openGLRenderWindow, + model._colorTexturesCore[shaderIndex], + colorTransferFunc ); - if (scalars !== model._scalars) { - model._openGLRenderWindow.registerGraphicsResourceUser( + model._colorTexturesCore[shaderIndex] = colorTransferFunc; + + // rebuild the scalarTexture if the data has changed + const tex = + model._openGLRenderWindow.getGraphicsResourceForObject(scalars); + const scalarsHash = getImageDataHash(imageData, scalars); + const reBuildTex = + !tex?.oglObject?.getHandle() || tex?.hash !== scalarsHash; + if (reBuildTex) { + const newScalarTexture = vtkOpenGLTexture.newInstance(); + newScalarTexture.setOpenGLRenderWindow(model._openGLRenderWindow); + // Build the textures + const dims = imageData.getDimensions(); + // Use norm16 for scalar texture if the extension is available + newScalarTexture.setOglNorm16Ext( + model.context.getExtension('EXT_texture_norm16') + ); + newScalarTexture.resetFormatAndType(); + newScalarTexture.create3DFilterableFromDataArray( + dims[0], + dims[1], + dims[2], scalars, - publicAPI + volumeProperty.getPreferSizeOverAccuracy() ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._scalars, - publicAPI + model._openGLRenderWindow.setGraphicsResourceForObject( + imageData, + newScalarTexture, + scalarsHash ); + model.scalarTextures[shaderIndex] = newScalarTexture; + } else { + model.scalarTextures[shaderIndex] = tex.oglObject; } - model._scalars = scalars; - } - } else { - model.scalarTexture = tex.oglObject; - } - - if (!model.tris.getCABO().getElementCount()) { - // build the CABO - const ptsArray = new Float32Array(12); - for (let i = 0; i < 4; i++) { - ptsArray[i * 3] = (i % 2) * 2 - 1.0; - ptsArray[i * 3 + 1] = i > 1 ? 1.0 : -1.0; - ptsArray[i * 3 + 2] = -1.0; + replaceGraphicsResource( + model._openGLRenderWindow, + model._scalarTexturesCore[shaderIndex], + imageData + ); + model._scalarTexturesCore[shaderIndex] = imageData; } + ); - const cellArray = new Uint16Array(8); - cellArray[0] = 3; - cellArray[1] = 0; - cellArray[2] = 1; - cellArray[3] = 3; - cellArray[4] = 3; - cellArray[5] = 0; - cellArray[6] = 3; - cellArray[7] = 2; - - // const dim = 12.0; - // const ptsArray = new Float32Array(3 * dim * dim); - // for (let i = 0; i < dim; i++) { - // for (let j = 0; j < dim; j++) { - // const offset = ((i * dim) + j) * 3; - // ptsArray[offset] = (2.0 * (i / (dim - 1.0))) - 1.0; - // ptsArray[offset + 1] = (2.0 * (j / (dim - 1.0))) - 1.0; - // ptsArray[offset + 2] = -1.0; - // } - // } - - // const cellArray = new Uint16Array(8 * (dim - 1) * (dim - 1)); - // for (let i = 0; i < dim - 1; i++) { - // for (let j = 0; j < dim - 1; j++) { - // const offset = 8 * ((i * (dim - 1)) + j); - // cellArray[offset] = 3; - // cellArray[offset + 1] = (i * dim) + j; - // cellArray[offset + 2] = (i * dim) + 1 + j; - // cellArray[offset + 3] = ((i + 1) * dim) + 1 + j; - // cellArray[offset + 4] = 3; - // cellArray[offset + 5] = (i * dim) + j; - // cellArray[offset + 6] = ((i + 1) * dim) + 1 + j; - // cellArray[offset + 7] = ((i + 1) * dim) + j; - // } - // } - - const points = vtkDataArray.newInstance({ - numberOfComponents: 3, - values: ptsArray, - }); - points.setName('points'); - const cells = vtkDataArray.newInstance({ - numberOfComponents: 1, - values: cellArray, - }); - model.tris.getCABO().createVBO(cells, 'polys', Representation.SURFACE, { - points, - cellOffset: 0, - }); - } - - model.VBOBuildTime.modified(); - }; - - publicAPI.updateLabelOutlineThicknessTexture = (volume) => { - const labelOutlineThicknessArray = volume - .getProperty() - .getLabelOutlineThickness(); - + // rebuild label outline thickness texture? + const firstVolumeProperty = + volumeProperties[model.currentValidInputs[0].inputIndex]; + const labelOutlineThicknessArray = + firstVolumeProperty.getLabelOutlineThickness(); const lTex = model._openGLRenderWindow.getGraphicsResourceForObject( labelOutlineThicknessArray ); - - // compute the join of the labelOutlineThicknessArray so that - // we can use it to decide whether to rebuild the labelOutlineThicknessTexture - // or not - const toString = `${labelOutlineThicknessArray.join('-')}`; - - const reBuildL = !lTex?.oglObject?.getHandle() || lTex?.hash !== toString; - + const labelOutlineThicknessHash = labelOutlineThicknessArray.join('-'); + const reBuildL = + !lTex?.oglObject?.getHandle() || lTex?.hash !== labelOutlineThicknessHash; if (reBuildL) { - model.labelOutlineThicknessTexture = vtkOpenGLTexture.newInstance(); - model.labelOutlineThicknessTexture.setOpenGLRenderWindow( + const newLabelOutlineThicknessTexture = vtkOpenGLTexture.newInstance(); + newLabelOutlineThicknessTexture.setOpenGLRenderWindow( model._openGLRenderWindow ); const lWidth = 1024; @@ -1853,12 +1774,12 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { lTable[i] = thickness; } - model.labelOutlineThicknessTexture.resetFormatAndType(); - model.labelOutlineThicknessTexture.setMinificationFilter(Filter.NEAREST); - model.labelOutlineThicknessTexture.setMagnificationFilter(Filter.NEAREST); + newLabelOutlineThicknessTexture.resetFormatAndType(); + newLabelOutlineThicknessTexture.setMinificationFilter(Filter.NEAREST); + newLabelOutlineThicknessTexture.setMagnificationFilter(Filter.NEAREST); // Create a 2D texture (acting as 1D) from the raw data - model.labelOutlineThicknessTexture.create2DFromRaw( + newLabelOutlineThicknessTexture.create2DFromRaw( lWidth, lHeight, 1, @@ -1869,34 +1790,56 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { if (labelOutlineThicknessArray) { model._openGLRenderWindow.setGraphicsResourceForObject( labelOutlineThicknessArray, - model.labelOutlineThicknessTexture, - toString + newLabelOutlineThicknessTexture, + labelOutlineThicknessHash ); - if (labelOutlineThicknessArray !== model._labelOutlineThicknessArray) { - model._openGLRenderWindow.registerGraphicsResourceUser( - labelOutlineThicknessArray, - publicAPI - ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._labelOutlineThicknessArray, - publicAPI - ); - } - model._labelOutlineThicknessArray = labelOutlineThicknessArray; } + model.labelOutlineThicknessTexture = newLabelOutlineThicknessTexture; } else { model.labelOutlineThicknessTexture = lTex.oglObject; } - }; + replaceGraphicsResource( + model._openGLRenderWindow, + model._labelOutlineThicknessTextureCore, + labelOutlineThicknessArray + ); + model._labelOutlineThicknessTextureCore = labelOutlineThicknessArray; + + if (!model.tris.getCABO().getElementCount()) { + // build the CABO + const ptsArray = new Float32Array(12); + for (let i = 0; i < 4; i++) { + ptsArray[i * 3] = (i % 2) * 2 - 1.0; + ptsArray[i * 3 + 1] = i > 1 ? 1.0 : -1.0; + ptsArray[i * 3 + 2] = -1.0; + } - publicAPI.isLabelmapOutlineRequired = (actor) => { - const prop = actor.getProperty(); - const renderable = model.renderable; + const cellArray = new Uint16Array(8); + cellArray[0] = 3; + cellArray[1] = 0; + cellArray[2] = 1; + cellArray[3] = 3; + cellArray[4] = 3; + cellArray[5] = 0; + cellArray[6] = 3; + cellArray[7] = 2; - return ( - prop.getUseLabelOutline() || - renderable.getBlendMode() === BlendMode.LABELMAP_EDGE_PROJECTION_BLEND - ); + const points = vtkDataArray.newInstance({ + numberOfComponents: 3, + values: ptsArray, + }); + points.setName('points'); + const cells = vtkDataArray.newInstance({ + numberOfComponents: 1, + values: cellArray, + }); + model.tris.getCABO().createVBO(cells, 'polys', Representation.SURFACE, { + points, + cellOffset: 0, + }); + } + + model.VBOBuildTime.modified(); }; } @@ -1907,14 +1850,15 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const DEFAULT_VALUES = { context: null, VBOBuildTime: null, - scalarTexture: null, - opacityTexture: null, - opacityTextureString: null, - colorTexture: null, - colorTextureString: null, + scalarTextures: [], + _scalarTexturesCore: [], + opacityTextures: [], + _opacityTexturesCore: [], + colorTextures: [], + _colorTexturesCore: [], + labelOutlineThicknessTextures: [], + _labelOutlineThicknessTextureCore: null, jitterTexture: null, - labelOutlineThicknessTexture: null, - labelOutlineThicknessTextureString: null, tris: null, framebuffer: null, copyShader: null, @@ -1923,7 +1867,6 @@ const DEFAULT_VALUES = { targetXYF: 1.0, zBufferTexture: null, lastZBufferTexture: null, - lightComplexity: 0, fullViewportTime: 1.0, idxToView: null, idxNormalMatrix: null, @@ -1931,10 +1874,6 @@ const DEFAULT_VALUES = { projectionToView: null, avgWindowArea: 0.0, avgFrameTime: 0.0, - // _scalars: null, - // _scalarOpacityFunc: null, - // _colorTransferFunc: null, - // _labelOutlineThicknessArray: null, }; // ---------------------------------------------------------------------------- @@ -1960,12 +1899,6 @@ export function extend(publicAPI, model, initialValues = {}) { model.jitterTexture.setWrapT(Wrap.REPEAT); model.framebuffer = vtkOpenGLFramebuffer.newInstance(); - model.idxToView = mat4.identity(new Float64Array(16)); - model.idxNormalMatrix = mat3.identity(new Float64Array(9)); - model.modelToView = mat4.identity(new Float64Array(16)); - model.projectionToView = mat4.identity(new Float64Array(16)); - model.projectionToWorld = mat4.identity(new Float64Array(16)); - // Build VTK API macro.setGet(publicAPI, model, ['context']); diff --git a/Sources/Rendering/OpenGL/VolumeMapper/test/testVolumeMapperShadowClip.js b/Sources/Rendering/OpenGL/VolumeMapper/test/testVolumeMapperShadowClip.js index 777aa4615f5..e44cef2647f 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/test/testVolumeMapperShadowClip.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/test/testVolumeMapperShadowClip.js @@ -69,9 +69,6 @@ test.onlyIfWebGL('Test Volume Mapper Shadow Clip', (t) => { im.getPointData().setScalars(farray); mapper.setInputData(im); - mapper.setComputeNormalFromOpacity(true); - mapper.setVolumetricScatteringBlending(1.0); - mapper.setGlobalIlluminationReach(1.0); const clipPlane = vtkPlane.newInstance(); clipPlane.setNormal(-0.5, -0.5, 1); @@ -85,6 +82,9 @@ test.onlyIfWebGL('Test Volume Mapper Shadow Clip', (t) => { const ofun = gc.registerResource(vtkPiecewiseFunction.newInstance()); ofun.addPoint(0, 0); ofun.addPoint(255, 1); + actor.getProperty().setComputeNormalFromOpacity(true); + actor.getProperty().setGlobalIlluminationReach(1.0); + actor.getProperty().setVolumetricScatteringBlending(1.0); actor.getProperty().setRGBTransferFunction(0, ctfun); actor.getProperty().setScalarOpacity(0, ofun); actor.getProperty().setAmbient(0.5); diff --git a/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl b/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl index 8779581ef49..d85cf6a08af 100644 --- a/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl +++ b/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl @@ -16,121 +16,198 @@ =========================================================================*/ // Template for the volume mappers fragment shader +const float infinity = 3.402823466e38; + // the output of this shader //VTK::Output::Dec -varying vec3 vertexVCVSOutput; - -// first declare the settings from the mapper -// that impact the code paths in here - -// always set vtkNumComponents 1,2,3,4 -//VTK::NumComponents - -// possibly define vtkTrilinearOn -//VTK::TrilinearOn +in vec3 vertexVCVSOutput; -// possibly define UseIndependentComponents -//VTK::IndependentComponentsOn +// From Sources\Rendering\Core\VolumeProperty\Constants.js +#define COMPOSITE_BLEND 0 +#define MAXIMUM_INTENSITY_BLEND 1 +#define MINIMUM_INTENSITY_BLEND 2 +#define AVERAGE_INTENSITY_BLEND 3 +#define ADDITIVE_INTENSITY_BLEND 4 +#define RADON_TRANSFORM_BLEND 5 +#define LABELMAP_EDGE_PROJECTION_BLEND 6 -// possibly define vtkCustomComponentsColorMix -//VTK::CustomComponentsColorMixOn - -// possibly define any "proportional" components -//VTK::vtkProportionalComponents +#define vtkNumberOfLights //VTK::NumberOfLights +#define vtkMaxLaoKernelSize //VTK::MaxLaoKernelSize +#define vtkNumberOfVolumes //VTK::NumberOfVolumes +#define vtkBlendMode //VTK::BlendMode -// possibly define any components that are forced to nearest interpolation -//VTK::vtkForceNearestComponents +//VTK::EnabledColorFunctions -// Define the blend mode to use -#define vtkBlendMode //VTK::BlendMode +//VTK::EnabledLightings -// Possibly define vtkImageLabelOutlineOn -//VTK::ImageLabelOutlineOn +//VTK::EnableForceNearestInterpolation -// Possibly define vtkLabelEdgeProjectionOn -//VTK::LabelEdgeProjectionOn +uniform int maximumNumberOfSamples; +uniform int twoSidedLighting; +#if vtkMaxLaoKernelSize > 0 +vec2 kernelSample[vtkMaxLaoKernelSize]; +#endif -#ifdef vtkImageLabelOutlineOn - uniform float outlineOpacity; - uniform float vpWidth; - uniform float vpHeight; - uniform float vpOffsetX; - uniform float vpOffsetY; - uniform mat4 PCWCMatrix; - uniform mat4 vWCtoIDX; +// Textures +uniform highp sampler3D volumeTexture[vtkNumberOfVolumes]; +uniform sampler2D colorTexture[vtkNumberOfVolumes]; +uniform sampler2D opacityTexture[vtkNumberOfVolumes]; - const int MAX_SEGMENT_INDEX = 256; // Define as per expected maximum - // bool seenSegmentsByOriginalPos[MAX_SEGMENT_INDEX]; - #define MAX_SEGMENTS 256 - #define UINT_SIZE 32 - #define BITMASK_SIZE ((MAX_SEGMENTS + UINT_SIZE - 1) / UINT_SIZE) +vec4 fetchVolumeTexture(ivec3 pos, int vIdx) { + // Texture arrays indices have to be constant, equivalent to: + // return texelFetch(volumeTexture[vIdx], pos, 0); + switch (vIdx) { + //VTK::fetchVolumeTexture + } +} - uint bitmask[BITMASK_SIZE]; +vec4 sampleVolumeTexture(vec3 pos, int vIdx) { + // Texture arrays indices have to be constant, equivalent to: + // return texture(volumeTexture[vIdx], pos); + switch (vIdx) { + //VTK::sampleVolumeTexture + } +} - // Set the corresponding bit in the bitmask - void setBit(int segmentIndex) { - int index = segmentIndex / UINT_SIZE; - int bitIndex = segmentIndex % UINT_SIZE; - bitmask[index] |= 1u << bitIndex; +vec4 sampleColorTexture(vec2 pos, int vIdx) { + // Texture arrays indices have to be constant, equivalent to: + // return texture2D(colorTexture[vIdx], pos); + switch (vIdx) { + //VTK::sampleColorTexture } +} - // Check if a bit is set in the bitmask - bool isBitSet(int segmentIndex) { - int index = segmentIndex / UINT_SIZE; - int bitIndex = segmentIndex % UINT_SIZE; - return ((bitmask[index] & (1u << bitIndex)) != 0u); +vec4 sampleOpacityTexture(vec2 pos, int vIdx) { + // Texture arrays indices have to be constant, equivalent to: + // return texture2D(opacityTexture[vIdx], pos); + switch (vIdx) { + //VTK::sampleOpacityTexture } -#endif +} -// define vtkLightComplexity -//VTK::LightComplexity -#if vtkLightComplexity > 0 -uniform float vSpecularPower; -uniform float vAmbient; -uniform float vDiffuse; -uniform float vSpecular; -//VTK::Light::Dec +struct Volume { + // ---- Volume geometry settings ---- + + vec3 originVC; // in VC + vec3 size; // in VC (spacing * dimensions) + vec3 inverseSize; // 1/size + vec3 spacing; // in VC per IC + vec3 inverseSpacing; // 1/spacing + ivec3 dimensions; // in IC + vec3 inverseDimensions; // 1/vec3(dimensions) + mat3 ISVCNormalMatrix; // pure rotation from VC to IS, transposed of + // VCISNormalMatrix + mat3 VCISNormalMatrix; // pure rotation from IS to VC, transposed of + // ISVCNormalMatrix + mat4 PCWCMatrix; + mat4 worldToIndex; + float diagonalLength; // in VC, this is: length(size) + + // ---- Main rendering settings ---- + + int useIndependentComponents; + int numberOfComponents; + int colorForValueFunctionId; + ivec4 isComponentNearestInterpolationForced; + ivec4 isComponentProportional; + + // ---- Texture settings ---- + + // Texture shift and scale + vec4 colorTextureScale; + vec4 colorTextureShift; + vec4 opacityTextureScale; + vec4 opacityTextureShift; + + // The heights defined below are the locations for the up to four components + // of the transfer functions. The transfer functions have a height of (2 * + // numberOfComponents) pixels so the values are computed to hit the middle of + // the two rows for that component + vec4 transferFunctionsSampleHeight; + + // ---- Mode specific settings ---- + + // Independent component default preset settings per component + vec4 independentComponentMix; + + // Additive / average blending mode settings + vec4 ipScalarRangeMin; + vec4 ipScalarRangeMax; + + // ---- Rendering settings ---- + + // Lighting + float ambient; + float diffuse; + float specular; + float specularPower; + int computeNormalFromOpacity; + + // Gradient opacity + int isGradientOpacityEnabled; + vec4 gradientOpacityScale; + vec4 gradientOpacityShift; + vec4 gradientOpacityMin; + vec4 gradientOpacityMax; + + // Volume shadow + float volumetricScatteringBlending; + float globalIlluminationReach; + float anisotropy; + float anisotropySquared; + + // LAO + int kernelSize; + int kernelRadius; + + // Label outline + float outlineOpacity; +}; +uniform Volume volumes[vtkNumberOfVolumes]; + +struct Light { + vec3 color; + vec3 positionVC; + vec3 directionVC; // normalized + vec3 halfAngleVC; + vec3 attenuation; + float exponent; + float coneAngle; + int isPositional; +}; +#if vtkNumberOfLights > 0 +uniform Light lights[vtkNumberOfLights]; #endif -//VTK::VolumeShadowOn -//VTK::SurfaceShadowOn -//VTK::localAmbientOcclusionOn -//VTK::LAO::Dec -//VTK::VolumeShadow::Dec - -// define vtkComputeNormalFromOpacity -//VTK::vtkComputeNormalFromOpacity - -// possibly define vtkGradientOpacityOn -//VTK::GradientOpacityOn -#ifdef vtkGradientOpacityOn -uniform float goscale0; -uniform float goshift0; -uniform float gomin0; -uniform float gomax0; -#ifdef UseIndependentComponents -#if vtkNumComponents > 1 -uniform float goscale1; -uniform float goshift1; -uniform float gomin1; -uniform float gomax1; -#if vtkNumComponents > 2 -uniform float goscale2; -uniform float goshift2; -uniform float gomin2; -uniform float gomax2; -#if vtkNumComponents > 3 -uniform float goscale3; -uniform float goshift3; -uniform float gomin3; -uniform float gomax3; -#endif -#endif -#endif -#endif -#endif +uniform float vpWidth; +uniform float vpHeight; +uniform float vpOffsetX; +uniform float vpOffsetY; + +// Bitmasks for label outline +const int MAX_SEGMENT_INDEX = 256; // Define as per expected maximum +#define MAX_SEGMENTS 256 +#define UINT_SIZE 32 +// We add UINT_SIZE - 1, as we want the ceil of the division instead of the +// floor +#define BITMASK_SIZE ((MAX_SEGMENTS + UINT_SIZE - 1) / UINT_SIZE) +uint labelOutlineBitmasks[BITMASK_SIZE]; + +// Set the corresponding bit in the bitmask +void setLabelOutlineBit(int segmentIndex) { + int arrayIndex = segmentIndex / UINT_SIZE; + int bitIndex = segmentIndex % UINT_SIZE; + labelOutlineBitmasks[arrayIndex] |= 1u << bitIndex; +} + +// Check if a bit is set in the bitmask +bool isLabelOutlineBitSet(int segmentIndex) { + int arrayIndex = segmentIndex / UINT_SIZE; + int bitIndex = segmentIndex % UINT_SIZE; + return ((labelOutlineBitmasks[arrayIndex] & (1u << bitIndex)) != 0u); +} // if you want to see the raw tiled // data in webgl1 uncomment the following line @@ -142,955 +219,680 @@ uniform float camNear; uniform float camFar; uniform int cameraParallel; -// values describing the volume geometry -uniform vec3 vOriginVC; -uniform vec3 vSpacing; -uniform ivec3 volumeDimensions; // 3d texture dimensions -uniform vec3 vPlaneNormal0; -uniform float vPlaneDistance0; -uniform vec3 vPlaneNormal1; -uniform float vPlaneDistance1; -uniform vec3 vPlaneNormal2; -uniform float vPlaneDistance2; -uniform vec3 vPlaneNormal3; -uniform float vPlaneDistance3; -uniform vec3 vPlaneNormal4; -uniform float vPlaneDistance4; -uniform vec3 vPlaneNormal5; -uniform float vPlaneDistance5; - //VTK::ClipPlane::Dec -// opacity and color textures -uniform sampler2D otexture; -uniform float oshift0; -uniform float oscale0; -uniform sampler2D ctexture; -uniform float cshift0; -uniform float cscale0; - -#if vtkNumComponents >= 2 -uniform float oshift1; -uniform float oscale1; -uniform float cshift1; -uniform float cscale1; -#endif -#if vtkNumComponents >= 3 -uniform float oshift2; -uniform float oscale2; -uniform float cshift2; -uniform float cscale2; -#endif -#if vtkNumComponents >= 4 -uniform float oshift3; -uniform float oscale3; -uniform float cshift3; -uniform float cscale3; -#endif - // jitter texture uniform sampler2D jtexture; -uniform sampler2D ttexture; +// label outline thickness texture +uniform sampler2D labelOutlineThicknessTexture; -// some 3D texture values -uniform float sampleDistance; -uniform vec3 vVCToIJK; -uniform vec3 volumeSpacings; // spacing in the world coorindates - - -// the heights defined below are the locations -// for the up to four components of the tfuns -// the tfuns have a height of 2XnumComps pixels so the -// values are computed to hit the middle of the two rows -// for that component -#ifdef UseIndependentComponents -#if vtkNumComponents == 1 -uniform float mix0; -#define height0 0.5 -#endif -#if vtkNumComponents == 2 -uniform float mix0; -uniform float mix1; -#define height0 0.25 -#define height1 0.75 -#endif -#if vtkNumComponents == 3 -uniform float mix0; -uniform float mix1; -uniform float mix2; -#define height0 0.17 -#define height1 0.5 -#define height2 0.83 -#endif -#if vtkNumComponents == 4 -uniform float mix0; -uniform float mix1; -uniform float mix2; -uniform float mix3; -#define height0 0.125 -#define height1 0.375 -#define height2 0.625 -#define height3 0.875 -#endif -#endif +// A random number between 0 and 1 that only depends on the fragment +// It uses the jtexture, so this random seed repeats by blocks of 32 fragments +// in screen space +float fragmentSeed; -uniform vec4 ipScalarRangeMin; -uniform vec4 ipScalarRangeMax; +// sample texture is global +uniform float sampleDistance; +uniform float volumeShadowSampleDistance; // declaration for intermixed geometry //VTK::ZBuffer::Dec //======================================================================= -// global and custom variables (a temporary section before photorealistics rendering module is complete) +// global and custom variables (a temporary section before photorealistics +// rendering module is complete) vec3 rayDirVC; -float sampleDistanceISVS; -float sampleDistanceIS; -#define SQRT3 1.7321 -#define INV4PI 0.0796 -#define EPSILON 0.001 -#define PI 3.1415 -#define PI2 9.8696 +#define INV4PI 0.0796 +#define EPSILON 0.001 +#define PI 3.1415 +#define PI2 9.8696 + +vec4 getTextureValue(vec3 pos, int vIdx) { + vec4 tmp = sampleVolumeTexture(pos, vIdx); + +#ifdef EnableForceNearestInterpolation + if (!all(equal(volumes[vIdx].isComponentNearestInterpolationForced, + ivec4(0)))) { + vec3 nearestPos = (floor(pos * vec3(volumes[vIdx].dimensions)) + 0.5) * + volumes[vIdx].inverseDimensions; + vec4 nearestValue = sampleVolumeTexture(nearestPos, vIdx); + vec4 forceNearestMask = + vec4(volumes[vIdx].isComponentNearestInterpolationForced); + tmp = tmp * (1.0 - forceNearestMask) + nearestValue * forceNearestMask; + } +#endif -//======================================================================= -// Webgl2 specific version of functions -#if __VERSION__ == 300 - -uniform highp sampler3D texture1; - -vec4 getTextureValue(vec3 pos) -{ - vec4 tmp = texture(texture1, pos); - - #if defined(vtkComponent0ForceNearest) || \ - defined(vtkComponent1ForceNearest) || \ - defined(vtkComponent2ForceNearest) || \ - defined(vtkComponent3ForceNearest) - vec3 nearestPos = (floor(pos * vec3(volumeDimensions)) + 0.5) / vec3(volumeDimensions); - vec4 nearestValue = texture(texture1, nearestPos); - #ifdef vtkComponent0ForceNearest - tmp[0] = nearestValue[0]; - #endif - #ifdef vtkComponent1ForceNearest - tmp[1] = nearestValue[1]; - #endif - #ifdef vtkComponent2ForceNearest - tmp[2] = nearestValue[2]; - #endif - #ifdef vtkComponent3ForceNearest - tmp[3] = nearestValue[3]; - #endif - #endif - - #ifndef UseIndependentComponents - #if vtkNumComponents == 1 + if (volumes[vIdx].useIndependentComponents == 0) { + int nComps = volumes[vIdx].numberOfComponents; + if (nComps == 1) { tmp.a = tmp.r; - #endif - #if vtkNumComponents == 2 + } else if (nComps == 2) { tmp.a = tmp.g; - #endif - #if vtkNumComponents == 3 + } else if (nComps == 3) { tmp.a = length(tmp.rgb); - #endif - #endif + } + } return tmp; } -//======================================================================= -// WebGL1 specific version of functions -#else - -uniform sampler2D texture1; - -uniform float texWidth; -uniform float texHeight; -uniform int xreps; -uniform int xstride; -uniform int ystride; - -// if computing trilinear values from multiple z slices -#ifdef vtkTrilinearOn -vec4 getTextureValue(vec3 ijk) -{ - float zoff = 1.0/float(volumeDimensions.z); - vec4 val1 = getOneTextureValue(ijk); - vec4 val2 = getOneTextureValue(vec3(ijk.xy, ijk.z + zoff)); - - float indexZ = float(volumeDimensions)*ijk.z; - float zmix = indexZ - floor(indexZ); - - return mix(val1, val2, zmix); +// `height` is usually `volumes[vIdx].transferFunctionsSampleHeight[component]` +// when using independent component and `0.5` otherwise. Don't move the if +// statement in these function, as the callers usually already knows if it is +// using independent component or not +float getOpacityFromTexture(float scalar, int vIdx, int component, + float height) { + float scaledScalar = scalar * volumes[vIdx].opacityTextureScale[component] + + volumes[vIdx].opacityTextureShift[component]; + return sampleOpacityTexture(vec2(scaledScalar, height), vIdx).r; } - -vec4 getOneTextureValue(vec3 ijk) -#else // nearest or fast linear -vec4 getTextureValue(vec3 ijk) -#endif -{ - vec3 tdims = vec3(volumeDimensions); - -#ifdef debugtile - vec2 tpos = vec2(ijk.x, ijk.y); - vec4 tmp = texture2D(texture1, tpos); - tmp.a = 1.0; - -#else - int z = int(ijk.z * tdims.z); - int yz = z / xreps; - int xz = z - yz*xreps; - - int tileWidth = volumeDimensions.x/xstride; - int tileHeight = volumeDimensions.y/ystride; - - xz *= tileWidth; - yz *= tileHeight; - - float ni = float(xz) + (ijk.x*float(tileWidth)); - float nj = float(yz) + (ijk.y*float(tileHeight)); - - vec2 tpos = vec2(ni/texWidth, nj/texHeight); - - vec4 tmp = texture2D(texture1, tpos); - -#if vtkNumComponents == 1 - tmp.a = tmp.r; -#endif -#if vtkNumComponents == 2 - tmp.g = tmp.a; -#endif -#if vtkNumComponents == 3 - tmp.a = length(tmp.rgb); -#endif -#endif - - return tmp; +vec3 getColorFromTexture(float scalar, int vIdx, int component, float height) { + float scaledScalar = scalar * volumes[vIdx].colorTextureScale[component] + + volumes[vIdx].colorTextureShift[component]; + return sampleColorTexture(vec2(scaledScalar, height), vIdx).rgb; } -// End of Webgl1 specific code -//======================================================================= -#endif - //======================================================================= // transformation between VC and IS space // convert vector position from idx to vc -#if (vtkLightComplexity > 0) || (defined vtkClippingPlanesOn) -vec3 IStoVC(vec3 posIS){ - vec3 posVC = posIS / vVCToIJK; - return posVC.x * vPlaneNormal0 + - posVC.y * vPlaneNormal2 + - posVC.z * vPlaneNormal4 + - vOriginVC; +vec3 IStoVC(vec3 posIS, int vIdx) { + return volumes[vIdx].ISVCNormalMatrix * (posIS * volumes[vIdx].size) + + volumes[vIdx].originVC; } // convert vector position from vc to idx -vec3 VCtoIS(vec3 posVC){ - posVC = posVC - vOriginVC; - posVC = vec3( - dot(posVC, vPlaneNormal0), - dot(posVC, vPlaneNormal2), - dot(posVC, vPlaneNormal4)); - return posVC * vVCToIJK; +vec3 VCtoIS(vec3 posVC, int vIdx) { + return (volumes[vIdx].VCISNormalMatrix * (posVC - volumes[vIdx].originVC)) * + volumes[vIdx].inverseSize; } -#endif -//Rotate vector to view coordinate -#if (vtkLightComplexity > 0) || (defined vtkGradientOpacityOn) -void rotateToViewCoord(inout vec3 dirIS){ - dirIS.xyz = - dirIS.x * vPlaneNormal0 + - dirIS.y * vPlaneNormal2 + - dirIS.z * vPlaneNormal4; +// Rotate vector to view coordinate +vec3 rotateToVC(vec3 dirIS, int vIdx) { + return volumes[vIdx].ISVCNormalMatrix * dirIS; } -//Rotate vector to idx coordinate -vec3 rotateToIDX(vec3 dirVC){ - vec3 dirIS; - dirIS.xyz = vec3( - dot(dirVC, vPlaneNormal0), - dot(dirVC, vPlaneNormal2), - dot(dirVC, vPlaneNormal4)); - return dirIS; +// Rotate vector to idx coordinate +vec3 rotateToIS(vec3 dirVC, int vIdx) { + return volumes[vIdx].VCISNormalMatrix * dirVC; } -#endif //======================================================================= // Given a normal compute the gradient opacity factors -float computeGradientOpacityFactor( - float normalMag, float goscale, float goshift, float gomin, float gomax) -{ +float computeGradientOpacityFactor(float normalMag, int vIdx, int component) { + float goscale = volumes[vIdx].gradientOpacityScale[component]; + float goshift = volumes[vIdx].gradientOpacityShift[component]; + float gomin = volumes[vIdx].gradientOpacityMin[component]; + float gomax = volumes[vIdx].gradientOpacityMax[component]; return clamp(normalMag * goscale + goshift, gomin, gomax); } -//======================================================================= -// compute the normal and gradient magnitude for a position, uses forward difference -#if (vtkLightComplexity > 0) || (defined vtkGradientOpacityOn) - #ifdef vtkClippingPlanesOn - void adjustClippedVoxelValues(vec3 pos, vec3 texPos[3], inout vec3 g1) - { - vec3 g1VC[3]; - for (int i = 0; i < 3; ++i) - { - g1VC[i] = IStoVC(texPos[i]); - } - vec3 posVC = IStoVC(pos); - for (int i = 0; i < clip_numPlanes; ++i) - { - for (int j = 0; j < 3; ++j) - { - if(dot(vec3(vClipPlaneOrigins[i] - g1VC[j].xyz), vClipPlaneNormals[i]) > 0.0) - { - g1[j] = 0.0; - } - } - } - } - #endif - - #ifdef vtkComputeNormalFromOpacity - vec4 computeDensityNormal(vec3 opacityUCoords[2], float opactityTextureHeight, float gradientOpacity) { - vec3 opacityG1, opacityG2; - opacityG1.x = texture2D(otexture, vec2(opacityUCoords[0].x, opactityTextureHeight)).r; - opacityG1.y = texture2D(otexture, vec2(opacityUCoords[0].y, opactityTextureHeight)).r; - opacityG1.z = texture2D(otexture, vec2(opacityUCoords[0].z, opactityTextureHeight)).r; - opacityG2.x = texture2D(otexture, vec2(opacityUCoords[1].x, opactityTextureHeight)).r; - opacityG2.y = texture2D(otexture, vec2(opacityUCoords[1].y, opactityTextureHeight)).r; - opacityG2.z = texture2D(otexture, vec2(opacityUCoords[1].z, opactityTextureHeight)).r; - opacityG1.xyz *= gradientOpacity; - opacityG2.xyz *= gradientOpacity; - - vec4 opacityG = vec4(opacityG1 - opacityG2, 1.0f); - // divide by spacing - opacityG.xyz /= vSpacing; - opacityG.w = length(opacityG.xyz); - // rotate to View Coords - rotateToViewCoord(opacityG.xyz); - if (!all(equal(opacityG.xyz, vec3(0.0)))) { - return vec4(normalize(opacityG.xyz),opacityG.w); - } else { - return vec4(0.0); - } +#ifdef vtkClippingPlanesOn +bool isPointClipped(vec3 posVC) { + for (int i = 0; i < clip_numPlanes; ++i) { + if (dot(vec3(vClipPlaneOrigins[i] - posVC), vClipPlaneNormals[i]) > 0.0) { + return true; } + } + return false; +} +#endif - vec4 computeNormalForDensity(vec3 pos, vec3 tstep, out vec3 scalarInterp[2], const int opacityComponent) - { - vec3 xvec = vec3(tstep.x, 0.0, 0.0); - vec3 yvec = vec3(0.0, tstep.y, 0.0); - vec3 zvec = vec3(0.0, 0.0, tstep.z); - vec3 texPosPVec[3]; - texPosPVec[0] = pos + xvec; - texPosPVec[1] = pos + yvec; - texPosPVec[2] = pos + zvec; - vec3 texPosNVec[3]; - texPosNVec[0] = pos - xvec; - texPosNVec[1] = pos - yvec; - texPosNVec[2] = pos - zvec; - vec3 g1, g2; - - scalarInterp[0].x = getTextureValue(texPosPVec[0])[opacityComponent]; - scalarInterp[0].y = getTextureValue(texPosPVec[1])[opacityComponent]; - scalarInterp[0].z = getTextureValue(texPosPVec[2])[opacityComponent]; - scalarInterp[1].x = getTextureValue(texPosNVec[0])[opacityComponent]; - scalarInterp[1].y = getTextureValue(texPosNVec[1])[opacityComponent]; - scalarInterp[1].z = getTextureValue(texPosNVec[2])[opacityComponent]; - - #ifdef vtkClippingPlanesOn - adjustClippedVoxelValues(pos, texPosPVec, scalarInterp[0]); - adjustClippedVoxelValues(pos, texPosNVec, scalarInterp[1]); - #endif - vec4 result; - result.x = scalarInterp[0].x - scalarInterp[1].x; - result.y = scalarInterp[0].y - scalarInterp[1].y; - result.z = scalarInterp[0].z - scalarInterp[1].z; - // divide by spacing - result.xyz /= vSpacing; - result.w = length(result.xyz); - // rotate to View Coords - rotateToViewCoord(result.xyz); - if (length(result.xyz) > 0.0) { - return vec4(normalize(result.xyz),result.w); - } else { - return vec4(0.0); - } - } - #endif +//======================================================================= +// compute the normal and gradient magnitude for a position, uses forward +// difference + +// The output normal is in VC +vec4 computeDensityNormal(vec3 opacityUCoords[2], float opacityTextureHeight, + float gradientOpacity, int component, int vIdx) { + // Pass the scalars through the opacity functions + vec4 opacityG; + opacityG.x += getOpacityFromTexture(opacityUCoords[0].x, vIdx, component, + opacityTextureHeight); + opacityG.y += getOpacityFromTexture(opacityUCoords[0].y, vIdx, component, + opacityTextureHeight); + opacityG.z += getOpacityFromTexture(opacityUCoords[0].z, vIdx, component, + opacityTextureHeight); + opacityG.x -= getOpacityFromTexture(opacityUCoords[1].x, vIdx, component, + opacityTextureHeight); + opacityG.y -= getOpacityFromTexture(opacityUCoords[1].y, vIdx, component, + opacityTextureHeight); + opacityG.z -= getOpacityFromTexture(opacityUCoords[1].z, vIdx, component, + opacityTextureHeight); + + // Divide by spacing + opacityG.xyz *= gradientOpacity * volumes[vIdx].inverseSpacing; + + // Get length + opacityG.w = length(opacityG.xyz); + if (opacityG.w == 0.0) { + return vec4(0.0); + } - // only works with dependent components - vec4 computeNormal(vec3 pos, vec3 tstep) - { - vec3 xvec = vec3(tstep.x, 0.0, 0.0); - vec3 yvec = vec3(0.0, tstep.y, 0.0); - vec3 zvec = vec3(0.0, 0.0, tstep.z); - vec3 texPosPVec[3]; - texPosPVec[0] = pos + xvec; - texPosPVec[1] = pos + yvec; - texPosPVec[2] = pos + zvec; - vec3 texPosNVec[3]; - texPosNVec[0] = pos - xvec; - texPosNVec[1] = pos - yvec; - texPosNVec[2] = pos - zvec; - vec3 g1, g2; - g1.x = getTextureValue(texPosPVec[0]).a; - g1.y = getTextureValue(texPosPVec[1]).a; - g1.z = getTextureValue(texPosPVec[2]).a; - g2.x = getTextureValue(texPosNVec[0]).a; - g2.y = getTextureValue(texPosNVec[1]).a; - g2.z = getTextureValue(texPosNVec[2]).a; - #ifdef vtkClippingPlanesOn - adjustClippedVoxelValues(pos, texPosPVec, g1); - adjustClippedVoxelValues(pos, texPosNVec, g2); - #endif - vec4 result; - result = vec4(g1 - g2, -1.0); - // divide by spacing - result.xyz /= vSpacing; - result.w = length(result.xyz); - if (result.w > 0.0){ - // rotate to View Coords - rotateToViewCoord(result.xyz); - return vec4(normalize(result.xyz),result.w); - } else { - return vec4(0.0); + // Normalize and rotate to VC + opacityG.xyz = rotateToVC(opacityG.xyz / opacityG.w, vIdx); + return opacityG; +} + +// The output normal is in VC +vec4 computeNormalForDensity(vec3 posIS, out vec3 scalarInterp[2], + const int opacityComponent, int vIdx) { + vec3 offsetedPosIS; + for (int axis = 0; axis < 3; ++axis) { + // Positive direction + offsetedPosIS = posIS; + offsetedPosIS[axis] += volumes[vIdx].inverseDimensions[axis]; + scalarInterp[0][axis] = + getTextureValue(offsetedPosIS, vIdx)[opacityComponent]; +#ifdef vtkClippingPlanesOn + if (isPointClipped(IStoVC(offsetedPosIS, vIdx))) { + scalarInterp[0][axis] = 0.0; } - } #endif - -#ifdef vtkImageLabelOutlineOn - vec4 fragCoordToPCPos(vec4 fragCoord) { - return vec4( - (fragCoord.x / vpWidth - vpOffsetX - 0.5) * 2.0, - (fragCoord.y / vpHeight - vpOffsetY - 0.5) * 2.0, - (fragCoord.z - 0.5) * 2.0, - 1.0); + // Negative direction + offsetedPosIS = posIS; + offsetedPosIS[axis] -= volumes[vIdx].inverseDimensions[axis]; + scalarInterp[1][axis] = + getTextureValue(offsetedPosIS, vIdx)[opacityComponent]; +#ifdef vtkClippingPlanesOn + if (isPointClipped(IStoVC(offsetedPosIS, vIdx))) { + scalarInterp[1][axis] = 0.0; + } +#endif } - vec4 pcPosToWorldCoord(vec4 pcPos) { - return PCWCMatrix * pcPos; + vec4 result; + result.xyz = + (scalarInterp[0] - scalarInterp[1]) * volumes[vIdx].inverseSpacing; + result.w = length(result.xyz); + if (result.w == 0.0) { + return vec4(0.0); } + result.xyz = rotateToVC(result.xyz, vIdx); + return vec4(result.xyz / result.w, result.w); +} - vec3 fragCoordToIndexSpace(vec4 fragCoord) { - vec4 pcPos = fragCoordToPCPos(fragCoord); - vec4 worldCoord = pcPosToWorldCoord(pcPos); - vec4 vertex = (worldCoord / worldCoord.w); +vec4 fragCoordToPCPos(vec4 fragCoord) { + return vec4((fragCoord.x / vpWidth - vpOffsetX - 0.5) * 2.0, + (fragCoord.y / vpHeight - vpOffsetY - 0.5) * 2.0, + (fragCoord.z - 0.5) * 2.0, 1.0); +} - vec3 index = (vWCtoIDX * vertex).xyz; +vec4 pcPosToWorldCoord(vec4 pcPos, int vIdx) { + return volumes[vIdx].PCWCMatrix * pcPos; +} - // half voxel fix for labelmapOutline - return (index + vec3(0.5)) / vec3(volumeDimensions); - } +vec3 fragCoordToIndexSpace(vec4 fragCoord, int vIdx) { + vec4 pcPos = fragCoordToPCPos(fragCoord); + vec4 worldCoord = pcPosToWorldCoord(pcPos, vIdx); + vec4 vertex = (worldCoord / worldCoord.w); - vec3 fragCoordToWorld(vec4 fragCoord) { - vec4 pcPos = fragCoordToPCPos(fragCoord); - vec4 worldCoord = pcPosToWorldCoord(pcPos); - return worldCoord.xyz; - } -#endif + vec3 index = (volumes[vIdx].worldToIndex * vertex).xyz; + + // half voxel fix for labelmapOutline + return (index + vec3(0.5)) * volumes[vIdx].inverseDimensions; +} + +vec3 fragCoordToWorld(vec4 fragCoord, int vIdx) { + vec4 pcPos = fragCoordToPCPos(fragCoord); + vec4 worldCoord = pcPosToWorldCoord(pcPos, vIdx); + return worldCoord.xyz; +} //======================================================================= -// compute the normals and gradient magnitudes for a position -// for independent components -mat4 computeMat4Normal(vec3 pos, vec4 tValue, vec3 tstep) -{ - mat4 result; - vec4 distX = getTextureValue(pos + vec3(tstep.x, 0.0, 0.0)) - tValue; - vec4 distY = getTextureValue(pos + vec3(0.0, tstep.y, 0.0)) - tValue; - vec4 distZ = getTextureValue(pos + vec3(0.0, 0.0, tstep.z)) - tValue; +// Compute the normals and gradient magnitudes for a position for independent +// components The output normals are in VC +mat4 computeMat4Normal(vec3 posIS, vec4 tValue, int vIdx) { + vec3 xvec = vec3(volumes[vIdx].inverseDimensions.x, 0.0, 0.0); + vec3 yvec = vec3(0.0, volumes[vIdx].inverseDimensions.y, 0.0); + vec3 zvec = vec3(0.0, 0.0, volumes[vIdx].inverseDimensions.z); - // divide by spacing - distX /= vSpacing.x; - distY /= vSpacing.y; - distZ /= vSpacing.z; - - mat3 rot; - rot[0] = vPlaneNormal0; - rot[1] = vPlaneNormal2; - rot[2] = vPlaneNormal4; - -#if !defined(vtkComponent0Proportional) - result[0].xyz = vec3(distX.r, distY.r, distZ.r); - result[0].a = length(result[0].xyz); - result[0].xyz *= rot; - if (result[0].w > 0.0) - { - result[0].xyz /= result[0].w; - } -#endif + vec4 distX = getTextureValue(posIS + xvec, vIdx) - tValue; + vec4 distY = getTextureValue(posIS + yvec, vIdx) - tValue; + vec4 distZ = getTextureValue(posIS + zvec, vIdx) - tValue; -// optionally compute the 2nd component -#if vtkNumComponents >= 2 && !defined(vtkComponent1Proportional) - result[1].xyz = vec3(distX.g, distY.g, distZ.g); - result[1].a = length(result[1].xyz); - result[1].xyz *= rot; - if (result[1].w > 0.0) - { - result[1].xyz /= result[1].w; - } -#endif + // divide by spacing + distX *= volumes[vIdx].inverseSpacing.x; + distY *= volumes[vIdx].inverseSpacing.y; + distZ *= volumes[vIdx].inverseSpacing.z; -// optionally compute the 3rd component -#if vtkNumComponents >= 3 && !defined(vtkComponent2Proportional) - result[2].xyz = vec3(distX.b, distY.b, distZ.b); - result[2].a = length(result[2].xyz); - result[2].xyz *= rot; - if (result[2].w > 0.0) - { - result[2].xyz /= result[2].w; - } -#endif + mat4 result; -// optionally compute the 4th component -#if vtkNumComponents >= 4 && !defined(vtkComponent3Proportional) - result[3].xyz = vec3(distX.a, distY.a, distZ.a); - result[3].a = length(result[3].xyz); - result[3].xyz *= rot; - if (result[3].w > 0.0) - { - result[3].xyz /= result[3].w; + for (int component = 0; component < volumes[vIdx].numberOfComponents; + ++component) { + if (volumes[vIdx].isComponentProportional[component] == 0) { + result[component].xyz = + vec3(distX[component], distY[component], distZ[component]); + result[component].a = length(result[component].xyz); + result[component].xyz = rotateToVC(result[component].xyz, vIdx); + if (result[component].w > 0.0) { + result[component].xyz /= result[component].w; + } + } } -#endif return result; } //======================================================================= // global shadow - secondary ray -#if defined(VolumeShadowOn) || defined(localAmbientOcclusionOn) -float random() -{ - float rand = fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453123); - float jitter=texture2D(jtexture,gl_FragCoord.xy/32.).r; - uint pcg_state = floatBitsToUint(jitter); - uint state = pcg_state; - pcg_state = pcg_state * uint(747796405) + uint(2891336453); - uint word = ((state >> ((state >> uint(28)) + uint(4))) ^ state) * uint(277803737); - return (float((((word >> uint(22)) ^ word) >> 1 ))/float(2147483647) + rand)/2.0; -} -#endif -#ifdef VolumeShadowOn // henyey greenstein phase function -float phase_function(float cos_angle) -{ +float phaseFunction(float cos_angle, int vIdx) { // divide by 2.0 instead of 4pi to increase intensity - return ((1.0-anisotropy2)/pow(1.0+anisotropy2-2.0*anisotropy*cos_angle, 1.5))/2.0; -} - -// Computes the intersection between a ray and a box -struct Hit -{ - float tmin; - float tmax; -}; - -struct Ray -{ - vec3 origin; - vec3 dir; - vec3 invDir; -}; - -bool BBoxIntersect(vec3 boundMin, vec3 boundMax, const Ray r, out Hit hit) -{ - vec3 tbot = r.invDir * (boundMin - r.origin); - vec3 ttop = r.invDir * (boundMax - r.origin); - vec3 tmin = min(ttop, tbot); - vec3 tmax = max(ttop, tbot); - vec2 t = max(tmin.xx, tmin.yz); - float t0 = max(t.x, t.y); - t = min(tmax.xx, tmax.yz); - float t1 = min(t.x, t.y); - hit.tmin = t0; - hit.tmax = t1; - return t1 > max(t0,0.0); + float anisotropy = volumes[vIdx].anisotropy; + if (abs(anisotropy) <= EPSILON) { + // isotropic scatter returns 0.5 instead of 1/4pi to increase intensity + return 0.5; + } + float anisotropy2 = volumes[vIdx].anisotropySquared; + return ((1.0 - anisotropy2) / + pow(1.0 + anisotropy2 - 2.0 * anisotropy * cos_angle, 1.5)) / + 2.0; } -// As BBoxIntersect requires the inverse of the ray coords, +// As rayIntersectVolumeDistances requires the inverse of the ray coords, // this function is used to avoid numerical issues -void safe_0_vector(inout Ray ray) -{ - if(abs(ray.dir.x) < EPSILON) ray.dir.x = sign(ray.dir.x) * EPSILON; - if(abs(ray.dir.y) < EPSILON) ray.dir.y = sign(ray.dir.y) * EPSILON; - if(abs(ray.dir.z) < EPSILON) ray.dir.z = sign(ray.dir.z) * EPSILON; +void safe_0_vector(inout vec3 dir) { + if (abs(dir.x) < EPSILON) + dir.x = sign(dir.x) * EPSILON; + if (abs(dir.y) < EPSILON) + dir.y = sign(dir.y) * EPSILON; + if (abs(dir.z) < EPSILON) + dir.z = sign(dir.z) * EPSILON; } -float volume_shadow(vec3 posIS, vec3 lightDirNormIS) -{ - float shadow = 1.0; - float opacity = 0.0; +// Compute the two intersection distances of the ray with the volume in VC +// The entry point is `rayOriginVC + distanceMin * rayDirVC` and the exit point +// is `rayOriginVC + distanceMax * rayDirVC` If distanceMin < distanceMax, the +// volume is not intersected The ray origin is inside the box when distanceMin < +// 0.0 < distanceMax +vec2 rayIntersectVolumeDistances(vec3 rayOriginVC, vec3 rayDirVC, int vIdx) { + // Compute origin and direction in IS + vec3 rayOriginIS = VCtoIS(rayOriginVC, vIdx); + vec3 rayDirIS = rotateToIS(rayDirVC, vIdx); + safe_0_vector(rayDirIS); + // Scale the inverse direction using the size, because we want the distances + // in VC instead of IS + vec3 invDir = volumes[vIdx].size / rayDirIS; + + // We have: bound = origin + t * dir + // So: t = (1/dir) * (bound - origin) + vec3 distancesTo0 = invDir * (vec3(0.0) - rayOriginIS); + vec3 distancesTo1 = invDir * (vec3(1.0) - rayOriginIS); + // Min and max distances to plane intersection per plane + vec3 dMin = min(distancesTo0, distancesTo1); + vec3 dMax = max(distancesTo0, distancesTo1); + // Overall first and last intersection + float distanceMin = max(dMin.x, max(dMin.y, dMin.z)); + float distanceMax = min(dMax.x, min(dMax.y, dMax.z)); + return vec2(distanceMin, distanceMax); +} +float computeVolumeShadowWithoutCache(vec3 posVC, vec3 lightDirNormVC) { // modify sample distance with a random number between 1.5 and 3.0 - float sampleDistanceISVS_jitter = sampleDistanceISVS * mix(1.5, 3.0, random()); - float opacityPrev = texture2D(otexture, vec2(getTextureValue(posIS).r * oscale0 + oshift0, 0.5)).r; + float rayStepLength = + volumeShadowSampleDistance * mix(1.5, 3.0, fragmentSeed); - // in case the first sample near surface has a very tiled light ray, we need to offset start position - posIS += sampleDistanceISVS_jitter * lightDirNormIS; + // in case the first sample near surface has a very tiled light ray, we need + // to offset start position + vec3 initialPosVC = posVC + rayStepLength * lightDirNormVC; - // compute the start and end points for the ray - Ray ray; - Hit hit; - ray.origin = posIS; - ray.dir = lightDirNormIS; - safe_0_vector(ray); - ray.invDir = 1.0/ray.dir; - - if(!BBoxIntersect(vec3(0.0),vec3(1.0), ray, hit)) - { - return 1.0; - } - float maxdist = hit.tmax; - - // interpolate shadow ray length between: 1 unit of sample distance in IS to SQRT3, based on globalIlluminationReach - float maxgi = mix(sampleDistanceISVS_jitter,SQRT3,giReach); - maxdist = min(maxdist,maxgi); - if(maxdist < EPSILON) { - return 1.0; +#ifdef vtkClippingPlanesOn + float clippingPlanesMaxDistance = infinity; + for (int i = 0; i < clip_numPlanes; ++i) { + // Find distance of intersection with the plane + // Points are clipped when: + // dot(planeOrigin - (rayOrigin + distance * rayDirection), planeNormal) > 0 + // This is equivalent to: + // dot(planeOrigin - rayOrigin, planeNormal) - distance * dot(rayDirection, + // planeNormal) > 0.0 + // We precompute the dot products, so we clip ray points when: + // dotOrigin - distance * dotDirection > 0.0 + float dotOrigin = + dot(vClipPlaneOrigins[i] - initialPosVC, vClipPlaneNormals[i]); + if (dotOrigin > 0.0) { + // The initialPosVC is clipped by this plane + return 1.0; + } + float dotDirection = dot(lightDirNormVC, vClipPlaneNormals[i]); + if (dotDirection < 0.0) { + // We only hit the plane if dotDirection is negative, as (distance is + // positive) + float intersectionDistance = + dotOrigin / dotDirection; // negative divided by negative => positive + clippingPlanesMaxDistance = + min(clippingPlanesMaxDistance, intersectionDistance); + } } +#endif - float current_dist = 0.0; - float current_step = length(sampleDistanceISVS_jitter * lightDirNormIS); - float clamped_step = 0.0; + float shadow = 1.0; + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec2 intersectionDistances = + rayIntersectVolumeDistances(initialPosVC, lightDirNormVC, vIdx); + + if (intersectionDistances[1] <= intersectionDistances[0] || + intersectionDistances[1] <= 0.0) { + // Volume not hit or behind the ray + continue; + } - vec4 scalar = vec4(0.0); - while(current_dist < maxdist) - { + // When globalIlluminationReach is 0, no sample at all + // When globalIlluminationReach is 1, the ray will go through the whole + // volume + float maxTravelDistance = mix(0.0, volumes[vIdx].diagonalLength, + volumes[vIdx].globalIlluminationReach); + float startDistance = max(intersectionDistances[0], 0.0); + float endDistance = + min(intersectionDistances[1], startDistance + maxTravelDistance); #ifdef vtkClippingPlanesOn - vec3 posVC = IStoVC(posIS); - for (int i = 0; i < clip_numPlanes; ++i) - { - if (dot(vec3(vClipPlaneOrigins[i] - posVC), vClipPlaneNormals[i]) > 0.0) - { - current_dist = maxdist; - } - } + endDistance = min(endDistance, clippingPlanesMaxDistance); #endif - scalar = getTextureValue(posIS); - opacity = texture2D(otexture, vec2(scalar.r * oscale0 + oshift0, 0.5)).r; - #if defined(vtkGradientOpacityOn) && !defined(UseIndependentComponents) - vec4 normal = computeNormal(posIS, vec3(1.0/vec3(volumeDimensions))); - opacity *= computeGradientOpacityFactor(normal.w, goscale0, goshift0, gomin0, gomax0); - #endif - shadow *= 1.0 - opacity; - - // optimization: early termination - if (shadow < EPSILON){ - return 0.0; + if (endDistance - startDistance < EPSILON) { + continue; } - clamped_step = min(maxdist - current_dist, current_step); - posIS += clamped_step * lightDirNormIS; - current_dist += current_step; - } + // These two variables are used to compute posIS, without having to call + // VCtoIS at each step + vec3 initialPosIS = VCtoIS(initialPosVC, vIdx); + // The light dir is scaled and rotated, but not translated, as it is a + // vector (w = 0) + vec3 scaledLightDirIS = + rotateToIS(lightDirNormVC, vIdx) * volumes[vIdx].inverseSize; + + bool useGradientOpacity = volumes[vIdx].isGradientOpacityEnabled == 1 && + volumes[vIdx].useIndependentComponents == 0; + + for (float currentDistance = startDistance; currentDistance < endDistance; + currentDistance += rayStepLength) { + vec3 posIS = initialPosIS + currentDistance * scaledLightDirIS; + vec4 scalar = getTextureValue(posIS, vIdx); + float opacity = getOpacityFromTexture(scalar.r, vIdx, 0, 0.5); + if (useGradientOpacity) { + vec3 scalarInterp[2]; + vec4 normal = computeNormalForDensity(posIS, scalarInterp, 3, vIdx); + float opacityFactor = computeGradientOpacityFactor(normal.w, vIdx, 0); + opacity *= opacityFactor; + } + shadow *= 1.0 - opacity; + // Early termination if shadow coeff is near 0.0 + if (shadow < EPSILON) { + return 0.0; + } + } + } return shadow; } -vec3 applyShadowRay(vec3 tColor, vec3 posIS, vec3 viewDirectionVC) -{ - vec3 vertLight = vec3(0.0); - vec3 secondary_contrib = vec3(0.0); - // here we assume only positional light, no effect of cones - for (int i = 0; i < lightNum; i++) - { - #if(vtkLightComplexity==3) - if (lightPositional[i] == 1){ - vertLight = lightPositionVC[i] - IStoVC(posIS); - }else{ - vertLight = - lightDirectionVC[i]; - } - #else - vertLight = - lightDirectionVC[i]; - #endif - // here we assume achromatic light, only intensity - float dDotL = dot(viewDirectionVC, normalize(vertLight)); - // isotropic scatter returns 0.5 instead of 1/4pi to increase intensity - float phase_attenuation = 0.5; - if (abs(anisotropy) > EPSILON){ - phase_attenuation = phase_function(dDotL); - } - float vol_shadow = volume_shadow(posIS, normalize(rotateToIDX(vertLight))); - secondary_contrib += tColor * vDiffuse * lightColor[i] * vol_shadow * phase_attenuation; - secondary_contrib += tColor * vAmbient; +// Some cache for volume shadows +struct { + vec3 posVC; + float shadow; +} cachedShadows[vtkNumberOfLights]; + +float computeVolumeShadow(vec3 posVC, vec3 lightDirNormVC, int lightIdx) { + if (posVC == cachedShadows[lightIdx].posVC) { + return cachedShadows[lightIdx].shadow; } - return secondary_contrib; + float shadow = computeVolumeShadowWithoutCache(posVC, lightDirNormVC); + cachedShadows[lightIdx].posVC = posVC; + cachedShadows[lightIdx].shadow = shadow; + return shadow; } -#endif //======================================================================= // local ambient occlusion -#ifdef localAmbientOcclusionOn -vec3 sample_direction_uniform(int i) -{ - float rand = random() * 0.5; - float theta = PI2 * (kernelSample[i][0] + rand); - float phi = acos(2.0 * (kernelSample[i][1] + rand) -1.0) / 2.5; - return normalize(vec3(cos(theta)*sin(phi), sin(theta)*sin(phi), cos(phi))); +#if vtkMaxLaoKernelSize > 0 + +// Return a random point on the unit sphere +vec3 sampleDirectionUniform(int rayIndex) { + // Each ray of each fragment should be different, two sources of randomness + // are used. Only depends on ray index + vec2 rayRandomness = kernelSample[rayIndex]; + // Only depends on fragment + float fragmentRandomness = fragmentSeed; + // Merge both source of randomness in a single uniform random variable using + // the formula (x+y < 1 ? x+y : x+y-1). The simpler formula (x+y)/2 doesn't + // result in a uniform distribution + vec2 mergedRandom = rayRandomness + vec2(fragmentRandomness); + mergedRandom -= vec2(greaterThanEqual(mergedRandom, vec2(1.0))); + + // Insipred by: + // https://karthikkaranth.me/blog/generating-random-points-in-a-sphere/#better-choice-of-spherical-coordinates + float u = mergedRandom[0]; + float v = mergedRandom[1]; + float theta = u * 2.0 * PI; + float phi = acos(2.0 * v - 1.0); + float sinTheta = sin(theta); + float cosTheta = cos(theta); + float sinPhi = sin(phi); + float cosPhi = cos(phi); + return vec3(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); } -// return a matrix that transform startDir into z axis; startDir should be normalized -mat3 zBaseRotationalMatrix(vec3 startDir){ - vec3 axis = cross(startDir, vec3(0.0,0.0,1.0)); - float cosA = startDir.z; - float k = 1.0 / (1.0 + cosA); - mat3 matrix = mat3((axis.x * axis.x * k) + cosA, (axis.y * axis.x * k) - axis.z, (axis.z * axis.x * k) + axis.y, - (axis.x * axis.y * k) + axis.z, (axis.y * axis.y * k) + cosA, (axis.z * axis.y * k) - axis.x, - (axis.x * axis.z * k) - axis.y, (axis.y * axis.z * k) + axis.x, (axis.z * axis.z * k) + cosA); - return matrix; -} - -float computeLAO(vec3 posIS, float op, vec3 lightDir, vec4 normal){ +float computeLAO(vec3 posVC, vec4 normalVC, float originalOpacity, int vIdx) { // apply LAO only at selected locations, otherwise return full brightness - if (normal.w > 0.0 && op > 0.05){ - float total_transmittance = 0.0; - mat3 inverseRotateBasis = inverse(zBaseRotationalMatrix(normalize(-normal.xyz))); - vec3 currPos, randomDirStep; - float weight, transmittance, opacity; - for (int i = 0; i < kernelSize; i++) - { - randomDirStep = inverseRotateBasis * sample_direction_uniform(i) * sampleDistanceIS; - weight = 1.0 - dot(normalize(lightDir), normalize(randomDirStep)); - currPos = posIS; - transmittance = 1.0; - for (int j = 0; j < kernelRadius ; j++){ - currPos += randomDirStep; - // check if it's at clipping plane, if so return full brightness - if (all(greaterThan(currPos, vec3(EPSILON))) && all(lessThan(currPos,vec3(1.0-EPSILON)))){ - opacity = texture2D(otexture, vec2(getTextureValue(currPos).r * oscale0 + oshift0, 0.5)).r; - #ifdef vtkGradientOpacityOn - opacity *= computeGradientOpacityFactor(normal.w, goscale0, goshift0, gomin0, gomax0); - #endif - transmittance *= 1.0 - opacity; - } - else{ - break; - } - } - total_transmittance += transmittance / float(kernelRadius) * weight; + if (volumes[vIdx].kernelSize > 0 || normalVC.w <= 0.0 || + originalOpacity <= 0.05) { + return 1.0; + } + + float occlusionSum = 0.0; + float weightSum = 0.0; + bool isGradientOpacityEnabled = volumes[vIdx].isGradientOpacityEnabled == 1; + for (int i = 0; i < volumes[vIdx].kernelSize; i++) { + // Only sample on an hemisphere around the -normalVC.xyz axis, so + // normalDotRay should be negative + vec3 rayDirectionVC = sampleDirectionUniform(i); + float normalDotRay = dot(normalVC.xyz, rayDirectionVC); + if (normalDotRay > 0.0) { + // Flip rayDirectionVC when it is in the wrong hemisphere + rayDirectionVC = -rayDirectionVC; + normalDotRay = -normalDotRay; + } - // early termination if fully translucent - if (total_transmittance > 1.0 - EPSILON){ - return 1.0; + vec3 currPosIS = VCtoIS(posVC, vIdx); + float transmittance = 1.0; + float gradientOpacityFactor = + computeGradientOpacityFactor(normalVC.w, vIdx, 0); + vec3 randomDirStepIS = rotateToIS(rayDirectionVC * sampleDistance, vIdx) * + volumes[vIdx].inverseSize; + for (int j = 0; j < volumes[vIdx].kernelRadius; j++) { + currPosIS += randomDirStepIS; + // Check if it's at clipping plane, if so return full brightness + if (any(lessThan(currPosIS, vec3(EPSILON))) || + any(greaterThan(currPosIS, vec3(1.0 - EPSILON)))) { + break; + } + float opacity = getOpacityFromTexture(getTextureValue(currPosIS, vIdx).r, + vIdx, 0, 0.5); + if (isGradientOpacityEnabled) { + opacity *= gradientOpacityFactor; + } + transmittance *= 1.0 - opacity; + if (transmittance < EPSILON) { + transmittance = 0.0; + break; } } - // average transmittance and reduce variance - return clamp(total_transmittance / float(kernelSize), 0.3, 1.0); - } else { - return 1.0; + float rayOcclusion = (1.0 - transmittance); + float rayWeight = -normalDotRay; + occlusionSum += rayOcclusion * rayWeight; + weightSum += rayWeight; } + // Lao is the average occlusion + float lao = occlusionSum / weightSum; + // Reduce variance by clamping + return clamp(lao, 0.3, 1.0); } #endif //======================================================================= // surface light contribution -#if vtkLightComplexity > 0 - void applyLighting(inout vec3 tColor, vec4 normal) - { - vec3 diffuse = vec3(0.0, 0.0, 0.0); - vec3 specular = vec3(0.0, 0.0, 0.0); - float df, sf = 0.0; - for (int i = 0; i < lightNum; i++){ - df = abs(dot(normal.rgb, -lightDirectionVC[i])); - diffuse += df * lightColor[i]; - sf = pow( abs(dot(lightHalfAngleVC[i],normal.rgb)), vSpecularPower); - specular += sf * lightColor[i]; - } - tColor.rgb = tColor.rgb*(diffuse*vDiffuse + vAmbient) + specular*vSpecular; - } - #ifdef SurfaceShadowOn - #if vtkLightComplexity < 3 - vec3 applyLightingDirectional(vec3 posIS, vec4 tColor, vec4 normal) - { - // everything in VC - vec3 diffuse = vec3(0.0); - vec3 specular = vec3(0.0); - #ifdef localAmbientOcclusionOn - vec3 ambient = vec3(0.0); - #endif - vec3 vertLightDirection; - for (int i = 0; i < lightNum; i++){ - float ndotL,vdotR; - vertLightDirection = lightDirectionVC[i]; - ndotL = dot(normal.xyz, vertLightDirection); - if (ndotL < 0.0 && twoSidedLighting) - { - ndotL = -ndotL; - } - if (ndotL > 0.0) - { - diffuse += ndotL * lightColor[i]; - //specular - vdotR = dot(-rayDirVC, normalize(2.0 * ndotL * -normal.xyz + vertLightDirection)); - if (vdotR > 0.0) - { - specular += pow(vdotR, vSpecularPower) * lightColor[i]; - } +#if vtkNumberOfLights > 0 +vec3 applyLighting(vec3 tColor, vec4 normalVC, int vIdx) { + vec3 diffuse = vec3(0.0, 0.0, 0.0); + vec3 specular = vec3(0.0, 0.0, 0.0); + for (int lightIdx = 0; lightIdx < vtkNumberOfLights; lightIdx++) { + float df = abs(dot(normalVC.xyz, -lights[lightIdx].directionVC)); + diffuse += df * lights[lightIdx].color; + float sf = pow(abs(dot(lights[lightIdx].halfAngleVC, normalVC.xyz)), + volumes[vIdx].specularPower); + specular += sf * lights[lightIdx].color; + } + return tColor * (diffuse * volumes[vIdx].diffuse + volumes[vIdx].ambient) + + specular * volumes[vIdx].specular; +} + +vec3 applySurfaceShadowLighting(vec3 tColor, float alpha, vec3 posVC, + vec4 normalVC, int vIdx) { + // everything in VC + vec3 diffuse = vec3(0.0); + vec3 specular = vec3(0.0); + for (int ligthIdx = 0; ligthIdx < vtkNumberOfLights; ligthIdx++) { + vec3 vertLightDirection; + float attenuation; + if (lights[ligthIdx].isPositional == 1) { + vertLightDirection = posVC - lights[ligthIdx].positionVC; + float lightDistance = length(vertLightDirection); + // Normalize with precomputed length + vertLightDirection = vertLightDirection / lightDistance; + // Base attenuation + vec3 attenuationPolynom = lights[ligthIdx].attenuation; + attenuation = + 1.0 / (attenuationPolynom[0] + + lightDistance * (attenuationPolynom[1] + + lightDistance * attenuationPolynom[2])); + // Cone attenuation + float coneDot = dot(vertLightDirection, lights[ligthIdx].directionVC); + // Per OpenGL standard cone angle is 90 or less for a spot light + if (lights[ligthIdx].coneAngle <= 90.0) { + if (coneDot >= cos(radians(lights[ligthIdx].coneAngle))) { + // Inside the cone + attenuation *= pow(coneDot, lights[ligthIdx].exponent); + } else { + // Outside the cone + attenuation = 0.0; } - #ifdef localAmbientOcclusionOn - ambient += computeLAO(posIS, tColor.a, vertLightDirection, normal); - #endif } - #ifdef localAmbientOcclusionOn - return tColor.rgb * (diffuse * vDiffuse + vAmbient * ambient) + specular*vSpecular; - #else - return tColor.rgb * (diffuse * vDiffuse + vAmbient) + specular*vSpecular; - #endif + } else { + vertLightDirection = lights[ligthIdx].directionVC; + attenuation = 1.0; } - #else - vec3 applyLightingPositional(vec3 posIS, vec4 tColor, vec4 normal, vec3 posVC) - { - // everything in VC - vec3 diffuse = vec3(0.0); - vec3 specular = vec3(0.0); - #ifdef localAmbientOcclusionOn - vec3 ambient = vec3(0.0); - #endif - vec3 vertLightDirection; - for (int i = 0; i < lightNum; i++){ - float distance,attenuation,ndotL,vdotR; - vec3 lightDir; - if (lightPositional[i] == 1){ - lightDir = lightDirectionVC[i]; - vertLightDirection = posVC - lightPositionVC[i]; - distance = length(vertLightDirection); - vertLightDirection = normalize(vertLightDirection); - attenuation = 1.0 / (lightAttenuation[i].x - + lightAttenuation[i].y * distance - + lightAttenuation[i].z * distance * distance); - // per OpenGL standard cone angle is 90 or less for a spot light - if (lightConeAngle[i] <= 90.0){ - float coneDot = dot(vertLightDirection, lightDir); - if (coneDot >= cos(radians(lightConeAngle[i]))){ // if inside cone - attenuation = attenuation * pow(coneDot, lightExponent[i]); - } - else { - attenuation = 0.0; - } - } - ndotL = dot(normal.xyz, vertLightDirection); - if (ndotL < 0.0 && twoSidedLighting) - { - ndotL = -ndotL; - } - if (ndotL > 0.0) - { - diffuse += ndotL * attenuation * lightColor[i]; - //specular - vdotR = dot(-rayDirVC, normalize(2.0 * ndotL * -normal.xyz + vertLightDirection)); - if (vdotR > 0.0) - { - specular += pow(vdotR, vSpecularPower) * attenuation * lightColor[i]; - } - } - #ifdef localAmbientOcclusionOn - ambient += computeLAO(posIS, tColor.a, vertLightDirection, normal); - #endif - } else { - vertLightDirection = lightDirectionVC[i]; - ndotL = dot(normal.xyz, vertLightDirection); - if (ndotL < 0.0 && twoSidedLighting) - { - ndotL = -ndotL; - } - if (ndotL > 0.0) - { - diffuse += ndotL * lightColor[i]; - //specular - vdotR = dot(-rayDirVC, normalize(2.0 * ndotL * -normal.xyz + vertLightDirection)); - if (vdotR > 0.0) - { - specular += pow(vdotR, vSpecularPower) * lightColor[i]; - } - } - #ifdef localAmbientOcclusionOn - ambient += computeLAO(posIS, tColor.a, vertLightDirection, normal); - #endif - } + + float ndotL = dot(normalVC.xyz, vertLightDirection); + if (ndotL < 0.0 && twoSidedLighting == 1) { + ndotL = -ndotL; + } + if (ndotL > 0.0) { + // Diffuse + diffuse += ndotL * attenuation * lights[ligthIdx].color; + // Specular + float vdotR = + dot(-rayDirVC, vertLightDirection - 2.0 * ndotL * normalVC.xyz); + if (vdotR > 0.0) { + specular += pow(vdotR, volumes[vIdx].specularPower) * attenuation * + lights[ligthIdx].color; } - #ifdef localAmbientOcclusionOn - return tColor.rgb * (diffuse * vDiffuse + vAmbient * ambient) + specular*vSpecular; - #else - return tColor.rgb * (diffuse * vDiffuse + vAmbient) + specular*vSpecular; - #endif } - #endif - #endif + } +#if vtkMaxLaoKernelSize > 0 + float laoFactor = computeLAO(posVC, normalVC, alpha, vIdx); +#else + const float laoFactor = 1.0; +#endif + return tColor * (diffuse * volumes[vIdx].diffuse + + volumes[vIdx].ambient * laoFactor) + + specular * volumes[vIdx].specular; +} + +vec3 applyVolumeShadowLighting(vec3 tColor, vec3 posVC, int vIdx) { + // Here we assume only positional light, no effect of cones and achromatic + // light (only intensity) + vec3 diffuse = vec3(0.0); + for (int lightIdx = 0; lightIdx < vtkNumberOfLights; lightIdx++) { + vec3 lightDirVC = lights[lightIdx].isPositional == 1 + ? normalize(lights[lightIdx].positionVC - posVC) + : -lights[lightIdx].directionVC; + float shadowCoeff = computeVolumeShadow(posVC, lightDirVC, lightIdx); + float phaseAttenuation = phaseFunction(dot(rayDirVC, lightDirVC), vIdx); + diffuse += phaseAttenuation * shadowCoeff * lights[lightIdx].color; + } + return tColor * (diffuse * volumes[vIdx].diffuse + volumes[vIdx].ambient); +} #endif // LAO of surface shadows and volume shadows only work with dependent components -vec3 applyAllLightning(vec3 tColor, float alpha, vec3 posIS, vec4 normalLight) { - #if vtkLightComplexity > 0 - // surface shadows if needed - #ifdef SurfaceShadowOn - #if vtkLightComplexity < 3 - vec3 tColorS = applyLightingDirectional(posIS, vec4(tColor, alpha), normalLight); - #else - vec3 tColorS = applyLightingPositional(posIS, vec4(tColor, alpha), normalLight, IStoVC(posIS)); - #endif - #endif - - // volume shadows if needed - #ifdef VolumeShadowOn - vec3 tColorVS = applyShadowRay(tColor, posIS, rayDirVC); - #endif - - // merge - #ifdef VolumeShadowOn - #ifdef SurfaceShadowOn - // surface shadows + volumetric shadows - float vol_coef = volumetricScatteringBlending * (1.0 - alpha / 2.0) * (1.0 - atan(normalLight.w) * INV4PI); - tColor = (1.0-vol_coef) * tColorS + vol_coef * tColorVS; - #else - // volumetric shadows only - tColor = tColorVS; - #endif - #else - #ifdef SurfaceShadowOn - // surface shadows only - tColor = tColorS; - #else - // no shadows - applyLighting(tColor, normal3); - #endif - #endif - #endif +vec3 applyAllLightning(vec3 tColor, float alpha, vec3 posVC, + vec4 surfaceNormalVC, int vIdx) { +#if vtkNumberOfLights > 0 + // 0 <= volCoeff < EPSILON => only surface shadows + // EPSILON <= volCoeff < 1 - EPSILON => mix of surface and volume shadows + // 1 - EPSILON <= volCoeff => only volume shadows + float volCoeff = volumes[vIdx].volumetricScatteringBlending * + (1.0 - alpha / 2.0) * + (1.0 - atan(surfaceNormalVC.w) * INV4PI); + + // Compute the different possible lightings + vec3 surfaceShadedColor = tColor; +#ifdef EnableSurfaceLighting + if (volCoeff < 1.0 - EPSILON) { + surfaceShadedColor = + applySurfaceShadowLighting(tColor, alpha, posVC, surfaceNormalVC, vIdx); + } +#endif + vec3 volumeShadedColor = tColor; +#ifdef EnableVolumeLighting + if (volCoeff >= EPSILON) { + volumeShadedColor = applyVolumeShadowLighting(tColor, posVC, vIdx); + } +#endif + + // Return the right mix + if (volCoeff < EPSILON) { + // Surface shadows + return surfaceShadedColor; + } + if (volCoeff >= 1.0 - EPSILON) { + // Volume shadows + return volumeShadedColor; + } + // Mix of surface and volume shadows + return mix(surfaceShadedColor, volumeShadedColor, volCoeff); +#endif return tColor; } - -vec4 getColorForValue(vec4 tValue, vec3 posIS, vec3 tstep) -{ - -// If labeloutline and not the edge labelmap, since in the edge labelmap blend -// we need the underlying data to sample through -#if defined(vtkImageLabelOutlineOn) && !defined(vtkLabelEdgeProjectionOn) - vec3 centerPosIS = fragCoordToIndexSpace(gl_FragCoord); // pos in texture space - vec4 centerValue = getTextureValue(centerPosIS); +vec4 getColorForLabelOutline(int vIdx) { + vec3 centerPosIS = + fragCoordToIndexSpace(gl_FragCoord, vIdx); // pos in texture space + vec4 centerValue = getTextureValue(centerPosIS, vIdx); bool pixelOnBorder = false; - vec4 tColor = texture2D(ctexture, vec2(centerValue.r * cscale0 + cshift0, 0.5)); - - // Get alpha of segment from opacity function. - tColor.a = texture2D(otexture, vec2(centerValue.r * oscale0 + oshift0, 0.5)).r; + vec4 tColor = vec4(getColorFromTexture(centerValue.r, vIdx, 0, 0.5), + getOpacityFromTexture(centerValue.r, vIdx, 0, 0.5)); int segmentIndex = int(centerValue.r * 255.0); - + // Use texture sampling for outlineThickness float textureCoordinate = float(segmentIndex - 1) / 1024.0; - float textureValue = texture2D(ttexture, vec2(textureCoordinate, 0.5)).r; - + float textureValue = + texture2D(labelOutlineThicknessTexture, vec2(textureCoordinate, 0.5)).r; int actualThickness = int(textureValue * 255.0); - - // If it is the background (segment index 0), we should quickly bail out. + // If it is the background (segment index 0), we should quickly bail out. // Previously, this was determined by tColor.a, which was incorrect as it // prevented the outline from appearing when the fill is 0. - if (segmentIndex == 0){ + if (segmentIndex == 0) { return vec4(0, 0, 0, 0); } - // Only perform outline check on fragments rendering voxels that aren't invisible. - // Saves a bunch of needless checks on the background. + // Only perform outline check on fragments rendering voxels that aren't + // invisible. Saves a bunch of needless checks on the background. // TODO define epsilon when building shader? for (int i = -actualThickness; i <= actualThickness; i++) { for (int j = -actualThickness; j <= actualThickness; j++) { @@ -1098,12 +900,12 @@ vec4 getColorForValue(vec4 tValue, vec3 posIS, vec3 tstep) continue; } - vec4 neighborPixelCoord = vec4(gl_FragCoord.x + float(i), - gl_FragCoord.y + float(j), - gl_FragCoord.z, gl_FragCoord.w); + vec4 neighborPixelCoord = + vec4(gl_FragCoord.x + float(i), gl_FragCoord.y + float(j), + gl_FragCoord.z, gl_FragCoord.w); - vec3 neighborPosIS = fragCoordToIndexSpace(neighborPixelCoord); - vec4 value = getTextureValue(neighborPosIS); + vec3 neighborPosIS = fragCoordToIndexSpace(neighborPixelCoord, vIdx); + vec4 value = getTextureValue(neighborPosIS, vIdx); // If any of my neighbours are not the same value as I // am, this means I am on the border of the segment. @@ -1121,706 +923,790 @@ vec4 getColorForValue(vec4 tValue, vec3 posIS, vec3 tstep) // If I am on the border, I am displayed at full opacity if (pixelOnBorder == true) { - tColor.a = outlineOpacity; + tColor.a = volumes[vIdx].outlineOpacity; } return tColor; +} -#else - // compute the normal and gradient magnitude if needed - // We compute it as a vec4 if possible otherwise a mat4 - - #ifdef UseIndependentComponents - - // sample textures - vec3 tColor0 = texture2D(ctexture, vec2(tValue.r * cscale0 + cshift0, height0)).rgb; - float pwfValue0 = texture2D(otexture, vec2(tValue.r * oscale0 + oshift0, height0)).r; - - #if vtkNumComponents > 1 - vec3 tColor1 = texture2D(ctexture, vec2(tValue.g * cscale1 + cshift1, height1)).rgb; - float pwfValue1 = texture2D(otexture, vec2(tValue.g * oscale1 + oshift1, height1)).r; - - #if vtkNumComponents > 2 - vec3 tColor2 = texture2D(ctexture, vec2(tValue.b * cscale2 + cshift2, height2)).rgb; - float pwfValue2 = texture2D(otexture, vec2(tValue.b * oscale2 + oshift2, height2)).r; - - #if vtkNumComponents > 3 - vec3 tColor3 = texture2D(ctexture, vec2(tValue.a * cscale3 + cshift3, height3)).rgb; - float pwfValue3 = texture2D(otexture, vec2(tValue.a * oscale3 + oshift3, height3)).r; - #endif - #endif - #endif - - #if !defined(vtkCustomComponentsColorMix) - // default path for component color mix - - // compute the normal vectors as needed - #if (vtkLightComplexity > 0) || defined(vtkGradientOpacityOn) - mat4 normalMat = computeMat4Normal(posIS, tValue, tstep); - #endif - - // compute gradient opacity factors as needed - vec4 goFactor = vec4(1.0, 1.0 ,1.0 ,1.0); - #if defined(vtkGradientOpacityOn) - #if !defined(vtkComponent0Proportional) - goFactor.x = - computeGradientOpacityFactor(normalMat[0].a, goscale0, goshift0, gomin0, gomax0); - #endif - #if vtkNumComponents > 1 - #if !defined(vtkComponent1Proportional) - goFactor.y = - computeGradientOpacityFactor(normalMat[1].a, goscale1, goshift1, gomin1, gomax1); - #endif - #if vtkNumComponents > 2 - #if !defined(vtkComponent2Proportional) - goFactor.z = - computeGradientOpacityFactor(normalMat[2].a, goscale2, goshift2, gomin2, gomax2); - #endif - #if vtkNumComponents > 3 - #if !defined(vtkComponent3Proportional) - goFactor.w = - computeGradientOpacityFactor(normalMat[3].a, goscale3, goshift3, gomin3, gomax3); - #endif - #endif - #endif - #endif - #endif - - // process color and opacity for each component - #if !defined(vtkComponent0Proportional) - float alpha = goFactor.x*mix0*pwfValue0; - #if vtkLightComplexity > 0 - applyLighting(tColor0, normalMat[0]); - #endif - #else - tColor0 *= pwfValue0; - float alpha = mix(pwfValue0, 1.0, (1.0 - mix0)); - #endif - - #if vtkNumComponents > 1 - #if !defined(vtkComponent1Proportional) - alpha += goFactor.y*mix1*pwfValue1; - #if vtkLightComplexity > 0 - applyLighting(tColor1, normalMat[1]); - #endif - #else - tColor1 *= pwfValue1; - alpha *= mix(pwfValue1, 1.0, (1.0 - mix1)); - #endif - - #if vtkNumComponents > 2 - #if !defined(vtkComponent2Proportional) - alpha += goFactor.z*mix2*pwfValue2; - #if vtkLightComplexity > 0 - applyLighting(tColor2, normalMat[2]); - #endif - #else - tColor2 *= pwfValue2; - alpha *= mix(pwfValue2, 1.0, (1.0 - mix2)); - #endif - #endif - - #if vtkNumComponents > 3 - #if !defined(vtkComponent3Proportional) - alpha += goFactor.w*mix3*pwfValue3; - #if vtkLightComplexity > 0 - applyLighting(tColor3, normalMat[3]); - #endif - #else - tColor3 *= pwfValue3; - alpha *= mix(pwfValue3, 1.0, (1.0 - mix3)); - #endif - #endif - #endif - - // perform final independent blend - vec3 tColor = mix0 * tColor0; - #if vtkNumComponents > 1 - tColor += mix1 * tColor1; - #if vtkNumComponents > 2 - tColor += mix2 * tColor2; - #if vtkNumComponents > 3 - tColor += mix3 * tColor3; - #endif - #endif - #endif - - return vec4(tColor, alpha); - #else - /* - * Mix the color information from all the independent components to get a single rgba output - * Gradient opactity factors and normals are not computed - * - * You can compute these using: - * - computeMat4Normal: always available, compute normal only for non proportional components, used by default independent component mix - * - computeDensityNormal & computeNormalForDensity: available if ((LightComplexity > 0) || GradientOpacityOn) && ComputeNormalFromOpacity), - * used by dependent component color mix, see code for Additive preset in OpenGl/VolumeMapper - * - computeGradientOpacityFactor: always available, used in a lot of places - * - * Using applyAllLightning() is advised for shading but some features don't work well with it (volume shadows, LAO) - * mix0, mix1, ... are defined for each component that is used and correspond to the componentWeight - */ - //VTK::CustomComponentsColorMix::Impl - #endif - #else - // dependent components - - // compute normal if needed - #if (vtkLightComplexity > 0) || defined(vtkGradientOpacityOn) - // use component 3 of the opacity texture as getTextureValue() sets alpha to the opacity value - #ifdef vtkComputeNormalFromOpacity - vec3 scalarInterp[2]; - vec4 normal0 = computeNormalForDensity(posIS, tstep, scalarInterp, 3); - #else - vec4 normal0 = computeNormal(posIS, tstep); - #endif - #endif - - // compute gradient opacity factor enabled - #if defined(vtkGradientOpacityOn) - float gradientOpacity = computeGradientOpacityFactor(normal0.a, goscale0, goshift0, gomin0, gomax0); - #else - const float gradientOpacity = 1.0; - #endif - - // get color and opacity - #if vtkNumComponents == 1 - vec3 tColor = texture2D(ctexture, vec2(tValue.r * cscale0 + cshift0, 0.5)).rgb; - float alpha = gradientOpacity*texture2D(otexture, vec2(tValue.r * oscale0 + oshift0, 0.5)).r; - if (alpha < EPSILON){ - return vec4(0.0); - } - #endif - #if vtkNumComponents == 2 - vec3 tColor = vec3(tValue.r * cscale0 + cshift0); - float alpha = gradientOpacity*texture2D(otexture, vec2(tValue.a * oscale1 + oshift1, 0.5)).r; - #endif - #if vtkNumComponents == 3 - vec3 tColor; - tColor.r = tValue.r * cscale0 + cshift0; - tColor.g = tValue.g * cscale1 + cshift1; - tColor.b = tValue.b * cscale2 + cshift2; - float alpha = gradientOpacity*texture2D(otexture, vec2(tValue.a * oscale0 + oshift0, 0.5)).r; - #endif - #if vtkNumComponents == 4 - vec3 tColor; - tColor.r = tValue.r * cscale0 + cshift0; - tColor.g = tValue.g * cscale1 + cshift1; - tColor.b = tValue.b * cscale2 + cshift2; - float alpha = gradientOpacity*texture2D(otexture, vec2(tValue.a * oscale3 + oshift3, 0.5)).r; - #endif - - // lighting - #if (vtkLightComplexity > 0) - #ifdef vtkComputeNormalFromOpacity - vec4 normalLight; - if (!all(equal(normal0, vec4(0.0)))) { - scalarInterp[0] = scalarInterp[0] * oscale0 + oshift0; - scalarInterp[1] = scalarInterp[1] * oscale0 + oshift0; - normalLight = computeDensityNormal(scalarInterp, 0.5, gradientOpacity); - if (all(equal(normalLight, vec4(0.0)))) { - normalLight = normal0; - } - } - #else - vec4 normalLight = normal0; - #endif - tColor = applyAllLightning(tColor, alpha, posIS, normalLight); - #endif - - return vec4(tColor, alpha); - #endif // dependent +vec4 getColorForAdditivePreset(vec4 tValue, vec3 posVC, vec3 posIS, int vIdx) { + // compute normals + mat4 normalMat = computeMat4Normal(posIS, tValue, vIdx); + vec4 normalLights[2]; + normalLights[0] = normalMat[0]; + normalLights[1] = normalMat[1]; +#if vtkNumberOfLights > 0 + if (volumes[vIdx].computeNormalFromOpacity == 1) { + for (int component = 0; component < 2; ++component) { + vec3 scalarInterp[2]; + float height = volumes[vIdx].transferFunctionsSampleHeight[component]; + computeNormalForDensity(posIS, scalarInterp, component, vIdx); + normalLights[component] = + computeDensityNormal(scalarInterp, height, 1.0, component, vIdx); + } + } #endif -} -bool valueWithinScalarRange(vec4 val, vec4 min, vec4 max) { - bool withinRange = false; - #if vtkNumComponents == 1 - if (val.r >= min.r && val.r <= max.r) { - withinRange = true; + // compute opacities + float opacities[2]; + opacities[0] = getOpacityFromTexture( + tValue.r, vIdx, 0, volumes[vIdx].transferFunctionsSampleHeight[0]); + opacities[1] = getOpacityFromTexture( + tValue.r, vIdx, 1, volumes[vIdx].transferFunctionsSampleHeight[1]); + if (volumes[vIdx].isGradientOpacityEnabled == 1) { + for (int component = 0; component < 2; ++component) { + opacities[component] *= + computeGradientOpacityFactor(normalMat[component].a, vIdx, component); } - #else - #ifdef UseIndependentComponents - #if vtkNumComponents == 2 - if (val.r >= min.r && val.r <= max.r && - val.g >= min.g && val.g <= max.g) { - withinRange = true; - } - #else - if (all(greaterThanEqual(val, ipScalarRangeMin)) && - all(lessThanEqual(val, ipScalarRangeMax))) { - withinRange = true; - } - #endif - #endif - #endif - return withinRange; + } + float opacitySum = opacities[0] + opacities[1]; + if (opacitySum <= 0.0) { + return vec4(0.0); + } + + // mix the colors and opacities + vec3 colors[2]; + for (int component = 0; component < 2; ++component) { + float sampleHeight = volumes[vIdx].transferFunctionsSampleHeight[component]; + vec3 color = getColorFromTexture(tValue.r, vIdx, component, sampleHeight); + color = applyAllLightning(color, opacities[component], posVC, + normalLights[component], vIdx); + colors[component] = color; + } + vec3 mixedColor = + (opacities[0] * colors[0] + opacities[1] * colors[1]) / opacitySum; + return vec4(mixedColor, min(1.0, opacitySum)); } -#if vtkBlendMode == 6 -bool checkOnEdgeForNeighbor(int i, int j, int s, vec3 stepIS) { - vec4 neighborPixelCoord = vec4(gl_FragCoord.x + float(i), gl_FragCoord.y + float(j), gl_FragCoord.z, gl_FragCoord.w); - vec3 originalNeighborPosIS = fragCoordToIndexSpace(neighborPixelCoord); +vec4 getColorForColorizePreset(vec4 tValue, vec3 posVC, vec3 posIS, int vIdx) { + // compute normals + mat4 normalMat = computeMat4Normal(posIS, tValue, vIdx); + vec4 normalLight = normalMat[0]; +#if vtkNumberOfLights > 0 + if (volumes[vIdx].computeNormalFromOpacity == 1) { + vec3 scalarInterp[2]; + float height = volumes[vIdx].transferFunctionsSampleHeight[0]; + computeNormalForDensity(posIS, scalarInterp, 0, vIdx); + normalLight = computeDensityNormal(scalarInterp, height, 1.0, 0, vIdx); + } +#endif + + // compute opacities + float opacity = getOpacityFromTexture( + tValue.r, vIdx, 0, volumes[vIdx].transferFunctionsSampleHeight[0]); + if (volumes[vIdx].isGradientOpacityEnabled == 1) { + opacity *= computeGradientOpacityFactor(normalMat[0].a, vIdx, 0); + } - bool justSawIt = false; + // colorizing component + vec3 colorizingColor = getColorFromTexture( + tValue.r, vIdx, 1, volumes[vIdx].transferFunctionsSampleHeight[1]); + float colorizingOpacity = getOpacityFromTexture( + tValue.r, vIdx, 1, volumes[vIdx].transferFunctionsSampleHeight[1]); + + // mix the colors and opacities + vec3 color = + getColorFromTexture(tValue.r, vIdx, 0, + volumes[vIdx].transferFunctionsSampleHeight[0]) * + mix(vec3(1.0), colorizingColor, colorizingOpacity); + color = applyAllLightning(color, opacity, posVC, normalLight, vIdx); + return vec4(color, opacity); +} - vec3 neighborPosIS = originalNeighborPosIS; +vec4 getColorForDefaultIndependentPreset(vec4 tValue, vec3 posIS, int vIdx) { + // compute the normal vectors as needed + mat4 normalMat; + if (vtkNumberOfLights > 0 || volumes[vIdx].isGradientOpacityEnabled == 1) { + normalMat = computeMat4Normal(posIS, tValue, vIdx); + } - float stepsTraveled = 0.0; + // compute gradient opacity factors as needed + vec4 goFactor = vec4(1.0); + if (volumes[vIdx].isGradientOpacityEnabled == 1) { + for (int component = 0; component < volumes[vIdx].numberOfComponents; + ++component) { + if (volumes[vIdx].isComponentProportional[component] == 0) { + goFactor[component] = computeGradientOpacityFactor( + normalMat[component].a, vIdx, component); + } + } + } + // sample textures + vec3 colors[4]; + + // process color and opacity for each component + // initial value of alpha is determined by wether the first component is + // proportional or not + // when it is not proportional, it starts at 0 (neutral for additions) + // when it is proportional, it starts at 1 (neutral for multiplications) + float alpha = float(volumes[vIdx].isComponentProportional[0] == 0); + vec3 mixedColor = vec3(0.0); + for (int component = 0; component < volumes[vIdx].numberOfComponents; + ++component) { + vec3 color = getColorFromTexture( + tValue.r, vIdx, component, + volumes[vIdx].transferFunctionsSampleHeight[component]); + float opacity = getOpacityFromTexture( + tValue.r, vIdx, component, + volumes[vIdx].transferFunctionsSampleHeight[component]); + if (volumes[vIdx].isComponentProportional[component] == 0) { + alpha += goFactor[component] * + volumes[vIdx].independentComponentMix[component] * opacity; +#if vtkNumberOfLights > 0 + color = applyLighting(color, normalMat[component], vIdx); +#endif + } else { + color *= opacity; + alpha *= mix(opacity, 1.0, + (1.0 - volumes[vIdx].independentComponentMix[component])); + } + mixedColor += volumes[vIdx].independentComponentMix[component] * color; + } + return vec4(mixedColor, alpha); +} - // float neighborValue; - for (int k = 0; k < //VTK::MaximumSamplesValue /2 ; ++k) { - ivec3 texCoord = ivec3(neighborPosIS * vec3(volumeDimensions)); - vec4 texValue = texelFetch(texture1, texCoord, 0); +vec4 getColorForDependentComponents(vec4 tValue, vec3 posVC, vec3 posIS, + int vIdx) { + // compute normal and scalarInterp if needed + vec4 normal0; + vec3 scalarInterp[2]; + if (vtkNumberOfLights > 0 || volumes[vIdx].isGradientOpacityEnabled == 1) { + // use component 3 of the opacity texture as getTextureValue() sets alpha to + // the opacity value + normal0 = computeNormalForDensity(posIS, scalarInterp, 3, vIdx); + } - if (int(texValue.g) == s) { - justSawIt = true; - break; - } - neighborPosIS += stepIS; + // compute gradient opacity factor + float gradientOpacity; + if (volumes[vIdx].isGradientOpacityEnabled == 1) { + gradientOpacity = computeGradientOpacityFactor(normal0.a, vIdx, 0); + } else { + gradientOpacity = 1.0; + } + + // get color and opacity + vec3 tColor; + float alpha; + switch (volumes[vIdx].numberOfComponents) { + case 1: + tColor = getColorFromTexture(tValue.r, vIdx, 0, 0.5); + alpha = gradientOpacity * getOpacityFromTexture(tValue.r, vIdx, 0, 0.5); + if (alpha < EPSILON) { + return vec4(0.0); } + break; + case 2: + tColor = vec3(tValue.r * volumes[vIdx].colorTextureScale[0] + + volumes[vIdx].colorTextureShift[0]); + alpha = gradientOpacity * getOpacityFromTexture(tValue.a, vIdx, 1, 0.5); + break; + case 3: + tColor = tValue.rgb * volumes[vIdx].colorTextureScale.rgb + + volumes[vIdx].colorTextureShift.rgb; + alpha = gradientOpacity * getOpacityFromTexture(tValue.a, vIdx, 0, 0.5); + break; + case 4: + tColor = tValue.rgb * volumes[vIdx].colorTextureScale.rgb + + volumes[vIdx].colorTextureShift.rgb; + alpha = gradientOpacity * getOpacityFromTexture(tValue.a, vIdx, 3, 0.5); + break; + } - if (justSawIt){ - return false; +// lighting +#if vtkNumberOfLights > 0 + vec4 normalLight; + if (volumes[vIdx].computeNormalFromOpacity == 1) { + if (normal0[3] != 0.0) { + normalLight = + computeDensityNormal(scalarInterp, 0.5, gradientOpacity, 0, vIdx); + if (normalLight[3] == 0.0) { + normalLight = normal0; + } } + } else { + normalLight = normal0; + } + tColor = applyAllLightning(tColor, alpha, posVC, normalLight, vIdx); +#endif - - neighborPosIS = originalNeighborPosIS; - for (int k = 0; k < //VTK::MaximumSamplesValue /2 ; ++k) { - ivec3 texCoord = ivec3(neighborPosIS * vec3(volumeDimensions)); - vec4 texValue = texelFetch(texture1, texCoord, 0); + return vec4(tColor, alpha); +} - if (int(texValue.g) == s) { - justSawIt = true; - break; - } - neighborPosIS -= stepIS; - } +vec4 getColorForValue(vec4 tValue, vec3 posVC, vec3 posIS, int vIdx) { + switch (volumes[vIdx].colorForValueFunctionId) { +#ifdef EnableColorForValueFunctionId0 + case 0: + return getColorForDependentComponents(tValue, posVC, posIS, vIdx); +#endif +#ifdef EnableColorForValueFunctionId1 + case 1: + return getColorForAdditivePreset(tValue, posVC, posIS, vIdx); +#endif - if (!justSawIt) { - // onedge - vec3 tColorSegment = texture2D(ctexture, vec2(float(s) * cscale1 + cshift1, height1)).rgb; - float pwfValueSegment = texture2D(otexture, vec2(float(s) * oscale1 + oshift1, height1)).r; - gl_FragData[0] = vec4(tColorSegment, pwfValueSegment); - return true; - } +#ifdef EnableColorForValueFunctionId2 + case 2: + return getColorForColorizePreset(tValue, posVC, posIS, vIdx); +#endif - // not on edge +#ifdef EnableColorForValueFunctionId3 + case 3: + /* + * Mix the color information from all the independent components to get a + * single rgba output. See other shader functions like + * `getColorForAdditivePreset` to learn how to create a custom color mix. + * The custom color mix should return a value, but if it doesn't, it will + * fallback on the default shading + */ + //VTK::CustomColorMix +#endif + +#ifdef EnableColorForValueFunctionId4 + case 4: + return getColorForDefaultIndependentPreset(tValue, posIS, vIdx); +#endif + +#ifdef EnableColorForValueFunctionId5 + case 5: + return getColorForLabelOutline(vIdx); +#endif + } +} + +bool valueWithinScalarRange(vec4 val, int vIdx) { + int numberOfComponents = volumes[vIdx].numberOfComponents; + if (numberOfComponents > 1 && volumes[vIdx].useIndependentComponents == 0) { return false; + } + vec4 rangeMin = volumes[vIdx].ipScalarRangeMin; + vec4 rangeMax = volumes[vIdx].ipScalarRangeMax; + for (int component = 0; component < numberOfComponents; ++component) { + if (val[component] < rangeMin[component] || + rangeMax[component] < val[component]) { + return false; + } + } + return true; } +#if vtkBlendMode == LABELMAP_EDGE_PROJECTION_BLEND +bool checkOnEdgeForNeighbor(int xFragmentOffset, int yFragmentOffset, + int segmentIndex, vec3 stepIS, int vIdx) { + vec3 volumeDimensions = vec3(volumes[vIdx].dimensions); + vec4 neighborPixelCoord = vec4(gl_FragCoord.x + float(xFragmentOffset), + gl_FragCoord.y + float(yFragmentOffset), + gl_FragCoord.z, gl_FragCoord.w); + vec3 originalNeighborPosIS = fragCoordToIndexSpace(neighborPixelCoord, vIdx); + + vec3 neighborPosIS = originalNeighborPosIS; + for (int k = 0; k < maximumNumberOfSamples / 2; ++k) { + ivec3 texCoord = ivec3(neighborPosIS * volumeDimensions); + vec4 texValue = fetchVolumeTexture(texCoord, vIdx); + if (int(texValue.g) == segmentIndex) { + // not on edge + return false; + } + neighborPosIS += stepIS; + } + + neighborPosIS = originalNeighborPosIS; + for (int k = 0; k < maximumNumberOfSamples / 2; ++k) { + ivec3 texCoord = ivec3(neighborPosIS * volumeDimensions); + vec4 texValue = fetchVolumeTexture(texCoord, vIdx); + if (int(texValue.g) == segmentIndex) { + // not on edge + return false; + } + neighborPosIS -= stepIS; + } + + // onedge + float sampleHeight = volumes[vIdx].transferFunctionsSampleHeight[1]; + vec3 tColorSegment = + getColorFromTexture(float(segmentIndex), vIdx, 1, sampleHeight); + float pwfValueSegment = + getOpacityFromTexture(float(segmentIndex), vIdx, 1, sampleHeight); + gl_FragData[0] = vec4(tColorSegment, pwfValueSegment); + return true; +} #endif +vec4 getColorAtPos(vec3 posVC) { + float transmittanceProduct = 1.0; + vec3 weightedColors = vec3(0.0); + float weightSum = 0.0; + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + if (any(lessThan(posIS, vec3(0.0))) || any(greaterThan(posIS, vec3(1.0)))) { + continue; + } + vec4 texValue = getTextureValue(posIS, vIdx); + vec4 currentColor = getColorForValue(texValue, posVC, posIS, vIdx); + float currentTransmittance = 1.0 - currentColor.a; + if (currentTransmittance == 0.0) { + return currentColor; + } + transmittanceProduct *= currentTransmittance; + float weight = -log(currentTransmittance); + weightedColors += currentColor.rgb * weight; + weightSum += weight; + } + if (weightSum == 0.0) { + return vec4(0.0); + } + vec3 finalColor = weightedColors / weightSum; + float finalOpacity = 1.0 - transmittanceProduct; + return vec4(finalColor, finalOpacity); +} //======================================================================= // Apply the specified blend mode operation along the ray's path. // -void applyBlend(vec3 posIS, vec3 endIS, vec3 tdims) -{ - vec3 tstep = 1.0/tdims; - +void applyBlend(vec3 rayOriginVC, vec3 rayDirVC, float minDistance, + float maxDistance, int minDistanceVolumeIdx) { // start slightly inside and apply some jitter - vec3 delta = endIS - posIS; - vec3 stepIS = normalize(delta)*sampleDistanceIS; - float raySteps = length(delta)/sampleDistanceIS; - - // Initialize arrays to false - // avoid 0.0 jitter - float jitter = 0.01 + 0.99*texture2D(jtexture, gl_FragCoord.xy/32.0).r; - float stepsTraveled = jitter; + vec3 stepVC = rayDirVC * sampleDistance; + float raySteps = (maxDistance - minDistance) / sampleDistance; - // local vars for the loop - vec4 color = vec4(0.0, 0.0, 0.0, 0.0); - vec4 tValue; - vec4 tColor; - - // if we have less than one step then pick the middle point - // as our value - // if (raySteps <= 1.0) - // { - // posIS = (posIS + endIS)*0.5; - // } - - // Perform initial step at the volume boundary - // compute the scalar - tValue = getTextureValue(posIS); - - #if vtkBlendMode == 6 - if (raySteps <= 1.0) - { - gl_FragData[0] = getColorForValue(tValue, posIS, tstep); - return; - } + // Avoid 0.0 jitter + float jitter = 0.01 + 0.99 * fragmentSeed; - vec4 value = tValue; - posIS += (jitter*stepIS); - vec3 maxPosIS = posIS; // Store the position of the max value - int segmentIndex = int(value.g); - bool originalPosHasSeenNonZero = false; +#if vtkBlendMode == COMPOSITE_BLEND + // now map through opacity and color + vec3 firstPosVC = rayOriginVC + minDistance * rayDirVC; + vec4 firstColor = getColorAtPos(firstPosVC); - uint bitmask = 0u; + // handle very thin volumes + if (raySteps <= 1.0) { + firstColor.a = 1.0 - pow(1.0 - firstColor.a, raySteps); + gl_FragData[0] = firstColor; + return; + } - if (segmentIndex != 0) { - // Tried using the segment index in an boolean array but reading - // from the array by dynamic indexing was horrondously slow - // so use bit masking instead and assign 1 to the bit corresponding to the segment index - // and later check if the bit is set via bit operations - setBit(segmentIndex); - } - - // Sample along the ray until MaximumSamplesValue, - // ending slightly inside the total distance - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - // If we have reached the last step, break - if (stepsTraveled + 1.0 >= raySteps) { break; } - - // compute the scalar - tValue = getTextureValue(posIS); - segmentIndex = int(tValue.g); - - if (segmentIndex != 0) { - originalPosHasSeenNonZero = true; - setBit(segmentIndex); - } + // first color only counts for `jitter` factor of the step + firstColor.a = 1.0 - pow(1.0 - firstColor.a, jitter); + vec4 color = vec4(firstColor.rgb * firstColor.a, firstColor.a); + vec3 posVC = firstPosVC + jitter * stepVC; + float stepsTraveled = jitter; - if (tValue.r > value.r) { - value = tValue; // Update the max value - maxPosIS = posIS; // Update the position where max occurred - } + while (stepsTraveled + 1.0 < raySteps) { + vec4 tColor = getColorAtPos(posVC); - // Otherwise, continue along the ray - stepsTraveled++; - posIS += stepIS; + color = color + vec4(tColor.rgb * tColor.a, tColor.a) * (1.0 - color.a); + stepsTraveled++; + posVC += stepVC; + if (color.a > 0.99) { + color.a = 1.0; + break; } + } - // Perform the last step along the ray using the - // residual distance - posIS = endIS; - tValue = getTextureValue(posIS); + if (color.a < 0.99 && (raySteps - stepsTraveled) > 0.0) { + vec3 endPosVC = rayOriginVC + maxDistance * rayDirVC; + vec4 tColor = getColorAtPos(endPosVC); + tColor.a = 1.0 - pow(1.0 - tColor.a, raySteps - stepsTraveled); - if (tValue.r > value.r) { - value = tValue; // Update the max value - maxPosIS = posIS; // Update the position where max occurred - } + float mix = (1.0 - color.a); + color = color + vec4(tColor.rgb * tColor.a, tColor.a) * mix; + } - // If we have not seen any non-zero segments, we can return early - // and grab color from the actual center value first component (image) - if (!originalPosHasSeenNonZero) { - gl_FragData[0] = getColorForValue(value, maxPosIS, tstep); - return; - } + gl_FragData[0] = vec4(color.rgb / color.a, color.a); +#endif - // probably we can make this configurable but for now we will use the same - // sample distance as the original sample distance - float neighborSampleDistanceIS = sampleDistanceIS; +#if vtkBlendMode == MAXIMUM_INTENSITY_BLEND || \ + vtkBlendMode == MINIMUM_INTENSITY_BLEND +// Find maximum/minimum intensity along the ray. - vec3 neighborRayStepsIS = stepIS; - float neighborRaySteps = raySteps; - bool shouldLookInAllNeighbors = false; +// Define the operation we will use (min or max) +#if vtkBlendMode == MAXIMUM_INTENSITY_BLEND +#define OP max +#else +#define OP min +#endif - float minVoxelSpacing = min(volumeSpacings[0], min(volumeSpacings[1], volumeSpacings[2])); - vec4 base = vec4(gl_FragCoord.x, gl_FragCoord.y, gl_FragCoord.z, gl_FragCoord.w); + vec3 posVC = rayOriginVC + minDistance * rayDirVC; + float stepsTraveled = 0.0; - vec4 baseXPlus = vec4(gl_FragCoord.x + 1.0, gl_FragCoord.y, gl_FragCoord.z, gl_FragCoord.w); - vec4 baseYPlus = vec4(gl_FragCoord.x, gl_FragCoord.y + 1.0, gl_FragCoord.z, gl_FragCoord.w); + // Find a value to initialize the selected variables + vec4 selectedValue; + vec3 selectedPosVC; + vec3 selectedPosIS; + int selectedVIdx; + { + int vIdx = minDistanceVolumeIdx; + vec3 posIS = VCtoIS(posVC, vIdx); + selectedValue = getTextureValue(posIS, vIdx); + selectedPosVC = posVC; + selectedPosIS = posIS; + selectedVIdx = vIdx; + } - vec3 baseWorld = fragCoordToWorld(base); - vec3 baseXPlusWorld = fragCoordToWorld(baseXPlus); - vec3 baseYPlusWorld = fragCoordToWorld(baseYPlus); + // If the clipping range is shorter than the sample distance + // we can skip the sampling loop along the ray. + if (raySteps <= 1.0) { + gl_FragData[0] = getColorForValue(selectedValue, selectedPosVC, + selectedPosIS, selectedVIdx); + return; + } - float XPlusDiff = length(baseXPlusWorld - baseWorld); - float YPlusDiff = length(baseYPlusWorld - baseWorld); + posVC += jitter * stepVC; + stepsTraveled += jitter; - float minFragSpacingWorld = min(XPlusDiff, YPlusDiff); + // Sample along the ray until maximumNumberOfSamples, + // ending slightly inside the total distance + for (int i = 0; i < maximumNumberOfSamples; ++i) { + // If we have reached the last step, break + if (stepsTraveled + 1.0 >= raySteps) { + break; + } - for (int s = 1; s < MAX_SEGMENT_INDEX; s++) { - // bail out quickly if the segment index has not - // been seen by the center segment - if (!isBitSet(s)) { - continue; + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + if (any(lessThan(posIS, vec3(0.0))) || + any(greaterThan(posIS, vec3(1.0)))) { + continue; } - // Use texture sampling for outlineThickness so that we can have - // per segment thickness - float textureCoordinate = float(s - 1) / 1024.0; - float textureValue = texture2D(ttexture, vec2(textureCoordinate, 0.5)).r; + // Get selected values + vec4 previousSelectedValue = selectedValue; + vec4 currentValue = getTextureValue(posIS, vIdx); + selectedValue = OP(selectedValue, currentValue); + if (previousSelectedValue != selectedValue) { + selectedPosVC = posVC; + selectedPosIS = posIS; + selectedVIdx = vIdx; + } + } - int actualThickness = int(textureValue * 255.0); + // Otherwise, continue along the ray + stepsTraveled++; + posVC += stepVC; + } - // check the extreme points in the neighborhood since there is a better - // chance of finding the edge there, so that we can bail out - // faster if we find the edge - bool onEdge = - checkOnEdgeForNeighbor(-actualThickness, -actualThickness, s, stepIS) || - checkOnEdgeForNeighbor(actualThickness, actualThickness, s, stepIS) || - checkOnEdgeForNeighbor(actualThickness, -actualThickness, s, stepIS) || - checkOnEdgeForNeighbor(-actualThickness, +actualThickness, s, stepIS); + // Perform the last step along the ray using the + // residual distance + posVC = rayOriginVC + maxDistance * rayDirVC; + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + // Epsilon margin to make sure that the selectedValue is initialized + if (any(lessThan(posIS, vec3(-EPSILON))) || + any(greaterThan(posIS, vec3(1.0 + EPSILON)))) { + continue; + } + posIS = clamp(posIS, 0.0, 1.0); + + // Get selected values + vec4 previousSelectedValue = selectedValue; + vec4 currentValue = getTextureValue(posIS, vIdx); + selectedValue = OP(selectedValue, currentValue); + if (previousSelectedValue != selectedValue) { + selectedPosVC = posVC; + selectedPosIS = posIS; + selectedVIdx = vIdx; + } + } - if (onEdge) { - return; - } + gl_FragData[0] = getColorForValue(selectedValue, selectedPosVC, selectedPosIS, + selectedVIdx); +#endif - // since the next step is computationally expensive, we need to perform - // some optimizations to avoid it if possible. One of the optimizations - // is to check the whether the minimum of the voxel spacing is greater than - // the 2 * the thickness of the outline segment. If that is the case - // then we can safely skip the next step since we can be sure that the - // the previous 4 checks on the extreme points would caught the entirety - // of the all the fragments inside. i.e., this happens when we zoom out, - if (minVoxelSpacing > (2.0 * float(actualThickness) - 1.0) * minFragSpacingWorld) { - continue; - } - - // Loop through the rest, skipping the processed extremes and the center - for (int i = -actualThickness; i <= actualThickness; i++) { - for (int j = -actualThickness; j <= actualThickness; j++) { - if (i == 0 && j == 0) continue; // Skip the center - if (abs(i) == actualThickness && abs(j) == actualThickness) continue; // Skip corners - if (checkOnEdgeForNeighbor(i, j, s, stepIS )) { - return; - } - } - } +#if vtkBlendMode == ADDITIVE_INTENSITY_BLEND || \ + vtkBlendMode == AVERAGE_INTENSITY_BLEND + vec4 sum = vec4(0.); +#if vtkBlendMode == AVERAGE_INTENSITY_BLEND + float totalWeight = 0.0; +#endif + vec3 posVC = rayOriginVC + minDistance * rayDirVC; + float stepsTraveled = 0.0; + int lastSampledVolumeIdx = 0; + + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + if (any(lessThan(posIS, vec3(0.0))) || any(greaterThan(posIS, vec3(1.0)))) { + continue; } - - vec3 tColor0 = texture2D(ctexture, vec2(value.r * cscale0 + cshift0, height0)).rgb; - float pwfValue0 = texture2D(otexture, vec2(value.r * oscale0 + oshift0, height0)).r; - gl_FragData[0] = vec4(tColor0, pwfValue0); - #endif - #if vtkBlendMode == 0 // COMPOSITE_BLEND - // now map through opacity and color - tColor = getColorForValue(tValue, posIS, tstep); - - // handle very thin volumes - if (raySteps <= 1.0) - { - tColor.a = 1.0 - pow(1.0 - tColor.a, raySteps); - gl_FragData[0] = tColor; - return; + vec4 value = getTextureValue(posIS, vIdx); + if (valueWithinScalarRange(value, vIdx)) { + sum += value; + totalWeight++; + lastSampledVolumeIdx = vIdx; } + } - tColor.a = 1.0 - pow(1.0 - tColor.a, jitter); - color = vec4(tColor.rgb*tColor.a, tColor.a); - posIS += (jitter*stepIS); + if (raySteps <= 1.0) { + vec3 posIS = VCtoIS(posVC, lastSampledVolumeIdx); + gl_FragData[0] = getColorForValue(sum, posVC, posIS, lastSampledVolumeIdx); + return; + } - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - if (stepsTraveled + 1.0 >= raySteps) { break; } + posVC += jitter * stepVC; + stepsTraveled += jitter; - // compute the scalar - tValue = getTextureValue(posIS); + // Sample along the ray until maximumNumberOfSamples, + // ending slightly inside the total distance + for (int i = 0; i < maximumNumberOfSamples; ++i) { + // If we have reached the last step, break + if (stepsTraveled + 1.0 >= raySteps) { + break; + } - // now map through opacity and color - tColor = getColorForValue(tValue, posIS, tstep); + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + if (any(lessThan(posIS, vec3(0.0))) || + any(greaterThan(posIS, vec3(1.0)))) { + continue; + } + vec4 value = getTextureValue(posIS, vIdx); + // One can control the scalar range by setting the AverageIPScalarRange to + // disregard scalar values, not in the range of interest, from the average + // computation. Notes: + // - We are comparing all values in the texture to see if any of them + // are outside of the scalar range. In the future we might want to allow + // scalar ranges for each component. + if (valueWithinScalarRange(value, vIdx)) { + sum += value; + totalWeight++; + lastSampledVolumeIdx = vIdx; + } + } - float mix = (1.0 - color.a); + stepsTraveled++; + posVC += stepVC; + } - // this line should not be needed but nvidia seems to not handle - // the break correctly on windows/chrome 58 angle - //mix = mix * sign(max(raySteps - stepsTraveled - 1.0, 0.0)); + // Perform the last step along the ray using the + // residual distance + posVC = rayOriginVC + maxDistance * rayDirVC; - color = color + vec4(tColor.rgb*tColor.a, tColor.a)*mix; - stepsTraveled++; - posIS += stepIS; - if (color.a > 0.99) { color.a = 1.0; break; } + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + if (any(lessThan(posIS, vec3(0.0))) || any(greaterThan(posIS, vec3(1.0)))) { + continue; + } + vec4 value = getTextureValue(posIS, vIdx); + if (valueWithinScalarRange(value, vIdx)) { + sum += value; + totalWeight++; + lastSampledVolumeIdx = vIdx; } + } - if (color.a < 0.99 && (raySteps - stepsTraveled) > 0.0) - { - posIS = endIS; +#if vtkBlendMode == AVERAGE_INTENSITY_BLEND + sum /= vec4(totalWeight, totalWeight, totalWeight, 1.0); +#endif + + vec3 posIS = VCtoIS(posVC, lastSampledVolumeIdx); + gl_FragData[0] = getColorForValue(sum, posVC, posIS, lastSampledVolumeIdx); +#endif - // compute the scalar - tValue = getTextureValue(posIS); +#if vtkBlendMode == RADON_TRANSFORM_BLEND + float normalizedRayIntensity = 1.0; + vec3 posVC = rayOriginVC + minDistance * rayDirVC; + float stepsTraveled = 0.0; + + // handle very thin volumes + if (raySteps <= 1.0) { + int vIdx = minDistanceVolumeIdx; + vec3 posIS = VCtoIS(posVC, vIdx); + vec4 tValue = getTextureValue(posIS, vIdx); + normalizedRayIntensity -= raySteps * sampleDistance * + getOpacityFromTexture(tValue.r, 0, vIdx, 0.5); + gl_FragData[0] = + vec4(getColorFromTexture(normalizedRayIntensity, 0, vIdx, 0.5), 1.0); + return; + } - // now map through opacity and color - tColor = getColorForValue(tValue, posIS, tstep); - tColor.a = 1.0 - pow(1.0 - tColor.a, raySteps - stepsTraveled); + posVC += jitter * stepVC; + stepsTraveled += jitter; - float mix = (1.0 - color.a); - color = color + vec4(tColor.rgb*tColor.a, tColor.a)*mix; + for (int i = 0; i < maximumNumberOfSamples; ++i) { + if (stepsTraveled + 1.0 >= raySteps) { + break; } - gl_FragData[0] = vec4(color.rgb/color.a, color.a); - #endif - #if vtkBlendMode == 1 || vtkBlendMode == 2 - // MAXIMUM_INTENSITY_BLEND || MINIMUM_INTENSITY_BLEND - // Find maximum/minimum intensity along the ray. - - // Define the operation we will use (min or max) - #if vtkBlendMode == 1 - #define OP max - #else - #define OP min - #endif - - // If the clipping range is shorter than the sample distance - // we can skip the sampling loop along the ray. - if (raySteps <= 1.0) - { - gl_FragData[0] = getColorForValue(tValue, posIS, tstep); - return; + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + vec3 posIS = VCtoIS(posVC, vIdx); + if (any(lessThan(posIS, vec3(0.0))) || + any(greaterThan(posIS, vec3(1.0)))) { + continue; + } + vec4 value = getTextureValue(posIS, vIdx); + // Convert scalar value to normalizedRayIntensity coefficient and + // accumulate normalizedRayIntensity + normalizedRayIntensity -= + sampleDistance * getOpacityFromTexture(value.r, 0, vIdx, 0.5); } - vec4 value = tValue; - posIS += (jitter*stepIS); + posVC += stepVC; + stepsTraveled++; + } - // Sample along the ray until MaximumSamplesValue, - // ending slightly inside the total distance - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - // If we have reached the last step, break - if (stepsTraveled + 1.0 >= raySteps) { break; } + // map normalizedRayIntensity to color + int vIdx = 0; + gl_FragData[0] = + vec4(getColorFromTexture(normalizedRayIntensity, 0, vIdx, 0.5), 1.0); +#endif - // compute the scalar - tValue = getTextureValue(posIS); +#if vtkBlendMode == LABELMAP_EDGE_PROJECTION_BLEND + // Only works with a single volume + const int vIdx = 0; + vec3 posVC = rayOriginVC + minDistance * rayDirVC; + float stepsTraveled = 0.0; + vec3 posIS = VCtoIS(posVC, vIdx); + vec4 tValue = getTextureValue(posIS, vIdx); + if (raySteps <= 1.0) { + gl_FragData[0] = getColorForValue(tValue, posVC, posIS, vIdx); + return; + } - // Update the maximum value if necessary - value = OP(tValue, value); + vec3 stepIS = rotateToIS(stepVC, vIdx) * volumes[vIdx].inverseSize; + vec4 value = tValue; + posIS += jitter * stepIS; + stepsTraveled += jitter; + vec3 maxPosIS = posIS; // Store the position of the max value + int segmentIndex = int(value.g); + bool originalPosHasSeenNonZero = false; + + if (segmentIndex != 0) { + // Tried using the segment index in an boolean array but reading + // from the array by dynamic indexing was horrondously slow + // so use bit masking instead and assign 1 to the bit corresponding to the + // segment index and later check if the bit is set via bit operations + setLabelOutlineBit(segmentIndex); + } - // Otherwise, continue along the ray - stepsTraveled++; - posIS += stepIS; + // Sample along the ray until maximumNumberOfSamples, + // ending slightly inside the total distance + for (int i = 0; i < maximumNumberOfSamples; ++i) { + // If we have reached the last step, break + if (stepsTraveled + 1.0 >= raySteps) { + break; } - // Perform the last step along the ray using the - // residual distance - posIS = endIS; - tValue = getTextureValue(posIS); - value = OP(tValue, value); - - // Now map through opacity and color - gl_FragData[0] = getColorForValue(value, posIS, tstep); - #endif - #if vtkBlendMode == 3 || vtkBlendMode == 4 //AVERAGE_INTENSITY_BLEND || ADDITIVE_BLEND - vec4 sum = vec4(0.); + // compute the scalar + tValue = getTextureValue(posIS, vIdx); + segmentIndex = int(tValue.g); - if (valueWithinScalarRange(tValue, ipScalarRangeMin, ipScalarRangeMax)) { - sum += tValue; + if (segmentIndex != 0) { + originalPosHasSeenNonZero = true; + setLabelOutlineBit(segmentIndex); } - if (raySteps <= 1.0) { - gl_FragData[0] = getColorForValue(sum, posIS, tstep); - return; + if (tValue.r > value.r) { + value = tValue; // Update the max value + maxPosIS = posIS; // Update the position where max occurred } - posIS += (jitter*stepIS); - - // Sample along the ray until MaximumSamplesValue, - // ending slightly inside the total distance - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - // If we have reached the last step, break - if (stepsTraveled + 1.0 >= raySteps) { break; } - - // compute the scalar - tValue = getTextureValue(posIS); - - // One can control the scalar range by setting the AverageIPScalarRange to disregard scalar values, not in the range of interest, from the average computation. - // Notes: - // - We are comparing all values in the texture to see if any of them - // are outside of the scalar range. In the future we might want to allow - // scalar ranges for each component. - if (valueWithinScalarRange(tValue, ipScalarRangeMin, ipScalarRangeMax)) { - // Sum the values across each step in the path - sum += tValue; - } - stepsTraveled++; - posIS += stepIS; - } + // Otherwise, continue along the ray + stepsTraveled++; + posIS += stepIS; + } - // Perform the last step along the ray using the - // residual distance - posIS = endIS; + // Perform the last step along the ray using the + // residual distance + posIS = VCtoIS(rayOriginVC + maxDistance * rayDirVC, vIdx); + tValue = getTextureValue(posIS, vIdx); - // compute the scalar - tValue = getTextureValue(posIS); + if (tValue.r > value.r) { + value = tValue; // Update the max value + maxPosIS = posIS; // Update the position where max occurred + } - // One can control the scalar range by setting the IPScalarRange to disregard scalar values, not in the range of interest, from the average computation - if (valueWithinScalarRange(tValue, ipScalarRangeMin, ipScalarRangeMax)) { - sum += tValue; + // If we have not seen any non-zero segments, we can return early + // and grab color from the actual center value first component (image) + if (!originalPosHasSeenNonZero) { + vec3 maxPosVC = IStoVC(maxPosIS, vIdx); + gl_FragData[0] = getColorForValue(value, maxPosVC, maxPosIS, vIdx); + return; + } - stepsTraveled++; - } + vec3 neighborRayStepsIS = stepIS; + float neighborRaySteps = raySteps; + bool shouldLookInAllNeighbors = false; - #if vtkBlendMode == 3 // Average - sum /= vec4(stepsTraveled, stepsTraveled, stepsTraveled, 1.0); - #endif - - gl_FragData[0] = getColorForValue(sum, posIS, tstep); - #endif - #if vtkBlendMode == 5 // RADON - float normalizedRayIntensity = 1.0; - - // handle very thin volumes - if (raySteps <= 1.0) - { - tValue = getTextureValue(posIS); - normalizedRayIntensity = normalizedRayIntensity - sampleDistance*texture2D(otexture, vec2(tValue.r * oscale0 + oshift0, 0.5)).r; - gl_FragData[0] = texture2D(ctexture, vec2(normalizedRayIntensity, 0.5)); - return; - } + vec3 volumeSpacings = volumes[vIdx].spacing; + float minVoxelSpacing = + min(volumeSpacings[0], min(volumeSpacings[1], volumeSpacings[2])); + vec4 base = + vec4(gl_FragCoord.x, gl_FragCoord.y, gl_FragCoord.z, gl_FragCoord.w); - posIS += (jitter*stepIS); + vec4 baseXPlus = vec4(gl_FragCoord.x + 1.0, gl_FragCoord.y, gl_FragCoord.z, + gl_FragCoord.w); + vec4 baseYPlus = vec4(gl_FragCoord.x, gl_FragCoord.y + 1.0, gl_FragCoord.z, + gl_FragCoord.w); - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - if (stepsTraveled + 1.0 >= raySteps) { break; } + vec3 baseWorld = fragCoordToWorld(base, vIdx); + vec3 baseXPlusWorld = fragCoordToWorld(baseXPlus, vIdx); + vec3 baseYPlusWorld = fragCoordToWorld(baseYPlus, vIdx); - // compute the scalar value - tValue = getTextureValue(posIS); + float XPlusDiff = length(baseXPlusWorld - baseWorld); + float YPlusDiff = length(baseYPlusWorld - baseWorld); - // Convert scalar value to normalizedRayIntensity coefficient and accumulate normalizedRayIntensity - normalizedRayIntensity = normalizedRayIntensity - sampleDistance*texture2D(otexture, vec2(tValue.r * oscale0 + oshift0, 0.5)).r; + float minFragSpacingWorld = min(XPlusDiff, YPlusDiff); - posIS += stepIS; - stepsTraveled++; + for (int s = 1; s < MAX_SEGMENT_INDEX; s++) { + // bail out quickly if the segment index has not + // been seen by the center segment + if (!isLabelOutlineBitSet(s)) { + continue; } - // map normalizedRayIntensity to color - gl_FragData[0] = texture2D(ctexture, vec2(normalizedRayIntensity , 0.5)); + // Use texture sampling for outlineThickness so that we can have + // per segment thickness + float textureCoordinate = float(s - 1) / 1024.0; + float textureValue = + texture2D(labelOutlineThicknessTexture, vec2(textureCoordinate, 0.5)).r; + + int actualThickness = int(textureValue * 255.0); + + // check the extreme points in the neighborhood since there is a better + // chance of finding the edge there, so that we can bail out + // faster if we find the edge + bool onEdge = checkOnEdgeForNeighbor(-actualThickness, -actualThickness, s, + stepIS, vIdx) || + checkOnEdgeForNeighbor(actualThickness, actualThickness, s, + stepIS, vIdx) || + checkOnEdgeForNeighbor(actualThickness, -actualThickness, s, + stepIS, vIdx) || + checkOnEdgeForNeighbor(-actualThickness, +actualThickness, s, + stepIS, vIdx); + + if (onEdge) { + return; + } - #endif -} + // since the next step is computationally expensive, we need to perform + // some optimizations to avoid it if possible. One of the optimizations + // is to check the whether the minimum of the voxel spacing is greater than + // the 2 * the thickness of the outline segment. If that is the case + // then we can safely skip the next step since we can be sure that the + // the previous 4 checks on the extreme points would caught the entirety + // of the all the fragments inside. i.e., this happens when we zoom out, + if (minVoxelSpacing > + (2.0 * float(actualThickness) - 1.0) * minFragSpacingWorld) { + continue; + } -//======================================================================= -// Compute a new start and end point for a given ray based -// on the provided bounded clipping plane (aka a rectangle) -void getRayPointIntersectionBounds( - vec3 rayPos, vec3 rayDir, - vec3 planeDir, float planeDist, - inout vec2 tbounds, vec3 vPlaneX, vec3 vPlaneY, - float vSize1, float vSize2) -{ - float result = dot(rayDir, planeDir); - if (abs(result) < 1e-6) - { - return; + // Loop through the rest, skipping the processed extremes and the center + for (int i = -actualThickness; i <= actualThickness; i++) { + for (int j = -actualThickness; j <= actualThickness; j++) { + if (i == 0 && j == 0) + continue; // Skip the center + if (abs(i) == actualThickness && abs(j) == actualThickness) + continue; // Skip corners + if (checkOnEdgeForNeighbor(i, j, s, stepIS, vIdx)) { + return; + } + } + } } - result = -1.0 * (dot(rayPos, planeDir) + planeDist) / result; - vec3 xposVC = rayPos + rayDir*result; - vec3 vxpos = xposVC - vOriginVC; - vec2 vpos = vec2( - dot(vxpos, vPlaneX), - dot(vxpos, vPlaneY)); - - // on some apple nvidia systems this does not work - // if (vpos.x < 0.0 || vpos.x > vSize1 || - // vpos.y < 0.0 || vpos.y > vSize2) - // even just - // if (vpos.x < 0.0 || vpos.y < 0.0) - // fails - // so instead we compute a value that represents in and out - //and then compute the return using this value - float xcheck = max(0.0, vpos.x * (vpos.x - vSize1)); // 0 means in bounds - float check = sign(max(xcheck, vpos.y * (vpos.y - vSize2))); // 0 means in bounds, 1 = out - - tbounds = mix( - vec2(min(tbounds.x, result), max(tbounds.y, result)), // in value - tbounds, // out value - check); // 0 in 1 out + + float sampleHeight = volumes[vIdx].transferFunctionsSampleHeight[0]; + vec3 tColor0 = getColorFromTexture(value.r, vIdx, 0, sampleHeight); + float pwfValue0 = getOpacityFromTexture(value.r, vIdx, 0, sampleHeight); + gl_FragData[0] = vec4(tColor0, pwfValue0); +#endif } //======================================================================= @@ -1831,40 +1717,17 @@ void getRayPointIntersectionBounds( // - optionally depth buffer values // - far clipping plane // compute the start/end distances of the ray we need to cast -vec2 computeRayDistances(vec3 rayDir, vec3 tdims) -{ - vec2 dists = vec2(100.0*camFar, -1.0); - - vec3 vSize = vSpacing*tdims; - - // all this is in View Coordinates - getRayPointIntersectionBounds(vertexVCVSOutput, rayDir, - vPlaneNormal0, vPlaneDistance0, dists, vPlaneNormal2, vPlaneNormal4, - vSize.y, vSize.z); - getRayPointIntersectionBounds(vertexVCVSOutput, rayDir, - vPlaneNormal1, vPlaneDistance1, dists, vPlaneNormal2, vPlaneNormal4, - vSize.y, vSize.z); - getRayPointIntersectionBounds(vertexVCVSOutput, rayDir, - vPlaneNormal2, vPlaneDistance2, dists, vPlaneNormal0, vPlaneNormal4, - vSize.x, vSize.z); - getRayPointIntersectionBounds(vertexVCVSOutput, rayDir, - vPlaneNormal3, vPlaneDistance3, dists, vPlaneNormal0, vPlaneNormal4, - vSize.x, vSize.z); - getRayPointIntersectionBounds(vertexVCVSOutput, rayDir, - vPlaneNormal4, vPlaneDistance4, dists, vPlaneNormal0, vPlaneNormal2, - vSize.x, vSize.y); - getRayPointIntersectionBounds(vertexVCVSOutput, rayDir, - vPlaneNormal5, vPlaneDistance5, dists, vPlaneNormal0, vPlaneNormal2, - vSize.x, vSize.y); +vec2 computeRayDistances(vec3 rayOriginVC, vec3 rayDirVC, int vIdx) { + vec2 dists = rayIntersectVolumeDistances(rayOriginVC, rayDirVC, vIdx); //VTK::ClipPlane::Impl // do not go behind front clipping plane - dists.x = max(0.0,dists.x); + dists.x = max(0.0, dists.x); // do not go PAST far clipping plane - float farDist = -camThick/rayDir.z; - dists.y = min(farDist,dists.y); + float farDist = -camThick / rayDirVC.z; + dists.y = min(farDist, dists.y); // Do not go past the zbuffer value if set // This is used for intermixing opaque geometry @@ -1873,45 +1736,22 @@ vec2 computeRayDistances(vec3 rayDir, vec3 tdims) return dists; } -//======================================================================= -// Compute the index space starting position (pos) and end -// position -// -void computeIndexSpaceValues(out vec3 pos, out vec3 endPos, vec3 rayDir, vec2 dists) -{ - // compute starting and ending values in volume space - pos = vertexVCVSOutput + dists.x*rayDir; - pos = pos - vOriginVC; - // convert to volume basis and origin - pos = vec3( - dot(pos, vPlaneNormal0), - dot(pos, vPlaneNormal2), - dot(pos, vPlaneNormal4)); - - endPos = vertexVCVSOutput + dists.y*rayDir; - endPos = endPos - vOriginVC; - endPos = vec3( - dot(endPos, vPlaneNormal0), - dot(endPos, vPlaneNormal2), - dot(endPos, vPlaneNormal4)); - - float delta = length(endPos - pos); - - pos *= vVCToIJK; - endPos *= vVCToIJK; - - float delta2 = length(endPos - pos); - sampleDistanceIS = sampleDistance*delta2/delta; - #ifdef VolumeShadowOn - sampleDistanceISVS = sampleDistanceIS * volumeShadowSamplingDistFactor; - #endif +float getFragmentSeed() { + // This first noise has a diagonal pattern + float firstNoise = + fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453); + // This second noise is made out of blocks of CPU generated noise + float secondNoise = texture2D(jtexture, gl_FragCoord.xy / 32.0).r; + // Combine the two sources of noise in a way that the distribution is uniform + // in [0,1[ + float noiseSum = firstNoise + secondNoise; + return noiseSum < 1.0 ? noiseSum : noiseSum - 1.0; } -void main() -{ +void main() { + fragmentSeed = getFragmentSeed(); - if (cameraParallel == 1) - { + if (cameraParallel == 1) { // Camera is parallel, so the rayDir is just the direction of the camera. rayDirVC = vec3(0.0, 0.0, -1.0); } else { @@ -1919,23 +1759,27 @@ void main() rayDirVC = normalize(vertexVCVSOutput); } - vec3 tdims = vec3(volumeDimensions); - - // compute the start and end points for the ray - vec2 rayStartEndDistancesVC = computeRayDistances(rayDirVC, tdims); - - // do we need to composite? aka does the ray have any length - // If not, bail out early - if (rayStartEndDistancesVC.y <= rayStartEndDistancesVC.x) - { - discard; + vec2 mergedStartEndDistancesVC = vec2(infinity, -infinity); + vec3 rayOriginVC = vertexVCVSOutput; + int minDistanceVolumeIdx = 0; + for (int vIdx = 0; vIdx < vtkNumberOfVolumes; ++vIdx) { + // compute the start and end points for the ray + vec2 rayStartEndDistancesVC = + computeRayDistances(rayOriginVC, rayDirVC, vIdx); + if (rayStartEndDistancesVC.y <= rayStartEndDistancesVC.x || + rayStartEndDistancesVC.y <= 0.0) { + continue; + } + if (rayStartEndDistancesVC.x < mergedStartEndDistancesVC.x) { + mergedStartEndDistancesVC.x = rayStartEndDistancesVC.x; + minDistanceVolumeIdx = vIdx; + } + if (rayStartEndDistancesVC.y > mergedStartEndDistancesVC.y) { + mergedStartEndDistancesVC.y = rayStartEndDistancesVC.y; + } } - // IS = Index Space - vec3 posIS; - vec3 endIS; - computeIndexSpaceValues(posIS, endIS, rayDirVC, rayStartEndDistancesVC); - // Perform the blending operation along the ray - applyBlend(posIS, endIS, tdims); + applyBlend(rayOriginVC, rayDirVC, mergedStartEndDistancesVC.x, + mergedStartEndDistancesVC.y, minDistanceVolumeIdx); } diff --git a/Sources/Rendering/WebGPU/VolumePassFSQ/index.js b/Sources/Rendering/WebGPU/VolumePassFSQ/index.js index 6138e3ae9a6..6d962678423 100644 --- a/Sources/Rendering/WebGPU/VolumePassFSQ/index.js +++ b/Sources/Rendering/WebGPU/VolumePassFSQ/index.js @@ -723,10 +723,10 @@ function vtkWebGPUVolumePassFSQ(publicAPI, model) { // handle filteringMode const tScale = model.textureViews[vidx + 4].getTexture().getScale(); - const ipScalarRange = volMapr.getIpScalarRange(); + const ipScalarRange = actor.getProperty().getIpScalarRange(); ipScalarRangeArray[vidx * 4] = ipScalarRange[0] / tScale; ipScalarRangeArray[vidx * 4 + 1] = ipScalarRange[1] / tScale; - ipScalarRangeArray[vidx * 4 + 2] = volMapr.getFilterMode(); + ipScalarRangeArray[vidx * 4 + 2] = actor.getProperty().getFilterMode(); } model.SSBO.addEntry('SCTCMatrix', 'mat4x4'); model.SSBO.addEntry('planeNormals', 'mat4x4'); diff --git a/Sources/macros.js b/Sources/macros.js index e6dd4e7f363..0268a382dd5 100644 --- a/Sources/macros.js +++ b/Sources/macros.js @@ -912,7 +912,7 @@ export function algo(publicAPI, model, numberOfInputs, numberOfOutputs) { count++; } } - if (publicAPI.shouldUpdate() && publicAPI.requestData) { + if (publicAPI.requestData && publicAPI.shouldUpdate()) { publicAPI.requestData(ins, model.output); } };