diff --git a/Examples/Rendering/QuadView/index.js b/Examples/Rendering/QuadView/index.js index 7eb6fbf3204..777f7ec93c3 100644 --- a/Examples/Rendering/QuadView/index.js +++ b/Examples/Rendering/QuadView/index.js @@ -158,12 +158,12 @@ function createVolumeView(renderer, source) { .reduce((a, b) => a + b, 0) ); mapper.setSampleDistance(sampleDistance / 2.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(); volProp.setScalarOpacityUnitDistance( diff --git a/Examples/Volume/VolumeMapperLightAndShadow/index.js b/Examples/Volume/VolumeMapperLightAndShadow/index.js index 963b17b6713..8fadc10aaec 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,11 +81,23 @@ function createVolumeShadowViewer(rootContainer, fileContents) { const source = vtiReader.getOutputData(0); const actor = vtkVolume.newInstance(); - const actorProperty = actor.getProperty(); + const actorProperty = actor.getProperty(0); const mapper = vtkVolumeMapper.newInstance(); actor.setMapper(mapper); - mapper.setInputData(source); + mapper.addInputData(source); + + for (let i = 0; i < 0; ++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(); @@ -125,6 +124,7 @@ function createVolumeShadowViewer(rootContainer, fileContents) { .reduce((a, b) => a + b, 0) ); mapper.setSampleDistance(sampleDistance / 2.5); + mapper.setVolumeShadowSamplingDistFactor(5.0); // Add transfer function const lookupTable = vtkColorTransferFunction.newInstance(); @@ -135,7 +135,6 @@ function createVolumeShadowViewer(rootContainer, fileContents) { // Set actor properties actorProperty.setComputeNormalFromOpacity(false); actorProperty.setGlobalIlluminationReach(0.0); - actorProperty.setVolumeShadowSamplingDistFactor(5.0); actorProperty.setVolumetricScatteringBlending(0.0); actorProperty.setInterpolationTypeToLinear(); actorProperty.setScalarOpacityUnitDistance( @@ -161,7 +160,6 @@ function createVolumeShadowViewer(rootContainer, fileContents) { actorProperty.setSpecularPower(0.0); actorProperty.setUseLabelOutline(false); actorProperty.setLabelOutlineThickness(2); - renderer.addActor(actor); // Control UI for sample distance, transfer function, and shadow on/off const controllerWidget = vtkVolumeController.newInstance({ @@ -201,7 +199,7 @@ function createVolumeShadowViewer(rootContainer, fileContents) { } function updateSD(e) { const sd = Number(e.target.value); - actorProperty.setVolumeShadowSamplingDistFactor(sd); + mapper.setVolumeShadowSamplingDistFactor(sd); renderWindow.render(); } function updateAT(e) { @@ -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 611a1c1c255..5a77c53ebf7 100644 --- a/Examples/Volume/WebXRChestCTBlendedCVR/index.js +++ b/Examples/Volume/WebXRChestCTBlendedCVR/index.js @@ -109,7 +109,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { actor.getProperty().setSpecular(0.0); actor.getProperty().setGlobalIlluminationReach(0.1); actor.getProperty().setVolumetricScatteringBlending(0.5); - actor.getProperty().setVolumeShadowSamplingDistFactor(1.0); + mapper.setVolumeShadowSamplingDistFactor(1.0); mapper.setAutoAdjustSampleDistances(false); // Set up rendering diff --git a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js index 59596c13407..9ce90298e58 100644 --- a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js +++ b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js @@ -141,7 +141,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { actor.getProperty().setSpecular(0.0); actor.getProperty().setGlobalIlluminationReach(1.0); actor.getProperty().setVolumetricScatteringBlending(1.0); - actor.getProperty().setVolumeShadowSamplingDistFactor(1.0); + mapper.setVolumeShadowSamplingDistFactor(1.0); mapper.setAutoAdjustSampleDistances(false); // Set up rendering diff --git a/Examples/Volume/WebXRHeadGradientCVR/index.js b/Examples/Volume/WebXRHeadGradientCVR/index.js index 15549f9628d..6be0f590198 100644 --- a/Examples/Volume/WebXRHeadGradientCVR/index.js +++ b/Examples/Volume/WebXRHeadGradientCVR/index.js @@ -138,7 +138,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { actor.getProperty().setShade(true); actor.getProperty().setGlobalIlluminationReach(0.0); actor.getProperty().setVolumetricScatteringBlending(0.0); - actor.getProperty().setVolumeShadowSamplingDistFactor(1.0); + mapper.setVolumeShadowSamplingDistFactor(1.0); mapper.setAutoAdjustSampleDistances(false); // Set up rendering diff --git a/Sources/Rendering/Core/VolumeMapper/Constants.d.ts b/Sources/Rendering/Core/VolumeMapper/Constants.d.ts index 568702ed087..eee908bef73 100644 --- a/Sources/Rendering/Core/VolumeMapper/Constants.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/Constants.d.ts @@ -1,8 +1,14 @@ -// Don't use the constants from this file +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, +} -// Prefer constants from volume property: -export { - default, - BlendMode, - FilterMode, -} from '../VolumeProperty/Constants'; +declare const _default: { + BlendMode: typeof BlendMode; +}; +export default _default; diff --git a/Sources/Rendering/Core/VolumeMapper/Constants.js b/Sources/Rendering/Core/VolumeMapper/Constants.js index e253a48af36..bd1c872fa0b 100644 --- a/Sources/Rendering/Core/VolumeMapper/Constants.js +++ b/Sources/Rendering/Core/VolumeMapper/Constants.js @@ -1,13 +1,11 @@ -// Don't use the constants from this file - -// Prefer constants from volume property: -import Constants, { - BlendMode as OriginalBlendMode, - FilterMode as OriginalFilterMode, -} from 'vtk.js/Sources/Rendering/Core/VolumeProperty/Constants'; - -export const BlendMode = OriginalBlendMode; - -export const FilterMode = OriginalFilterMode; - -export default Constants; +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 default { BlendMode }; diff --git a/Sources/Rendering/Core/VolumeMapper/example/index.js b/Sources/Rendering/Core/VolumeMapper/example/index.js index 5a82c617c07..853173db204 100644 --- a/Sources/Rendering/Core/VolumeMapper/example/index.js +++ b/Sources/Rendering/Core/VolumeMapper/example/index.js @@ -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'; diff --git a/Sources/Rendering/Core/VolumeMapper/index.d.ts b/Sources/Rendering/Core/VolumeMapper/index.d.ts index 41727af45e3..77baa39278d 100755 --- a/Sources/Rendering/Core/VolumeMapper/index.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/index.d.ts @@ -41,6 +41,12 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D { */ getSampleDistance(): number; + /** + * Get the multipler for volume shadow sampling distance + * @default 5.0 + */ + getVolumeShadowSamplingDistFactor(): number; + /** * Sampling distance in the XY image dimensions. * Default value of 1 meaning 1 ray cast per pixel. If set to 0.5, 4 rays will be cast per pixel. @@ -113,6 +119,13 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D { */ setSampleDistance(sampleDistance: number): boolean; + /** + * 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; + /** * * @param imageSampleDistance diff --git a/Sources/Rendering/Core/VolumeMapper/index.js b/Sources/Rendering/Core/VolumeMapper/index.js index 989a605c78e..a4f169a48b8 100644 --- a/Sources/Rendering/Core/VolumeMapper/index.js +++ b/Sources/Rendering/Core/VolumeMapper/index.js @@ -45,7 +45,6 @@ const methodNamesMovedToVolumeProperties = [ 'getLAOKernelSize', 'getLocalAmbientOcclusion', 'getPreferSizeOverAccuracy', - 'getVolumeShadowSamplingDistFactor', 'getVolumetricScatteringBlending', 'setAnisotropy', 'setAverageIPScalarRange', @@ -61,7 +60,6 @@ const methodNamesMovedToVolumeProperties = [ 'setLAOKernelSize', 'setLocalAmbientOcclusion', 'setPreferSizeOverAccuracy', - 'setVolumeShadowSamplingDistFactor', 'setVolumetricScatteringBlending', ]; @@ -81,17 +79,13 @@ function vtkVolumeMapper(publicAPI, model) { // Set our className model.classHierarchy.push('vtkVolumeMapper'); + const superClass = { ...publicAPI }; + publicAPI.getBounds = () => { - 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 = [...publicAPI.getInputData().getBounds()]; return model.bounds; }; @@ -122,6 +116,9 @@ function vtkVolumeMapper(publicAPI, model) { publicAPI.getBlendModeAsString = () => macro.enumToString(BlendMode, model.blendMode); + publicAPI.setVolumeShadowSamplingDistFactor = (vsdf) => + superClass.setVolumeShadowSamplingDistFactor(vsdf >= 1.0 ? vsdf : 1.0); + // Instead of a "undefined is not a function" error, give more context and advice for these widely used methods methodNamesMovedToVolumeProperties.forEach((removedMethodName) => { const removedMethod = () => { @@ -149,6 +146,7 @@ const DEFAULT_VALUES = { initialInteractionScale: 1.0, interactionSampleDistanceFactor: 1.0, blendMode: BlendMode.COMPOSITE_BLEND, + volumeShadowSamplingDistFactor: 5.0, }; // ---------------------------------------------------------------------------- @@ -166,6 +164,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'initialInteractionScale', 'interactionSampleDistanceFactor', 'blendMode', + 'volumeShadowSamplingDistFactor', ]); macro.event(publicAPI, model, 'lightingActivated'); diff --git a/Sources/Rendering/Core/VolumeMapper/test/testColorMix.js b/Sources/Rendering/Core/VolumeMapper/test/testColorMix.js index 063efe26ea9..f009f7b9d05 100644 --- a/Sources/Rendering/Core/VolumeMapper/test/testColorMix.js +++ b/Sources/Rendering/Core/VolumeMapper/test/testColorMix.js @@ -138,17 +138,20 @@ test('Test Volume Rendering: custom shader code', async (t) => { originalValue: '//VTK::CustomColorMix', replaceFirst: false, replacementValue: ` - if (pwfValue1 > 0.5) { + float opacity1 = getOpacityFromTexture(tValue[1], 1, volume.transferFunctionsSampleHeight[1]); + if (opacity1 > 0.5) { return vec4(0.0, 1.0, 1.0, 0.1); } else { - mat4 normalMat = computeMat4Normal(posIS, tValue, tstep); - float opacity0 = pwfValue0; - #ifdef vtkGradientOpacityOn - float gof0 = computeGradientOpacityFactor(normalMat[0].a, goscale0, goshift0, gomin0, gomax0); - opacity0 *= gof0; + vec3 posIS = VCtoIS(posVC); + mat4 normalMat = computeMat4Normal(posIS, tValue); + float opacity0 = getOpacityFromTexture(tValue[0], 0, volume.transferFunctionsSampleHeight[0]); + vec3 color0 = getColorFromTexture(tValue[0], 0, volume.transferFunctionsSampleHeight[0]); + #if defined(EnabledGradientOpacity) + float gradientOpacity = computeGradientOpacityFactor(normal0.a, 0); + opacity0 *= gradientOpacity; #endif - tColor0 = applyAllLightning(tColor0, opacity0, posIS, normalMat[0]); - return vec4(tColor0, opacity0); + color0 = applyAllLightning(color0, opacity0, posVC, normalMat[0]); + return vec4(color0, opacity0); } `, replaceAll: false, diff --git a/Sources/Rendering/Core/VolumeProperty/Constants.d.ts b/Sources/Rendering/Core/VolumeProperty/Constants.d.ts index ad72d695085..92ea3466ce3 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,16 +21,10 @@ 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, -} -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, + // Add a `//VTK::CustomColorMix` tag to the Fragment shader + // See usage in file `testColorMix` and in function `setColorMixPreset` + CUSTOM = 3, } export declare enum FilterMode { @@ -45,7 +37,6 @@ 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 ff375c7c40e..166b22e0b2e 100644 --- a/Sources/Rendering/Core/VolumeProperty/Constants.js +++ b/Sources/Rendering/Core/VolumeProperty/Constants.js @@ -10,19 +10,10 @@ export const OpacityMode = { }; export const ColorMixPreset = { - CUSTOM: 0, + DEFAULT: 0, ADDITIVE: 1, COLORIZE: 2, -}; - -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, + CUSTOM: 3, }; export const FilterMode = { @@ -35,6 +26,5 @@ 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 261f3c86d77..d2e624833a4 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? @@ -393,12 +394,6 @@ export interface vtkVolumeProperty extends vtkObject { */ getGlobalIlluminationReach(): number; - /** - * Get the multipler for volume shadow sampling distance - * @default 5.0 - */ - getVolumeShadowSamplingDistFactor(): number; - /** * Get anisotropy of volume shadow scatter * @default 0.0 @@ -463,20 +458,6 @@ export interface vtkVolumeProperty extends vtkObject { */ 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. diff --git a/Sources/Rendering/Core/VolumeProperty/index.js b/Sources/Rendering/Core/VolumeProperty/index.js index 1c484ed7c5f..5574212cf01 100644 --- a/Sources/Rendering/Core/VolumeProperty/index.js +++ b/Sources/Rendering/Core/VolumeProperty/index.js @@ -4,7 +4,8 @@ import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransfe import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeProperty/Constants'; -const { InterpolationType, OpacityMode, FilterMode } = Constants; +const { InterpolationType, OpacityMode, FilterMode, ColorMixPreset } = + Constants; const { vtkErrorMacro } = macro; const VTK_MAX_VRCOMP = 4; @@ -271,9 +272,6 @@ function vtkVolumeProperty(publicAPI, model) { 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)); @@ -289,7 +287,7 @@ function vtkVolumeProperty(publicAPI, model) { // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { - colorMixPreset: null, + colorMixPreset: ColorMixPreset.DEFAULT, independentComponents: true, interpolationType: InterpolationType.FAST_LINEAR, shade: false, @@ -309,7 +307,6 @@ const DEFAULT_VALUES = { // volume shadow parameters volumetricScatteringBlending: 0.0, globalIlluminationReach: 0.0, - volumeShadowSamplingDistFactor: 5.0, anisotropy: 0.0, // local ambient occlusion localAmbientOcclusion: false, @@ -365,7 +362,6 @@ export function extend(publicAPI, model, initialValues = {}) { 'computeNormalFromOpacity', 'volumetricScatteringBlending', 'globalIlluminationReach', - 'volumeShadowSamplingDistFactor', 'anisotropy', 'localAmbientOcclusion', 'LAOKernelSize', diff --git a/Sources/Rendering/OpenGL/VolumeMapper/index.js b/Sources/Rendering/OpenGL/VolumeMapper/index.js index 0f8244404f0..73baf0655f6 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/index.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/index.js @@ -1,7 +1,7 @@ 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'; @@ -39,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)), + vecISToVCMatrix: 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 @@ -123,12 +56,8 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { // Set our className model.classHierarchy.push('vtkOpenGLVolumeMapper'); - function useIndependentComponents(actorProperty, image) { + function getUseIndependentComponents(actorProperty, numComp) { const iComps = actorProperty.getIndependentComponents(); - const numComp = image - ?.getPointData() - ?.getScalars() - ?.getNumberOfComponents(); const colorMixPreset = actorProperty.getColorMixPreset(); return (iComps && numComp >= 2) || !!colorMixPreset; } @@ -141,15 +70,53 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { ); } + // 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) { - [ - model._scalars, - model._scalarOpacityFunc, - model._colorTransferFunc, - model._labelOutlineThicknessArray, - ].forEach((coreObject) => - renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI) - ); + graphicsResourceReferenceCount + .keys() + .forEach((coreObject) => + renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI) + ); } publicAPI.buildPass = () => { @@ -209,183 +176,71 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { }; 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; - } + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::EnabledColorFunctions', + `#define EnableColorForValueFunctionId${model.previousState.colorForValueFunctionId}` + ).result; - const vtkImageLabelOutline = isLabelmapOutlineRequired(actorProps); - if (vtkImageLabelOutline === true) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::ImageLabelOutlineOn', - '#define vtkImageLabelOutlineOn' - ).result; + const enabledLightings = []; + if (model.previousState.surfaceLightingEnabled) { + enabledLightings.push('Surface'); } - - const LabelEdgeProjection = - model.renderable.getBlendMode() === - BlendMode.LABELMAP_EDGE_PROJECTION_BLEND; - - if (LabelEdgeProjection) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::LabelEdgeProjectionOn', - '#define vtkLabelEdgeProjectionOn' - ).result; + if (model.previousState.volumeLightingEnabled) { + enabledLightings.push('Volume'); } - - const numComp = model.scalarTexture.getComponents(); FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::NumComponents', - `#define vtkNumComponents ${numComp}` + '//VTK::EnabledLightings', + enabledLightings.map( + (lightingType) => `#define Enable${lightingType}Lighting` + ) ).result; - const useIndependentComps = useIndependentComponents( - actorProps, - model.currentInput - ); - if (useIndependentComps) { + if (model.previousState.multiTexturePerVolumeEnabled) { FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::IndependentComponentsOn', - '#define UseIndependentComponents' + '//VTK::EnabledMultiTexturePerVolume', + '#define EnabledMultiTexturePerVolume' ).result; } - // 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`); - } - } - - 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) { + if (model.previousState.useIndependentComponents) { FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::CustomComponentsColorMixOn', - '#define vtkCustomComponentsColorMix' + '//VTK::EnabledIndependentComponents', + '#define EnabledIndependentComponents' ).result; + } + + if (model.previousState.gradientOpacityEnabled) { FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::CustomComponentsColorMix::Impl', - colorMixCode + '//VTK::EnabledGradientOpacity', + '#define EnabledGradientOpacity' ).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::vtkProportionalComponents', + model.previousState.proportionalComponents + .map((component) => `#define vtkComponent${component}Proportional`) + .join('\n') ).result; - // set light complexity FSSource = vtkShaderProgram.substitute( FSSource, - '//VTK::LightComplexity', - `#define vtkLightComplexity ${model.lightComplexity}` + '//VTK::vtkForceNearestComponents', + model.previousState.forceNearestComponents + .map((component) => `#define vtkComponent${component}ForceNearest`) + .join('\n') ).result; - // set shadow blending flag - if (model.lightComplexity > 0) { - if (actorProps.getVolumetricScatteringBlending() > 0.0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::VolumeShadowOn', - `#define VolumeShadowOn` - ).result; - } - if (actorProps.getVolumetricScatteringBlending() < 1.0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::SurfaceShadowOn', - `#define SurfaceShadowOn` - ).result; - } - if ( - actorProps.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 (actorProps.getComputeNormalFromOpacity()) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::vtkComputeNormalFromOpacity', - `#define vtkComputeNormalFromOpacity` - ).result; - } - // if we have a ztexture then declare it and use it - if (model.zBufferTexture !== null) { + if (model.previousState.hasZBufferTexture) { FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::ZBuffer::Dec', [ 'uniform sampler2D zBufferTexture;', 'uniform float vpZWidth;', @@ -399,7 +254,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; } @@ -408,96 +263,37 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { FSSource = vtkShaderProgram.substitute( FSSource, '//VTK::BlendMode', - `${model.renderable.getBlendMode()}` + `${model.previousState.blendMode}` ).result; - shaders.Fragment = FSSource; - - publicAPI.replaceShaderLight(shaders, ren, actor); - publicAPI.replaceShaderClippingPlane(shaders, ren, actor); - }; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::NumberOfLights', + `${model.previousState.numberOfLights}` + ).result; - publicAPI.replaceShaderLight = (shaders, ren, actor) => { - if (model.lightComplexity === 0) { - return; - } - const vprop = actor.getProperty(); - let FSSource = shaders.Fragment; - // check for shadow maps - not implemented yet, skip - // const shadowFactor = ''; + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::MaxLaoKernelSize', + `${model.previousState.maxLaoKernelSize}` + ).result; - // to-do: single out the case when complexity = 1 + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::NumberOfComponents', + `${model.previousState.numberOfComponents}` + ).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::MaximumNumberOfSamples', + `${model.previousState.maximumNumberOfSamples}` ).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 (vprop.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 (vprop.getLocalAmbientOcclusion() && vprop.getAmbient() > 0.0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::LAO::Dec', - [ - `uniform int kernelRadius;`, - `uniform vec2 kernelSample[${vprop.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.previousState.numberOfClippingPlanes; + if (numberOfClippingPlanes > 0) { FSSource = vtkShaderProgram.substitute( FSSource, '//VTK::ClipPlane::Dec', @@ -516,8 +312,8 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { FSSource, '//VTK::ClipPlane::Impl', [ - `for(int i = 0; i < ${clipPlaneSize}; i++) {`, - ' float rayDirRatio = dot(rayDir, vClipPlaneNormals[i]);', + `for(int i = 0; i < ${numberOfClippingPlanes}; i++) {`, + ' float rayDirRatio = dot(rayDirVC, vClipPlaneNormals[i]);', ' float equationResult = dot(vertexVCVSOutput, vClipPlaneNormals[i]) + vClipPlaneDistances[i];', ' if (rayDirRatio == 0.0)', ' {', @@ -537,76 +333,148 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { shaders.Fragment = FSSource; }; - const recomputeLightComplexity = (actor, lights) => { - // do we need lighting? - let lightComplexity = 0; + 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 numberOfComponents = model.numberOfComponents; + const useIndependentComponents = model.useIndependentComponents; + + // The volume property that is used is always the first one + const volumeProperties = actor.getProperties(); + const firstValidInput = model.currentValidInputs[0]; + const firstVolumeProperty = volumeProperties[firstValidInput.inputIndex]; + + // There are two modes: + // - single volume with multiple components + // - multiple volumes with one component per volume + const multiTexturePerVolumeEnabled = numberOfValidInputs > 1; + + // Get maximum number of samples + const boundsMC = firstValidInput.imageData.getBounds(); + const maximumRayLength = vtkBoundingBox.getDiagonalLength(boundsMC); + const maximumNumberOfSamples = Math.ceil( + maximumRayLength / publicAPI.getCurrentSampleDistance(ren) + ); + 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.' + ); + } + + // Gradient opacity + const numberOfIndependantComponents = useIndependentComponents + ? numberOfComponents + : 1; + let gradientOpacityEnabled = false; + for (let i = 0; i < numberOfIndependantComponents; ++i) { + if (firstVolumeProperty.getUseGradientOpacity(i)) { + gradientOpacityEnabled = true; + break; + } + } + + // Get the max kernel size from volume properties that use LAO and + // that are linked to a valid input imageData + let maxLaoKernelSize = 0; + const kernelSize = firstVolumeProperty.getLAOKernelSize(); if ( - actor.getProperty().getShade() && - model.renderable.getBlendMode() === BlendMode.COMPOSITE_BLEND + kernelSize > maxLaoKernelSize && + firstVolumeProperty.getLocalAmbientOcclusion() && + firstVolumeProperty.getAmbient() > 0.0 ) { - // 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; - } - } + maxLaoKernelSize = kernelSize; + } - if ( - lightComplexity === 1 && - (model.numberOfLights > 1 || - light.getIntensity() !== 1.0 || - !light.lightTypeIsHeadLight()) - ) { - lightComplexity = 2; - } - if (lightComplexity < 3 && light.getPositional()) { - lightComplexity = 3; + const numberOfClippingPlanes = model.renderable.getClippingPlanes().length; + // These are from the buildShader function in vtkReplacementShaderMapper + const mapperShaderReplacements = + model.renderable.getViewSpecificProperties().OpenGL?.ShaderReplacements; + const renderPassShaderReplacements = + 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 colorForValueFunctionId = (() => { + // If labeloutline and not the edge labelmap, since in the edge labelmap blend + // we need the underlying data to sample through + if ( + blendMode !== BlendMode.LABELMAP_EDGE_PROJECTION_BLEND && + isLabelmapOutlineRequired(firstVolumeProperty) + ) { + return 5; + } + if (useIndependentComponents) { + switch (firstVolumeProperty.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(); + } + return 0; + })(); + + // Get which types of lighting are enabled + const surfaceLightingEnabled = + firstVolumeProperty.getVolumetricScatteringBlending() < 1.0; + const volumeLightingEnabled = + firstVolumeProperty.getVolumetricScatteringBlending() > 0.0; + + // Is any volume using ForceNearestInterpolation + let forceNearestInterpolationEnabled = false; + for (let component = 0; component < numberOfComponents; ++component) { + if (firstVolumeProperty.getForceNearestInterpolation(component)) { + forceNearestInterpolationEnabled = true; + break; + } } - }; - - publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => { - recomputeLightComplexity(actor, ren.getLights()); - - const numComp = model.scalarTexture.getComponents(); - - 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); - - const hasZBufferTexture = !!model.zBufferTexture; + // Define any proportional components + const proportionalComponents = []; + const forceNearestComponents = []; + for (let component = 0; component < numberOfComponents; component++) { + if ( + firstVolumeProperty.getOpacityMode(component) === + OpacityMode.PROPORTIONAL + ) { + proportionalComponents.push(component); + } + if (firstVolumeProperty.getForceNearestInterpolation(component)) { + forceNearestComponents.push(component); + } + } - const state = { - numComp, - maxSamples, - blendMode: model.renderable.getBlendMode(), + const currentState = { + numberOfComponents, + useIndependentComponents, + proportionalComponents, + forceNearestComponents, + blendMode, + numberOfLights, + numberOfValidInputs, + maximumNumberOfSamples, hasZBufferTexture, - // The actor MTime takes its properties into account - // When it changes, it means that the shaders need to be updated because either: - // - the actor has been updated - // - a different actor is used with this mapper - actorTime: actor.getMTime(), + maxLaoKernelSize, + numberOfClippingPlanes, + mapperShaderReplacements, + renderPassShaderReplacements, + colorForValueFunctionId, + surfaceLightingEnabled, + volumeLightingEnabled, + forceNearestInterpolationEnabled, + multiTexturePerVolumeEnabled, + gradientOpacityEnabled, }; // We need to rebuild the shader if one of these variables has changed, @@ -614,12 +482,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; @@ -694,47 +560,78 @@ 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 = actor.getProperty().getIpScalarRange(); + // Volume textures + model.scalarTextures.forEach((scalarTexture, component) => { + program.setUniformi( + `volumeTexture[${component}]`, + scalarTexture.getTextureUnit() + ); + }); - // 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++) { + const volumeProperties = actor.getProperties(); + const firstValidInput = model.currentValidInputs[0]; + const firstVolumeProperty = volumeProperties[firstValidInput.inputIndex]; + const ipScalarRange = firstVolumeProperty.getIpScalarRange(); + const minVals = new Float32Array(4); + const maxVals = new Float32Array(4); + const setMinMaxVal = (component, volInfo, volInfoIndex) => { + // 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) { // 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]; + minVals[component] = + ipScalarRange[0] * volInfo.dataComputedScale[volInfoIndex] + + volInfo.dataComputedOffset[volInfoIndex]; + maxVals[component] = + ipScalarRange[1] * volInfo.dataComputedScale[volInfoIndex] + + volInfo.dataComputedOffset[volInfoIndex]; // 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]; + minVals[component] = + (minVals[component] - volInfo.offset[volInfoIndex]) / + volInfo.scale[volInfoIndex]; + maxVals[component] = + (maxVals[component] - volInfo.offset[volInfoIndex]) / + volInfo.scale[volInfoIndex]; + } + }; + if (model.previousState.multiTexturePerVolumeEnabled) { + // Use the first component of all texture infos + model.scalarTextures.forEach((scalarTexture, component) => { + const volInfo = scalarTexture.getVolumeInfo(); + setMinMaxVal(component, volInfo, 0); + }); + } else { + // Use all components of the first texture info + const firstVolInfo = model.scalarTextures[0].getVolumeInfo(); + for (let component = 0; component < 4; ++component) { + setMinMaxVal(component, firstVolInfo, component); } - program.setUniform4f( - 'ipScalarRangeMin', - minVals[0], - minVals[1], - minVals[2], - minVals[3] - ); - program.setUniform4f( - 'ipScalarRangeMax', - maxVals[0], - maxVals[1], - maxVals[2], - maxVals[3] - ); } + const uniformPrefix = 'volume'; + program.setUniform4f( + `${uniformPrefix}.ipScalarRangeMin`, + minVals[0], + minVals[1], + minVals[2], + minVals[3] + ); + program.setUniform4f( + `${uniformPrefix}.ipScalarRangeMax`, + maxVals[0], + maxVals[1], + maxVals[2], + maxVals[3] + ); // if we have a zbuffer texture then set it if (model.zBufferTexture !== null) { @@ -751,181 +648,210 @@ 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, + vecISToVCMatrix, + 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 firstValidInput = model.currentValidInputs[0]; + const boundsMC = firstValidInput.imageData.getBounds(); + 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); - program.setUniformf('dcxmin', dcxmin); - program.setUniformf('dcxmax', dcxmax); - program.setUniformf('dcymin', dcymin); - program.setUniformf('dcymax', dcymax); + 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]); - if (program.isUniformUsed('cameraParallel')) { - program.setUniformi('cameraParallel', cam.getParallelProjection()); - } + 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]); - 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]); + mat4.invert(projectionToView, keyMats.vcpc); + program.setUniformMatrix('PCVCMatrix', projectionToView); - vec3.set(pos, ext[0], ext[2], ext[4]); - model.currentInput.indexToWorldVec3(pos, pos); + program.setUniformi('twoSidedLighting', ren.getTwoSidedLighting()); - vec3.transformMat4(pos, pos, model.modelToView); - program.setUniform3f('vOriginVC', pos[0], pos[1], pos[2]); + const kernelSample = new Array(2 * model.previousState.maxLaoKernelSize); + for (let i = 0; i < model.previousState.maxLaoKernelSize; i++) { + kernelSample[i * 2] = Math.random(); + kernelSample[i * 2 + 1] = Math.random(); + } + program.setUniform2fv('kernelSample', kernelSample); - // apply the image directions - const i2wmat4 = model.currentInput.getIndexToWorld(); - mat4.multiply(model.idxToView, model.modelToView, i2wmat4); + // 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); - mat3.multiply( - model.idxNormalMatrix, - keyMats.normalMatrix, - actMats.normalMatrix - ); - mat3.multiply( - model.idxNormalMatrix, - model.idxNormalMatrix, - model.currentInput.getDirectionByReference() - ); + // Exponent + const exponent = light.getExponent(); + program.setUniformf(`${lightPrefix}.exponent`, exponent); - 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.`); - } + // Cone angle + const coneAngle = light.getConeAngle(); + program.setUniformf(`${lightPrefix}.coneAngle`, coneAngle); - 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); - } + // Positional flag + const isPositional = light.getPositional(); + program.setUniformi(`${lightPrefix}.isPositional`, isPositional); - // 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); + lightIndex++; + } + }); } - const vprop = actor.getProperty(); - if (isLabelmapOutlineRequired(vprop)) { - const image = model.currentInput; - const worldToIndex = image.getWorldToIndex(); + // Set uniforms for the volume + const uniformPrefix = 'volume'; + const volumeProperties = actor.getProperties(); + const firstVolumeProperty = volumeProperties[firstValidInput.inputIndex]; + const firstImageData = firstValidInput.imageData; + const spatialExtent = firstImageData.getSpatialExtent(); + const spacing = firstImageData.getSpacing(); + const dimensions = firstImageData.getDimensions(); + const idxToModel = firstImageData.getIndexToWorld(); + const worldToIndex = firstImageData.getWorldToIndex(); + const imageDirection = firstImageData.getDirectionByReference(); + + // idxToView is equivalent to applying idxToModel then modelToView + mat4.multiply(idxToView, modelToView, idxToModel); + + // 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); + + // Set inverse dimensions uniform + program.setUniform3fv( + `${uniformPrefix}.inverseDimensions`, + vec3.inverse([], dimensions) + ); + + // Set world to index + program.setUniformMatrix(`${uniformPrefix}.worldToIndex`, worldToIndex); + + // Create the vecISToVCMatrix, that transform a point from texture coordinates (IS in the shader) to VC coordinates + vecISToVCMatrix.fill(0); + // First apply scaling + // mat3.fromScaling can't be used, because it uses a vec2 for scaling + const sizeVC = vec3.multiply(new Float64Array(3), dimensions, spacing); + vecISToVCMatrix[0] = sizeVC[0]; + vecISToVCMatrix[4] = sizeVC[1]; + vecISToVCMatrix[8] = sizeVC[2]; + // Then apply the image direction matrix + mat3.multiply(vecISToVCMatrix, imageDirection, vecISToVCMatrix); + // Then apply the actor matrix + mat3.multiply(vecISToVCMatrix, actMats.normalMatrix, vecISToVCMatrix); + // Then apply the camera matrix + mat3.multiply(vecISToVCMatrix, keyMats.normalMatrix, vecISToVCMatrix); + program.setUniformMatrix3x3( + `${uniformPrefix}.vecISToVCMatrix`, + vecISToVCMatrix + ); + program.setUniformMatrix3x3( + `${uniformPrefix}.vecVCToISMatrix`, + mat3.invert(new Float32Array(9), vecISToVCMatrix) + ); + + // Set originVC uniform that will be used to convert points from IS to VC + // It will be done in this way: posVC = vecISToVCMatrix * posIS + originVC + // Or the other way around: posIS = vecVCtoISMatrix * (posVC - originVC) + const spacialExtentMinIC = vec3.fromValues( + spatialExtent[0], + spatialExtent[2], + spatialExtent[4] + ); + const originVC = vec3.transformMat4( + new Float64Array(3), + spacialExtentMinIC, + idxToView + ); + program.setUniform3fv(`${uniformPrefix}.originVC`, originVC); - program.setUniformMatrix('vWCtoIDX', worldToIndex); + const diagonalLength = vec3.length(sizeVC); + program.setUniformf(`${uniformPrefix}.diagonalLength`, diagonalLength); - const camera = ren.getActiveCamera(); - const [cRange0, cRange1] = camera.getClippingRange(); + if (isLabelmapOutlineRequired(firstVolumeProperty)) { const distance = camera.getDistance(); // set the clipping range to be model.distance and model.distance + 0.1 @@ -938,233 +864,267 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const labelOutlineKeyMats = model.openGLCamera.getKeyMatrices(ren); // Get the projection coordinate to world coordinate transformation matrix. - mat4.invert(model.projectionToWorld, labelOutlineKeyMats.wcpc); + mat4.invert(projectionToWorld, labelOutlineKeyMats.wcpc); // reset the clipping range since the keyMats are cached - camera.setClippingRange(cRange0, cRange1); + camera.setClippingRange(clippingRange[0], clippingRange[1]); // to re compute the matrices for the current camera and cache them model.openGLCamera.getKeyMatrices(ren); - program.setUniformMatrix('PCWCMatrix', model.projectionToWorld); - - 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]); + program.setUniformMatrix( + `${uniformPrefix}.PCWCMatrix`, + projectionToWorld + ); } - mat4.invert(model.projectionToView, keyMats.vcpc); - program.setUniformMatrix('PCVCMatrix', model.projectionToView); - - // 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; - } - }); - program.setUniform3fv('lightPositionVC', lightPositionVC); - program.setUniform3fv('lightAttenuation', lightAttenuation); - program.setUniformfv('lightConeAngle', lightConeAngle); - program.setUniformfv('lightExponent', lightExponent); - program.setUniformiv('lightPositional', lightPositional); - } - if (vprop.getVolumetricScatteringBlending() > 0.0) { - program.setUniformf('giReach', vprop.getGlobalIlluminationReach()); + if (firstVolumeProperty.getVolumetricScatteringBlending() > 0.0) { program.setUniformf( - 'volumetricScatteringBlending', - vprop.getVolumetricScatteringBlending() + `${uniformPrefix}.globalIlluminationReach`, + firstVolumeProperty.getGlobalIlluminationReach() ); program.setUniformf( - 'volumeShadowSamplingDistFactor', - vprop.getVolumeShadowSamplingDistFactor() + `${uniformPrefix}.volumetricScatteringBlending`, + firstVolumeProperty.getVolumetricScatteringBlending() + ); + program.setUniformf( + `${uniformPrefix}.anisotropy`, + firstVolumeProperty.getAnisotropy() + ); + program.setUniformf( + `${uniformPrefix}.anisotropySquared`, + firstVolumeProperty.getAnisotropy() ** 2.0 ); - program.setUniformf('anisotropy', vprop.getAnisotropy()); - program.setUniformf('anisotropy2', vprop.getAnisotropy() ** 2.0); } - if (vprop.getLocalAmbientOcclusion() && vprop.getAmbient() > 0.0) { - const ks = vprop.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', vprop.getLAOKernelRadius()); + + if ( + firstVolumeProperty.getLocalAmbientOcclusion() && + firstVolumeProperty.getAmbient() > 0.0 + ) { + const kernelSize = firstVolumeProperty.getLAOKernelSize(); + program.setUniformi(`${uniformPrefix}.kernelSize`, kernelSize); + + const kernelRadius = firstVolumeProperty.getLAOKernelRadius(); + program.setUniformi(`${uniformPrefix}.kernelRadius`, kernelRadius); + } else { + program.setUniformi(`${uniformPrefix}.kernelSize`, 0); } }; 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() ); + program.setUniformi( + 'opacityTexture', + model.opacityTexture.getTextureUnit() + ); + program.setUniformi('colorTexture', model.colorTexture.getTextureUnit()); - const volInfo = model.scalarTexture.getVolumeInfo(); - const vprop = actor.getProperty(); + const uniformPrefix = 'volume'; + const firstValidInput = model.currentValidInputs[0]; + const firstVolumeProperty = volumeProperties[firstValidInput.inputIndex]; + const numberOfComponents = model.previousState.numberOfComponents; + const useIndependentComponents = + model.previousState.useIndependentComponents; // set the component mix when independent - const numComp = model.scalarTexture.getComponents(); - const useIndependentComps = useIndependentComponents( - vprop, - model.currentInput - ); - if (useIndependentComps) { - for (let i = 0; i < numComp; i++) { - program.setUniformf( - `mix${i}`, - actor.getProperty().getComponentWeight(i) - ); + if (useIndependentComponents) { + const independentComponentMix = new Float32Array(4); + for (let i = 0; i < numberOfComponents; i++) { + independentComponentMix[i] = firstVolumeProperty.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 + ); } + const colorForValueFunctionId = model.colorForValueFunctionId; + program.setUniformi( + `${uniformPrefix}.colorForValueFunctionId`, + colorForValueFunctionId + ); + + const computeNormalFromOpacity = + firstVolumeProperty.getComputeNormalFromOpacity(); + program.setUniformi( + `${uniformPrefix}.computeNormalFromOpacity`, + computeNormalFromOpacity + ); + // 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 colorTextureScale = new Float32Array(4); + const colorTextureShift = new Float32Array(4); + const opacityTextureScale = new Float32Array(4); + const opacityTextureShift = new Float32Array(4); + for (let component = 0; component < numberOfComponents; component++) { + const useMultiTexture = model.previousState.multiTexturePerVolumeEnabled; + const textureIndex = useMultiTexture ? component : 0; + const volInfoIndex = useMultiTexture ? 0 : component; + const scalarTexture = model.scalarTextures[textureIndex]; + const volInfo = scalarTexture.getVolumeInfo(); + const target = useIndependentComponents ? component : 0; + const sscale = volInfo.scale[volInfoIndex]; + + // Color + const colorFunction = firstVolumeProperty.getRGBTransferFunction(target); + const colorRange = colorFunction.getRange(); + colorTextureScale[component] = sscale / (colorRange[1] - colorRange[0]); + colorTextureShift[component] = + (volInfo.offset[volInfoIndex] - colorRange[0]) / + (colorRange[1] - colorRange[0]); + + // Opacity + const opacityFunction = firstVolumeProperty.getScalarOpacity(target); + const opacityRange = opacityFunction.getRange(); + opacityTextureScale[component] = + sscale / (opacityRange[1] - opacityRange[0]); + opacityTextureShift[component] = + (volInfo.offset[volInfoIndex] - 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 + ); - if (model.gopacity) { - if (useIndependentComps) { - for (let nc = 0; nc < numComp; ++nc) { - const sscale = volInfo.scale[nc]; - const useGO = vprop.getUseGradientOpacity(nc); + if (model.previousState.gradientOpacityEnabled) { + const gradientOpacityScale = new Array(4); + const gradientOpacityShift = new Array(4); + const gradientOpacityMin = new Array(4); + const gradientOpacityMax = new Array(4); + if (useIndependentComponents) { + for (let component = 0; component < numberOfComponents; ++component) { + const useMultiTexture = + model.previousState.multiTexturePerVolumeEnabled; + const textureIndex = useMultiTexture ? component : 0; + const volInfoIndex = useMultiTexture ? 0 : component; + const scalarTexture = model.scalarTextures[textureIndex]; + const volInfo = scalarTexture.getVolumeInfo(); + const sscale = volInfo.scale[volInfoIndex]; + const useGO = firstVolumeProperty.getUseGradientOpacity(component); 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), + const goOpacityRange = [ + firstVolumeProperty.getGradientOpacityMinimumOpacity(component), + firstVolumeProperty.getGradientOpacityMaximumOpacity(component), ]; - program.setUniformf( - `goscale${nc}`, - (sscale * (gomax - gomin)) / (goRange[1] - goRange[0]) - ); - program.setUniformf( - `goshift${nc}`, - (-goRange[0] * (gomax - gomin)) / (goRange[1] - goRange[0]) + - gomin - ); + const goValueRange = [ + firstVolumeProperty.getGradientOpacityMinimumValue(component), + firstVolumeProperty.getGradientOpacityMaximumValue(component), + ]; + gradientOpacityMin[component] = goOpacityRange[0]; + gradientOpacityMax[component] = goOpacityRange[1]; + gradientOpacityScale[component] = + (sscale * (goOpacityRange[1] - goOpacityRange[0])) / + (goValueRange[1] - goValueRange[0]); + gradientOpacityShift[component] = + (-goValueRange[0] * (goOpacityRange[1] - goOpacityRange[0])) / + (goValueRange[1] - goValueRange[0]) + + goOpacityRange[0]; } 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); + gradientOpacityMin[component] = 1; + gradientOpacityMax[component] = 1; + gradientOpacityScale[component] = 0; + gradientOpacityShift[component] = 1; } } } 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), + const component = numberOfComponents - 1; + const useMultiTexture = + model.previousState.multiTexturePerVolumeEnabled; + const textureIndex = useMultiTexture ? component : 0; + const volInfoIndex = useMultiTexture ? 0 : component; + const scalarTexture = model.scalarTextures[textureIndex]; + const volInfo = scalarTexture.getVolumeInfo(); + const sscale = volInfo.scale[volInfoIndex]; + const goOpacityRange = [ + firstVolumeProperty.getGradientOpacityMinimumOpacity(0), + firstVolumeProperty.getGradientOpacityMaximumOpacity(0), ]; - program.setUniformf( - 'goscale0', - (sscale * (gomax - gomin)) / (goRange[1] - goRange[0]) - ); - program.setUniformf( - 'goshift0', - (-goRange[0] * (gomax - gomin)) / (goRange[1] - goRange[0]) + gomin - ); + const goValueRange = [ + firstVolumeProperty.getGradientOpacityMinimumValue(0), + firstVolumeProperty.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 vtkImageLabelOutline = isLabelmapOutlineRequired(vprop); - if (vtkImageLabelOutline === true) { - const labelOutlineOpacity = vprop.getLabelOutlineOpacity(); - program.setUniformf('outlineOpacity', labelOutlineOpacity); - } + const outlineOpacity = firstVolumeProperty.getLabelOutlineOpacity(); + program.setUniformf(`${uniformPrefix}.outlineOpacity`, outlineOpacity); - if (model.lightComplexity > 0) { - program.setUniformf('vAmbient', vprop.getAmbient()); - program.setUniformf('vDiffuse', vprop.getDiffuse()); - program.setUniformf('vSpecular', vprop.getSpecular()); - program.setUniformf('vSpecularPower', vprop.getSpecularPower()); + if (model.numberOfLights > 0) { + program.setUniformf( + `${uniformPrefix}.ambient`, + firstVolumeProperty.getAmbient() + ); + program.setUniformf( + `${uniformPrefix}.diffuse`, + firstVolumeProperty.getDiffuse() + ); + program.setUniformf( + `${uniformPrefix}.specular`, + firstVolumeProperty.getSpecular() + ); + const specularPower = firstVolumeProperty.getSpecularPower(); + program.setUniformf( + `${uniformPrefix}.specularPower`, + specularPower === 0 ? 1.0 : specularPower + ); } }; @@ -1333,14 +1293,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) { @@ -1352,26 +1317,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.colorTexture, + model.opacityTexture, + 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) => { @@ -1467,14 +1428,57 @@ 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 }); + } + } + const volumeProperties = actor.getProperties(); + const firstValidInput = model.currentValidInputs[0]; + const firstImageData = firstValidInput.imageData; + const firstScalars = firstImageData.getPointData().getScalars(); + const firstVolumeProperty = volumeProperties[firstValidInput.inputIndex]; + + // Get the number of lights + let newNumberOfLights = 0; + if ( + firstVolumeProperty.getShade() && + model.renderable.getBlendMode() === BlendMode.COMPOSITE_BLEND + ) { + ren.getLights().forEach((light) => { + if (light.getSwitch() > 0) { + newNumberOfLights++; + } + }); + } + if (newNumberOfLights !== model.numberOfLights) { + model.numberOfLights = newNumberOfLights; + publicAPI.modified(); + } + publicAPI.invokeEvent({ type: 'EndEvent' }); - if (!model.currentInput) { + if (model.currentValidInputs.length === 0) { return; } + // Number of components + const numberOfValidInputs = model.currentValidInputs.length; + const multiTexturePerVolumeEnabled = numberOfValidInputs > 1; + model.numberOfComponents = multiTexturePerVolumeEnabled + ? numberOfValidInputs + : firstScalars.getNumberOfComponents(); + model.useIndependentComponents = getUseIndependentComponents( + firstVolumeProperty, + model.numberOfComponents + ); + publicAPI.renderPieceStart(ren, actor); publicAPI.renderPieceDraw(ren, actor); publicAPI.renderPieceFinish(ren, actor); @@ -1487,81 +1491,67 @@ 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() < model.currentInput.getMTime() || - !model.scalarTexture?.getHandle() || - !model.colorTexture?.getHandle() || - !model.labelOutlineThicknessTexture?.getHandle() - ) { - return true; - } - return false; - }; + publicAPI.getNeedToRebuildBufferObjects = (ren, actor) => + model.VBOBuildTime.getMTime() < publicAPI.getMTime() || + model.VBOBuildTime.getMTime() < actor.getMTime() || + model.VBOBuildTime.getMTime() < model.renderable.getMTime() || + model.currentValidInputs.some( + ({ imageData }) => model.VBOBuildTime.getMTime() < imageData.getMTime() + ) || + model.scalarTextures.length !== model.currentValidInputs.length || + !model.scalarTextures.every((texture) => !!texture?.getHandle()) || + !model.colorTexture?.getHandle() || + !model.opacityTexture?.getHandle() || + !model.labelOutlineThicknessTexture?.getHandle() || + !model.jitterTexture?.getHandle(); 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); + const jitterArray = new Float32Array(32 * 32); for (let i = 0; i < 32 * 32; ++i) { - oTable[i] = 255.0 * Math.random(); + jitterArray[i] = Math.random(); } - model.jitterTexture.setMinificationFilter(Filter.LINEAR); - model.jitterTexture.setMagnificationFilter(Filter.LINEAR); + model.jitterTexture.setMinificationFilter(Filter.NEAREST); + model.jitterTexture.setMagnificationFilter(Filter.NEAREST); model.jitterTexture.create2DFromRaw( 32, 32, 1, - VtkDataTypes.UNSIGNED_CHAR, - oTable + VtkDataTypes.FLOAT, + jitterArray ); } - const numComp = scalars.getNumberOfComponents(); - const useIndependentComps = useIndependentComponents( - vprop, - model.currentInput - ); - const numIComps = useIndependentComps ? numComp : 1; + const volumeProperties = actor.getProperties(); + const firstValidInput = model.currentValidInputs[0]; + const firstVolumeProperty = volumeProperties[firstValidInput.inputIndex]; + const numberOfComponents = model.numberOfComponents; + const useIndependentComps = model.useIndependentComponents; + const numIComps = useIndependentComps ? numberOfComponents : 1; - const scalarOpacityFunc = vprop.getScalarOpacity(); + // rebuild opacity tfun? + const scalarOpacityFunc = firstVolumeProperty.getScalarOpacity(); const opTex = model._openGLRenderWindow.getGraphicsResourceForObject(scalarOpacityFunc); - let toString = getTransferFunctionHash( + const opacityFuncHash = getTransferFunctionHash( scalarOpacityFunc, useIndependentComps, numIComps ); - const reBuildOp = !opTex?.oglObject || opTex.hash !== toString; + const reBuildOp = !opTex?.oglObject || opTex.hash !== opacityFuncHash; if (reBuildOp) { - model.opacityTexture = vtkOpenGLTexture.newInstance(); - model.opacityTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - // rebuild opacity tfun? + const newOpacityTexture = vtkOpenGLTexture.newInstance(); + newOpacityTexture.setOpenGLRenderWindow(model._openGLRenderWindow); 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 ofun = firstVolumeProperty.getScalarOpacity(c); const opacityFactor = publicAPI.getCurrentSampleDistance(ren) / - vprop.getScalarOpacityUnitDistance(c); + firstVolumeProperty.getScalarOpacityUnitDistance(c); const oRange = ofun.getRange(); ofun.getTable(oRange[0], oRange[1], oWidth, tmpTable, 1); @@ -1573,9 +1563,9 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { } } - model.opacityTexture.resetFormatAndType(); - model.opacityTexture.setMinificationFilter(Filter.LINEAR); - model.opacityTexture.setMagnificationFilter(Filter.LINEAR); + 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 @@ -1586,7 +1576,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { (model.context.getExtension('OES_texture_float') && model.context.getExtension('OES_texture_float_linear')) ) { - model.opacityTexture.create2DFromRaw( + newOpacityTexture.create2DFromRaw( oWidth, 2 * numIComps, 1, @@ -1598,7 +1588,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { for (let i = 0; i < oSize; ++i) { oTable[i] = 255.0 * ofTable[i]; } - model.opacityTexture.create2DFromRaw( + newOpacityTexture.create2DFromRaw( oWidth, 2 * numIComps, 1, @@ -1609,45 +1599,42 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { if (scalarOpacityFunc) { model._openGLRenderWindow.setGraphicsResourceForObject( scalarOpacityFunc, - model.opacityTexture, - toString + newOpacityTexture, + opacityFuncHash ); - if (scalarOpacityFunc !== model._scalarOpacityFunc) { - model._openGLRenderWindow.registerGraphicsResourceUser( - scalarOpacityFunc, - publicAPI - ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._scalarOpacityFunc, - publicAPI - ); - } - model._scalarOpacityFunc = scalarOpacityFunc; } + model.opacityTexture = newOpacityTexture; } else { model.opacityTexture = opTex.oglObject; } + replaceGraphicsResource( + model._openGLRenderWindow, + model._opacityTextureCore, + scalarOpacityFunc + ); + model._opacityTextureCore = scalarOpacityFunc; // rebuild color tfun? - const colorTransferFunc = vprop.getRGBTransferFunction(); - toString = getTransferFunctionHash( + const colorTransferFunc = firstVolumeProperty.getRGBTransferFunction(); + const colorFuncHash = getTransferFunctionHash( colorTransferFunc, useIndependentComps, numIComps ); const cTex = model._openGLRenderWindow.getGraphicsResourceForObject(colorTransferFunc); - const reBuildC = !cTex?.oglObject?.getHandle() || cTex?.hash !== toString; + const reBuildC = + !cTex?.oglObject?.getHandle() || cTex?.hash !== colorFuncHash; if (reBuildC) { - model.colorTexture = vtkOpenGLTexture.newInstance(); - model.colorTexture.setOpenGLRenderWindow(model._openGLRenderWindow); + 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 = vprop.getRGBTransferFunction(c); + const cfun = firstVolumeProperty.getRGBTransferFunction(c); const cRange = cfun.getRange(); cfun.getTable(cRange[0], cRange[1], cWidth, tmpTable, 1); for (let i = 0; i < cWidth * 3; ++i) { @@ -1656,11 +1643,11 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { } } - model.colorTexture.resetFormatAndType(); - model.colorTexture.setMinificationFilter(Filter.LINEAR); - model.colorTexture.setMagnificationFilter(Filter.LINEAR); + newColorTexture.resetFormatAndType(); + newColorTexture.setMinificationFilter(Filter.LINEAR); + newColorTexture.setMagnificationFilter(Filter.LINEAR); - model.colorTexture.create2DFromRaw( + newColorTexture.create2DFromRaw( cWidth, 2 * numIComps, 3, @@ -1670,152 +1657,77 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { if (colorTransferFunc) { model._openGLRenderWindow.setGraphicsResourceForObject( colorTransferFunc, - model.colorTexture, - toString + newColorTexture, + colorFuncHash ); - if (colorTransferFunc !== model._colorTransferFunc) { - model._openGLRenderWindow.registerGraphicsResourceUser( - colorTransferFunc, - publicAPI - ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._colorTransferFunc, - publicAPI - ); - } - model._colorTransferFunc = colorTransferFunc; } + model.colorTexture = newColorTexture; } 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, - vprop.getPreferSizeOverAccuracy() - ); - if (scalars) { + replaceGraphicsResource( + model._openGLRenderWindow, + model._colorTextureCore, + colorTransferFunc + ); + model._colorTextureCore = colorTransferFunc; + + // rebuild scalarTextures? + model.currentValidInputs.forEach(({ imageData }, component) => { + // rebuild the scalarTexture if the data has changed + const volumeProperty = volumeProperties[component]; + const scalars = imageData.getPointData().getScalars(); + 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, + volumeProperty.getPreferSizeOverAccuracy() + ); model._openGLRenderWindow.setGraphicsResourceForObject( scalars, - model.scalarTexture, - toString + newScalarTexture, + scalarsHash ); - if (scalars !== model._scalars) { - model._openGLRenderWindow.registerGraphicsResourceUser( - scalars, - publicAPI - ); - model._openGLRenderWindow.unregisterGraphicsResourceUser( - model._scalars, - publicAPI - ); - } - 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; + model.scalarTextures[component] = newScalarTexture; + } else { + model.scalarTextures[component] = tex.oglObject; } + replaceGraphicsResource( + model._openGLRenderWindow, + model._scalarTexturesCore[component], + scalars + ); + model._scalarTexturesCore[component] = scalars; + }); - 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 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; @@ -1835,12 +1747,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, @@ -1851,24 +1763,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; + + // rebuild the CABO? + if (!model.tris.getCABO().getElementCount()) { + 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; + } + + 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 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(); }; } @@ -1879,14 +1823,15 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const DEFAULT_VALUES = { context: null, VBOBuildTime: null, - scalarTexture: null, + scalarTextures: [], + _scalarTexturesCore: [], opacityTexture: null, - opacityTextureString: null, + _opacityTextureCore: null, colorTexture: null, - colorTextureString: null, - jitterTexture: null, + _colorTextureCore: null, labelOutlineThicknessTexture: null, - labelOutlineThicknessTextureString: null, + _labelOutlineThicknessTextureCore: null, + jitterTexture: null, tris: null, framebuffer: null, copyShader: null, @@ -1895,18 +1840,13 @@ const DEFAULT_VALUES = { targetXYF: 1.0, zBufferTexture: null, lastZBufferTexture: null, - lightComplexity: 0, fullViewportTime: 1.0, idxToView: null, - idxNormalMatrix: null, + vecISToVCMatrix: null, modelToView: null, projectionToView: null, avgWindowArea: 0.0, avgFrameTime: 0.0, - // _scalars: null, - // _scalarOpacityFunc: null, - // _colorTransferFunc: null, - // _labelOutlineThicknessArray: null, }; // ---------------------------------------------------------------------------- @@ -1932,12 +1872,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/glsl/vtkVolumeFS.glsl b/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl index 8779581ef49..816744d4fef 100644 --- a/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl +++ b/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl @@ -16,122 +16,169 @@ =========================================================================*/ // 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 - -// possibly define UseIndependentComponents -//VTK::IndependentComponentsOn - -// possibly define vtkCustomComponentsColorMix -//VTK::CustomComponentsColorMixOn - -// possibly define any "proportional" components -//VTK::vtkProportionalComponents +in vec3 vertexVCVSOutput; -// possibly define any components that are forced to nearest interpolation -//VTK::vtkForceNearestComponents +// 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 -// Define the blend mode to use +#define vtkNumberOfLights //VTK::NumberOfLights +#define vtkMaxLaoKernelSize //VTK::MaxLaoKernelSize +#define vtkNumberOfComponents //VTK::NumberOfComponents #define vtkBlendMode //VTK::BlendMode +#define vtkMaximumNumberOfSamples //VTK::MaximumNumberOfSamples -// Possibly define vtkImageLabelOutlineOn -//VTK::ImageLabelOutlineOn +//VTK::EnabledColorFunctions -// Possibly define vtkLabelEdgeProjectionOn -//VTK::LabelEdgeProjectionOn +//VTK::EnabledLightings +//VTK::EnabledMultiTexturePerVolume -#ifdef vtkImageLabelOutlineOn - uniform float outlineOpacity; - uniform float vpWidth; - uniform float vpHeight; - uniform float vpOffsetX; - uniform float vpOffsetY; - uniform mat4 PCWCMatrix; - uniform mat4 vWCtoIDX; +//VTK::EnabledGradientOpacity - 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) +//VTK::EnabledIndependentComponents - uint bitmask[BITMASK_SIZE]; +//VTK::vtkProportionalComponents - // 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; - } +//VTK::vtkForceNearestComponents - // 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); - } -#endif +uniform int twoSidedLighting; -// define vtkLightComplexity -//VTK::LightComplexity -#if vtkLightComplexity > 0 -uniform float vSpecularPower; -uniform float vAmbient; -uniform float vDiffuse; -uniform float vSpecular; -//VTK::Light::Dec +#if vtkMaxLaoKernelSize > 0 + vec2 kernelSample[vtkMaxLaoKernelSize]; #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 +// Textures +#ifdef EnabledMultiTexturePerVolume + #define vtkNumberOfVolumeTextures vtkNumberOfComponents +#else + #define vtkNumberOfVolumeTextures 1 #endif +uniform highp sampler3D volumeTexture[vtkNumberOfVolumeTextures]; +uniform sampler2D colorTexture; +uniform sampler2D opacityTexture; +uniform sampler2D jtexture; +uniform sampler2D labelOutlineThicknessTexture; + +struct Volume { + // ---- Volume geometry settings ---- + + vec3 originVC; // in VC + vec3 spacing; // in VC per IC + vec3 inverseSpacing; // 1/spacing + ivec3 dimensions; // in IC + vec3 inverseDimensions; // 1/vec3(dimensions) + mat3 vecISToVCMatrix; // convert from IS to VC without translation + mat3 vecVCToISMatrix; // convert from VC to IS without translation + mat4 PCWCMatrix; + mat4 worldToIndex; + float diagonalLength; // in VC, this is: length(size) + + // ---- 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 + 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 volume; + +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 +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 // #define debugtile @@ -142,133 +189,81 @@ 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; - +// 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; -// some 3D texture values +// sample texture is global 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 - -uniform vec4 ipScalarRangeMin; -uniform vec4 ipScalarRangeMax; +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 - -//======================================================================= -// Webgl2 specific version of functions -#if __VERSION__ == 300 +#define INV4PI 0.0796 +#define EPSILON 0.001 +#define PI 3.1415 +#define PI2 9.8696 + +vec4 rawSampleTexture(vec3 pos) { + #ifdef EnabledMultiTexturePerVolume + vec4 rawSample; + rawSample[0] = texture(volumeTexture[0], pos)[0]; + #if vtkNumberOfComponents > 1 + rawSample[1] = texture(volumeTexture[1], pos)[0]; + #endif + #if vtkNumberOfComponents > 2 + rawSample[2] = texture(volumeTexture[2], pos)[0]; + #endif + #if vtkNumberOfComponents > 3 + rawSample[3] = texture(volumeTexture[3], pos)[0]; + #endif + return rawSample; + #else + return texture(volumeTexture[0], pos); + #endif +} -uniform highp sampler3D texture1; +vec4 rawFetchTexture(ivec3 pos) { + #ifdef EnabledMultiTexturePerVolume + vec4 rawSample; + #if vtkNumberOfComponents > 0 + rawSample[0] = texelFetch(volumeTexture[0], pos, 0)[0]; + #endif + #if vtkNumberOfComponents > 1 + rawSample[1] = texelFetch(volumeTexture[1], pos, 0)[0]; + #endif + #if vtkNumberOfComponents > 2 + rawSample[2] = texelFetch(volumeTexture[2], pos, 0)[0]; + #endif + #if vtkNumberOfComponents > 3 + rawSample[3] = texelFetch(volumeTexture[3], pos, 0)[0]; + #endif + return rawSample; + #else + return texelFetch(volumeTexture[0], pos, 0); + #endif +} -vec4 getTextureValue(vec3 pos) -{ - vec4 tmp = texture(texture1, pos); +vec4 getTextureValue(vec3 pos) { + vec4 tmp = rawSampleTexture(pos); + // Force nearest #if defined(vtkComponent0ForceNearest) || \ defined(vtkComponent1ForceNearest) || \ defined(vtkComponent2ForceNearest) || \ defined(vtkComponent3ForceNearest) - vec3 nearestPos = (floor(pos * vec3(volumeDimensions)) + 0.5) / vec3(volumeDimensions); - vec4 nearestValue = texture(texture1, nearestPos); + vec3 nearestPos = (floor(pos * vec3(volume.dimensions)) + 0.5) * + volume.inverseDimensions; + vec4 nearestValue = rawSampleTexture(nearestPos); #ifdef vtkComponent0ForceNearest tmp[0] = nearestValue[0]; #endif @@ -283,14 +278,15 @@ vec4 getTextureValue(vec3 pos) #endif #endif - #ifndef UseIndependentComponents - #if vtkNumComponents == 1 + // Set alpha when using dependent components + #ifndef EnabledIndependentComponents + #if vtkNumberOfComponents == 1 tmp.a = tmp.r; #endif - #if vtkNumComponents == 2 + #if vtkNumberOfComponents == 2 tmp.a = tmp.g; #endif - #if vtkNumComponents == 3 + #if vtkNumberOfComponents == 3 tmp.a = length(tmp.rgb); #endif #endif @@ -298,799 +294,655 @@ vec4 getTextureValue(vec3 pos) 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 `volume.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 component, float height) { + float scaledScalar = scalar * volume.opacityTextureScale[component] + + volume.opacityTextureShift[component]; + return texture2D(opacityTexture, vec2(scaledScalar, height)).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 component, float height) { + float scaledScalar = scalar * volume.colorTextureScale[component] + + volume.colorTextureShift[component]; + return texture2D(colorTexture, vec2(scaledScalar, height)).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 posIStoVC(vec3 posIS) { + return volume.vecISToVCMatrix * posIS + volume.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 posVCtoIS(vec3 posVC) { + return volume.vecVCToISMatrix * (posVC - volume.originVC); } -#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 vecISToVC(vec3 dirIS) { + return volume.vecISToVCMatrix * 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 vecVCToIS(vec3 dirVC) { + return volume.vecVCToISMatrix * 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 component) { + float goscale = volume.gradientOpacityScale[component]; + float goshift = volume.gradientOpacityShift[component]; + float gomin = volume.gradientOpacityMin[component]; + float gomax = volume.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; - } - } +#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; } } - #endif + return false; + } +#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); - } - } +//======================================================================= +// 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) { + // Pass the scalars through the opacity functions + vec4 opacityG; + opacityG.x += getOpacityFromTexture(opacityUCoords[0].x, component, + opacityTextureHeight); + opacityG.y += getOpacityFromTexture(opacityUCoords[0].y, component, + opacityTextureHeight); + opacityG.z += getOpacityFromTexture(opacityUCoords[0].z, component, + opacityTextureHeight); + opacityG.x -= getOpacityFromTexture(opacityUCoords[1].x, component, + opacityTextureHeight); + opacityG.y -= getOpacityFromTexture(opacityUCoords[1].y, component, + opacityTextureHeight); + opacityG.z -= getOpacityFromTexture(opacityUCoords[1].z, component, + opacityTextureHeight); + + // Divide by spacing and convert to VC + opacityG.xyz *= gradientOpacity * volume.inverseSpacing; + opacityG.w = length(opacityG.xyz); + if (opacityG.w == 0.0) { + return vec4(0.0); + } - 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); + // Normalize + opacityG.xyz = normalize(vecISToVC(opacityG.xyz)); + + return opacityG; +} + +// The output normal is in VC +vec4 computeNormalForDensity(vec3 posIS, out vec3 scalarInterp[2], + const int opacityComponent) { + vec3 offsetedPosIS; + for (int axis = 0; axis < 3; ++axis) { + // Positive direction + offsetedPosIS = posIS; + offsetedPosIS[axis] += volume.inverseDimensions[axis]; + scalarInterp[0][axis] = + getTextureValue(offsetedPosIS)[opacityComponent]; + #ifdef vtkClippingPlanesOn + if (isPointClipped(posIStoVC(offsetedPosIS))) { + scalarInterp[0][axis] = 0.0; } - } - #endif + #endif - // 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; + // Negative direction + offsetedPosIS = posIS; + offsetedPosIS[axis] -= volume.inverseDimensions[axis]; + scalarInterp[1][axis] = + getTextureValue(offsetedPosIS)[opacityComponent]; #ifdef vtkClippingPlanesOn - adjustClippedVoxelValues(pos, texPosPVec, g1); - adjustClippedVoxelValues(pos, texPosNVec, g2); + if (isPointClipped(posIStoVC(offsetedPosIS))) { + scalarInterp[1][axis] = 0.0; + } #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); - } } -#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); + vec4 result; + result.xyz = (scalarInterp[0] - scalarInterp[1]) * volume.inverseSpacing; + result.w = length(result.xyz); + if (result.w == 0.0) { + return vec4(0.0); } + result.xyz = normalize(vecISToVC(result.xyz)); + return result; +} - vec4 pcPosToWorldCoord(vec4 pcPos) { - return PCWCMatrix * pcPos; - } +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 fragCoordToIndexSpace(vec4 fragCoord) { - vec4 pcPos = fragCoordToPCPos(fragCoord); - vec4 worldCoord = pcPosToWorldCoord(pcPos); - vec4 vertex = (worldCoord / worldCoord.w); +vec4 pcPosToWorldCoord(vec4 pcPos) { + return volume.PCWCMatrix * pcPos; +} - vec3 index = (vWCtoIDX * vertex).xyz; +vec3 fragCoordToIndexSpace(vec4 fragCoord) { + vec4 pcPos = fragCoordToPCPos(fragCoord); + vec4 worldCoord = pcPosToWorldCoord(pcPos); + vec4 vertex = (worldCoord / worldCoord.w); - // half voxel fix for labelmapOutline - return (index + vec3(0.5)) / vec3(volumeDimensions); - } + vec3 index = (volume.worldToIndex * vertex).xyz; - vec3 fragCoordToWorld(vec4 fragCoord) { - vec4 pcPos = fragCoordToPCPos(fragCoord); - vec4 worldCoord = pcPosToWorldCoord(pcPos); - return worldCoord.xyz; - } -#endif + // half voxel fix for labelmapOutline + return (index + vec3(0.5)) * volume.inverseDimensions; +} + +vec3 fragCoordToWorld(vec4 fragCoord) { + vec4 pcPos = fragCoordToPCPos(fragCoord); + vec4 worldCoord = pcPosToWorldCoord(pcPos); + 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) { + vec3 xvec = vec3(volume.inverseDimensions.x, 0.0, 0.0); + vec3 yvec = vec3(0.0, volume.inverseDimensions.y, 0.0); + vec3 zvec = vec3(0.0, 0.0, volume.inverseDimensions.z); + + vec4 distX = getTextureValue(posIS + xvec) - getTextureValue(posIS - xvec); + vec4 distY = getTextureValue(posIS + yvec) - getTextureValue(posIS - yvec); + vec4 distZ = getTextureValue(posIS + zvec) - getTextureValue(posIS - zvec); // 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 + distX *= 0.5 * volume.inverseSpacing.x; + distY *= 0.5 * volume.inverseSpacing.y; + distZ *= 0.5 * volume.inverseSpacing.z; -// 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 + mat4 result; -// 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 + // optionally compute the 1st component + #if vtkNumberOfComponents > 0 && !defined(vtkComponent0Proportional) + { + const int component = 0; + vec3 normal = vec3(distX[component], distY[component], distZ[component]); + float normalLength = length(normal); + if (normalLength > 0.0) { + normal = normalize(vecISToVC(normal)); + } + result[component] = vec4(normal, normalLength); + } + #endif -// 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; - } -#endif + // optionally compute the 2nd component + #if vtkNumberOfComponents > 1 && !defined(vtkComponent1Proportional) + { + const int component = 1; + vec3 normal = vec3(distX[component], distY[component], distZ[component]); + float normalLength = length(normal); + if (normalLength > 0.0) { + normal = normalize(vecISToVC(normal)); + } + result[component] = vec4(normal, normalLength); + } + #endif + + // optionally compute the 3rd component + #if vtkNumberOfComponents > 2 && !defined(vtkComponent2Proportional) + { + const int component = 2; + vec3 normal = vec3(distX[component], distY[component], distZ[component]); + float normalLength = length(normal); + if (normalLength > 0.0) { + normal = normalize(vecISToVC(normal)); + } + result[component] = vec4(normal, normalLength); + } + #endif + + // optionally compute the 4th component + #if vtkNumberOfComponents > 3 && !defined(vtkComponent3Proportional) + { + const int component = 3; + vec3 normal = vec3(distX[component], distY[component], distZ[component]); + float normalLength = length(normal); + if (normalLength > 0.0) { + normal = normalize(vecISToVC(normal)); + } + result[component] = vec4(normal, normalLength); + } + #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) { // 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 = volume.anisotropy; + if (abs(anisotropy) <= EPSILON) { + // isotropic scatter returns 0.5 instead of 1/4pi to increase intensity + return 0.5; + } + float anisotropy2 = volume.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, -// 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; +// 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) { + // Compute origin and direction in IS + vec3 rayOriginIS = posVCtoIS(rayOriginVC); + vec3 rayDirIS = vecVCToIS(rayDirVC); + // Don't check for infinity as the min/max combination afterward will always + // find an intersection before infinity + vec3 invDir = 1.0 / 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 dMinPerAxis = min(distancesTo0, distancesTo1); + vec3 dMaxPerAxis = max(distancesTo0, distancesTo1); + // Overall first and last intersection + float distanceMin = max(dMinPerAxis.x, max(dMinPerAxis.y, dMinPerAxis.z)); + float distanceMax = min(dMaxPerAxis.x, min(dMaxPerAxis.y, dMaxPerAxis.z)); + return vec2(distanceMin, distanceMax); } -float volume_shadow(vec3 posIS, vec3 lightDirNormIS) -{ - float shadow = 1.0; - float opacity = 0.0; - - // 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; - - // in case the first sample near surface has a very tiled light ray, we need to offset start position - posIS += sampleDistanceISVS_jitter * lightDirNormIS; - - // 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; +//======================================================================= +// local ambient occlusion +#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); } - 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; - } + float computeLAO(vec3 posVC, vec4 normalVC, float originalOpacity) { + // apply LAO only at selected locations, otherwise return full brightness + if (normalVC.w <= 0.0 || originalOpacity <= 0.05) { + return 1.0; + } - float current_dist = 0.0; - float current_step = length(sampleDistanceISVS_jitter * lightDirNormIS); - float clamped_step = 0.0; + #ifdef EnabledGradientOpacity + float gradientOpacityFactor = computeGradientOpacityFactor(normalVC.w, 0); + #endif - vec4 scalar = vec4(0.0); - while(current_dist < maxdist) - { -#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; + float visibilitySum = 0.0; + float weightSum = 0.0; + for (int i = 0; i < volume.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; } - } -#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; + vec3 currPosIS = posVCtoIS(posVC); + float visibility = 1.0; + vec3 randomDirStepIS = vecVCToIS(rayDirectionVC * sampleDistance); + for (int j = 0; j < volume.kernelRadius; j++) { + currPosIS += randomDirStepIS; + // If out of the volume, we are done + if (any(lessThan(currPosIS, vec3(0.0))) || + any(greaterThan(currPosIS, vec3(1.0)))) { + break; + } + float opacity = getOpacityFromTexture(getTextureValue(currPosIS).r, 0, 0.5); + #ifdef EnabledGradientOpacity + opacity *= gradientOpacityFactor; + #endif + visibility *= 1.0 - opacity; + // If visibility is less than EPSILON, consider it to be 0 + if (visibility < EPSILON) { + visibility = 0.0; + break; + } + } + float rayWeight = -normalDotRay; + visibilitySum += visibility * rayWeight; + weightSum += rayWeight; } - clamped_step = min(maxdist - current_dist, current_step); - posIS += clamped_step * lightDirNormIS; - current_dist += current_step; - } + // If no sample, LAO factor is one + if (weightSum == 0.0) { + return 1.0; + } - return shadow; -} + // LAO factor is the average visibility: + // - visibility low => ambient low + // - visibility high => ambient high + float lao = visibilitySum / weightSum; -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; + // Reduce variance by clamping + return clamp(lao, 0.3, 1.0); } - return secondary_contrib; -} #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))); -} +// Volume shadows +#if vtkNumberOfLights > 0 -// 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; -} + // Non-memoised version + float computeVolumeShadowWithoutCache(vec3 posVC, vec3 lightDirNormVC) { + // modify sample distance with a random number between 1.5 and 3.0 + float rayStepLength = + volumeShadowSampleDistance * mix(1.5, 3.0, fragmentSeed); -float computeLAO(vec3 posIS, float op, vec3 lightDir, vec4 normal){ - // 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; + // in case the first sample near surface has a very tiled light ray, we need + // to offset start position + vec3 initialPosVC = posVC + rayStepLength * lightDirNormVC; + + #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; } - else{ - break; + 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); } } - total_transmittance += transmittance / float(kernelRadius) * weight; + #endif + + vec2 intersectionDistances = + rayIntersectVolumeDistances(initialPosVC, lightDirNormVC); + + if (intersectionDistances[1] <= intersectionDistances[0] || + intersectionDistances[1] <= 0.0) { + // Volume not hit or behind the ray + return 1.0; + } - // early termination if fully translucent - if (total_transmittance > 1.0 - EPSILON){ - return 1.0; + // 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, volume.diagonalLength, + volume.globalIlluminationReach); + float startDistance = max(intersectionDistances[0], 0.0); + float endDistance = min(intersectionDistances[1], startDistance + maxTravelDistance); + #ifdef vtkClippingPlanesOn + endDistance = min(endDistance, clippingPlanesMaxDistance); + #endif + if (endDistance - startDistance < 0.0) { + return 1.0; + } + + // These two variables are used to compute posIS, without having to call + // VCtoIS at each step + vec3 initialPosIS = posVCtoIS(initialPosVC); + // The light dir is scaled and rotated, but not translated, as it is a + // vector (w = 0) + vec3 scaledLightDirIS = vecVCToIS(lightDirNormVC); + + float shadow = 1.0; + for (float currentDistance = startDistance; currentDistance <= endDistance; + currentDistance += rayStepLength) { + vec3 posIS = initialPosIS + currentDistance * scaledLightDirIS; + vec4 scalar = getTextureValue(posIS); + float opacity = getOpacityFromTexture(scalar.r, 0, 0.5); + #if defined(EnabledGradientOpacity) && !defined(EnabledIndependentComponents) + vec3 scalarInterp[2]; + vec4 normal = computeNormalForDensity(posIS, scalarInterp, 3); + float opacityFactor = computeGradientOpacityFactor(normal.w, 0); + opacity *= opacityFactor; + #endif + shadow *= 1.0 - opacity; + + // Early termination if shadow coeff is near 0.0 + if (shadow < EPSILON) { + return 0.0; } } - // average transmittance and reduce variance - return clamp(total_transmittance / float(kernelSize), 0.3, 1.0); - } else { - return 1.0; + return shadow; } -} + + // Some cache for volume shadows + struct { + vec3 posVC; + float shadow; + } cachedShadows[vtkNumberOfLights]; + + // Memoised version + float computeVolumeShadow(vec3 posVC, vec3 lightDirNormVC, int lightIdx) { + if (posVC == cachedShadows[lightIdx].posVC) { + return cachedShadows[lightIdx].shadow; + } + float shadow = computeVolumeShadowWithoutCache(posVC, lightDirNormVC); + cachedShadows[lightIdx].posVC = posVC; + cachedShadows[lightIdx].shadow = shadow; + return shadow; + } + #endif //======================================================================= // surface light contribution -#if vtkLightComplexity > 0 - void applyLighting(inout vec3 tColor, vec4 normal) - { +#if vtkNumberOfLights > 0 + vec3 applyLighting(vec3 tColor, vec4 normalVC) { 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]; + for (int lightIdx = 0; lightIdx < vtkNumberOfLights; lightIdx++) { + float df = dot(normalVC.xyz, lights[lightIdx].directionVC); + if (df > 0.0) { + diffuse += df * lights[lightIdx].color; + float sf = dot(normalVC.xyz, -lights[lightIdx].halfAngleVC); + if (sf > 0.0) { + specular += pow(sf, volume.specularPower) * lights[lightIdx].color; + } + } } - tColor.rgb = tColor.rgb*(diffuse*vDiffuse + vAmbient) + specular*vSpecular; + return tColor * (diffuse * volume.diffuse + volume.ambient) + + specular * volume.specular; } - #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 applySurfaceShadowLighting(vec3 tColor, float alpha, vec3 posVC, + vec4 normalVC) { + // everything in VC + vec3 diffuse = vec3(0.0); + vec3 specular = vec3(0.0); + for (int ligthIdx = 0; ligthIdx < vtkNumberOfLights; ligthIdx++) { 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]; + 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 + } else { + vertLightDirection = lights[ligthIdx].directionVC; + attenuation = 1.0; } - #ifdef localAmbientOcclusionOn - return tColor.rgb * (diffuse * vDiffuse + vAmbient * ambient) + specular*vSpecular; - #else - return tColor.rgb * (diffuse * vDiffuse + vAmbient) + specular*vSpecular; - #endif - } - #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, normalize(vertLightDirection - 2.0 * ndotL * normalVC.xyz)); + if (vdotR > 0.0) { + specular += pow(vdotR, volume.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); + #else + const float laoFactor = 1.0; + #endif + return tColor * (diffuse * volume.diffuse + + volume.ambient * laoFactor) + + specular * volume.specular; + } + + vec3 applyVolumeShadowLighting(vec3 tColor, vec3 posVC) { + // Here we have no effect of cones and no attenuation + 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)); + diffuse += phaseAttenuation * shadowCoeff * lights[lightIdx].color; + } + return tColor * (diffuse * volume.diffuse + volume.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 +vec3 applyAllLightning(vec3 tColor, float alpha, vec3 posVC, + vec4 surfaceNormalVC) { + #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 = volume.volumetricScatteringBlending * + (1.0 - alpha / 2.0) * + (1.0 - atan(surfaceNormalVC.w) * INV4PI); + + // Compute surface lighting if needed + vec3 surfaceShadedColor = tColor; + #ifdef EnableSurfaceLighting + if (volCoeff < 1.0 - EPSILON) { + surfaceShadedColor = + applySurfaceShadowLighting(tColor, alpha, posVC, surfaceNormalVC); + } #endif - // volume shadows if needed - #ifdef VolumeShadowOn - vec3 tColorVS = applyShadowRay(tColor, posIS, rayDirVC); + // Compute volume lighting if needed + vec3 volumeShadedColor = tColor; + #ifdef EnableVolumeLighting + if (volCoeff >= EPSILON) { + volumeShadedColor = applyVolumeShadowLighting(tColor, posVC); + } #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 + // 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 getColorForLabelOutline() { + vec3 centerPosIS = + fragCoordToIndexSpace(gl_FragCoord); // pos in texture space vec4 centerValue = getTextureValue(centerPosIS); 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, 0, 0.5), + getOpacityFromTexture(centerValue.r, 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,9 +950,9 @@ 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); @@ -1121,361 +973,659 @@ 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 = volume.outlineOpacity; } return tColor; +} -#else - // compute the normal and gradient magnitude if needed - // We compute it as a vec4 if possible otherwise a mat4 +vec4 getColorForAdditivePreset(vec4 tValue, vec3 posVC, vec3 posIS) { + // compute normals + mat4 normalMat = computeMat4Normal(posIS, tValue); + vec4 normalLights[2]; + normalLights[0] = normalMat[0]; + normalLights[1] = normalMat[1]; + #if vtkNumberOfLights > 0 + if (volume.computeNormalFromOpacity == 1) { + for (int component = 0; component < 2; ++component) { + vec3 scalarInterp[2]; + float height = volume.transferFunctionsSampleHeight[component]; + computeNormalForDensity(posIS, scalarInterp, component); + normalLights[component] = + computeDensityNormal(scalarInterp, height, 1.0, component); + } + } + #endif - #ifdef UseIndependentComponents + // compute opacities + float opacities[2]; + opacities[0] = getOpacityFromTexture( + tValue[0], 0, volume.transferFunctionsSampleHeight[0]); + opacities[1] = getOpacityFromTexture( + tValue[1], 1, volume.transferFunctionsSampleHeight[1]); + #ifdef EnabledGradientOpacity + for (int component = 0; component < 2; ++component) { + opacities[component] *= + computeGradientOpacityFactor(normalMat[component].a, component); + } + #endif + float opacitySum = opacities[0] + opacities[1]; + if (opacitySum <= 0.0) { + return vec4(0.0); + } - // sample textures - vec3 tColor0 = texture2D(ctexture, vec2(tValue.r * cscale0 + cshift0, height0)).rgb; - float pwfValue0 = texture2D(otexture, vec2(tValue.r * oscale0 + oshift0, height0)).r; + // mix the colors and opacities + vec3 colors[2]; + for (int component = 0; component < 2; ++component) { + float sampleHeight = volume.transferFunctionsSampleHeight[component]; + vec3 color = getColorFromTexture(tValue[component], component, sampleHeight); + color = applyAllLightning(color, opacities[component], posVC, + normalLights[component]); + colors[component] = color; + } + vec3 mixedColor = + (opacities[0] * colors[0] + opacities[1] * colors[1]) / opacitySum; + return vec4(mixedColor, min(1.0, opacitySum)); +} - #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; +vec4 getColorForColorizePreset(vec4 tValue, vec3 posVC, vec3 posIS) { + // compute normals + mat4 normalMat = computeMat4Normal(posIS, tValue); + vec4 normalLight = normalMat[0]; + #if vtkNumberOfLights > 0 + if (volume.computeNormalFromOpacity == 1) { + vec3 scalarInterp[2]; + float height = volume.transferFunctionsSampleHeight[0]; + computeNormalForDensity(posIS, scalarInterp, 0); + normalLight = computeDensityNormal(scalarInterp, height, 1.0, 0); + } + #endif - #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; + // compute opacities + float opacity = getOpacityFromTexture( + tValue[0], 0, volume.transferFunctionsSampleHeight[0]); + #ifdef EnabledGradientOpacity + opacity *= computeGradientOpacityFactor(normalMat[0].a, 0); + #endif - #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 + // colorizing component + vec3 colorizingColor = getColorFromTexture( + tValue[0], 1, volume.transferFunctionsSampleHeight[1]); + float colorizingOpacity = getOpacityFromTexture( + tValue[1], 1, volume.transferFunctionsSampleHeight[1]); + + // mix the colors and opacities + vec3 color = + getColorFromTexture(tValue[0], 0, + volume.transferFunctionsSampleHeight[0]) * + mix(vec3(1.0), colorizingColor, colorizingOpacity); + color = applyAllLightning(color, opacity, posVC, normalLight); + return vec4(color, opacity); +} - #if !defined(vtkCustomComponentsColorMix) - // default path for component color mix +vec4 getColorForDefaultIndependentPreset(vec4 tValue, vec3 posIS) { - // compute the normal vectors as needed - #if (vtkLightComplexity > 0) || defined(vtkGradientOpacityOn) - mat4 normalMat = computeMat4Normal(posIS, tValue, tstep); - #endif + // compute the normal vectors as needed + #if defined(EnabledGradientOpacity) || vtkNumberOfLights > 0 + mat4 normalMat = computeMat4Normal(posIS, tValue); + #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 + // initial value of alpha is determined by wether the first component is + // proportional or not + #if defined(vtkComponent0Proportional) + // when it is proportional, it starts at 1 (neutral for multiplications) + float alpha = 1.0; + #else + // when it is not proportional, it starts at 0 (neutral for additions) + float alpha = 0.0; + #endif - // process color and opacity for each component + vec3 mixedColor = vec3(0.0); + #if vtkNumberOfComponents > 0 + { + const int component = 0; + vec3 color = getColorFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + float opacity = getOpacityFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); #if !defined(vtkComponent0Proportional) - float alpha = goFactor.x*mix0*pwfValue0; - #if vtkLightComplexity > 0 - applyLighting(tColor0, normalMat[0]); + float alphaContribution = volume.independentComponentMix[component] * opacity; + #ifdef EnabledGradientOpacity + alphaContribution *= computeGradientOpacityFactor(normalMat[component].a, component); + #endif + alpha += alphaContribution; + #if vtkNumberOfLights > 0 + color = applyLighting(color, normalMat[component]); #endif #else - tColor0 *= pwfValue0; - float alpha = mix(pwfValue0, 1.0, (1.0 - mix0)); + color *= opacity; + alpha *= mix(opacity, 1.0, + (1.0 - volume.independentComponentMix[component])); #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)); + mixedColor += volume.independentComponentMix[component] * color; + } + #endif + #if vtkNumberOfComponents > 1 + { + const int component = 1; + vec3 color = getColorFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + float opacity = getOpacityFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + #if !defined(vtkComponent1Proportional) + float alphaContribution = volume.independentComponentMix[component] * opacity; + #ifdef EnabledGradientOpacity + alphaContribution *= computeGradientOpacityFactor(normalMat[component].a, component); #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 + alpha += alphaContribution; + #if vtkNumberOfLights > 0 + color = applyLighting(color, normalMat[component]); #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 + #else + color *= opacity; + alpha *= mix(opacity, 1.0, + (1.0 - volume.independentComponentMix[component])); + #endif + mixedColor += volume.independentComponentMix[component] * color; + } + #endif + #if vtkNumberOfComponents > 2 + { + const int component = 2; + vec3 color = getColorFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + float opacity = getOpacityFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + #if !defined(vtkComponent2Proportional) + float alphaContribution = volume.independentComponentMix[component] * opacity; + #ifdef EnabledGradientOpacity + alphaContribution *= computeGradientOpacityFactor(normalMat[component].a, component); #endif + alpha += alphaContribution; + #if vtkNumberOfLights > 0 + color = applyLighting(color, normalMat[component]); + #endif + #else + color *= opacity; + alpha *= mix(opacity, 1.0, + (1.0 - volume.independentComponentMix[component])); #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 + mixedColor += volume.independentComponentMix[component] * color; + } + #endif + #if vtkNumberOfComponents > 3 + { + const int component = 3; + vec3 color = getColorFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + float opacity = getOpacityFromTexture( + tValue[component], component, + volume.transferFunctionsSampleHeight[component]); + #if !defined(vtkComponent3Proportional) + float alphaContribution = volume.independentComponentMix[component] * opacity; + #ifdef EnabledGradientOpacity + alphaContribution *= computeGradientOpacityFactor(normalMat[component].a, component); #endif + alpha += alphaContribution; + #if vtkNumberOfLights > 0 + color = applyLighting(color, normalMat[component]); + #endif + #else + color *= opacity; + alpha *= mix(opacity, 1.0, + (1.0 - volume.independentComponentMix[component])); #endif + mixedColor += volume.independentComponentMix[component] * color; + } + #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 + return vec4(mixedColor, alpha); +} - // 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 +vec4 getColorForDependentComponents(vec4 tValue, vec3 posVC, vec3 posIS) { + #if defined(EnabledGradientOpacity) || vtkNumberOfLights > 0 + // use component 3 of the opacity texture as getTextureValue() sets alpha to + // the opacity value + vec3 scalarInterp[2]; + vec4 normal0 = computeNormalForDensity(posIS, scalarInterp, 3); + float gradientOpacity = computeGradientOpacityFactor(normal0.a, 0); + #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 vtkNumberOfComponents == 1 + vec3 tColor = getColorFromTexture(tValue.r, 0, 0.5); + float alpha = getOpacityFromTexture(tValue.r, 0, 0.5); + #endif + #if vtkNumberOfComponents == 2 + vec3 tColor = vec3(tValue.r * volume.colorTextureScale[0] + + volume.colorTextureShift[0]); + float alpha = getOpacityFromTexture(tValue.a, 1, 0.5); + #endif + #if vtkNumberOfComponents == 3 + vec3 tColor = tValue.rgb * volume.colorTextureScale.rgb + + volume.colorTextureShift.rgb; + float alpha = getOpacityFromTexture(tValue.a, 0, 0.5); + #endif + #if vtkNumberOfComponents == 4 + vec3 tColor = tValue.rgb * volume.colorTextureScale.rgb + + volume.colorTextureShift.rgb; + float alpha = getOpacityFromTexture(tValue.a, 3, 0.5); + #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 + // Apply gradient opacity + #if defined(EnabledGradientOpacity) + alpha *= gradientOpacity; + #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; - } + #if vtkNumberOfComponents == 1 + if (alpha < EPSILON) { + return vec4(0.0); + } + #endif + + // lighting + #if vtkNumberOfLights > 0 + vec4 normalLight; + if (volume.computeNormalFromOpacity == 1) { + if (normal0[3] != 0.0) { + normalLight = + computeDensityNormal(scalarInterp, 0.5, gradientOpacity, 0); + if (normalLight[3] == 0.0) { + normalLight = normal0; } - #else - vec4 normalLight = normal0; - #endif - tColor = applyAllLightning(tColor, alpha, posIS, normalLight); - #endif + } + } else { + normalLight = normal0; + } + tColor = applyAllLightning(tColor, alpha, posVC, normalLight); + #endif - return vec4(tColor, alpha); - #endif // dependent -#endif + return vec4(tColor, alpha); } -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; - } - #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 +vec4 getColorForValue(vec4 tValue, vec3 posVC, vec3 posIS) { + #ifdef EnableColorForValueFunctionId0 + return getColorForDependentComponents(tValue, posVC, posIS); + #endif + + #ifdef EnableColorForValueFunctionId1 + return getColorForAdditivePreset(tValue, posVC, posIS); + #endif + + #ifdef EnableColorForValueFunctionId2 + return getColorForColorizePreset(tValue, posVC, posIS); + #endif + + #ifdef EnableColorForValueFunctionId3 + /* + * 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 + + #if defined(EnableColorForValueFunctionId4) || defined(EnableColorForValueFunctionId3) + return getColorForDefaultIndependentPreset(tValue, posIS); + #endif + + #ifdef EnableColorForValueFunctionId5 + return getColorForLabelOutline(); #endif - return withinRange; } -#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); +bool valueWithinScalarRange(vec4 val) { + #if vtkNumberOfComponents > 1 && !defined(EnabledIndependentComponents) + return false; + #endif + vec4 rangeMin = volume.ipScalarRangeMin; + vec4 rangeMax = volume.ipScalarRangeMax; + for (int component = 0; component < vtkNumberOfComponents; ++component) { + if (val[component] < rangeMin[component] || + rangeMax[component] < val[component]) { + return false; + } + } + return true; +} - bool justSawIt = false; +#if vtkBlendMode == LABELMAP_EDGE_PROJECTION_BLEND + bool checkOnEdgeForNeighbor(int xFragmentOffset, int yFragmentOffset, + int segmentIndex, vec3 stepIS) { + vec3 volumeDimensions = vec3(volume.dimensions); + vec4 neighborPixelCoord = vec4(gl_FragCoord.x + float(xFragmentOffset), + gl_FragCoord.y + float(yFragmentOffset), + gl_FragCoord.z, gl_FragCoord.w); + vec3 originalNeighborPosIS = fragCoordToIndexSpace(neighborPixelCoord); vec3 neighborPosIS = originalNeighborPosIS; + for (int k = 0; k < vtkMaximumNumberOfSamples / 2; ++k) { + ivec3 texCoord = ivec3(neighborPosIS * volumeDimensions); + vec4 texValue = rawFetchTexture(texCoord); + if (int(texValue.g) == segmentIndex) { + // not on edge + return false; + } + neighborPosIS += stepIS; + } - float stepsTraveled = 0.0; + neighborPosIS = originalNeighborPosIS; + for (int k = 0; k < vtkMaximumNumberOfSamples / 2; ++k) { + ivec3 texCoord = ivec3(neighborPosIS * volumeDimensions); + vec4 texValue = rawFetchTexture(texCoord); + if (int(texValue.g) == segmentIndex) { + // not on edge + return false; + } + neighborPosIS -= stepIS; + } + // onedge + float sampleHeight = volume.transferFunctionsSampleHeight[1]; + vec3 tColorSegment = + getColorFromTexture(float(segmentIndex), 1, sampleHeight); + float pwfValueSegment = + getOpacityFromTexture(float(segmentIndex), 1, sampleHeight); + gl_FragData[0] = vec4(tColorSegment, pwfValueSegment); + return true; + } +#endif - // float neighborValue; - for (int k = 0; k < //VTK::MaximumSamplesValue /2 ; ++k) { - ivec3 texCoord = ivec3(neighborPosIS * vec3(volumeDimensions)); - vec4 texValue = texelFetch(texture1, texCoord, 0); +vec4 getColorAtPos(vec3 posVC) { + vec3 posIS = posVCtoIS(posVC); + vec4 texValue = getTextureValue(posIS); + return getColorForValue(texValue, posVC, posIS); +} - if (int(texValue.g) == s) { - justSawIt = true; - break; - } - neighborPosIS += stepIS; +//======================================================================= +// Apply the specified blend mode operation along the ray's path. +// +void applyBlend(vec3 rayOriginVC, vec3 rayDirVC, float minDistance, + float maxDistance) { + // start slightly inside and apply some jitter + vec3 stepVC = rayDirVC * sampleDistance; + float raySteps = (maxDistance - minDistance) / sampleDistance; + + // Avoid 0.0 jitter + float jitter = 0.01 + 0.99 * fragmentSeed; + + #if vtkBlendMode == COMPOSITE_BLEND + // now map through opacity and color + vec3 firstPosVC = rayOriginVC + minDistance * rayDirVC; + vec4 firstColor = getColorAtPos(firstPosVC); + + // handle very thin volumes + if (raySteps <= 1.0) { + firstColor.a = 1.0 - pow(1.0 - firstColor.a, raySteps); + gl_FragData[0] = firstColor; + return; } - if (justSawIt){ - return false; + // 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; + + for (int i = 0; i < vtkMaximumNumberOfSamples; ++i) { + // If we have reached the last step, break + if (stepsTraveled + 1.0 >= raySteps) { + break; + } + vec4 tColor = getColorAtPos(posVC); + + 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; + } } - - neighborPosIS = originalNeighborPosIS; - for (int k = 0; k < //VTK::MaximumSamplesValue /2 ; ++k) { - ivec3 texCoord = ivec3(neighborPosIS * vec3(volumeDimensions)); - vec4 texValue = texelFetch(texture1, texCoord, 0); + 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 (int(texValue.g) == s) { - justSawIt = true; - break; - } - neighborPosIS -= stepIS; + float mix = (1.0 - color.a); + color = color + vec4(tColor.rgb * tColor.a, tColor.a) * mix; } + gl_FragData[0] = vec4(color.rgb / color.a, color.a); + #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; + #if vtkBlendMode == MAXIMUM_INTENSITY_BLEND || \ + vtkBlendMode == MINIMUM_INTENSITY_BLEND + // Find maximum/minimum intensity along the ray. + + // Define the operation we will use (min or max) + #if vtkBlendMode == MAXIMUM_INTENSITY_BLEND + #define OP max + #else + #define OP min + #endif + + vec3 posVC = rayOriginVC + minDistance * rayDirVC; + float stepsTraveled = 0.0; + + // Find a value to initialize the selected variables + vec4 selectedValue; + vec3 selectedPosVC; + vec3 selectedPosIS; + { + vec3 posIS = posVCtoIS(posVC); + selectedValue = getTextureValue(posIS); + selectedPosVC = posVC; + selectedPosIS = posIS; } - // not on edge - return false; -} + // 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); + return; + } -#endif + posVC += jitter * stepVC; + stepsTraveled += jitter; + + // Sample along the ray until vtkMaximumNumberOfSamples, + // ending slightly inside the total distance + for (int i = 0; i < vtkMaximumNumberOfSamples; ++i) { + // If we have reached the last step, break + if (stepsTraveled + 1.0 >= raySteps) { + break; + } + // Get selected values + vec3 posIS = posVCtoIS(posVC); + vec4 previousSelectedValue = selectedValue; + vec4 currentValue = getTextureValue(posIS); + selectedValue = OP(selectedValue, currentValue); + if (previousSelectedValue != selectedValue) { + selectedPosVC = posVC; + selectedPosIS = posIS; + } -//======================================================================= -// Apply the specified blend mode operation along the ray's path. -// -void applyBlend(vec3 posIS, vec3 endIS, vec3 tdims) -{ - vec3 tstep = 1.0/tdims; + // Otherwise, continue along the ray + stepsTraveled++; + posVC += stepVC; + } - // 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; - - // 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) + // Perform the last step along the ray using the + // residual distance + posVC = rayOriginVC + maxDistance * rayDirVC; { - gl_FragData[0] = getColorForValue(tValue, posIS, tstep); + vec3 posIS = posVCtoIS(posVC); + vec4 previousSelectedValue = selectedValue; + vec4 currentValue = getTextureValue(posIS); + selectedValue = OP(selectedValue, currentValue); + if (previousSelectedValue != selectedValue) { + selectedPosVC = posVC; + selectedPosIS = posIS; + } + } + + gl_FragData[0] = getColorForValue(selectedValue, selectedPosVC, selectedPosIS); + #endif + + #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; + + vec3 posIS = posVCtoIS(posVC); + vec4 value = getTextureValue(posIS); + + if (raySteps <= 1.0) { + gl_FragData[0] = getColorForValue(value * raySteps, posVC, posIS); + return; + } + + if (valueWithinScalarRange(value)) { + sum += value * jitter; + #if vtkBlendMode == AVERAGE_INTENSITY_BLEND + totalWeight += jitter; + #endif + } + posVC += jitter * stepVC; + stepsTraveled += jitter; + + // Sample along the ray until vtkMaximumNumberOfSamples, + // ending slightly inside the total distance + for (int i = 0; i < vtkMaximumNumberOfSamples; ++i) { + // If we have reached the last step, break + if (stepsTraveled + 1.0 >= raySteps) { + break; + } + + posIS = posVCtoIS(posVC); + value = 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(value)) { + sum += value; + #if vtkBlendMode == AVERAGE_INTENSITY_BLEND + totalWeight++; + #endif + } + + stepsTraveled++; + posVC += stepVC; + } + + // Perform the last step along the ray using the + // residual distance + posVC = rayOriginVC + maxDistance * rayDirVC; + posIS = posVCtoIS(posVC); + value = getTextureValue(posIS); + if (valueWithinScalarRange(value)) { + sum += value; + #if vtkBlendMode == AVERAGE_INTENSITY_BLEND + totalWeight += raySteps - stepsTraveled; + #endif + } + + #if vtkBlendMode == AVERAGE_INTENSITY_BLEND + sum /= vec4(totalWeight, totalWeight, totalWeight, 1.0); + #endif + + gl_FragData[0] = getColorForValue(sum, posVC, posIS); + #endif + + #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) { + vec3 posIS = posVCtoIS(posVC); + vec4 tValue = getTextureValue(posIS); + normalizedRayIntensity -= raySteps * sampleDistance * + getOpacityFromTexture(tValue.r, 0, 0.5); + gl_FragData[0] = + vec4(getColorFromTexture(normalizedRayIntensity, 0, 0.5), 1.0); + return; + } + + posVC += jitter * stepVC; + stepsTraveled += jitter; + + for (int i = 0; i < vtkMaximumNumberOfSamples; ++i) { + if (stepsTraveled + 1.0 >= raySteps) { + break; + } + + vec3 posIS = posVCtoIS(posVC); + vec4 value = getTextureValue(posIS); + // Convert scalar value to normalizedRayIntensity coefficient and + // accumulate normalizedRayIntensity + normalizedRayIntensity -= + sampleDistance * getOpacityFromTexture(value.r, 0, 0.5); + + posVC += stepVC; + stepsTraveled++; + } + + // map normalizedRayIntensity to color + gl_FragData[0] = + vec4(getColorFromTexture(normalizedRayIntensity, 0, 0.5), 1.0); + #endif + + #if vtkBlendMode == LABELMAP_EDGE_PROJECTION_BLEND + // Only works with a single volume + vec3 posVC = rayOriginVC + minDistance * rayDirVC; + float stepsTraveled = 0.0; + vec3 posIS = posVCtoIS(posVC); + vec4 tValue = getTextureValue(posIS); + if (raySteps <= 1.0) { + gl_FragData[0] = getColorForValue(tValue, posVC, posIS); return; } + vec3 stepIS = vecVCToIS(stepVC); vec4 value = tValue; - posIS += (jitter*stepIS); + posIS += jitter * stepIS; + stepsTraveled += jitter; vec3 maxPosIS = posIS; // Store the position of the max value int segmentIndex = int(value.g); bool originalPosHasSeenNonZero = false; - uint bitmask = 0u; - if (segmentIndex != 0) { - // Tried using the segment index in an boolean array but reading + // 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); + // 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); } - - // Sample along the ray until MaximumSamplesValue, + + // Sample along the ray until vtkMaximumNumberOfSamples, // ending slightly inside the total distance - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { + for (int i = 0; i < vtkMaximumNumberOfSamples; ++i) { // If we have reached the last step, break - if (stepsTraveled + 1.0 >= raySteps) { break; } + if (stepsTraveled + 1.0 >= raySteps) { + break; + } // compute the scalar tValue = getTextureValue(posIS); @@ -1483,11 +1633,11 @@ void applyBlend(vec3 posIS, vec3 endIS, vec3 tdims) if (segmentIndex != 0) { originalPosHasSeenNonZero = true; - setBit(segmentIndex); + setLabelOutlineBit(segmentIndex); } if (tValue.r > value.r) { - value = tValue; // Update the max value + value = tValue; // Update the max value maxPosIS = posIS; // Update the position where max occurred } @@ -1498,34 +1648,36 @@ void applyBlend(vec3 posIS, vec3 endIS, vec3 tdims) // Perform the last step along the ray using the // residual distance - posIS = endIS; + posIS = posVCtoIS(rayOriginVC + maxDistance * rayDirVC); tValue = getTextureValue(posIS); if (tValue.r > value.r) { - value = tValue; // Update the max value + value = tValue; // Update the max value maxPosIS = posIS; // Update the position where max occurred - } + } // 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); + vec3 maxPosVC = posIStoVC(maxPosIS); + gl_FragData[0] = getColorForValue(value, maxPosVC, maxPosIS); return; } - // probably we can make this configurable but for now we will use the same - // sample distance as the original sample distance - float neighborSampleDistanceIS = sampleDistanceIS; - vec3 neighborRayStepsIS = stepIS; float neighborRaySteps = raySteps; bool shouldLookInAllNeighbors = false; - 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 volumeSpacings = volume.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); - 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); + 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); vec3 baseWorld = fragCoordToWorld(base); vec3 baseXPlusWorld = fragCoordToWorld(baseXPlus); @@ -1537,27 +1689,31 @@ void applyBlend(vec3 posIS, vec3 endIS, vec3 tdims) float minFragSpacingWorld = min(XPlusDiff, YPlusDiff); for (int s = 1; s < MAX_SEGMENT_INDEX; s++) { - // bail out quickly if the segment index has not + // bail out quickly if the segment index has not // been seen by the center segment - if (!isBitSet(s)) { - continue; + if (!isLabelOutlineBitSet(s)) { + continue; } - // Use texture sampling for outlineThickness so that we can have + // 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; + 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 + // 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); + bool onEdge = checkOnEdgeForNeighbor(-actualThickness, -actualThickness, s, + stepIS) || + checkOnEdgeForNeighbor(actualThickness, actualThickness, s, + stepIS) || + checkOnEdgeForNeighbor(actualThickness, -actualThickness, s, + stepIS) || + checkOnEdgeForNeighbor(-actualThickness, +actualThickness, s, + stepIS); if (onEdge) { return; @@ -1565,262 +1721,35 @@ void applyBlend(vec3 posIS, vec3 endIS, vec3 tdims) // 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 + // 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) { + // 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; - } + 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; } + } } } - vec3 tColor0 = texture2D(ctexture, vec2(value.r * cscale0 + cshift0, height0)).rgb; - float pwfValue0 = texture2D(otexture, vec2(value.r * oscale0 + oshift0, height0)).r; + float sampleHeight = volume.transferFunctionsSampleHeight[0]; + vec3 tColor0 = getColorFromTexture(value.r, 0, sampleHeight); + float pwfValue0 = getOpacityFromTexture(value.r, 0, sampleHeight); 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; - } - - tColor.a = 1.0 - pow(1.0 - tColor.a, jitter); - color = vec4(tColor.rgb*tColor.a, tColor.a); - posIS += (jitter*stepIS); - - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - if (stepsTraveled + 1.0 >= raySteps) { break; } - - // compute the scalar - tValue = getTextureValue(posIS); - - // now map through opacity and color - tColor = getColorForValue(tValue, posIS, tstep); - - float mix = (1.0 - color.a); - - // 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)); - - color = color + vec4(tColor.rgb*tColor.a, tColor.a)*mix; - stepsTraveled++; - posIS += stepIS; - if (color.a > 0.99) { color.a = 1.0; break; } - } - - if (color.a < 0.99 && (raySteps - stepsTraveled) > 0.0) - { - posIS = endIS; - - // compute the scalar - tValue = getTextureValue(posIS); - - // now map through opacity and color - tColor = getColorForValue(tValue, posIS, tstep); - tColor.a = 1.0 - pow(1.0 - tColor.a, raySteps - stepsTraveled); - - float mix = (1.0 - color.a); - color = color + vec4(tColor.rgb*tColor.a, tColor.a)*mix; - } - - 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; - } - - vec4 value = tValue; - 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); - - // Update the maximum value if necessary - value = OP(tValue, value); - - // Otherwise, continue along the ray - stepsTraveled++; - posIS += stepIS; - } - - // 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.); - - if (valueWithinScalarRange(tValue, ipScalarRangeMin, ipScalarRangeMax)) { - sum += tValue; - } - - if (raySteps <= 1.0) { - gl_FragData[0] = getColorForValue(sum, posIS, tstep); - return; - } - - 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; - } - - // Perform the last step along the ray using the - // residual distance - posIS = endIS; - - // compute the scalar - tValue = getTextureValue(posIS); - - // 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; - - stepsTraveled++; - } - - #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; - } - - posIS += (jitter*stepIS); - - for (int i = 0; i < //VTK::MaximumSamplesValue ; ++i) - { - if (stepsTraveled + 1.0 >= raySteps) { break; } - - // compute the scalar value - tValue = getTextureValue(posIS); - - // Convert scalar value to normalizedRayIntensity coefficient and accumulate normalizedRayIntensity - normalizedRayIntensity = normalizedRayIntensity - sampleDistance*texture2D(otexture, vec2(tValue.r * oscale0 + oshift0, 0.5)).r; - - posIS += stepIS; - stepsTraveled++; - } - - // map normalizedRayIntensity to color - gl_FragData[0] = texture2D(ctexture, vec2(normalizedRayIntensity , 0.5)); - - #endif -} - -//======================================================================= -// 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; - } - 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 } //======================================================================= @@ -1831,40 +1760,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) { + vec2 dists = rayIntersectVolumeDistances(rayOriginVC, rayDirVC); //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 +1779,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 +1802,14 @@ 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) - { + vec3 rayOriginVC = vertexVCVSOutput; + vec2 rayStartEndDistancesVC = computeRayDistances(rayOriginVC, rayDirVC); + if (rayStartEndDistancesVC[1] <= rayStartEndDistancesVC[0] || + rayStartEndDistancesVC[1] <= 0.0) { + // Volume not hit or behind the ray discard; } - // 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, rayStartEndDistancesVC[0], rayStartEndDistancesVC[1]); }