diff --git a/Sources/Rendering/Core/VolumeMapper/example/controller.html b/Sources/Rendering/Core/VolumeMapper/example/controller.html index b844d146fe6..a8619eedd26 100644 --- a/Sources/Rendering/Core/VolumeMapper/example/controller.html +++ b/Sources/Rendering/Core/VolumeMapper/example/controller.html @@ -10,4 +10,5 @@ +
diff --git a/Sources/Rendering/Core/VolumeMapper/example/index.js b/Sources/Rendering/Core/VolumeMapper/example/index.js index 589accbbf96..860af4c9e77 100644 --- a/Sources/Rendering/Core/VolumeMapper/example/index.js +++ b/Sources/Rendering/Core/VolumeMapper/example/index.js @@ -49,6 +49,7 @@ const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); const volumeOptions = {}; const volumeSelectElem = document.getElementById('volume'); const presetSelectElem = document.getElementById('preset'); +const forceNearestElem = document.getElementById('forceNearest'); const actor = vtkVolume.newInstance(); const mapper = vtkVolumeMapper.newInstance(); @@ -144,6 +145,14 @@ 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 @@ -157,20 +166,32 @@ Object.keys(ColorMixPreset).forEach((key) => { presetSelectElem.appendChild(optionElem); }); -{ - const optionElem = document.createElement('option'); - optionElem.label = 'Default'; - optionElem.value = ''; - presetSelectElem.appendChild(optionElem); - presetSelectElem.value = optionElem.value; -} - const setColorMixPreset = (presetKey) => { const preset = presetKey ? ColorMixPreset[presetKey] : null; actor.getProperty().setColorMixPreset(preset); presetSelectElem.value = presetKey; }; +function updateForceNearestElem(comp) { + forceNearestElem.replaceChildren(); + for (let c = 0; c < comp; ++c) { + const checkboxElem = document.createElement('input'); + checkboxElem.type = 'checkbox'; + checkboxElem.checked = actor.getProperty().getForceNearestInterpolation(c); + checkboxElem.addEventListener('change', () => { + actor.getProperty().setForceNearestInterpolation(c, checkboxElem.checked); + renderWindow.render(); + }); + forceNearestElem.appendChild(checkboxElem); + const labelElem = document.createElement('label'); + labelElem.innerText = `Force linear interpolation for component ${c}`; + forceNearestElem.appendChild(labelElem); + forceNearestElem.appendChild(document.createElement('br')); + } +} + +updateForceNearestElem(1); + volumeSelectElem.addEventListener('change', () => { const { comp, data } = volumeOptions[volumeSelectElem.value]; if (comp === 1) { @@ -179,6 +200,7 @@ volumeSelectElem.addEventListener('change', () => { } else { presetSelectElem.style.display = 'block'; } + updateForceNearestElem(comp); const array = mapper.getInputData().getPointData().getArray(0); array.setData(data); array.setNumberOfComponents(comp); diff --git a/Sources/Rendering/Core/VolumeProperty/index.d.ts b/Sources/Rendering/Core/VolumeProperty/index.d.ts index ef494ba7c08..fecbef17e8f 100755 --- a/Sources/Rendering/Core/VolumeProperty/index.d.ts +++ b/Sources/Rendering/Core/VolumeProperty/index.d.ts @@ -135,6 +135,12 @@ export interface vtkVolumeProperty extends vtkObject { */ getUseGradientOpacity(index: number): boolean; + /** + * @see setForceNearestInterpolation + * @param {Number} index + */ + getForceNearestInterpolation(index: number): boolean; + /** * */ @@ -315,6 +321,15 @@ export interface vtkVolumeProperty extends vtkObject { */ setComponentWeight(index: number, value: number): boolean; + /** + * Force the nearest neighbor interpolation of one or more of the components + * The interpolation for the rest of the volume is set using `setInterpolationType` + * @see setInterpolationType + * @param {Number} index + * @param {Boolean} value + */ + setForceNearestInterpolation(index: number, value: boolean): boolean; + /** * Get the scalar component weights. * @param {Number} index diff --git a/Sources/Rendering/Core/VolumeProperty/index.js b/Sources/Rendering/Core/VolumeProperty/index.js index b81d0cb7f59..f8c178a65f9 100644 --- a/Sources/Rendering/Core/VolumeProperty/index.js +++ b/Sources/Rendering/Core/VolumeProperty/index.js @@ -211,6 +211,7 @@ function vtkVolumeProperty(publicAPI, model) { 'gradientOpacityMaximumValue', 'gradientOpacityMaximumOpacity', 'opacityMode', + 'forceNearestInterpolation', ]; sets.forEach((val) => { const cap = macro.capitalize(val); @@ -232,6 +233,7 @@ function vtkVolumeProperty(publicAPI, model) { 'gradientOpacityMaximumValue', 'gradientOpacityMaximumOpacity', 'opacityMode', + 'forceNearestInterpolation', ]; gets.forEach((val) => { const cap = macro.capitalize(val); @@ -283,6 +285,7 @@ export function extend(publicAPI, model, initialValues = {}) { useGradientOpacity: false, componentWeight: 1.0, + forceNearestInterpolation: false, }); } } diff --git a/Sources/Rendering/OpenGL/VolumeMapper/index.js b/Sources/Rendering/OpenGL/VolumeMapper/index.js index ab471615f49..bb5ba65ce1e 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/index.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/index.js @@ -190,10 +190,11 @@ 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 = actor.getProperty().getInterpolationType(); + const iType = actorProps.getInterpolationType(); if (iType === InterpolationType.LINEAR) { FSSource = vtkShaderProgram.substitute( FSSource, @@ -202,7 +203,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { ).result; } - const vtkImageLabelOutline = actor.getProperty().getUseLabelOutline(); + const vtkImageLabelOutline = actorProps.getUseLabelOutline(); if (vtkImageLabelOutline === true) { FSSource = vtkShaderProgram.substitute( FSSource, @@ -225,27 +226,33 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { '//VTK::IndependentComponentsOn', '#define UseIndependentComponents' ).result; + } - // Define any proportional components - const proportionalComponents = []; - for (let nc = 0; nc < numComp; nc++) { - if ( - actor.getProperty().getOpacityMode(nc) === OpacityMode.PROPORTIONAL - ) { - proportionalComponents.push(`#define vtkComponent${nc}Proportional`); - } + // 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 (proportionalComponents.length > 0) { - FSSource = vtkShaderProgram.substitute( - FSSource, - '//VTK::vtkProportionalComponents', - proportionalComponents.join('\n') - ).result; + if (actorProps.getForceNearestInterpolation(nc)) { + forceNearestComponents.push(`#define vtkComponent${nc}ForceNearest`); } } - const colorMixPreset = actor.getProperty().getColorMixPreset(); + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::vtkProportionalComponents', + proportionalComponents.join('\n') + ).result; + + FSSource = vtkShaderProgram.substitute( + FSSource, + '//VTK::vtkForceNearestComponents', + forceNearestComponents.join('\n') + ).result; + + const colorMixPreset = actorProps.getColorMixPreset(); const colorMixCode = getColorCodeFromPreset(colorMixPreset); if (colorMixCode) { FSSource = vtkShaderProgram.substitute( @@ -310,7 +317,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { } if ( model.renderable.getLocalAmbientOcclusion() && - actor.getProperty().getAmbient() > 0.0 + actorProps.getAmbient() > 0.0 ) { FSSource = vtkShaderProgram.substitute( FSSource, @@ -324,7 +331,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const numIComps = useIndependentComps ? numComp : 1; model.gopacity = false; for (let nc = 0; !model.gopacity && nc < numIComps; ++nc) { - model.gopacity ||= actor.getProperty().getUseGradientOpacity(nc); + model.gopacity ||= actorProps.getUseGradientOpacity(nc); } if (model.gopacity) { FSSource = vtkShaderProgram.substitute( @@ -544,10 +551,11 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { recomputeLightComplexity(actor, ren.getLights()); const numComp = model.scalarTexture.getComponents(); - const iComps = actorProps.getIndependentComponents(); const opacityModes = []; + const forceNearestInterps = []; for (let nc = 0; nc < numComp; nc++) { opacityModes.push(actorProps.getOpacityMode(nc)); + forceNearestInterps.push(actorProps.getForceNearestInterpolation(nc)); } const ext = model.currentInput.getSpatialExtent(); @@ -566,16 +574,17 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const hasZBufferTexture = !!model.zBufferTexture; const state = { + iComps: actorProps.getIndependentComponents(), colorMixPreset: actorProps.getColorMixPreset(), interpolationType: actorProps.getInterpolationType(), useLabelOutline: actorProps.getUseLabelOutline(), numComp, - iComps, maxSamples, useGradientOpacity: actorProps.getUseGradientOpacity(0), blendMode: model.renderable.getBlendMode(), hasZBufferTexture, opacityModes, + forceNearestInterps, }; // We need to rebuild the shader if one of these variables has changed, diff --git a/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl b/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl index 34d748330e8..aa87b6dd715 100644 --- a/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl +++ b/Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl @@ -39,6 +39,9 @@ varying vec3 vertexVCVSOutput; // possibly define any "proportional" components //VTK::vtkProportionalComponents +// possibly define any components that are forced to nearest interpolation +//VTK::vtkForceNearestComponents + // Define the blend mode to use #define vtkBlendMode //VTK::BlendMode @@ -232,17 +235,39 @@ uniform highp sampler3D texture1; vec4 getTextureValue(vec3 pos) { vec4 tmp = texture(texture1, pos); -#ifndef UseIndependentComponents - #if vtkNumComponents == 1 - tmp.a = tmp.r; - #endif - #if vtkNumComponents == 2 - tmp.a = tmp.g; + + #if defined(vtkComponent0ForceNearest) || \ + defined(vtkComponent1ForceNearest) || \ + defined(vtkComponent2ForceNearest) || \ + defined(vtkComponent3ForceNearest) + vec3 nearestPos = (floor(pos * vec3(volumeDimensions)) + 0.5) / vec3(volumeDimensions); + vec4 nearestValue = texture(texture1, nearestPos); + #ifdef vtkComponent0ForceNearest + tmp[0] = nearestValue[0]; + #endif + #ifdef vtkComponent1ForceNearest + tmp[1] = nearestValue[1]; + #endif + #ifdef vtkComponent2ForceNearest + tmp[2] = nearestValue[2]; + #endif + #ifdef vtkComponent3ForceNearest + tmp[3] = nearestValue[3]; + #endif #endif - #if vtkNumComponents == 3 - tmp.a = length(tmp.rgb); + + #ifndef UseIndependentComponents + #if vtkNumComponents == 1 + tmp.a = tmp.r; + #endif + #if vtkNumComponents == 2 + tmp.a = tmp.g; + #endif + #if vtkNumComponents == 3 + tmp.a = length(tmp.rgb); + #endif #endif -#endif + return tmp; }