@kitware/vtk.js 34.16.7 → 34.18.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.
- package/CONTRIBUTING.md +1 -1
- package/Common/Core/Math/index.js +15 -1
- package/Common/Core/Math.d.ts +8 -0
- package/Common/Core/Math.js +1 -1
- package/Filters/General/WindowedSincPolyDataFilter.js +2 -4
- package/Filters/Modeling/RibbonFilter.d.ts +196 -0
- package/Filters/Modeling/RibbonFilter.js +386 -0
- package/Filters/Modeling.js +7 -0
- package/Filters/Sources/PlatonicSolidSource.d.ts +3 -3
- package/Filters/Sources/RegularPolygonSource.d.ts +183 -0
- package/Filters/Sources/RegularPolygonSource.js +150 -0
- package/Filters/Sources.js +2 -0
- package/Proxy/Representations/GlyphRepresentationProxy.js +1 -0
- package/Rendering/Core/AbstractImageMapper.d.ts +4 -0
- package/Rendering/Core/ImageProperty.d.ts +7 -0
- package/Rendering/OpenGL/ImageResliceMapper.js +477 -81
- package/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -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
|
|
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) =>
|
|
241
|
-
|
|
242
|
-
|
|
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[
|
|
254
|
-
const updatedExtents = actorProperty
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
const
|
|
318
|
-
cfun
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
mat4.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
622
|
-
let cl =
|
|
623
|
-
const cfun =
|
|
624
|
-
if (cfun &&
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
933
|
+
const useLabelOutline = model.labelOutlineProperties.length > 0;
|
|
706
934
|
const slabThickness = model.renderable.getSlabThickness();
|
|
707
|
-
VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Dec',
|
|
708
|
-
|
|
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
|
|
712
|
-
|
|
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
|
-
//
|
|
725
|
-
|
|
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],
|
|
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 <
|
|
731
|
-
tcoordFSDec.push(`rawSample[${component}] = texture(volumeTexture[${component}],
|
|
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(
|
|
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
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
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;'
|
|
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;'
|
|
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;'
|
|
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();
|