From 31494bc8761eb05faf00a40b2572ec1e35a59f94 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 3 Jul 2026 12:55:53 -0400 Subject: [PATCH 1/3] feat(ImageResliceMapper): add slab-projected label outline Label outline rendering on the image reslice mapper previously broke as soon as a slab was active: the border detection compared the slab-composited center value against single-slice neighbors, which classified the entire projected footprint of a label as border and painted it solid. Labels are now projected across the slab (per-label presence bitmask marched in texture space, clamped to the volume so arbitrarily large slab thicknesses only cost the in-volume path) and each label is outlined where its projected footprint ends, probed at that label's own outline thickness. When several labels compete for a fragment, the one nearest the viewer along the slab wins, for both outlines and fill. Adds the LabelmapEdgeProjectionReslice example (reslice-mapper counterpart of LabelmapEdgeProjection) and an image regression test whose label spheres sit away from the slice plane so their outlines can only come from the slab projection. --- .../LabelmapEdgeProjectionReslice/index.js | 273 ++++++++++++++++++ .../testImageResliceMapperLabelOutlineSlab.js | 184 ++++++++++++ ...testImageResliceMapperLabelOutlineSlab.png | Bin 0 -> 79144 bytes .../OpenGL/ImageResliceMapper/index.js | 161 ++++++++++- 4 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 Examples/Rendering/LabelmapEdgeProjectionReslice/index.js create mode 100644 Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.js create mode 100644 Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.png diff --git a/Examples/Rendering/LabelmapEdgeProjectionReslice/index.js b/Examples/Rendering/LabelmapEdgeProjectionReslice/index.js new file mode 100644 index 00000000000..226a27ca9b8 --- /dev/null +++ b/Examples/Rendering/LabelmapEdgeProjectionReslice/index.js @@ -0,0 +1,273 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/All'; + +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; +import vtkImageResliceMapper from '@kitware/vtk.js/Rendering/Core/ImageResliceMapper'; +import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import { SlabTypes } from '@kitware/vtk.js/Rendering/Core/ImageResliceMapper/Constants'; +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; +import GUI from 'lil-gui'; + +/** + * This example demonstrates slab-projected label outlines with the image + * reslice mapper. + * + * It is the reslice-mapper counterpart of the LabelmapEdgeProjection example + * (which uses the volume mapper's LABELMAP_EDGE_PROJECTION_BLEND). The base + * image renders as a thick-slab MIP through vtkImageResliceMapper, and the + * labelmap renders as a second reslice actor over the same slab with + * useLabelOutline enabled. + * + * With a slab active, the mapper projects label presence across the whole + * slab and draws each label's outline where its projected footprint ends, so + * segment edges remain visible over the MIP instead of the labelmap either + * disappearing (single-slice labels) or filling the whole projected footprint. + */ +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + background: [0, 0, 0], +}); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Base image pipeline (thick-slab MIP through the reslice mapper) +// ---------------------------------------------------------------------------- + +const slicePlane = vtkPlane.newInstance(); +slicePlane.setNormal(0, 0, 1); + +const imageActor = vtkImageSlice.newInstance(); +const imageMapper = vtkImageResliceMapper.newInstance(); +imageMapper.setSlicePlane(slicePlane); +imageMapper.setSlabType(SlabTypes.MAX); +imageActor.setMapper(imageMapper); + +const labelmapActor = vtkImageSlice.newInstance(); +const labelmapMapper = vtkImageResliceMapper.newInstance(); +labelmapMapper.setSlicePlane(slicePlane); +labelmapMapper.setSlabType(SlabTypes.MAX); +labelmapActor.setMapper(labelmapMapper); + +// ---------------------------------------------------------------------------- +// GUI controls +// ---------------------------------------------------------------------------- +const gui = new GUI(); +const guiParams = { + slabThickness: 1, + thickness1: 3, + thickness2: 3, + outlineOpacity: 1, +}; + +let maxSlabThickness = 100; +let slabThicknessController = null; + +// ---------------------------------------------------------------------------- +// Common functions +// ---------------------------------------------------------------------------- + +function createSphericalLabel(data, d, c, radius, label) { + for (let k = 0; k < d[2]; k++) { + for (let j = 0; j < d[1]; j++) { + for (let i = 0; i < d[0]; i++) { + const dx = i - c[0]; + const dy = j - c[1]; + const dz = k - c[2]; + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (distance <= radius) { + const index = k * d[1] * d[0] + j * d[0] + i; + data[index] = label; + } + } + } + } +} + +function setupImageTransferFunctions() { + const ctfun = vtkColorTransferFunction.newInstance(); + ctfun.addRGBPoint(0, 0, 0, 0); + ctfun.addRGBPoint(255, 1.0, 1.0, 1.0); + + const ofun = vtkPiecewiseFunction.newInstance(); + ofun.addPoint(0.0, 1.0); + ofun.addPoint(255.0, 1.0); + + imageActor.getProperty().setRGBTransferFunction(0, ctfun); + imageActor.getProperty().setPiecewiseFunction(0, ofun); + imageActor.getProperty().setUseLookupTableScalarRange(true); +} + +function createLabelmap(imgData, d, c, r1, r2) { + const values = new Uint8Array(imgData.getNumberOfPoints()); + const labelMapData = vtkImageData.newInstance( + imgData.get('spacing', 'origin', 'direction') + ); + + const dataArray = vtkDataArray.newInstance({ + numberOfComponents: 1, // labelmap with single component + values, + }); + labelMapData.getPointData().setScalars(dataArray); + + labelMapData.setDimensions(...imgData.getDimensions()); + labelMapData.setSpacing(...imgData.getSpacing()); + labelMapData.setOrigin(...imgData.getOrigin()); + labelMapData.setDirection(...imgData.getDirection()); + + const labelmapArray = labelMapData.getPointData().getScalars().getData(); + + // Same labels as the LabelmapEdgeProjection example + createSphericalLabel(labelmapArray, d, c, r1, 1); + createSphericalLabel( + labelmapArray, + d, + [c[0] + 2 * r1, c[1] + 2 * r1, c[2] + 2 * r1], + r2, + 2 + ); + + return labelMapData; +} + +function setupLabelmapTransferFunctions() { + const labelMapColorFunction = vtkColorTransferFunction.newInstance(); + labelMapColorFunction.addRGBPoint(0, 0, 0, 0); + labelMapColorFunction.addRGBPoint(1, 0, 0, 1); // Blue for label 1 + labelMapColorFunction.addRGBPoint(2, 1, 0, 0); // Red for label 2 + + // Edge projection only: the label fill stays fully transparent + const labelMapOpacityFunction = vtkPiecewiseFunction.newInstance(); + labelMapOpacityFunction.addPoint(0, 0); + labelMapOpacityFunction.addPoint(1, 0); + labelMapOpacityFunction.addPoint(2, 0); + + const property = labelmapActor.getProperty(); + property.setRGBTransferFunction(0, labelMapColorFunction); + property.setPiecewiseFunction(0, labelMapOpacityFunction); + property.setUseLookupTableScalarRange(true); + + // set interpolation to nearest to avoid blending labels + property.setInterpolationTypeToNearest(); + + property.setUseLabelOutline(true); + property.setLabelOutlineOpacity(guiParams.outlineOpacity); + property.setLabelOutlineThickness([ + guiParams.thickness1, + guiParams.thickness2, + ]); +} + +function updateOutlineThickness() { + labelmapActor + .getProperty() + .setLabelOutlineThickness([guiParams.thickness1, guiParams.thickness2]); + renderWindow.render(); +} + +function updateOutlineOpacity() { + labelmapActor.getProperty().setLabelOutlineOpacity(guiParams.outlineOpacity); + renderWindow.render(); +} + +function updateSlabThickness() { + imageMapper.setSlabThickness(guiParams.slabThickness); + labelmapMapper.setSlabThickness(guiParams.slabThickness); + renderWindow.render(); +} + +gui + .add(guiParams, 'thickness1', 1, 10, 1) + .name('Segment 1 Thickness') + .onChange(updateOutlineThickness); +gui + .add(guiParams, 'thickness2', 1, 10, 1) + .name('Segment 2 Thickness') + .onChange(updateOutlineThickness); +gui + .add(guiParams, 'outlineOpacity', 0, 1, 0.05) + .name('Outline Opacity') + .onChange(updateOutlineOpacity); + +// ---------------------------------------------------------------------------- +// Main execution +// ---------------------------------------------------------------------------- + +const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); + +reader.setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`).then(() => { + reader.loadData().then(() => { + const imageData = reader.getOutputData(); + + const dims = imageData.getDimensions(); + const center = dims.map((d) => Math.floor(d / 2)); + const minDim = Math.min(...dims); + const radius1 = Math.floor(minDim / 6); + const radius2 = Math.floor(minDim / 6); + + const labelMapData = createLabelmap( + imageData, + dims, + center, + radius1, + radius2 + ); + + imageMapper.setInputData(imageData); + labelmapMapper.setInputData(labelMapData); + + setupImageTransferFunctions(); + setupLabelmapTransferFunctions(); + + const worldCenter = imageData.getCenter(); + slicePlane.setOrigin(worldCenter); + + // Default the slab to the full volume extent along the slice normal so + // the base image renders as a full MIP like the volume-mapper example + const bounds = imageData.getBounds(); + maxSlabThickness = Math.ceil(bounds[5] - bounds[4]); + guiParams.slabThickness = maxSlabThickness; + slabThicknessController = gui + .add(guiParams, 'slabThickness', 0, maxSlabThickness, 1) + .name('Slab Thickness') + .onChange(updateSlabThickness); + slabThicknessController.updateDisplay(); + updateSlabThickness(); + + renderer.addActor(imageActor); + renderer.addActor(labelmapActor); + + // Keep the reslice plane facing the camera so rotating with the default + // trackball style spins the slab MIP like the volume-mapper example + const camera = renderer.getActiveCamera(); + camera.onModified(() => { + slicePlane.setNormal(camera.getDirectionOfProjection()); + slicePlane.setOrigin(worldCenter); + }); + + renderer.resetCamera(); + renderer.resetCameraClippingRange(); + renderWindow.render(); + }); +}); + +// ---------------------------------------------------------------------------- +// Make some variables global so that you can inspect and +// modify objects in your browser's developer console: +// ---------------------------------------------------------------------------- + +global.source = reader; +global.imageMapper = imageMapper; +global.imageActor = imageActor; +global.labelmapMapper = labelmapMapper; +global.labelmapActor = labelmapActor; +global.renderer = renderer; +global.renderWindow = renderWindow; diff --git a/Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.js b/Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.js new file mode 100644 index 00000000000..48c5582c040 --- /dev/null +++ b/Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.js @@ -0,0 +1,184 @@ +import { it, expect } from 'vitest'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/OpenGL/Profiles/All'; + +import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; +import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData'; +import vtkImageProperty from 'vtk.js/Sources/Rendering/Core/ImageProperty'; +import vtkImageResliceMapper from 'vtk.js/Sources/Rendering/Core/ImageResliceMapper'; +import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice'; +import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; +import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import { SlabTypes } from 'vtk.js/Sources/Rendering/Core/ImageResliceMapper/Constants'; + +import baseline from './testImageResliceMapperLabelOutlineSlab.png'; + +it.skipIf(__VTK_TEST_NO_WEBGL__)( + 'Test ImageResliceMapper LabelOutline with slab projection', + async () => { + const gc = testUtils.createGarbageCollector(); + expect( + 'rendering', + 'vtkImageResliceMapper testImageResliceMapperLabelOutlineSlab' + ).toBeTruthy(); + + const bodyElem = document.querySelector('body'); + const container = gc.registerDOMElement(document.createElement('div')); + bodyElem.appendChild(container); + + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const glwindow = gc.registerResource(renderWindow.newAPISpecificView()); + glwindow.setContainer(container); + renderWindow.addView(glwindow); + glwindow.setSize(400, 400); + + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + renderer.setBackground(0.3, 0.3, 0.3); + + const imageDimension = 48; + + // Background image with gradient, rendered as a full-extent MAX slab + const bgImage = gc.registerResource(vtkImageData.newInstance()); + bgImage.setDimensions(imageDimension, imageDimension, imageDimension); + bgImage.setSpacing(1, 1, 1); + bgImage.setOrigin(0, 0, 0); + + const bgValues = new Uint8Array(imageDimension ** 3); + for (let k = 0; k < imageDimension; k++) { + for (let j = 0; j < imageDimension; j++) { + for (let i = 0; i < imageDimension; i++) { + const idx = i + imageDimension * (j + imageDimension * k); + bgValues[idx] = Math.floor(((i + j) / (2 * imageDimension)) * 255); + } + } + } + + const bgScalars = gc.registerResource( + vtkDataArray.newInstance({ + numberOfComponents: 1, + values: bgValues, + }) + ); + bgImage.getPointData().setScalars(bgScalars); + + // Labelmap with two spherical segments placed AWAY from the slice plane + // (z = 10): label 1 around z = 5, label 2 around z = 15. Their outlines can + // only appear through the slab projection of label presence. + const labelImage = gc.registerResource(vtkImageData.newInstance()); + labelImage.setDimensions(imageDimension, imageDimension, imageDimension); + labelImage.setSpacing(1, 1, 1); + labelImage.setOrigin(0, 0, 0); + + const sphereCenters = [ + { c: [15, 24, 12], r: 9, label: 1 }, + { c: [33, 24, 36], r: 9, label: 2 }, + ]; + const labelValues = new Uint8Array(imageDimension ** 3); + sphereCenters.forEach(({ c, r, label }) => { + for (let k = 0; k < imageDimension; k++) { + for (let j = 0; j < imageDimension; j++) { + for (let i = 0; i < imageDimension; i++) { + const dx = i - c[0]; + const dy = j - c[1]; + const dz = k - c[2]; + if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= r) { + const idx = i + imageDimension * (j + imageDimension * k); + labelValues[idx] = label; + } + } + } + } + }); + + const labelScalars = gc.registerResource( + vtkDataArray.newInstance({ + numberOfComponents: 1, + values: labelValues, + }) + ); + labelImage.getPointData().setScalars(labelScalars); + + // Shared slice plane through the volume center + const slicePlane = gc.registerResource(vtkPlane.newInstance()); + slicePlane.setNormal(0, 0, 1); + slicePlane.setOrigin(bgImage.getCenter()); + + // Background pipeline + const bgMapper = gc.registerResource(vtkImageResliceMapper.newInstance()); + bgMapper.setSlicePlane(slicePlane); + bgMapper.setSlabType(SlabTypes.MAX); + bgMapper.setSlabThickness(imageDimension); + bgMapper.setInputData(bgImage); + + const bgPpty = gc.registerResource(vtkImageProperty.newInstance()); + bgPpty.setColorWindow(255); + bgPpty.setColorLevel(127); + + const bgActor = gc.registerResource(vtkImageSlice.newInstance()); + bgActor.setMapper(bgMapper); + bgActor.setProperty(bgPpty); + renderer.addActor(bgActor); + + // Labelmap pipeline (single component, dependent components) over the + // same slab + const labelMapper = gc.registerResource( + vtkImageResliceMapper.newInstance() + ); + labelMapper.setSlicePlane(slicePlane); + labelMapper.setSlabType(SlabTypes.MAX); + labelMapper.setSlabThickness(imageDimension); + labelMapper.setInputData(labelImage); + + const labelPpty = gc.registerResource(vtkImageProperty.newInstance()); + labelPpty.setIndependentComponents(false); + labelPpty.setUseLookupTableScalarRange(true); + labelPpty.setInterpolationTypeToNearest(); + labelPpty.setUseLabelOutline(true); + labelPpty.setLabelOutlineThickness([1, 2]); + labelPpty.setLabelOutlineOpacity(1.0); + + const labelRgb = gc.registerResource( + vtkColorTransferFunction.newInstance() + ); + labelRgb.addRGBPoint(0, 0, 0, 0); + labelRgb.addRGBPoint(1, 1, 0, 0); // Red + labelRgb.addRGBPoint(2, 0, 1, 0); // Green + labelPpty.setRGBTransferFunction(0, labelRgb); + + const labelOfun = gc.registerResource(vtkPiecewiseFunction.newInstance()); + labelOfun.addPoint(0, 0); + labelOfun.addPoint(1, 0.3); + labelOfun.addPoint(2, 0.3); + labelPpty.setPiecewiseFunction(0, labelOfun); + + const labelActor = gc.registerResource(vtkImageSlice.newInstance()); + labelActor.setMapper(labelMapper); + labelActor.setProperty(labelPpty); + renderer.addActor(labelActor); + + renderer.resetCamera(); + renderer.getActiveCamera().dolly(1.6); + renderer.resetCameraClippingRange(); + + const promise = glwindow.captureNextImage().then((image) => + testUtils.compareImages( + image, + [baseline], + 'Rendering/Core/ImageResliceMapper', + { + pixelThreshold: 0.005, + mismatchTolerance: 2, + } + ) + ); + renderWindow.render(); + await promise; + + gc.releaseResources(); + } +); diff --git a/Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.png b/Sources/Rendering/Core/ImageResliceMapper/test/testImageResliceMapperLabelOutlineSlab.png new file mode 100644 index 0000000000000000000000000000000000000000..94e48921364474253838b2645bbf55b9e92cc92a GIT binary patch literal 79144 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 literal 0 HcmV?d00001 diff --git a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js index c6fc9831f1e..59fe1dc73ca 100644 --- a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js @@ -1291,6 +1291,158 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { ]; } + function getSlabLabelMaskDecLines() { + return [ + '// Returns a bitmask of the labels (1..31) present along the slab', + '// centered at startTC. Marches in texture space and exits as soon as', + '// the ray leaves the unit cube, so only the in-volume portion of the', + '// slab is sampled regardless of how large the slab thickness is.', + 'int labelSlabMask(vec3 startTC, vec3 stepTC, float halfSlab, float stepLen)', + '{', + ' int mask = 0;', + ' vec3 tc = startTC;', + ' float dist = 0.0;', + ' for (int i = 0; i < 4096; ++i)', + ' {', + ' if (dist > halfSlab) { break; }', + ' if (any(greaterThan(tc, vec3(1.0))) || any(lessThan(tc, vec3(0.0)))) { break; }', + ' int label = int(texture(volumeTexture[0], tc).r * 255.0 + 0.5);', + ' if (label > 0 && label < 32) { mask |= (1 << label); }', + ' tc += stepTC;', + ' dist += stepLen;', + ' }', + ' tc = startTC - stepTC;', + ' dist = stepLen;', + ' for (int i = 0; i < 4096; ++i)', + ' {', + ' if (dist > halfSlab) { break; }', + ' if (any(greaterThan(tc, vec3(1.0))) || any(lessThan(tc, vec3(0.0)))) { break; }', + ' int label = int(texture(volumeTexture[0], tc).r * 255.0 + 0.5);', + ' if (label > 0 && label < 32) { mask |= (1 << label); }', + ' tc -= stepTC;', + ' dist += stepLen;', + ' }', + ' return mask;', + '}', + '', + '// Number of stepTC-sized steps from p that stay inside the unit cube', + 'float labelSlabBoxSteps(vec3 p, vec3 stepTC)', + '{', + ' vec3 limit = vec3(65536.0);', + ' if (stepTC.x > 1e-8) { limit.x = (1.0 - p.x) / stepTC.x; }', + ' else if (stepTC.x < -1e-8) { limit.x = -p.x / stepTC.x; }', + ' if (stepTC.y > 1e-8) { limit.y = (1.0 - p.y) / stepTC.y; }', + ' else if (stepTC.y < -1e-8) { limit.y = -p.y / stepTC.y; }', + ' if (stepTC.z > 1e-8) { limit.z = (1.0 - p.z) / stepTC.z; }', + ' else if (stepTC.z < -1e-8) { limit.z = -p.z / stepTC.z; }', + ' return min(limit.x, min(limit.y, limit.z));', + '}', + '', + '// First label of labelMask found when marching the slab from its', + '// viewer-side end toward the back, so overlapping labels resolve in', + '// depth order; returns 0 when none of the mask labels is found', + 'int labelSlabFrontLabel(vec3 startTC, vec3 towardCameraTC, float halfSlab, float stepLen, int labelMask)', + '{', + ' float slabSteps = halfSlab / stepLen;', + ' float nFront = min(slabSteps, labelSlabBoxSteps(startTC, towardCameraTC));', + ' float nBack = min(slabSteps, labelSlabBoxSteps(startTC, -towardCameraTC));', + ' vec3 tc = startTC + towardCameraTC * nFront;', + ' int totalSteps = int(nFront + nBack) + 1;', + ' for (int i = 0; i < 8192; ++i)', + ' {', + ' if (i >= totalSteps) { break; }', + ' int label = int(texture(volumeTexture[0], tc).r * 255.0 + 0.5);', + ' if (label > 0 && label < 32 && (labelMask & (1 << label)) != 0) { return label; }', + ' tc -= towardCameraTC;', + ' }', + ' return 0;', + '}', + ]; + } + + // Slab-projected label outline for a single-component labelmap: labels are + // projected across the slab and the outline is drawn where a label's + // projected footprint ends. The single-slice border detection cannot be + // used here since comparing the slab-composited center value against + // single-slice neighbors classifies the entire projected footprint as + // border. Expects the locals declared by getSlabSampleLoopLines() + // (scaling, slabNormal) and the helpers from getSlabLabelMaskDecLines(). + function getSlabLabelOutlineImplLines() { + return splitStringOnEnter(` + // Slab-projected label outline for single component + if (slabType == 1 && tvalue.r < 0.5 / 255.0) { + // MAX projection saw no label anywhere along the slab + gl_FragData[0] = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + + vec3 outlineStepTC = (WCTCMatrix0 * vec4(scaling * slabNormal * vboScaling, 0.0)).xyz; + float halfSlab = slabThickness * 0.5; + int centerMask = labelSlabMask(fragTexCoord, outlineStepTC, halfSlab, scaling); + + if (centerMask == 0) { + gl_FragData[0] = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + + // A label is on its projected edge when present here but missing in + // some neighbor probed at that label's own outline thickness. Labels + // sharing a thickness reuse the same neighbor masks, so the common + // case still costs four neighbor marches. + float labelmapRow = 0.5 / numLabelmaps; + int edgeLabels = 0; + int prevThickness = -1; + int neighborMask = 0; + for (int s = 1; s < 32; ++s) { + if ((centerMask & (1 << s)) == 0) { continue; } + float thicknessCoordinate = (float(s) - 1.0) / labelOutlineTextureWidth; + int segmentThickness = max(1, int(texture2D(labelOutlineThicknessTexture, vec2(thicknessCoordinate, labelmapRow)).r * 255.0)); + if (segmentThickness != prevThickness) { + vec3 outlineOffset1 = outlineTangent1_0 * texelSize0 * float(segmentThickness); + vec3 outlineOffset2 = outlineTangent2_0 * texelSize0 * float(segmentThickness); + neighborMask = + labelSlabMask(fragTexCoord + outlineOffset1, outlineStepTC, halfSlab, scaling) & + labelSlabMask(fragTexCoord - outlineOffset1, outlineStepTC, halfSlab, scaling) & + labelSlabMask(fragTexCoord + outlineOffset2, outlineStepTC, halfSlab, scaling) & + labelSlabMask(fragTexCoord - outlineOffset2, outlineStepTC, halfSlab, scaling); + prevThickness = segmentThickness; + } + if ((neighborMask & (1 << s)) == 0) { edgeLabels |= (1 << s); } + } + + // When several edge labels compete for this fragment, the one nearest + // the viewer along the slab wins. The camera looks down -z in view + // coordinates, so the slab normal points toward the viewer when its + // view-space z component is positive. + float slabNormalTowardCamera = (MCVCMatrix * vec4(slabNormal, 0.0)).z; + vec3 towardCameraTC = slabNormalTowardCamera > 0.0 ? outlineStepTC : -outlineStepTC; + + if (edgeLabels != 0) { + int edgeLabel = labelSlabFrontLabel(fragTexCoord, towardCameraTC, halfSlab, scaling, edgeLabels); + if (edgeLabel == 0) { + // numeric fallback: smallest edge label + for (int s = 1; s < 32; ++s) { + if ((edgeLabels & (1 << s)) != 0) { edgeLabel = s; break; } + } + } + float labelValue = float(edgeLabel) / 255.0; + vec3 edgeColor = texture2D(colorTexture1, vec2(labelValue * cscale0 + cshift0, 0.5)).rgb; + float opacityCoordinate = (float(edgeLabel) - 1.0) / labelOutlineTextureWidth; + float edgeOpacity = texture2D(labelOutlineOpacityTexture, vec2(opacityCoordinate, labelmapRow)).r; + gl_FragData[0] = vec4(edgeColor, edgeOpacity); + return; + } + + // Interior of the projected labels: regular fill through the transfer + // functions using the label nearest the viewer + int fillLabel = labelSlabFrontLabel(fragTexCoord, towardCameraTC, halfSlab, scaling, centerMask); + float fillValue = fillLabel != 0 ? float(fillLabel) / 255.0 : tvalue.r; + vec3 fillColor = texture2D(colorTexture1, vec2(fillValue * cscale0 + cshift0, 0.5)).rgb; + float fillOpacity = texture2D(pwfTexture1, vec2(fillValue * pwfscale0 + pwfshift0, 0.5)).r; + gl_FragData[0] = vec4(fillColor, fillOpacity * opacity); + `); + } + function collectLabelmapInputIndices(numberOfComponents, isLabelmap) { const labelmapInputs = []; for (let i = 0; i < numberOfComponents; i++) { @@ -1450,6 +1602,11 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { 'uniform vec3 vboScaling;', ]); tcoordFSDec = tcoordFSDec.concat(getSlabCompositeDecLines()); + if (useLabelOutline && !iComps && tNumComp === 1) { + // view matrix used to resolve which slab end faces the camera + tcoordFSDec = tcoordFSDec.concat(['uniform mat4 MCVCMatrix;']); + tcoordFSDec = tcoordFSDec.concat(getSlabLabelMaskDecLines()); + } } FSSource = vtkShaderProgram.substitute( FSSource, @@ -1516,7 +1673,9 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { // dependent components switch (tNumComp) { case 1: - if (useLabelOutline) { + if (useLabelOutline && slabThickness > 0.0) { + tcoordFSImpl = tcoordFSImpl.concat(getSlabLabelOutlineImplLines()); + } else if (useLabelOutline) { tcoordFSImpl = tcoordFSImpl.concat([ ...splitStringOnEnter(` // Label outline mode for single component From 7ee650402a7ad263e06a28df0e9273831a9a1e26 Mon Sep 17 00:00:00 2001 From: Alireza Date: Sat, 4 Jul 2026 21:58:44 -0400 Subject: [PATCH 2/3] perf(ImageResliceMapper): exit slab loop when both rays leave the volume The slab sampling loop marched slabThickness / (0.5 * minSpacing) iterations per fragment regardless of the volume bounds, so slab thicknesses much larger than the volume (whole-volume projections) spent almost all iterations sampling nothing. Track when each march direction leaves the unit texture cube - sample positions advance monotonically along a line and the cube is convex, so an exited direction can never re-enter - and stop the loop once both have exited. The cost is now proportional to the in-volume portion of the slab. --- .../OpenGL/ImageResliceMapper/index.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js index 59fe1dc73ca..ef6dde3d218 100644 --- a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js @@ -1256,15 +1256,24 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { 'vec3 normalxspacing = scaling * slabNormal;', 'float distTraveled = length(normalxspacing);', 'int trapezoid = 0;', + '// Each march direction leaves the volume for good once it exits the', + '// unit cube (the sample positions are monotonic along a line and the', + '// cube is convex), so the loop can stop as soon as both directions', + '// have exited. Slab thicknesses larger than the volume then only cost', + '// the in-volume portion of the march.', + 'bool negExited = false;', + 'bool posExited = false;', 'while (distTraveled < slabThickness * 0.5)', '{', ' distTraveled += length(normalxspacing);', ' float fnumSlices = float(numSlices);', + ' bool atSlabBoundary = false;', ' if (distTraveled > slabThickness * 0.5)', ' {', ' // Before stepping outside the slab, sample at the boundaries', ' normalxspacing = slabNormal * slabThickness * 0.5 / fnumSlices;', ' trapezoid = slabTrapezoid;', + ' atSlabBoundary = true;', ' }', ' vec3 worldPosNeg = vertexWCVSOutput.xyz - fnumSlices * normalxspacing * vboScaling;', ' vec3 fragTCoordNeg = (WCTCMatrix0 * vec4(worldPosNeg, 1.0)).xyz;', @@ -1274,6 +1283,10 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { ' tvalue = compositeValue(tvalue, newVal, trapezoid);', ' numSlices += 1;', ' }', + ' else if (!atSlabBoundary)', + ' {', + ' negExited = true;', + ' }', ' vec3 worldPosPos = vertexWCVSOutput.xyz + fnumSlices * normalxspacing * vboScaling;', ' vec3 fragTCoordPos = (WCTCMatrix0 * vec4(worldPosPos, 1.0)).xyz;', ' if (!any(greaterThan(fragTCoordPos, vec3(1.0))) && !any(lessThan(fragTCoordPos, vec3(0.0))))', @@ -1282,6 +1295,11 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { ' tvalue = compositeValue(tvalue, newVal, trapezoid);', ' numSlices += 1;', ' }', + ' else if (!atSlabBoundary)', + ' {', + ' posExited = true;', + ' }', + ' if (negExited && posExited) { break; }', '}', '// Finally, if slab type is *mean*, divide the sum by the numSlices', 'if (slabType == 2)', From f71fd76b9523ec213846707096820401dd0f8225 Mon Sep 17 00:00:00 2001 From: Alireza Date: Sat, 4 Jul 2026 21:58:56 -0400 Subject: [PATCH 3/3] fix(ImageResliceMapper): keep slab label outline compatible with render pass postambles The slab label outline fragment block returned early for transparent, edge, and fill fragments. Render passes append code after the mapper's fragment block - the order-independent translucency pass premultiplies gl_FragData[0] by a depth weight and writes that weight to a second render target - so returning early left those outputs unwritten and corrupted the pass's resolve (outlines composited washed-out/white under a translucent pass). Restructure the block to assign gl_FragData[0] exactly once and fall through; transparent fragments now exit through the pass's own zero-alpha discard. --- .../OpenGL/ImageResliceMapper/index.js | 135 +++++++++--------- 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js index ef6dde3d218..529c2b5b55b 100644 --- a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js @@ -1385,79 +1385,82 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { // single-slice neighbors classifies the entire projected footprint as // border. Expects the locals declared by getSlabSampleLoopLines() // (scaling, slabNormal) and the helpers from getSlabLabelMaskDecLines(). + // + // The block assigns gl_FragData[0] exactly once and never returns early: + // render passes append code after the mapper's fragment block (e.g. the + // order-independent translucency pass premultiplies gl_FragData[0] by a + // depth weight and writes that weight to a second render target), so an + // early return would leave those outputs unwritten and corrupt the pass's + // resolve step. Transparent fragments fall through with alpha 0 instead. function getSlabLabelOutlineImplLines() { return splitStringOnEnter(` // Slab-projected label outline for single component - if (slabType == 1 && tvalue.r < 0.5 / 255.0) { - // MAX projection saw no label anywhere along the slab - gl_FragData[0] = vec4(0.0, 0.0, 0.0, 0.0); - return; - } - - vec3 outlineStepTC = (WCTCMatrix0 * vec4(scaling * slabNormal * vboScaling, 0.0)).xyz; - float halfSlab = slabThickness * 0.5; - int centerMask = labelSlabMask(fragTexCoord, outlineStepTC, halfSlab, scaling); - - if (centerMask == 0) { - gl_FragData[0] = vec4(0.0, 0.0, 0.0, 0.0); - return; - } - - // A label is on its projected edge when present here but missing in - // some neighbor probed at that label's own outline thickness. Labels - // sharing a thickness reuse the same neighbor masks, so the common - // case still costs four neighbor marches. - float labelmapRow = 0.5 / numLabelmaps; - int edgeLabels = 0; - int prevThickness = -1; - int neighborMask = 0; - for (int s = 1; s < 32; ++s) { - if ((centerMask & (1 << s)) == 0) { continue; } - float thicknessCoordinate = (float(s) - 1.0) / labelOutlineTextureWidth; - int segmentThickness = max(1, int(texture2D(labelOutlineThicknessTexture, vec2(thicknessCoordinate, labelmapRow)).r * 255.0)); - if (segmentThickness != prevThickness) { - vec3 outlineOffset1 = outlineTangent1_0 * texelSize0 * float(segmentThickness); - vec3 outlineOffset2 = outlineTangent2_0 * texelSize0 * float(segmentThickness); - neighborMask = - labelSlabMask(fragTexCoord + outlineOffset1, outlineStepTC, halfSlab, scaling) & - labelSlabMask(fragTexCoord - outlineOffset1, outlineStepTC, halfSlab, scaling) & - labelSlabMask(fragTexCoord + outlineOffset2, outlineStepTC, halfSlab, scaling) & - labelSlabMask(fragTexCoord - outlineOffset2, outlineStepTC, halfSlab, scaling); - prevThickness = segmentThickness; - } - if ((neighborMask & (1 << s)) == 0) { edgeLabels |= (1 << s); } - } - - // When several edge labels compete for this fragment, the one nearest - // the viewer along the slab wins. The camera looks down -z in view - // coordinates, so the slab normal points toward the viewer when its - // view-space z component is positive. - float slabNormalTowardCamera = (MCVCMatrix * vec4(slabNormal, 0.0)).z; - vec3 towardCameraTC = slabNormalTowardCamera > 0.0 ? outlineStepTC : -outlineStepTC; - - if (edgeLabels != 0) { - int edgeLabel = labelSlabFrontLabel(fragTexCoord, towardCameraTC, halfSlab, scaling, edgeLabels); - if (edgeLabel == 0) { - // numeric fallback: smallest edge label + vec4 slabOutlineResult = vec4(0.0, 0.0, 0.0, 0.0); + // For MAX slabs, a composited value of 0 means no label anywhere + // along the slab, so the expensive mask marches can be skipped. + if (slabType != 1 || tvalue.r >= 0.5 / 255.0) { + vec3 outlineStepTC = (WCTCMatrix0 * vec4(scaling * slabNormal * vboScaling, 0.0)).xyz; + float halfSlab = slabThickness * 0.5; + int centerMask = labelSlabMask(fragTexCoord, outlineStepTC, halfSlab, scaling); + + if (centerMask != 0) { + // A label is on its projected edge when present here but missing in + // some neighbor probed at that label's own outline thickness. Labels + // sharing a thickness reuse the same neighbor masks, so the common + // case still costs four neighbor marches. + float labelmapRow = 0.5 / numLabelmaps; + int edgeLabels = 0; + int prevThickness = -1; + int neighborMask = 0; for (int s = 1; s < 32; ++s) { - if ((edgeLabels & (1 << s)) != 0) { edgeLabel = s; break; } + if ((centerMask & (1 << s)) == 0) { continue; } + float thicknessCoordinate = (float(s) - 1.0) / labelOutlineTextureWidth; + int segmentThickness = max(1, int(texture2D(labelOutlineThicknessTexture, vec2(thicknessCoordinate, labelmapRow)).r * 255.0)); + if (segmentThickness != prevThickness) { + vec3 outlineOffset1 = outlineTangent1_0 * texelSize0 * float(segmentThickness); + vec3 outlineOffset2 = outlineTangent2_0 * texelSize0 * float(segmentThickness); + neighborMask = + labelSlabMask(fragTexCoord + outlineOffset1, outlineStepTC, halfSlab, scaling) & + labelSlabMask(fragTexCoord - outlineOffset1, outlineStepTC, halfSlab, scaling) & + labelSlabMask(fragTexCoord + outlineOffset2, outlineStepTC, halfSlab, scaling) & + labelSlabMask(fragTexCoord - outlineOffset2, outlineStepTC, halfSlab, scaling); + prevThickness = segmentThickness; + } + if ((neighborMask & (1 << s)) == 0) { edgeLabels |= (1 << s); } + } + + // When several edge labels compete for this fragment, the one nearest + // the viewer along the slab wins. The camera looks down -z in view + // coordinates, so the slab normal points toward the viewer when its + // view-space z component is positive. + float slabNormalTowardCamera = (MCVCMatrix * vec4(slabNormal, 0.0)).z; + vec3 towardCameraTC = slabNormalTowardCamera > 0.0 ? outlineStepTC : -outlineStepTC; + + if (edgeLabels != 0) { + int edgeLabel = labelSlabFrontLabel(fragTexCoord, towardCameraTC, halfSlab, scaling, edgeLabels); + if (edgeLabel == 0) { + // numeric fallback: smallest edge label + for (int s = 1; s < 32; ++s) { + if ((edgeLabels & (1 << s)) != 0) { edgeLabel = s; break; } + } + } + float labelValue = float(edgeLabel) / 255.0; + vec3 edgeColor = texture2D(colorTexture1, vec2(labelValue * cscale0 + cshift0, 0.5)).rgb; + float opacityCoordinate = (float(edgeLabel) - 1.0) / labelOutlineTextureWidth; + float edgeOpacity = texture2D(labelOutlineOpacityTexture, vec2(opacityCoordinate, labelmapRow)).r; + slabOutlineResult = vec4(edgeColor, edgeOpacity); + } else { + // Interior of the projected labels: regular fill through the + // transfer functions using the label nearest the viewer + int fillLabel = labelSlabFrontLabel(fragTexCoord, towardCameraTC, halfSlab, scaling, centerMask); + float fillValue = fillLabel != 0 ? float(fillLabel) / 255.0 : tvalue.r; + vec3 fillColor = texture2D(colorTexture1, vec2(fillValue * cscale0 + cshift0, 0.5)).rgb; + float fillOpacity = texture2D(pwfTexture1, vec2(fillValue * pwfscale0 + pwfshift0, 0.5)).r; + slabOutlineResult = vec4(fillColor, fillOpacity * opacity); } } - float labelValue = float(edgeLabel) / 255.0; - vec3 edgeColor = texture2D(colorTexture1, vec2(labelValue * cscale0 + cshift0, 0.5)).rgb; - float opacityCoordinate = (float(edgeLabel) - 1.0) / labelOutlineTextureWidth; - float edgeOpacity = texture2D(labelOutlineOpacityTexture, vec2(opacityCoordinate, labelmapRow)).r; - gl_FragData[0] = vec4(edgeColor, edgeOpacity); - return; } - - // Interior of the projected labels: regular fill through the transfer - // functions using the label nearest the viewer - int fillLabel = labelSlabFrontLabel(fragTexCoord, towardCameraTC, halfSlab, scaling, centerMask); - float fillValue = fillLabel != 0 ? float(fillLabel) / 255.0 : tvalue.r; - vec3 fillColor = texture2D(colorTexture1, vec2(fillValue * cscale0 + cshift0, 0.5)).rgb; - float fillOpacity = texture2D(pwfTexture1, vec2(fillValue * pwfscale0 + pwfshift0, 0.5)).r; - gl_FragData[0] = vec4(fillColor, fillOpacity * opacity); + gl_FragData[0] = slabOutlineResult; `); }