@kitware/vtk.js 34.17.0 → 35.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,20 @@ import { Resolve } from '../Core/Mapper/Static.js';
27
27
  const {
28
28
  vtkErrorMacro
29
29
  } = macro;
30
+ const splitStringOnEnter = str => str.split('\n').map(s => s.trim()).filter(Boolean);
31
+ function findLabelOutlineProperties(actor, currentValidInputs) {
32
+ const labelmapProperties = [];
33
+ for (let i = 0; i < currentValidInputs.length; i++) {
34
+ const property = actor.getProperty(currentValidInputs[i].inputIndex);
35
+ if (property?.getUseLabelOutline()) {
36
+ labelmapProperties.push({
37
+ property,
38
+ arrayIndex: i
39
+ });
40
+ }
41
+ }
42
+ return labelmapProperties;
43
+ }
30
44
 
31
45
  // ----------------------------------------------------------------------------
32
46
  // helper methods
@@ -161,6 +175,7 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
161
175
  vtkErrorMacro('No input!');
162
176
  return;
163
177
  }
178
+ model.labelOutlineProperties = findLabelOutlineProperties(actor, model.currentValidInputs);
164
179
 
165
180
  // Number of components
166
181
  const firstImageData = model.currentValidInputs[0].imageData;
@@ -183,10 +198,11 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
183
198
  const actorProperties = actor.getProperties();
184
199
  model.currentValidInputs.forEach(({
185
200
  inputIndex
186
- }) => {
201
+ }, component) => {
187
202
  const actorProperty = actorProperties[inputIndex];
203
+ const scalarTexture = model.scalarTextures[component];
204
+ if (!actorProperty || !scalarTexture) return;
188
205
  const interpolationType = actorProperty.getInterpolationType();
189
- const scalarTexture = model.scalarTextures[inputIndex];
190
206
  if (interpolationType === InterpolationType.NEAREST) {
191
207
  scalarTexture.setMinificationFilter(Filter.NEAREST);
192
208
  scalarTexture.setMagnificationFilter(Filter.NEAREST);
@@ -199,7 +215,7 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
199
215
  // Update color and opacity texture filters
200
216
  const firstValidInput = model.currentValidInputs[0];
201
217
  const firstProperty = actorProperties[firstValidInput.inputIndex];
202
- const iType = firstProperty.getInterpolationType();
218
+ const iType = firstProperty?.getInterpolationType();
203
219
  if (iType === InterpolationType.NEAREST) {
204
220
  model.colorTexture.setMinificationFilter(Filter.NEAREST);
205
221
  model.colorTexture.setMagnificationFilter(Filter.NEAREST);
@@ -217,9 +233,14 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
217
233
  };
218
234
  publicAPI.renderPieceDraw = (ren, actor) => {
219
235
  const gl = model.context;
236
+ const useLabelOutline = model.labelOutlineProperties.length > 0;
220
237
 
221
238
  // render the texture
222
239
  const allTextures = [...model.scalarTextures, model.colorTexture, model.pwfTexture];
240
+ if (useLabelOutline) {
241
+ allTextures.push(model.labelOutlineThicknessTexture);
242
+ allTextures.push(model.labelOutlineOpacityTexture);
243
+ }
223
244
  allTextures.forEach(texture => texture.activate());
224
245
 
225
246
  // update shaders if required
@@ -237,21 +258,26 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
237
258
  publicAPI.buildBufferObjects(ren, actor);
238
259
  }
239
260
  };
240
- publicAPI.getNeedToRebuildBufferObjects = (ren, actor) => model.VBOBuildTime.getMTime() < publicAPI.getMTime() || model.VBOBuildTime.getMTime() < actor.getMTime() || model.VBOBuildTime.getMTime() < model.renderable.getMTime() || model.VBOBuildTime.getMTime() < actor.getProperty(model.currentValidInputs[0].inputIndex)?.getMTime() || model.currentValidInputs.some(({
241
- imageData
242
- }) => model.VBOBuildTime.getMTime() < imageData.getMTime()) || model.VBOBuildTime.getMTime() < model.resliceGeom.getMTime() || model.scalarTextures.length !== model.currentValidInputs.length || !model.scalarTextures.every(texture => !!texture?.getHandle()) || !model.colorTexture?.getHandle() || !model.pwfTexture?.getHandle();
261
+ publicAPI.getNeedToRebuildBufferObjects = (ren, actor) => {
262
+ const firstActorProperty = actor.getProperty(model.currentValidInputs[0].inputIndex);
263
+ const useLabelOutline = model.labelOutlineProperties.length > 0;
264
+ return model.VBOBuildTime.getMTime() < publicAPI.getMTime() || model.VBOBuildTime.getMTime() < actor.getMTime() || model.VBOBuildTime.getMTime() < model.renderable.getMTime() || model.VBOBuildTime.getMTime() < firstActorProperty?.getMTime() || model.currentValidInputs.some(({
265
+ imageData
266
+ }) => model.VBOBuildTime.getMTime() < imageData.getMTime()) || model.VBOBuildTime.getMTime() < model.resliceGeom.getMTime() || model.scalarTextures.length !== model.currentValidInputs.length || !model.scalarTextures.every(texture => !!texture?.getHandle()) || !model.colorTexture?.getHandle() || !model.pwfTexture?.getHandle() || useLabelOutline && (!model.labelOutlineThicknessTexture?.getHandle() || !model.labelOutlineOpacityTexture?.getHandle());
267
+ };
243
268
  publicAPI.buildBufferObjects = (ren, actor) => {
244
269
  const actorProperties = actor.getProperties();
245
270
  model.currentValidInputs.forEach(({
246
- imageData
271
+ imageData,
272
+ inputIndex
247
273
  }, component) => {
248
274
  // rebuild the scalarTexture if the data has changed
249
275
  const scalars = imageData.getPointData().getScalars();
250
276
  const tex = model._openGLRenderWindow.getGraphicsResourceForObject(scalars);
251
277
  const scalarsHash = getImageDataHash(imageData, scalars);
252
278
  const reBuildTex = !tex?.oglObject?.getHandle() || tex?.hash !== scalarsHash;
253
- const actorProperty = actorProperties[component];
254
- const updatedExtents = actorProperty.getUpdatedExtents();
279
+ const actorProperty = actorProperties[inputIndex];
280
+ const updatedExtents = actorProperty?.getUpdatedExtents() ?? [];
255
281
  const hasUpdatedExtents = !!updatedExtents.length;
256
282
  if (reBuildTex && !hasUpdatedExtents) {
257
283
  const newScalarTexture = vtkOpenGLTexture.newInstance();
@@ -290,12 +316,24 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
290
316
  });
291
317
  const firstValidInput = model.currentValidInputs[0];
292
318
  const firstActorProperty = actorProperties[firstValidInput.inputIndex];
319
+ if (!firstActorProperty) {
320
+ vtkErrorMacro('Missing property for first input');
321
+ return;
322
+ }
293
323
  const iComps = firstActorProperty.getIndependentComponents();
294
324
  const numIComps = iComps ? model.numberOfComponents : 1;
295
325
  const textureHeight = iComps ? 2 * numIComps : 1;
326
+
327
+ // Collect color transfer functions - in multi-texture mode, get from each input's property
296
328
  const colorTransferFunctions = [];
297
329
  for (let component = 0; component < numIComps; ++component) {
298
- colorTransferFunctions.push(firstActorProperty.getRGBTransferFunction(component));
330
+ if (model.multiTexturePerVolumeEnabled) {
331
+ const validInput = model.currentValidInputs[component];
332
+ const prop = validInput ? actorProperties[validInput.inputIndex] : null;
333
+ colorTransferFunctions.push(prop?.getRGBTransferFunction() || null);
334
+ } else {
335
+ colorTransferFunctions.push(firstActorProperty.getRGBTransferFunction(component));
336
+ }
299
337
  }
300
338
  const colorFuncHash = getTransferFunctionsHash(colorTransferFunctions, iComps, numIComps);
301
339
  const firstColorTransferFunc = firstActorProperty.getRGBTransferFunction();
@@ -313,17 +351,20 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
313
351
  if (firstColorTransferFunc) {
314
352
  const tmpTable = new Float32Array(cWidth * 3);
315
353
  for (let c = 0; c < numIComps; c++) {
316
- const cfun = firstActorProperty.getRGBTransferFunction(c);
317
- const cRange = cfun.getRange();
318
- cfun.getTable(cRange[0], cRange[1], cWidth, tmpTable, 1);
319
- if (iComps) {
320
- for (let i = 0; i < cWidth * 3; i++) {
321
- cTable[c * cWidth * 6 + i] = 255.0 * tmpTable[i];
322
- cTable[c * cWidth * 6 + i + cWidth * 3] = 255.0 * tmpTable[i];
323
- }
324
- } else {
325
- for (let i = 0; i < cWidth * 3; i++) {
326
- cTable[c * cWidth * 3 + i] = 255.0 * tmpTable[i];
354
+ // Use pre-collected color transfer functions (handles both single and multi-texture modes)
355
+ const cfun = colorTransferFunctions[c];
356
+ if (cfun) {
357
+ const cRange = cfun.getRange();
358
+ cfun.getTable(cRange[0], cRange[1], cWidth, tmpTable, 1);
359
+ if (iComps) {
360
+ for (let i = 0; i < cWidth * 3; i++) {
361
+ cTable[c * cWidth * 6 + i] = 255.0 * tmpTable[i];
362
+ cTable[c * cWidth * 6 + i + cWidth * 3] = 255.0 * tmpTable[i];
363
+ }
364
+ } else {
365
+ for (let i = 0; i < cWidth * 3; i++) {
366
+ cTable[c * cWidth * 3 + i] = 255.0 * tmpTable[i];
367
+ }
327
368
  }
328
369
  }
329
370
  }
@@ -367,9 +408,16 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
367
408
  // Build piecewise function buffer. This buffer is used either
368
409
  // for component weighting or opacity, depending on whether we're
369
410
  // rendering components independently or not.
411
+ // In multi-texture mode, get from each input's property
370
412
  const opacityFunctions = [];
371
413
  for (let component = 0; component < numIComps; ++component) {
372
- opacityFunctions.push(firstActorProperty.getPiecewiseFunction(component));
414
+ if (model.multiTexturePerVolumeEnabled) {
415
+ const validInput = model.currentValidInputs[component];
416
+ const prop = validInput ? actorProperties[validInput.inputIndex] : null;
417
+ opacityFunctions.push(prop?.getPiecewiseFunction() || null);
418
+ } else {
419
+ opacityFunctions.push(firstActorProperty.getPiecewiseFunction(component));
420
+ }
373
421
  }
374
422
  const opacityFuncHash = getTransferFunctionsHash(opacityFunctions, iComps, numIComps);
375
423
  const firstPwFunc = firstActorProperty.getPiecewiseFunction();
@@ -388,7 +436,8 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
388
436
  const pwfFloatTable = new Float32Array(pwfSize);
389
437
  const tmpTable = new Float32Array(pwfWidth);
390
438
  for (let c = 0; c < numIComps; ++c) {
391
- const pwfun = firstActorProperty.getPiecewiseFunction(c);
439
+ // Use pre-collected opacity functions (handles both single and multi-texture modes)
440
+ const pwfun = opacityFunctions[c];
392
441
  if (pwfun === null) {
393
442
  // Piecewise constant max if no function supplied for this component
394
443
  pwfFloatTable.fill(1.0);
@@ -437,6 +486,12 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
437
486
  }
438
487
  replaceGraphicsResource(model._openGLRenderWindow, model._pwfTextureCore, firstPwFunc);
439
488
  model._pwfTextureCore = firstPwFunc;
489
+
490
+ // Build label outline textures if needed (2D textures for per-labelmap settings)
491
+ if (model.labelOutlineProperties.length > 0) {
492
+ publicAPI.updateLabelOutlineThicknessTexture(model.labelOutlineProperties);
493
+ publicAPI.updateLabelOutlineOpacityTexture(model.labelOutlineProperties);
494
+ }
440
495
  const vboString = `${model.resliceGeom.getMTime()}A${model.renderable.getSlabThickness()}`;
441
496
  if (!model.tris.getCABO().getElementCount() || model.VBOBuildString !== vboString) {
442
497
  const points = vtkDataArray.newInstance({
@@ -526,26 +581,27 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
526
581
  if (program.isUniformUsed('slabType')) {
527
582
  program.setUniformi('slabType', model.renderable.getSlabType());
528
583
  }
529
- if (program.isUniformUsed('slabType')) {
530
- program.setUniformi('slabType', model.renderable.getSlabType());
531
- }
532
584
  if (program.isUniformUsed('slabTrapezoid')) {
533
585
  program.setUniformi('slabTrapezoid', model.renderable.getSlabTrapezoidIntegration());
534
586
  }
535
587
  const shiftScaleEnabled = cellBO.getCABO().getCoordShiftAndScaleEnabled();
536
588
  const inverseShiftScaleMatrix = shiftScaleEnabled ? cellBO.getCABO().getInverseShiftAndScaleMatrix() : null;
537
589
 
538
- // Set the world->texture matrix
539
- if (program.isUniformUsed('WCTCMatrix')) {
540
- const dim = firstImageData.getDimensions();
541
- mat4.copy(model.tmpMat4, firstImageData.getIndexToWorld());
542
- mat4.translate(model.tmpMat4, model.tmpMat4, [-0.5, -0.5, -0.5]);
543
- mat4.scale(model.tmpMat4, model.tmpMat4, dim);
544
- mat4.invert(model.tmpMat4, model.tmpMat4);
545
- if (inverseShiftScaleMatrix) {
546
- mat4.multiply(model.tmpMat4, model.tmpMat4, inverseShiftScaleMatrix);
590
+ // Set per-input world->texture matrices
591
+ for (let i = 0; i < model.currentValidInputs.length; i++) {
592
+ const uniformName = `WCTCMatrix${i}`;
593
+ if (program.isUniformUsed(uniformName)) {
594
+ const imageData = model.currentValidInputs[i].imageData;
595
+ const dim = imageData.getDimensions();
596
+ mat4.copy(model.tmpMat4, imageData.getIndexToWorld());
597
+ mat4.translate(model.tmpMat4, model.tmpMat4, [-0.5, -0.5, -0.5]);
598
+ mat4.scale(model.tmpMat4, model.tmpMat4, dim);
599
+ mat4.invert(model.tmpMat4, model.tmpMat4);
600
+ if (inverseShiftScaleMatrix) {
601
+ mat4.multiply(model.tmpMat4, model.tmpMat4, inverseShiftScaleMatrix);
602
+ }
603
+ program.setUniformMatrix(uniformName, model.tmpMat4);
547
604
  }
548
- program.setUniformMatrix('WCTCMatrix', model.tmpMat4);
549
605
  }
550
606
  if (program.isUniformUsed('vboScaling')) {
551
607
  program.setUniform3fv('vboScaling', cellBO.getCABO().getCoordScale() ?? [1, 1, 1]);
@@ -588,7 +644,11 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
588
644
  publicAPI.setPropertyShaderParameters = (cellBO, ren, actor) => {
589
645
  const program = cellBO.getProgram();
590
646
  const firstPpty = actor.getProperty(model.currentValidInputs[0].inputIndex);
591
- const opacity = firstPpty.getOpacity();
647
+
648
+ // In multi-texture mode, use 1.0 for global opacity since each input's
649
+ // piecewise function controls its own opacity through component weights.
650
+ // This prevents a labelmap's opacity setting from affecting all inputs.
651
+ const opacity = model.multiTexturePerVolumeEnabled ? 1.0 : firstPpty.getOpacity();
592
652
  program.setUniformf('opacity', opacity);
593
653
 
594
654
  // Component mix
@@ -599,16 +659,18 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
599
659
  // - 4 comps => RGBA
600
660
  const numComp = model.numberOfComponents;
601
661
  const iComps = firstPpty.getIndependentComponents();
662
+ const useMultiTexture = model.multiTexturePerVolumeEnabled;
663
+ const actorProperties = actor.getProperties();
602
664
  if (iComps) {
603
665
  for (let i = 0; i < numComp; ++i) {
604
- program.setUniformf(`mix${i}`, firstPpty.getComponentWeight(i));
666
+ const property = useMultiTexture ? actorProperties[model.currentValidInputs[i].inputIndex] : firstPpty;
667
+ program.setUniformf(`mix${i}`, property.getComponentWeight(0));
605
668
  }
606
669
  }
607
670
 
608
671
  // three levels of shift scale combined into one
609
672
  // for performance in the fragment shader
610
673
  for (let component = 0; component < numComp; component++) {
611
- const useMultiTexture = model.multiTexturePerVolumeEnabled;
612
674
  const textureIndex = useMultiTexture ? component : 0;
613
675
  const volInfoIndex = useMultiTexture ? 0 : component;
614
676
  const scalarTexture = model.scalarTextures[textureIndex];
@@ -616,12 +678,13 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
616
678
  const volScale = volInfo.scale[volInfoIndex];
617
679
  const volOffset = volInfo.offset[volInfoIndex];
618
680
  const target = iComps ? component : 0;
681
+ const property = useMultiTexture ? actorProperties[model.currentValidInputs[component].inputIndex] : firstPpty;
619
682
 
620
683
  // color shift/scale
621
- let cw = firstPpty.getColorWindow();
622
- let cl = firstPpty.getColorLevel();
623
- const cfun = firstPpty.getRGBTransferFunction(target);
624
- if (cfun && firstPpty.getUseLookupTableScalarRange()) {
684
+ let cw = property.getColorWindow();
685
+ let cl = property.getColorLevel();
686
+ const cfun = property.getRGBTransferFunction(useMultiTexture ? 0 : target);
687
+ if (cfun && property.getUseLookupTableScalarRange()) {
625
688
  const cRange = cfun.getRange();
626
689
  cw = cRange[1] - cRange[0];
627
690
  cl = 0.5 * (cRange[1] + cRange[0]);
@@ -634,7 +697,7 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
634
697
  // pwf shift/scale
635
698
  let pwfScale = 1.0;
636
699
  let pwfShift = 0.0;
637
- const pwfun = firstPpty.getPiecewiseFunction(target);
700
+ const pwfun = property.getPiecewiseFunction(useMultiTexture ? 0 : target);
638
701
  if (pwfun) {
639
702
  const pwfRange = pwfun.getRange();
640
703
  const length = pwfRange[1] - pwfRange[0];
@@ -652,6 +715,62 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
652
715
 
653
716
  // Background color
654
717
  program.setUniform4fv('backgroundColor', model.renderable.getBackgroundColor());
718
+
719
+ // Label outline uniforms
720
+ if (model.labelOutlineProperties.length > 0) {
721
+ const outlineThicknessUnit = model.labelOutlineThicknessTexture.getTextureUnit();
722
+ program.setUniformi('labelOutlineThicknessTexture', outlineThicknessUnit);
723
+ const outlineOpacityUnit = model.labelOutlineOpacityTexture.getTextureUnit();
724
+ program.setUniformi('labelOutlineOpacityTexture', outlineOpacityUnit);
725
+ let textureWidth = model.renderable.getLabelOutlineTextureWidth();
726
+ if (textureWidth <= 0) {
727
+ textureWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
728
+ }
729
+ program.setUniformf('labelOutlineTextureWidth', textureWidth);
730
+ program.setUniformf('numLabelmaps', model.labelOutlineProperties.length);
731
+
732
+ // Calculate tangent vectors for the slice plane in each input's texture space
733
+ const slicePlane = model.renderable.getSlicePlane();
734
+ model._tmpTangent1.fill(0);
735
+ model._tmpTangent2.fill(0);
736
+ if (slicePlane) {
737
+ const normal = slicePlane.getNormal();
738
+ vtkMath.perpendiculars(normal, model._tmpTangent1, model._tmpTangent2, 0);
739
+ } else {
740
+ model._tmpTangent1[0] = 1;
741
+ model._tmpTangent2[1] = 1;
742
+ }
743
+
744
+ // Set per-input tangent vectors (transformed to each input's texture space)
745
+ for (let i = 0; i < model.currentValidInputs.length; i++) {
746
+ const imageData = model.currentValidInputs[i].imageData;
747
+ mat3.set(model._tmpMat3, ...imageData.getDirection());
748
+ mat3.invert(model._tmpMat3, model._tmpMat3);
749
+ vec3.transformMat3(model._tmpVec3a, model._tmpTangent1, model._tmpMat3);
750
+ vec3.transformMat3(model._tmpVec3b, model._tmpTangent2, model._tmpMat3);
751
+ const t1Name = `outlineTangent1_${i}`;
752
+ const t2Name = `outlineTangent2_${i}`;
753
+ if (program.isUniformUsed(t1Name)) {
754
+ program.setUniform3fv(t1Name, model._tmpVec3a);
755
+ }
756
+ if (program.isUniformUsed(t2Name)) {
757
+ program.setUniform3fv(t2Name, model._tmpVec3b);
758
+ }
759
+ }
760
+
761
+ // Set per-input texel sizes in texture coordinates
762
+ for (let i = 0; i < model.currentValidInputs.length; i++) {
763
+ const uniformName = `texelSize${i}`;
764
+ if (program.isUniformUsed(uniformName)) {
765
+ const imageData = model.currentValidInputs[i].imageData;
766
+ const inputDims = imageData.getDimensions();
767
+ model._tmpTexelSize[0] = 1.0 / inputDims[0];
768
+ model._tmpTexelSize[1] = 1.0 / inputDims[1];
769
+ model._tmpTexelSize[2] = 1.0 / inputDims[2];
770
+ program.setUniform3fv(uniformName, model._tmpTexelSize);
771
+ }
772
+ }
773
+ }
655
774
  };
656
775
  publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => {
657
776
  // has something changed that would require us to recreate the shader?
@@ -660,7 +779,9 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
660
779
  // input modified
661
780
  // light complexity changed
662
781
  // render pass shader replacement changed
663
- const iComp = actor.getProperty(model.currentValidInputs[0].inputIndex).getIndependentComponents();
782
+ const firstActorProperty = actor.getProperty(model.currentValidInputs[0].inputIndex);
783
+ const iComp = firstActorProperty.getIndependentComponents();
784
+ const useLabelOutline = model.labelOutlineProperties.length > 0;
664
785
  const slabTh = model.renderable.getSlabThickness();
665
786
  const slabType = model.renderable.getSlabType();
666
787
  const slabTrap = model.renderable.getSlabTrapezoidIntegration();
@@ -670,11 +791,14 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
670
791
  if (!model.currentRenderPass && model.lastRenderPassShaderReplacement || model.currentRenderPass && model.currentRenderPass.getShaderReplacement() !== model.lastRenderPassShaderReplacement) {
671
792
  needRebuild = true;
672
793
  }
673
- if (needRebuild || model.lastHaveSeenDepthRequest !== model.haveSeenDepthRequest || model.lastNumberOfComponents !== model.numberOfComponents || model.lastMultiTexturePerVolumeEnabled !== model.multiTexturePerVolumeEnabled || cellBO.getProgram()?.getHandle() === 0 || model.lastIndependentComponents !== iComp || model.lastSlabThickness !== slabTh || model.lastSlabType !== slabType || model.lastSlabTrapezoidIntegration !== slabTrap) {
794
+ const numValidInputs = model.currentValidInputs?.length ?? 0;
795
+ if (needRebuild || model.lastHaveSeenDepthRequest !== model.haveSeenDepthRequest || model.lastNumberOfComponents !== model.numberOfComponents || model.lastMultiTexturePerVolumeEnabled !== model.multiTexturePerVolumeEnabled || cellBO.getProgram()?.getHandle() === 0 || model.lastIndependentComponents !== iComp || model.lastUseLabelOutline !== useLabelOutline || model.lastNumValidInputs !== numValidInputs || model.lastSlabThickness !== slabTh || model.lastSlabType !== slabType || model.lastSlabTrapezoidIntegration !== slabTrap) {
674
796
  model.lastHaveSeenDepthRequest = model.haveSeenDepthRequest;
675
797
  model.lastNumberOfComponents = model.numberOfComponents;
676
798
  model.lastMultiTexturePerVolumeEnabled = model.multiTexturePerVolumeEnabled;
677
799
  model.lastIndependentComponents = iComp;
800
+ model.lastUseLabelOutline = useLabelOutline;
801
+ model.lastNumValidInputs = numValidInputs;
678
802
  model.lastSlabThickness = slabTh;
679
803
  model.lastSlabType = slabType;
680
804
  model.lastSlabTrapezoidIntegration = slabTrap;
@@ -698,18 +822,123 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
698
822
  }
699
823
  publicAPI.replaceShaderCoincidentOffset(shaders, ren, actor);
700
824
  };
825
+
826
+ // Helper to generate shader code for compositing multiple inputs
827
+ // Some inputs may be labelmaps (with outline), others may be background images
828
+ // labelmapInputs: array of input indices that are labelmaps (e.g., [0, 2])
829
+ // totalInputs: total number of inputs (1-4)
830
+ function generateMultiInputCompositeShader(labelmapInputs, totalInputs) {
831
+ const rgba = ['r', 'g', 'b', 'a'];
832
+ const allInputs = Array.from({
833
+ length: totalInputs
834
+ }, (_, i) => i);
835
+ const backgroundInputs = allInputs.filter(i => !labelmapInputs.includes(i));
836
+
837
+ // Generate texture coordinate lines for labelmap inputs
838
+ const texCoordLines = labelmapInputs.map(i => `vec3 labelTexCoord${i} = (WCTCMatrix${i} * vec4(fragWorldPos, 1.0)).xyz;`).join('\n ');
839
+
840
+ // Build texture sampling conditional for neighbor checking
841
+ const textureSampling = (() => {
842
+ if (labelmapInputs.length === 0) return '';
843
+ const conditions = labelmapInputs.map((inputIdx, arrayIdx) => {
844
+ if (arrayIdx === 0) {
845
+ return `(labelInputIdx == ${arrayIdx}) ? texture(volumeTexture[${inputIdx}], neighborTexCoord).r`;
846
+ }
847
+ return ` : (labelInputIdx == ${arrayIdx}) ? texture(volumeTexture[${inputIdx}], neighborTexCoord).r`;
848
+ });
849
+ return `float neighborLabel = ${conditions.join('')} : 0.0;`;
850
+ })();
851
+
852
+ // Process backgrounds first, then labelmaps on top
853
+ const orderedInputs = [...backgroundInputs, ...labelmapInputs];
854
+ const processInputs = orderedInputs.map(inputIdx => {
855
+ const isLabelmap = labelmapInputs.includes(inputIdx);
856
+ const labelArrayIdx = labelmapInputs.indexOf(inputIdx);
857
+ if (isLabelmap) {
858
+ return `
859
+ // Process input ${inputIdx} as labelmap
860
+ {
861
+ float labelValue = tvalue.${rgba[inputIdx]};
862
+ int segmentIndex = int(labelValue * 255.0);
863
+
864
+ if (segmentIndex > 0) {
865
+ float textureCoordinate = float(segmentIndex - 1) / labelOutlineTextureWidth;
866
+ float labelmapRow = (float(${labelArrayIdx}) + 0.5) / numLabelmaps;
867
+ float thicknessValue = texture2D(labelOutlineThicknessTexture, vec2(textureCoordinate, labelmapRow)).r;
868
+ float labelOutlineOpacityValue = texture2D(labelOutlineOpacityTexture, vec2(textureCoordinate, labelmapRow)).r;
869
+ int actualThickness = int(thicknessValue * 255.0);
870
+
871
+ vec3 currentLabelTC = labelTexCoord${inputIdx};
872
+ vec3 currentTexelSize = texelSize${inputIdx};
873
+ vec3 currentTangent1 = outlineTangent1_${inputIdx};
874
+ vec3 currentTangent2 = outlineTangent2_${inputIdx};
875
+
876
+ bool pixelOnBorder = false;
877
+ int labelInputIdx = ${labelArrayIdx};
878
+ for (int i = -actualThickness; i <= actualThickness; i++) {
879
+ for (int j = -actualThickness; j <= actualThickness; j++) {
880
+ if (i == 0 && j == 0) continue;
881
+ vec3 neighborTexCoord = currentLabelTC + float(i) * currentTangent1 * currentTexelSize + float(j) * currentTangent2 * currentTexelSize;
882
+ if (any(greaterThan(neighborTexCoord, vec3(1.0))) || any(lessThan(neighborTexCoord, vec3(0.0)))) {
883
+ pixelOnBorder = true;
884
+ break;
885
+ }
886
+ ${textureSampling}
887
+ if (neighborLabel != labelValue) {
888
+ pixelOnBorder = true;
889
+ break;
890
+ }
891
+ }
892
+ if (pixelOnBorder) break;
893
+ }
894
+
895
+ if (pixelOnBorder) {
896
+ convergentColor.rgb = mix(convergentColor.rgb, tcolor${inputIdx}.rgb, labelOutlineOpacityValue);
897
+ convergentColor.a = max(convergentColor.a, labelOutlineOpacityValue);
898
+ } else if (compWeight${inputIdx} > 0.0) {
899
+ float fillAlpha = compWeight${inputIdx} * opacity;
900
+ convergentColor.rgb = mix(convergentColor.rgb, tcolor${inputIdx}.rgb, fillAlpha);
901
+ convergentColor.a = max(convergentColor.a, fillAlpha);
902
+ }
903
+ }
904
+ }`;
905
+ }
906
+ return `
907
+ // Process input ${inputIdx} as background image
908
+ {
909
+ float bgAlpha = compWeight${inputIdx} * opacity;
910
+ convergentColor.rgb = mix(convergentColor.rgb, tcolor${inputIdx}.rgb, bgAlpha);
911
+ convergentColor.a = max(convergentColor.a, bgAlpha);
912
+ }`;
913
+ }).join('\n ');
914
+ const labelDesc = labelmapInputs.length > 0 ? `labelmaps at input${labelmapInputs.length > 1 ? 's' : ''} ${labelmapInputs.join(', ')}` : 'no labelmaps';
915
+ const bgDesc = backgroundInputs.length > 0 ? `background at input${backgroundInputs.length > 1 ? 's' : ''} ${backgroundInputs.join(', ')}` : 'no background';
916
+ return splitStringOnEnter(`
917
+ // Multi-texture mode: ${labelDesc}, ${bgDesc}
918
+ vec4 convergentColor = vec4(0.0, 0.0, 0.0, 0.0);
919
+
920
+ // Compute labelmap texture coordinates
921
+ ${texCoordLines}
922
+
923
+ // Process each input in order
924
+ ${processInputs}
925
+
926
+ gl_FragData[0] = convergentColor;
927
+ `);
928
+ }
701
929
  publicAPI.replaceShaderTCoord = (shaders, ren, actor) => {
702
930
  let VSSource = shaders.Vertex;
703
931
  const GSSource = shaders.Geometry;
704
932
  let FSSource = shaders.Fragment;
705
- const tcoordVSDec = ['uniform mat4 WCTCMatrix;', 'out vec3 fragTexCoord;'];
933
+ const useLabelOutline = model.labelOutlineProperties.length > 0;
706
934
  const slabThickness = model.renderable.getSlabThickness();
707
- VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Dec', tcoordVSDec).result;
708
- const tcoordVSImpl = ['fragTexCoord = (WCTCMatrix * vertexWC).xyz;'];
709
- VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Impl', tcoordVSImpl).result;
935
+ VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Dec', []).result;
936
+ VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Impl', []).result;
710
937
  const tNumComp = model.numberOfComponents;
711
- const iComps = actor.getProperty(model.currentValidInputs[0].inputIndex).getIndependentComponents();
712
- let tcoordFSDec = ['in vec3 fragTexCoord;', `uniform highp sampler3D volumeTexture[${model.scalarTextures.length}];`, 'uniform mat4 WCTCMatrix;',
938
+ const firstActorPropertyForIComps = actor.getProperty(model.currentValidInputs[0].inputIndex);
939
+ const iComps = firstActorPropertyForIComps.getIndependentComponents();
940
+ const numInputs = model.scalarTextures.length;
941
+ let tcoordFSDec = [`uniform highp sampler3D volumeTexture[${numInputs}];`,
713
942
  // color shift and scale
714
943
  'uniform float cshift0;', 'uniform float cscale0;',
715
944
  // pwf shift and scale
@@ -721,14 +950,30 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
721
950
  // background color
722
951
  'uniform vec4 backgroundColor;'];
723
952
 
724
- // Function to sample texture
725
- tcoordFSDec.push('vec4 rawSampleTexture(vec3 pos) {');
953
+ // Add per-input WCTCMatrix uniforms
954
+ for (let i = 0; i < numInputs; i++) {
955
+ tcoordFSDec.push(`uniform mat4 WCTCMatrix${i};`);
956
+ }
957
+
958
+ // Add label outline uniforms if enabled
959
+ if (useLabelOutline) {
960
+ tcoordFSDec = tcoordFSDec.concat(['uniform sampler2D labelOutlineThicknessTexture;', 'uniform sampler2D labelOutlineOpacityTexture;', 'uniform float labelOutlineTextureWidth;', 'uniform float numLabelmaps;']);
961
+ // Add per-input tangent vectors and texelSize
962
+ for (let i = 0; i < numInputs; i++) {
963
+ tcoordFSDec.push(`uniform vec3 outlineTangent1_${i};`);
964
+ tcoordFSDec.push(`uniform vec3 outlineTangent2_${i};`);
965
+ tcoordFSDec.push(`uniform vec3 texelSize${i};`);
966
+ }
967
+ }
968
+
969
+ // Function to sample texture - takes world position, computes per-input texture coords
970
+ tcoordFSDec.push('vec4 rawSampleTexture(vec3 worldPos) {');
726
971
  if (!model.multiTexturePerVolumeEnabled) {
727
- tcoordFSDec.push('return texture(volumeTexture[0], pos);', '}');
972
+ tcoordFSDec.push('vec3 tc0 = (WCTCMatrix0 * vec4(worldPos, 1.0)).xyz;', 'return texture(volumeTexture[0], tc0);', '}');
728
973
  } else {
729
974
  tcoordFSDec.push('vec4 rawSample;');
730
- for (let component = 0; component < model.scalarTextures.length; ++component) {
731
- tcoordFSDec.push(`rawSample[${component}] = texture(volumeTexture[${component}], pos)[0];`);
975
+ for (let component = 0; component < numInputs; ++component) {
976
+ tcoordFSDec.push(`vec3 tc${component} = (WCTCMatrix${component} * vec4(worldPos, 1.0)).xyz;`, `rawSample[${component}] = texture(volumeTexture[${component}], tc${component})[0];`);
732
977
  }
733
978
  tcoordFSDec.push('return rawSample;', '}');
734
979
  }
@@ -767,36 +1012,108 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
767
1012
  tcoordFSDec = tcoordFSDec.concat(['vec4 compositeValue(vec4 currVal, vec4 valToComp, int trapezoid)', '{', ' vec4 retVal = vec4(1.0);', ' if (slabType == 0) // min', ' {', ' retVal = min(currVal, valToComp);', ' }', ' else if (slabType == 1) // max', ' {', ' retVal = max(currVal, valToComp);', ' }', ' else if (slabType == 3) // sum', ' {', ' retVal = currVal + (trapezoid > 0 ? 0.5 * valToComp : valToComp); ', ' }', ' else // mean', ' {', ' retVal = currVal + (trapezoid > 0 ? 0.5 * valToComp : valToComp); ', ' }', ' return retVal;', '}']);
768
1013
  }
769
1014
  FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::TCoord::Dec', tcoordFSDec).result;
770
- let tcoordFSImpl = ['if (any(greaterThan(fragTexCoord, vec3(1.0))) || any(lessThan(fragTexCoord, vec3(0.0))))', '{', ' // set the background color and exit', ' gl_FragData[0] = backgroundColor;', ' return;', '}', 'vec4 tvalue = rawSampleTexture(fragTexCoord);'];
1015
+ let tcoordFSImpl = ['vec3 fragWorldPos = vertexWCVSOutput.xyz;', 'vec3 fragTexCoord = (WCTCMatrix0 * vec4(fragWorldPos, 1.0)).xyz;', 'if (any(greaterThan(fragTexCoord, vec3(1.0))) || any(lessThan(fragTexCoord, vec3(0.0))))', '{', ' // set the background color and exit', ' gl_FragData[0] = backgroundColor;', ' return;', '}', 'vec4 tvalue = rawSampleTexture(fragWorldPos);'];
771
1016
  if (slabThickness > 0.0) {
772
- tcoordFSImpl = tcoordFSImpl.concat(['// Get the first and last samples', 'int numSlices = 1;', 'float scaling = min(min(spacing.x, spacing.y), spacing.z) * 0.5;', 'vec3 normalxspacing = scaling * normalWCVSOutput;', 'float distTraveled = length(normalxspacing);', 'int trapezoid = 0;', 'while (distTraveled < slabThickness * 0.5)', '{', ' distTraveled += length(normalxspacing);', ' float fnumSlices = float(numSlices);', ' if (distTraveled > slabThickness * 0.5)', ' {', ' // Before stepping outside the slab, sample at the boundaries', ' normalxspacing = normalWCVSOutput * slabThickness * 0.5 / fnumSlices;', ' trapezoid = slabTrapezoid;', ' }', ' vec3 fragTCoordNeg = (WCTCMatrix * vec4(vertexWCVSOutput.xyz - fnumSlices * normalxspacing * vboScaling, 1.0)).xyz;', ' if (!any(greaterThan(fragTCoordNeg, vec3(1.0))) && !any(lessThan(fragTCoordNeg, vec3(0.0))))', ' {', ' vec4 newVal = rawSampleTexture(fragTCoordNeg);', ' tvalue = compositeValue(tvalue, newVal, trapezoid);', ' numSlices += 1;', ' }', ' vec3 fragTCoordPos = (WCTCMatrix * vec4(vertexWCVSOutput.xyz + fnumSlices * normalxspacing * vboScaling, 1.0)).xyz;', ' if (!any(greaterThan(fragTCoordNeg, vec3(1.0))) && !any(lessThan(fragTCoordNeg, vec3(0.0))))', ' {', ' vec4 newVal = rawSampleTexture(fragTCoordPos);', ' tvalue = compositeValue(tvalue, newVal, trapezoid);', ' numSlices += 1;', ' }', '}', '// Finally, if slab type is *mean*, divide the sum by the numSlices', 'if (slabType == 2)', '{', ' tvalue = tvalue / float(numSlices);', '}']);
1017
+ tcoordFSImpl = tcoordFSImpl.concat(['// Get the first and last samples', 'int numSlices = 1;', 'float scaling = min(min(spacing.x, spacing.y), spacing.z) * 0.5;', 'vec3 normalxspacing = scaling * normalWCVSOutput;', 'float distTraveled = length(normalxspacing);', 'int trapezoid = 0;', 'while (distTraveled < slabThickness * 0.5)', '{', ' distTraveled += length(normalxspacing);', ' float fnumSlices = float(numSlices);', ' if (distTraveled > slabThickness * 0.5)', ' {', ' // Before stepping outside the slab, sample at the boundaries', ' normalxspacing = normalWCVSOutput * slabThickness * 0.5 / fnumSlices;', ' trapezoid = slabTrapezoid;', ' }', ' vec3 worldPosNeg = vertexWCVSOutput.xyz - fnumSlices * normalxspacing * vboScaling;', ' vec3 fragTCoordNeg = (WCTCMatrix0 * vec4(worldPosNeg, 1.0)).xyz;', ' if (!any(greaterThan(fragTCoordNeg, vec3(1.0))) && !any(lessThan(fragTCoordNeg, vec3(0.0))))', ' {', ' vec4 newVal = rawSampleTexture(worldPosNeg);', ' tvalue = compositeValue(tvalue, newVal, trapezoid);', ' numSlices += 1;', ' }', ' 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))))', ' {', ' vec4 newVal = rawSampleTexture(worldPosPos);', ' tvalue = compositeValue(tvalue, newVal, trapezoid);', ' numSlices += 1;', ' }', '}', '// Finally, if slab type is *mean*, divide the sum by the numSlices', 'if (slabType == 2)', '{', ' tvalue = tvalue / float(numSlices);', '}']);
773
1018
  }
774
1019
  if (iComps) {
775
1020
  const rgba = ['r', 'g', 'b', 'a'];
776
1021
  for (let comp = 0; comp < tNumComp; ++comp) {
777
1022
  tcoordFSImpl = tcoordFSImpl.concat([`vec3 tcolor${comp} = texture2D(colorTexture1, vec2(tvalue.${rgba[comp]} * cscale${comp} + cshift${comp}, height${comp})).rgb;`, `float compWeight${comp} = mix${comp} * texture2D(pwfTexture1, vec2(tvalue.${rgba[comp]} * pwfscale${comp} + pwfshift${comp}, height${comp})).r;`]);
778
1023
  }
779
- switch (tNumComp) {
780
- case 1:
781
- tcoordFSImpl = tcoordFSImpl.concat(['gl_FragData[0] = vec4(tcolor0.rgb, compWeight0 * opacity);']);
782
- break;
783
- case 2:
784
- tcoordFSImpl = tcoordFSImpl.concat(['float weightSum = compWeight0 + compWeight1;', 'gl_FragData[0] = vec4(vec3((tcolor0.rgb * (compWeight0 / weightSum)) + (tcolor1.rgb * (compWeight1 / weightSum))), opacity);']);
785
- break;
786
- case 3:
787
- tcoordFSImpl = tcoordFSImpl.concat(['float weightSum = compWeight0 + compWeight1 + compWeight2;', 'gl_FragData[0] = vec4(vec3((tcolor0.rgb * (compWeight0 / weightSum)) + (tcolor1.rgb * (compWeight1 / weightSum)) + (tcolor2.rgb * (compWeight2 / weightSum))), opacity);']);
788
- break;
789
- case 4:
790
- tcoordFSImpl = tcoordFSImpl.concat(['float weightSum = compWeight0 + compWeight1 + compWeight2 + compWeight3;', 'gl_FragData[0] = vec4(vec3((tcolor0.rgb * (compWeight0 / weightSum)) + (tcolor1.rgb * (compWeight1 / weightSum)) + (tcolor2.rgb * (compWeight2 / weightSum)) + (tcolor3.rgb * (compWeight3 / weightSum))), opacity);']);
791
- break;
792
- default:
793
- vtkErrorMacro('Unsupported number of independent coordinates.');
1024
+
1025
+ // Determine which inputs are labelmaps
1026
+ const labelmapInputs = [];
1027
+ if (useLabelOutline) {
1028
+ for (let i = 0; i < tNumComp; i++) {
1029
+ const inputProperty = actor.getProperty(model.currentValidInputs[i].inputIndex);
1030
+ if (inputProperty?.getUseLabelOutline()) {
1031
+ labelmapInputs.push(i);
1032
+ }
1033
+ }
1034
+ }
1035
+
1036
+ // Generate weighted sum shader for non-labelmap cases
1037
+ const generateWeightedSumShader = inputCount => {
1038
+ if (inputCount === 1) {
1039
+ return ['gl_FragData[0] = vec4(tcolor0.rgb, compWeight0 * opacity);'];
1040
+ }
1041
+ const inputs = Array.from({
1042
+ length: inputCount
1043
+ }, (_, i) => i);
1044
+ const weightSum = inputs.map(i => `compWeight${i}`).join(' + ');
1045
+ const colorSum = inputs.map(i => `(tcolor${i}.rgb * (compWeight${i} / weightSum))`).join(' + ');
1046
+ return [`float weightSum = ${weightSum};`, `gl_FragData[0] = vec4(vec3(${colorSum}), opacity);`];
1047
+ };
1048
+ if (labelmapInputs.length > 0) {
1049
+ tcoordFSImpl = tcoordFSImpl.concat(generateMultiInputCompositeShader(labelmapInputs, tNumComp));
1050
+ } else {
1051
+ tcoordFSImpl = tcoordFSImpl.concat(generateWeightedSumShader(tNumComp));
794
1052
  }
795
1053
  } else {
796
1054
  // dependent components
797
1055
  switch (tNumComp) {
798
1056
  case 1:
799
- tcoordFSImpl = tcoordFSImpl.concat(['// Dependent components', 'float intensity = tvalue.r;', 'vec3 tcolor = texture2D(colorTexture1, vec2(intensity * cscale0 + cshift0, 0.5)).rgb;', 'float scalarOpacity = texture2D(pwfTexture1, vec2(intensity * pwfscale0 + pwfshift0, 0.5)).r;', 'gl_FragData[0] = vec4(tcolor, scalarOpacity * opacity);']);
1057
+ if (useLabelOutline) {
1058
+ tcoordFSImpl = tcoordFSImpl.concat([...splitStringOnEnter(`
1059
+ // Label outline mode for single component
1060
+ float centerValue = tvalue.r;
1061
+ int segmentIndex = int(centerValue * 255.0);
1062
+
1063
+ // Skip background (segment 0)
1064
+ if (segmentIndex == 0) {
1065
+ gl_FragData[0] = vec4(0.0, 0.0, 0.0, 0.0);
1066
+ return;
1067
+ }
1068
+
1069
+ // Get outline parameters for this segment (row 0 for single labelmap)
1070
+ float textureCoordinate = float(segmentIndex - 1) / labelOutlineTextureWidth;
1071
+ float labelmapRow = 0.5 / numLabelmaps;
1072
+ float thicknessValue = texture2D(labelOutlineThicknessTexture, vec2(textureCoordinate, labelmapRow)).r;
1073
+ float outlineOpacity = texture2D(labelOutlineOpacityTexture, vec2(textureCoordinate, labelmapRow)).r;
1074
+ int actualThickness = int(thicknessValue * 255.0);
1075
+
1076
+ // Get color for this segment
1077
+ vec3 tColor = texture2D(colorTexture1, vec2(centerValue * cscale0 + cshift0, 0.5)).rgb;
1078
+ float scalarOpacity = texture2D(pwfTexture1, vec2(centerValue * pwfscale0 + pwfshift0, 0.5)).r;
1079
+ float opacityToUse = scalarOpacity * opacity;
1080
+
1081
+ // Check neighbors for border detection
1082
+ bool pixelOnBorder = false;
1083
+ for (int i = -actualThickness; i <= actualThickness; i++) {
1084
+ for (int j = -actualThickness; j <= actualThickness; j++) {
1085
+ if (i == 0 && j == 0) {
1086
+ continue;
1087
+ }
1088
+ // Sample neighbor using tangent vectors in texture space
1089
+ vec3 neighborTexCoord = fragTexCoord + float(i) * outlineTangent1_0 * texelSize0 + float(j) * outlineTangent2_0 * texelSize0;
1090
+
1091
+ // Skip if outside texture bounds
1092
+ if (any(greaterThan(neighborTexCoord, vec3(1.0))) || any(lessThan(neighborTexCoord, vec3(0.0)))) {
1093
+ pixelOnBorder = true;
1094
+ break;
1095
+ }
1096
+
1097
+ float neighborValue = texture(volumeTexture[0], neighborTexCoord).r;
1098
+ if (neighborValue != centerValue) {
1099
+ pixelOnBorder = true;
1100
+ break;
1101
+ }
1102
+ }
1103
+ if (pixelOnBorder) {
1104
+ break;
1105
+ }
1106
+ }
1107
+
1108
+ if (pixelOnBorder) {
1109
+ gl_FragData[0] = vec4(tColor, outlineOpacity);
1110
+ } else {
1111
+ gl_FragData[0] = vec4(tColor, opacityToUse);
1112
+ }
1113
+ `)]);
1114
+ } else {
1115
+ tcoordFSImpl = tcoordFSImpl.concat(['// Dependent components', 'float intensity = tvalue.r;', 'vec3 tcolor = texture2D(colorTexture1, vec2(intensity * cscale0 + cshift0, 0.5)).rgb;', 'float scalarOpacity = texture2D(pwfTexture1, vec2(intensity * pwfscale0 + pwfshift0, 0.5)).r;', 'gl_FragData[0] = vec4(tcolor, scalarOpacity * opacity);']);
1116
+ }
800
1117
  break;
801
1118
  case 2:
802
1119
  tcoordFSImpl = tcoordFSImpl.concat(['float intensity = tvalue.r*cscale0 + cshift0;', 'gl_FragData[0] = vec4(texture2D(colorTexture1, vec2(intensity, 0.5)).rgb, pwfscale0*tvalue.g + pwfshift0);']);
@@ -818,22 +1135,22 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
818
1135
  const GSSource = shaders.Geometry;
819
1136
  let FSSource = shaders.Fragment;
820
1137
  const slabThickness = model.renderable.getSlabThickness();
821
- let posVCVSDec = ['attribute vec4 vertexWC;'];
1138
+ let posVCVSDec = ['attribute vec4 vertexWC;', 'varying vec4 vertexWCVSOutput;'];
822
1139
  // Add a unique hash to the shader to ensure that the shader program is unique to this mapper.
823
1140
  posVCVSDec = posVCVSDec.concat([`//${publicAPI.getMTime()}${model.resliceGeomUpdateString}`]);
824
1141
  if (slabThickness > 0.0) {
825
- posVCVSDec = posVCVSDec.concat(['attribute vec3 normalWC;', 'varying vec3 normalWCVSOutput;', 'varying vec4 vertexWCVSOutput;']);
1142
+ posVCVSDec = posVCVSDec.concat(['attribute vec3 normalWC;', 'varying vec3 normalWCVSOutput;']);
826
1143
  }
827
1144
  VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::PositionVC::Dec', posVCVSDec).result;
828
- let posVCVSImpl = ['gl_Position = MCPCMatrix * vertexWC;'];
1145
+ let posVCVSImpl = ['gl_Position = MCPCMatrix * vertexWC;', 'vertexWCVSOutput = vertexWC;'];
829
1146
  if (slabThickness > 0.0) {
830
- posVCVSImpl = posVCVSImpl.concat(['normalWCVSOutput = normalWC;', 'vertexWCVSOutput = vertexWC;']);
1147
+ posVCVSImpl = posVCVSImpl.concat(['normalWCVSOutput = normalWC;']);
831
1148
  }
832
1149
  VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::PositionVC::Impl', posVCVSImpl).result;
833
1150
  VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Camera::Dec', ['uniform mat4 MCPCMatrix;', 'uniform mat4 MCVCMatrix;']).result;
834
- let posVCFSDec = [];
1151
+ let posVCFSDec = ['varying vec4 vertexWCVSOutput;'];
835
1152
  if (slabThickness > 0.0) {
836
- posVCFSDec = posVCFSDec.concat(['varying vec3 normalWCVSOutput;', 'varying vec4 vertexWCVSOutput;']);
1153
+ posVCFSDec = posVCFSDec.concat(['varying vec3 normalWCVSOutput;']);
837
1154
  }
838
1155
  FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::PositionVC::Dec', posVCFSDec).result;
839
1156
  shaders.Vertex = VSSource;
@@ -993,6 +1310,64 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
993
1310
  model.resliceGeom?.modified();
994
1311
  }
995
1312
  };
1313
+ function buildLabelOutline2DTexture(dataArrays, ArrayType, vtkDataType) {
1314
+ let width = model.renderable.getLabelOutlineTextureWidth();
1315
+ if (width <= 0) {
1316
+ width = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
1317
+ }
1318
+ const height = dataArrays.length;
1319
+ const table = new ArrayType(width * height);
1320
+ for (let row = 0; row < height; row++) {
1321
+ const dataArray = dataArrays[row];
1322
+ for (let col = 0; col < width; col++) {
1323
+ table[row * width + col] = dataArray[col] ?? dataArray[0];
1324
+ }
1325
+ }
1326
+ const newTexture = vtkOpenGLTexture.newInstance({
1327
+ resizable: false
1328
+ });
1329
+ newTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
1330
+ newTexture.resetFormatAndType();
1331
+ newTexture.setMinificationFilter(Filter.NEAREST);
1332
+ newTexture.setMagnificationFilter(Filter.NEAREST);
1333
+ newTexture.create2DFromRaw({
1334
+ width,
1335
+ height,
1336
+ numComps: 1,
1337
+ dataType: vtkDataType,
1338
+ data: table
1339
+ });
1340
+ return newTexture;
1341
+ }
1342
+ function updateLabelOutlineTexture(dataArrays, ArrayType, vtkDataType, hashKey, textureKey) {
1343
+ const hash = dataArrays.map(arr => arr.join('-')).join('|');
1344
+ if (hash === model[hashKey]) {
1345
+ return;
1346
+ }
1347
+ model[hashKey] = hash;
1348
+ if (model[textureKey]) {
1349
+ model[textureKey].releaseGraphicsResources();
1350
+ }
1351
+ model[textureKey] = buildLabelOutline2DTexture(dataArrays, ArrayType, vtkDataType);
1352
+ }
1353
+ publicAPI.updateLabelOutlineThicknessTexture = labelOutlineProperties => {
1354
+ const dataArrays = labelOutlineProperties.map(({
1355
+ property
1356
+ }) => property.getLabelOutlineThicknessByReference());
1357
+ updateLabelOutlineTexture(dataArrays, Uint8Array, VtkDataTypes.UNSIGNED_CHAR, '_labelOutlineThicknessHash', 'labelOutlineThicknessTexture');
1358
+ };
1359
+ publicAPI.updateLabelOutlineOpacityTexture = labelOutlineProperties => {
1360
+ const dataArrays = labelOutlineProperties.map(({
1361
+ property
1362
+ }) => {
1363
+ let dataArray = property.getLabelOutlineOpacity();
1364
+ if (typeof dataArray === 'number') {
1365
+ dataArray = [dataArray];
1366
+ }
1367
+ return dataArray;
1368
+ });
1369
+ updateLabelOutlineTexture(dataArrays, Float32Array, VtkDataTypes.FLOAT, '_labelOutlineOpacityHash', 'labelOutlineOpacityTexture');
1370
+ };
996
1371
  publicAPI.setScalarTextures = scalarTextures => {
997
1372
  model.scalarTextures = [...scalarTextures];
998
1373
  model._externalOpenGLTexture = true;
@@ -1001,6 +1376,14 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
1001
1376
  if (model._openGLRenderWindow) {
1002
1377
  unregisterGraphicsResources(model._openGLRenderWindow);
1003
1378
  }
1379
+ if (model.labelOutlineThicknessTexture) {
1380
+ model.labelOutlineThicknessTexture.releaseGraphicsResources();
1381
+ model.labelOutlineThicknessTexture = null;
1382
+ }
1383
+ if (model.labelOutlineOpacityTexture) {
1384
+ model.labelOutlineOpacityTexture.releaseGraphicsResources();
1385
+ model.labelOutlineOpacityTexture = null;
1386
+ }
1004
1387
  }, publicAPI.delete);
1005
1388
  }
1006
1389
 
@@ -1014,6 +1397,8 @@ const DEFAULT_VALUES = {
1014
1397
  haveSeenDepthRequest: false,
1015
1398
  lastHaveSeenDepthRequest: false,
1016
1399
  lastIndependentComponents: false,
1400
+ lastUseLabelOutline: false,
1401
+ lastNumValidInputs: 0,
1017
1402
  lastNumberOfComponents: 0,
1018
1403
  lastMultiTexturePerVolumeEnabled: false,
1019
1404
  lastSlabThickness: 0,
@@ -1025,6 +1410,11 @@ const DEFAULT_VALUES = {
1025
1410
  _colorTextureCore: null,
1026
1411
  pwfTexture: null,
1027
1412
  _pwfTextureCore: null,
1413
+ labelOutlineProperties: [],
1414
+ labelOutlineThicknessTexture: null,
1415
+ _labelOutlineThicknessHash: null,
1416
+ labelOutlineOpacityTexture: null,
1417
+ _labelOutlineOpacityHash: null,
1028
1418
  _externalOpenGLTexture: false,
1029
1419
  resliceGeom: null,
1030
1420
  resliceGeomUpdateString: null,
@@ -1047,6 +1437,12 @@ function extend(publicAPI, model, initialValues = {}) {
1047
1437
  model.VBOBuildTime = {};
1048
1438
  obj(model.VBOBuildTime);
1049
1439
  model.tmpMat4 = mat4.identity(new Float64Array(16));
1440
+ model._tmpMat3 = mat3.create();
1441
+ model._tmpVec3a = vec3.create();
1442
+ model._tmpVec3b = vec3.create();
1443
+ model._tmpTangent1 = [0, 0, 0];
1444
+ model._tmpTangent2 = [0, 0, 0];
1445
+ model._tmpTexelSize = [0, 0, 0];
1050
1446
 
1051
1447
  // Implicit plane to polydata related cache:
1052
1448
  model.outlineFilter = vtkImageDataOutlineFilter.newInstance();