@kitware/vtk.js
Version:
Visualization Toolkit for the Web
1,017 lines (967 loc) • 70.2 kB
JavaScript
import { n as newInstance$1, o as obj, g as get, h as chain, c as macro } from '../../macros2.js';
import { mat4, mat3, vec3 } from 'gl-matrix';
import vtkClosedPolyLineToSurfaceFilter from '../../Filters/General/ClosedPolyLineToSurfaceFilter.js';
import vtkCutter from '../../Filters/Core/Cutter.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import vtkHelper from './Helper.js';
import vtkImageDataOutlineFilter from '../../Filters/General/ImageDataOutlineFilter.js';
import { b as vtkMath } from '../../Common/Core/Math/index.js';
import vtkOpenGLTexture from './Texture.js';
import vtkPlane from '../../Common/DataModel/Plane.js';
import vtkPolyData from '../../Common/DataModel/PolyData.js';
import vtkReplacementShaderMapper from './ReplacementShaderMapper.js';
import vtkShaderProgram from './ShaderProgram.js';
import vtkTransform from '../../Common/Transform/Transform.js';
import vtkViewNode from '../SceneGraph/ViewNode.js';
import { getImageDataHash, getTransferFunctionsHash } from './RenderWindow/resourceSharingHelper.js';
import { v as vtkImageResliceMapperVS } from './glsl/vtkImageResliceMapperVS.glsl.js';
import { v as vtkImageResliceMapperFS } from './glsl/vtkImageResliceMapperFS.glsl.js';
import { Filter } from './Texture/Constants.js';
import { InterpolationType } from '../Core/ImageProperty/Constants.js';
import { Representation } from '../Core/Property/Constants.js';
import { VtkDataTypes } from '../../Common/Core/DataArray/Constants.js';
import { registerOverride } from './ViewNodeFactory.js';
import '../Core/Mapper/CoincidentTopologyHelper.js';
import { Resolve } from '../Core/Mapper/Static.js';
const {
vtkErrorMacro
} = macro;
const splitStringOnEnter = str => str.split('\n').map(s => s.trim()).filter(Boolean);
function findLabelOutlineProperties(actor, currentValidInputs) {
const labelmapProperties = [];
for (let i = 0; i < currentValidInputs.length; i++) {
const property = actor.getProperty(currentValidInputs[i].inputIndex);
if (property?.getUseLabelOutline()) {
labelmapProperties.push({
property,
arrayIndex: i
});
}
}
return labelmapProperties;
}
// ----------------------------------------------------------------------------
// helper methods
// ----------------------------------------------------------------------------
function safeMatrixMultiply(matrixArray, matrixType, tmpMat) {
matrixType.identity(tmpMat);
return matrixArray.reduce((res, matrix, index) => {
if (index === 0) {
return matrix ? matrixType.copy(res, matrix) : matrixType.identity(res);
}
return matrix ? matrixType.multiply(res, res, matrix) : res;
}, tmpMat);
}
// ----------------------------------------------------------------------------
// vtkOpenGLImageResliceMapper methods
// ----------------------------------------------------------------------------
function vtkOpenGLImageResliceMapper(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkOpenGLImageResliceMapper');
// Associate a reference counter to each graphics resource
const graphicsResourceReferenceCount = new Map();
function decreaseGraphicsResourceCount(openGLRenderWindow, coreObject) {
if (!coreObject) {
return;
}
const oldCount = graphicsResourceReferenceCount.get(coreObject) ?? 0;
const newCount = oldCount - 1;
if (newCount <= 0) {
openGLRenderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI);
graphicsResourceReferenceCount.delete(coreObject);
} else {
graphicsResourceReferenceCount.set(coreObject, newCount);
}
}
function increaseGraphicsResourceCount(openGLRenderWindow, coreObject) {
if (!coreObject) {
return;
}
const oldCount = graphicsResourceReferenceCount.get(coreObject) ?? 0;
const newCount = oldCount + 1;
graphicsResourceReferenceCount.set(coreObject, newCount);
if (oldCount <= 0) {
openGLRenderWindow.registerGraphicsResourceUser(coreObject, publicAPI);
}
}
function replaceGraphicsResource(openGLRenderWindow, oldResourceCoreObject, newResourceCoreObject) {
if (oldResourceCoreObject === newResourceCoreObject) {
return;
}
decreaseGraphicsResourceCount(openGLRenderWindow, oldResourceCoreObject);
increaseGraphicsResourceCount(openGLRenderWindow, newResourceCoreObject);
}
function unregisterGraphicsResources(renderWindow) {
// Convert to an array using the spread operator as Firefox doesn't support Iterator.forEach()
[...graphicsResourceReferenceCount.keys()].forEach(coreObject => renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI));
}
publicAPI.buildPass = prepass => {
if (prepass) {
model.currentRenderPass = null;
model._openGLImageSlice = publicAPI.getFirstAncestorOfType('vtkOpenGLImageSlice');
model._openGLRenderer = publicAPI.getFirstAncestorOfType('vtkOpenGLRenderer');
const ren = model._openGLRenderer.getRenderable();
model._openGLCamera = model._openGLRenderer.getViewNodeFor(ren.getActiveCamera(), model.openGLCamera);
const oldOglRenderWindow = model._openGLRenderWindow;
model._openGLRenderWindow = model._openGLRenderer.getLastAncestorOfType('vtkOpenGLRenderWindow');
if (oldOglRenderWindow && !oldOglRenderWindow.isDeleted() && oldOglRenderWindow !== model._openGLRenderWindow) {
// Unregister the mapper when the render window changes
unregisterGraphicsResources(oldOglRenderWindow);
}
model.context = model._openGLRenderWindow.getContext();
model.tris.setOpenGLRenderWindow(model._openGLRenderWindow);
}
};
publicAPI.translucentPass = (prepass, renderPass) => {
if (prepass) {
model.currentRenderPass = renderPass;
publicAPI.render();
}
};
publicAPI.zBufferPass = prepass => {
if (prepass) {
model.haveSeenDepthRequest = true;
model.renderDepth = true;
publicAPI.render();
model.renderDepth = false;
}
};
publicAPI.opaqueZBufferPass = prepass => publicAPI.zBufferPass(prepass);
publicAPI.opaquePass = prepass => {
if (prepass) {
publicAPI.render();
}
};
publicAPI.getCoincidentParameters = (ren, actor) => {
if (
// backwards compat with code that (errorneously) set this to boolean
// eslint-disable-next-line eqeqeq
model.renderable.getResolveCoincidentTopology() == Resolve.PolygonOffset) {
return model.renderable.getCoincidentTopologyPolygonOffsetParameters();
}
return null;
};
// Renders myself
publicAPI.render = () => {
const actor = model._openGLImageSlice.getRenderable();
const ren = model._openGLRenderer.getRenderable();
publicAPI.renderPiece(ren, actor);
};
publicAPI.renderPiece = (ren, actor) => {
publicAPI.invokeEvent({
type: 'StartEvent'
});
model.renderable.update();
const numberOfInputs = model.renderable.getNumberOfInputPorts();
model.currentValidInputs = [];
for (let inputIndex = 0; inputIndex < numberOfInputs; ++inputIndex) {
const imageData = model.renderable.getInputData(inputIndex);
if (imageData && !imageData.isDeleted()) {
model.currentValidInputs.push({
imageData,
inputIndex
});
}
}
const numberOfValidInputs = model.currentValidInputs.length;
if (numberOfValidInputs <= 0) {
vtkErrorMacro('No input!');
return;
}
model.labelOutlineProperties = findLabelOutlineProperties(actor, model.currentValidInputs);
// Number of components
const firstImageData = model.currentValidInputs[0].imageData;
const firstScalars = firstImageData.getPointData().getScalars();
model.multiTexturePerVolumeEnabled = numberOfValidInputs > 1;
model.numberOfComponents = model.multiTexturePerVolumeEnabled ? numberOfValidInputs : firstScalars.getNumberOfComponents();
publicAPI.updateResliceGeometry();
publicAPI.renderPieceStart(ren, actor);
publicAPI.renderPieceDraw(ren, actor);
publicAPI.renderPieceFinish(ren, actor);
publicAPI.invokeEvent({
type: 'EndEvent'
});
};
publicAPI.renderPieceStart = (ren, actor) => {
// make sure the BOs are up to date
publicAPI.updateBufferObjects(ren, actor);
// Update filters for scalar textures
const actorProperties = actor.getProperties();
model.currentValidInputs.forEach(({
inputIndex
}, component) => {
const actorProperty = actorProperties[inputIndex];
const scalarTexture = model.scalarTextures[component];
if (!actorProperty || !scalarTexture) return;
const interpolationType = actorProperty.getInterpolationType();
if (interpolationType === InterpolationType.NEAREST) {
scalarTexture.setMinificationFilter(Filter.NEAREST);
scalarTexture.setMagnificationFilter(Filter.NEAREST);
} else {
scalarTexture.setMinificationFilter(Filter.LINEAR);
scalarTexture.setMagnificationFilter(Filter.LINEAR);
}
});
// Update color and opacity texture filters
const firstValidInput = model.currentValidInputs[0];
const firstProperty = actorProperties[firstValidInput.inputIndex];
const iType = firstProperty?.getInterpolationType();
if (iType === InterpolationType.NEAREST) {
model.colorTexture.setMinificationFilter(Filter.NEAREST);
model.colorTexture.setMagnificationFilter(Filter.NEAREST);
model.pwfTexture.setMinificationFilter(Filter.NEAREST);
model.pwfTexture.setMagnificationFilter(Filter.NEAREST);
} else {
model.colorTexture.setMinificationFilter(Filter.LINEAR);
model.colorTexture.setMagnificationFilter(Filter.LINEAR);
model.pwfTexture.setMinificationFilter(Filter.LINEAR);
model.pwfTexture.setMagnificationFilter(Filter.LINEAR);
}
// No buffer objects bound.
model.lastBoundBO = null;
};
publicAPI.renderPieceDraw = (ren, actor) => {
const gl = model.context;
const useLabelOutline = model.labelOutlineProperties.length > 0;
// render the texture
const allTextures = [...model.scalarTextures, model.colorTexture, model.pwfTexture];
if (useLabelOutline) {
allTextures.push(model.labelOutlineThicknessTexture);
allTextures.push(model.labelOutlineOpacityTexture);
}
allTextures.forEach(texture => texture.activate());
// update shaders if required
publicAPI.updateShaders(model.tris, ren, actor);
// Finally draw
gl.drawArrays(gl.TRIANGLES, 0, model.tris.getCABO().getElementCount());
model.tris.getVAO().release();
allTextures.forEach(texture => texture.deactivate());
};
publicAPI.renderPieceFinish = (ren, actor) => {};
publicAPI.updateBufferObjects = (ren, actor) => {
// Rebuild buffer objects if needed
if (publicAPI.getNeedToRebuildBufferObjects(ren, actor)) {
publicAPI.buildBufferObjects(ren, actor);
}
};
publicAPI.getNeedToRebuildBufferObjects = (ren, actor) => {
const firstActorProperty = actor.getProperty(model.currentValidInputs[0].inputIndex);
const useLabelOutline = model.labelOutlineProperties.length > 0;
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(({
imageData
}) => 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());
};
publicAPI.buildBufferObjects = (ren, actor) => {
const actorProperties = actor.getProperties();
model.currentValidInputs.forEach(({
imageData,
inputIndex
}, component) => {
// rebuild the scalarTexture if the data has changed
const scalars = imageData.getPointData().getScalars();
const tex = model._openGLRenderWindow.getGraphicsResourceForObject(scalars);
const scalarsHash = getImageDataHash(imageData, scalars);
const reBuildTex = !tex?.oglObject?.getHandle() || tex?.hash !== scalarsHash;
const actorProperty = actorProperties[inputIndex];
const updatedExtents = actorProperty?.getUpdatedExtents() ?? [];
const hasUpdatedExtents = !!updatedExtents.length;
if (reBuildTex && !hasUpdatedExtents) {
const newScalarTexture = vtkOpenGLTexture.newInstance();
newScalarTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
// Build the textures
const dims = imageData.getDimensions();
// Use norm16 for scalar texture if the extension is available
newScalarTexture.setOglNorm16Ext(model.context.getExtension('EXT_texture_norm16'));
newScalarTexture.resetFormatAndType();
newScalarTexture.create3DFilterableFromDataArray({
width: dims[0],
height: dims[1],
depth: dims[2],
dataArray: scalars
});
model._openGLRenderWindow.setGraphicsResourceForObject(scalars, newScalarTexture, scalarsHash);
model.scalarTextures[component] = newScalarTexture;
} else {
model.scalarTextures[component] = tex.oglObject;
}
if (hasUpdatedExtents) {
// If hasUpdatedExtents, then the texture is partially updated.
// clear the array to acknowledge the update.
actorProperty.setUpdatedExtents([]);
const dims = imageData.getDimensions();
model.scalarTextures[component].create3DFilterableFromDataArray({
width: dims[0],
height: dims[1],
depth: dims[2],
dataArray: scalars,
updatedExtents
});
}
replaceGraphicsResource(model._openGLRenderWindow, model._scalarTexturesCore[component], scalars);
model._scalarTexturesCore[component] = scalars;
});
const firstValidInput = model.currentValidInputs[0];
const firstActorProperty = actorProperties[firstValidInput.inputIndex];
if (!firstActorProperty) {
vtkErrorMacro('Missing property for first input');
return;
}
const iComps = firstActorProperty.getIndependentComponents();
const numIComps = iComps ? model.numberOfComponents : 1;
const textureHeight = iComps ? 2 * numIComps : 1;
// Collect color transfer functions - in multi-texture mode, get from each input's property
const colorTransferFunctions = [];
for (let component = 0; component < numIComps; ++component) {
if (model.multiTexturePerVolumeEnabled) {
const validInput = model.currentValidInputs[component];
const prop = validInput ? actorProperties[validInput.inputIndex] : null;
colorTransferFunctions.push(prop?.getRGBTransferFunction() || null);
} else {
colorTransferFunctions.push(firstActorProperty.getRGBTransferFunction(component));
}
}
const colorFuncHash = getTransferFunctionsHash(colorTransferFunctions, iComps, numIComps);
const firstColorTransferFunc = firstActorProperty.getRGBTransferFunction();
const cTex = model._openGLRenderWindow.getGraphicsResourceForObject(firstColorTransferFunc);
const reBuildC = !cTex?.oglObject?.getHandle() || cTex?.hash !== colorFuncHash;
if (reBuildC) {
let cWidth = model.renderable.getColorTextureWidth();
if (cWidth <= 0) {
cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
const cSize = cWidth * textureHeight * 3;
const cTable = new Uint8ClampedArray(cSize);
const newColorTexture = vtkOpenGLTexture.newInstance();
newColorTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
if (firstColorTransferFunc) {
const tmpTable = new Float32Array(cWidth * 3);
for (let c = 0; c < numIComps; c++) {
// Use pre-collected color transfer functions (handles both single and multi-texture modes)
const cfun = colorTransferFunctions[c];
if (cfun) {
const cRange = cfun.getRange();
cfun.getTable(cRange[0], cRange[1], cWidth, tmpTable, 1);
if (iComps) {
for (let i = 0; i < cWidth * 3; i++) {
cTable[c * cWidth * 6 + i] = 255.0 * tmpTable[i];
cTable[c * cWidth * 6 + i + cWidth * 3] = 255.0 * tmpTable[i];
}
} else {
for (let i = 0; i < cWidth * 3; i++) {
cTable[c * cWidth * 3 + i] = 255.0 * tmpTable[i];
}
}
}
}
newColorTexture.resetFormatAndType();
newColorTexture.create2DFromRaw({
width: cWidth,
height: textureHeight,
numComps: 3,
dataType: VtkDataTypes.UNSIGNED_CHAR,
data: cTable
});
} else {
for (let column = 0; column < cWidth * 3; ++column) {
const opacity = 255.0 * column / ((cWidth - 1) * 3);
for (let row = 0; row < textureHeight; ++row) {
// R, G, B
cTable[row * cWidth * 3 + column + 0] = opacity;
cTable[row * cWidth * 3 + column + 1] = opacity;
cTable[row * cWidth * 3 + column + 2] = opacity;
}
}
newColorTexture.resetFormatAndType();
newColorTexture.create2DFromRaw({
width: cWidth,
height: 1,
numComps: 3,
dataType: VtkDataTypes.UNSIGNED_CHAR,
data: cTable
});
}
if (firstColorTransferFunc) {
model._openGLRenderWindow.setGraphicsResourceForObject(firstColorTransferFunc, newColorTexture, colorFuncHash);
}
model.colorTexture = newColorTexture;
} else {
model.colorTexture = cTex.oglObject;
}
replaceGraphicsResource(model._openGLRenderWindow, model._colorTextureCore, firstColorTransferFunc);
model._colorTextureCore = firstColorTransferFunc;
// Build piecewise function buffer. This buffer is used either
// for component weighting or opacity, depending on whether we're
// rendering components independently or not.
// In multi-texture mode, get from each input's property
const opacityFunctions = [];
for (let component = 0; component < numIComps; ++component) {
if (model.multiTexturePerVolumeEnabled) {
const validInput = model.currentValidInputs[component];
const prop = validInput ? actorProperties[validInput.inputIndex] : null;
opacityFunctions.push(prop?.getPiecewiseFunction() || null);
} else {
opacityFunctions.push(firstActorProperty.getPiecewiseFunction(component));
}
}
const opacityFuncHash = getTransferFunctionsHash(opacityFunctions, iComps, numIComps);
const firstPwFunc = firstActorProperty.getPiecewiseFunction();
const pwfTex = model._openGLRenderWindow.getGraphicsResourceForObject(firstPwFunc);
const reBuildPwf = !pwfTex?.oglObject?.getHandle() || pwfTex?.hash !== opacityFuncHash;
if (reBuildPwf) {
let pwfWidth = model.renderable.getOpacityTextureWidth();
if (pwfWidth <= 0) {
pwfWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
const pwfSize = pwfWidth * textureHeight;
const pwfTable = new Uint8ClampedArray(pwfSize);
const newOpacityTexture = vtkOpenGLTexture.newInstance();
newOpacityTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
if (firstPwFunc) {
const pwfFloatTable = new Float32Array(pwfSize);
const tmpTable = new Float32Array(pwfWidth);
for (let c = 0; c < numIComps; ++c) {
// Use pre-collected opacity functions (handles both single and multi-texture modes)
const pwfun = opacityFunctions[c];
if (pwfun === null) {
// Piecewise constant max if no function supplied for this component
pwfFloatTable.fill(1.0);
} else {
const pwfRange = pwfun.getRange();
pwfun.getTable(pwfRange[0], pwfRange[1], pwfWidth, tmpTable, 1);
// adjust for sample distance etc
if (iComps) {
for (let i = 0; i < pwfWidth; i++) {
pwfFloatTable[c * pwfWidth * 2 + i] = tmpTable[i];
pwfFloatTable[c * pwfWidth * 2 + i + pwfWidth] = tmpTable[i];
}
} else {
for (let i = 0; i < pwfWidth; i++) {
pwfFloatTable[i] = tmpTable[i];
}
}
}
}
newOpacityTexture.resetFormatAndType();
newOpacityTexture.create2DFromRaw({
width: pwfWidth,
height: textureHeight,
numComps: 1,
dataType: VtkDataTypes.FLOAT,
data: pwfFloatTable
});
} else {
// default is opaque
pwfTable.fill(255.0);
newOpacityTexture.resetFormatAndType();
newOpacityTexture.create2DFromRaw({
width: pwfWidth,
height: textureHeight,
numComps: 1,
dataType: VtkDataTypes.UNSIGNED_CHAR,
data: pwfTable
});
}
if (firstPwFunc) {
model._openGLRenderWindow.setGraphicsResourceForObject(firstPwFunc, newOpacityTexture, opacityFuncHash);
}
model.pwfTexture = newOpacityTexture;
} else {
model.pwfTexture = pwfTex.oglObject;
}
replaceGraphicsResource(model._openGLRenderWindow, model._pwfTextureCore, firstPwFunc);
model._pwfTextureCore = firstPwFunc;
// Build label outline textures if needed (2D textures for per-labelmap settings)
if (model.labelOutlineProperties.length > 0) {
publicAPI.updateLabelOutlineThicknessTexture(model.labelOutlineProperties);
publicAPI.updateLabelOutlineOpacityTexture(model.labelOutlineProperties);
}
const vboString = `${model.resliceGeom.getMTime()}A${model.renderable.getSlabThickness()}`;
if (!model.tris.getCABO().getElementCount() || model.VBOBuildString !== vboString) {
const points = vtkDataArray.newInstance({
numberOfComponents: 3,
values: model.resliceGeom.getPoints().getData()
});
points.setName('points');
const cells = vtkDataArray.newInstance({
numberOfComponents: 1,
values: model.resliceGeom.getPolys().getData()
});
const options = {
points,
cellOffset: 0
};
if (model.renderable.getSlabThickness() > 0.0) {
const n = model.resliceGeom.getPointData().getNormals();
if (!n) {
vtkErrorMacro('Slab mode requested without normals');
} else {
options.normals = n;
}
}
model.tris.getCABO().createVBO(cells, 'polys', Representation.SURFACE, options);
}
model.VBOBuildString = vboString;
model.VBOBuildTime.modified();
};
publicAPI.updateShaders = (cellBO, ren, actor) => {
model.lastBoundBO = cellBO;
// has something changed that would require us to recreate the shader?
if (publicAPI.getNeedToRebuildShaders(cellBO, ren, actor)) {
const shaders = {
Vertex: null,
Fragment: null,
Geometry: null
};
publicAPI.buildShaders(shaders, ren, actor);
// compile and bind the program if needed
const newShader = model._openGLRenderWindow.getShaderCache().readyShaderProgramArray(shaders.Vertex, shaders.Fragment, shaders.Geometry);
// if the shader changed reinitialize the VAO
if (newShader !== cellBO.getProgram()) {
cellBO.setProgram(newShader);
// reset the VAO as the shader has changed
cellBO.getVAO().releaseGraphicsResources();
}
cellBO.getShaderSourceTime().modified();
} else {
model._openGLRenderWindow.getShaderCache().readyShaderProgram(cellBO.getProgram());
}
cellBO.getVAO().bind();
publicAPI.setMapperShaderParameters(cellBO, ren, actor);
publicAPI.setCameraShaderParameters(cellBO, ren, actor);
publicAPI.setPropertyShaderParameters(cellBO, ren, actor);
};
publicAPI.setMapperShaderParameters = (cellBO, ren, actor) => {
const program = cellBO.getProgram();
const firstImageData = model.currentValidInputs[0].imageData;
if (cellBO.getCABO().getElementCount() && (model.VBOBuildTime.getMTime() > cellBO.getAttributeUpdateTime().getMTime() || cellBO.getShaderSourceTime().getMTime() > cellBO.getAttributeUpdateTime().getMTime())) {
// Set the 3D texture
model.scalarTextures.forEach((scalarTexture, component) => {
program.setUniformi(`volumeTexture[${component}]`, scalarTexture.getTextureUnit());
});
// Set the plane vertex attributes
if (program.isAttributeUsed('vertexWC')) {
if (!cellBO.getVAO().addAttributeArray(program, cellBO.getCABO(), 'vertexWC', cellBO.getCABO().getVertexOffset(), cellBO.getCABO().getStride(), model.context.FLOAT, 3, model.context.FALSE)) {
vtkErrorMacro('Error setting vertexWC in shader VAO.');
}
}
// If we are doing slab mode, we need normals
if (program.isAttributeUsed('normalWC')) {
if (!cellBO.getVAO().addAttributeArray(program, cellBO.getCABO(), 'normalWC', cellBO.getCABO().getNormalOffset(), cellBO.getCABO().getStride(), model.context.FLOAT, 3, model.context.FALSE)) {
vtkErrorMacro('Error setting normalWC in shader VAO.');
}
}
if (program.isUniformUsed('slabThickness')) {
program.setUniformf('slabThickness', model.renderable.getSlabThickness());
}
if (program.isUniformUsed('spacing')) {
program.setUniform3fv('spacing', firstImageData.getSpacing());
}
if (program.isUniformUsed('slabType')) {
program.setUniformi('slabType', model.renderable.getSlabType());
}
if (program.isUniformUsed('slabTrapezoid')) {
program.setUniformi('slabTrapezoid', model.renderable.getSlabTrapezoidIntegration());
}
const shiftScaleEnabled = cellBO.getCABO().getCoordShiftAndScaleEnabled();
const inverseShiftScaleMatrix = shiftScaleEnabled ? cellBO.getCABO().getInverseShiftAndScaleMatrix() : null;
// Set per-input world->texture matrices
for (let i = 0; i < model.currentValidInputs.length; i++) {
const uniformName = `WCTCMatrix${i}`;
if (program.isUniformUsed(uniformName)) {
const imageData = model.currentValidInputs[i].imageData;
const dim = imageData.getDimensions();
mat4.copy(model.tmpMat4, imageData.getIndexToWorld());
mat4.translate(model.tmpMat4, model.tmpMat4, [-0.5, -0.5, -0.5]);
mat4.scale(model.tmpMat4, model.tmpMat4, dim);
mat4.invert(model.tmpMat4, model.tmpMat4);
if (inverseShiftScaleMatrix) {
mat4.multiply(model.tmpMat4, model.tmpMat4, inverseShiftScaleMatrix);
}
program.setUniformMatrix(uniformName, model.tmpMat4);
}
}
if (program.isUniformUsed('vboScaling')) {
program.setUniform3fv('vboScaling', cellBO.getCABO().getCoordScale() ?? [1, 1, 1]);
}
cellBO.getAttributeUpdateTime().modified();
}
// Depth request
if (model.haveSeenDepthRequest) {
cellBO.getProgram().setUniformi('depthRequest', model.renderDepth ? 1 : 0);
}
// handle coincident
if (cellBO.getProgram().isUniformUsed('coffset')) {
const cp = publicAPI.getCoincidentParameters(ren, actor);
cellBO.getProgram().setUniformf('coffset', cp.offset);
// cfactor isn't always used when coffset is.
if (cellBO.getProgram().isUniformUsed('cfactor')) {
cellBO.getProgram().setUniformf('cfactor', cp.factor);
}
}
};
publicAPI.setCameraShaderParameters = (cellBO, ren, actor) => {
// [WMVP]C == {world, model, view, projection} coordinates
// e.g. WCPC == world to projection coordinate transformation
const keyMats = model._openGLCamera.getKeyMatrices(ren);
const actMats = model._openGLImageSlice.getKeyMatrices();
const shiftScaleEnabled = cellBO.getCABO().getCoordShiftAndScaleEnabled();
const inverseShiftScaleMatrix = shiftScaleEnabled ? cellBO.getCABO().getInverseShiftAndScaleMatrix() : null;
const program = cellBO.getProgram();
if (program.isUniformUsed('MCPCMatrix')) {
mat4.identity(model.tmpMat4);
program.setUniformMatrix('MCPCMatrix', safeMatrixMultiply([keyMats.wcpc, actMats.mcwc, inverseShiftScaleMatrix], mat4, model.tmpMat4));
}
if (program.isUniformUsed('MCVCMatrix')) {
mat4.identity(model.tmpMat4);
program.setUniformMatrix('MCVCMatrix', safeMatrixMultiply([keyMats.wcvc, actMats.mcwc, inverseShiftScaleMatrix], mat4, model.tmpMat4));
}
};
publicAPI.setPropertyShaderParameters = (cellBO, ren, actor) => {
const program = cellBO.getProgram();
const firstPpty = actor.getProperty(model.currentValidInputs[0].inputIndex);
// In multi-texture mode, use 1.0 for global opacity since each input's
// piecewise function controls its own opacity through component weights.
// This prevents a labelmap's opacity setting from affecting all inputs.
const opacity = model.multiTexturePerVolumeEnabled ? 1.0 : firstPpty.getOpacity();
program.setUniformf('opacity', opacity);
// Component mix
// Independent components: Mixed according to component weights
// Dependent components: Mixed using the following logic:
// - 2 comps => LA
// - 3 comps => RGB + opacity from pwf
// - 4 comps => RGBA
const numComp = model.numberOfComponents;
const iComps = firstPpty.getIndependentComponents();
const useMultiTexture = model.multiTexturePerVolumeEnabled;
const actorProperties = actor.getProperties();
if (iComps) {
for (let i = 0; i < numComp; ++i) {
const property = useMultiTexture ? actorProperties[model.currentValidInputs[i].inputIndex] : firstPpty;
program.setUniformf(`mix${i}`, property.getComponentWeight(0));
}
}
// three levels of shift scale combined into one
// for performance in the fragment shader
for (let component = 0; component < numComp; component++) {
const textureIndex = useMultiTexture ? component : 0;
const volInfoIndex = useMultiTexture ? 0 : component;
const scalarTexture = model.scalarTextures[textureIndex];
const volInfo = scalarTexture.getVolumeInfo();
const volScale = volInfo.scale[volInfoIndex];
const volOffset = volInfo.offset[volInfoIndex];
const target = iComps ? component : 0;
const property = useMultiTexture ? actorProperties[model.currentValidInputs[component].inputIndex] : firstPpty;
// color shift/scale
let cw = property.getColorWindow();
let cl = property.getColorLevel();
const cfun = property.getRGBTransferFunction(useMultiTexture ? 0 : target);
if (cfun && property.getUseLookupTableScalarRange()) {
const cRange = cfun.getRange();
cw = cRange[1] - cRange[0];
cl = 0.5 * (cRange[1] + cRange[0]);
}
const colorScale = volScale / cw;
const colorShift = (volOffset - cl) / cw + 0.5;
program.setUniformf(`cshift${component}`, colorShift);
program.setUniformf(`cscale${component}`, colorScale);
// pwf shift/scale
let pwfScale = 1.0;
let pwfShift = 0.0;
const pwfun = property.getPiecewiseFunction(useMultiTexture ? 0 : target);
if (pwfun) {
const pwfRange = pwfun.getRange();
const length = pwfRange[1] - pwfRange[0];
const mid = 0.5 * (pwfRange[0] + pwfRange[1]);
pwfScale = volScale / length;
pwfShift = (volOffset - mid) / length + 0.5;
}
program.setUniformf(`pwfshift${component}`, pwfShift);
program.setUniformf(`pwfscale${component}`, pwfScale);
}
const texColorUnit = model.colorTexture.getTextureUnit();
program.setUniformi('colorTexture1', texColorUnit);
const texOpacityUnit = model.pwfTexture.getTextureUnit();
program.setUniformi('pwfTexture1', texOpacityUnit);
// Background color
program.setUniform4fv('backgroundColor', model.renderable.getBackgroundColor());
// Label outline uniforms
if (model.labelOutlineProperties.length > 0) {
const outlineThicknessUnit = model.labelOutlineThicknessTexture.getTextureUnit();
program.setUniformi('labelOutlineThicknessTexture', outlineThicknessUnit);
const outlineOpacityUnit = model.labelOutlineOpacityTexture.getTextureUnit();
program.setUniformi('labelOutlineOpacityTexture', outlineOpacityUnit);
let textureWidth = model.renderable.getLabelOutlineTextureWidth();
if (textureWidth <= 0) {
textureWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
program.setUniformf('labelOutlineTextureWidth', textureWidth);
program.setUniformf('numLabelmaps', model.labelOutlineProperties.length);
// Calculate tangent vectors for the slice plane in each input's texture space
const slicePlane = model.renderable.getSlicePlane();
model._tmpTangent1.fill(0);
model._tmpTangent2.fill(0);
if (slicePlane) {
const normal = slicePlane.getNormal();
vtkMath.perpendiculars(normal, model._tmpTangent1, model._tmpTangent2, 0);
} else {
model._tmpTangent1[0] = 1;
model._tmpTangent2[1] = 1;
}
// Set per-input tangent vectors (transformed to each input's texture space)
for (let i = 0; i < model.currentValidInputs.length; i++) {
const imageData = model.currentValidInputs[i].imageData;
mat3.set(model._tmpMat3, ...imageData.getDirection());
mat3.invert(model._tmpMat3, model._tmpMat3);
vec3.transformMat3(model._tmpVec3a, model._tmpTangent1, model._tmpMat3);
vec3.transformMat3(model._tmpVec3b, model._tmpTangent2, model._tmpMat3);
const t1Name = `outlineTangent1_${i}`;
const t2Name = `outlineTangent2_${i}`;
if (program.isUniformUsed(t1Name)) {
program.setUniform3fv(t1Name, model._tmpVec3a);
}
if (program.isUniformUsed(t2Name)) {
program.setUniform3fv(t2Name, model._tmpVec3b);
}
}
// Set per-input texel sizes in texture coordinates
for (let i = 0; i < model.currentValidInputs.length; i++) {
const uniformName = `texelSize${i}`;
if (program.isUniformUsed(uniformName)) {
const imageData = model.currentValidInputs[i].imageData;
const inputDims = imageData.getDimensions();
model._tmpTexelSize[0] = 1.0 / inputDims[0];
model._tmpTexelSize[1] = 1.0 / inputDims[1];
model._tmpTexelSize[2] = 1.0 / inputDims[2];
program.setUniform3fv(uniformName, model._tmpTexelSize);
}
}
}
};
publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => {
// has something changed that would require us to recreate the shader?
// candidates are
// property modified (representation interpolation and lighting)
// input modified
// light complexity changed
// render pass shader replacement changed
const firstActorProperty = actor.getProperty(model.currentValidInputs[0].inputIndex);
const iComp = firstActorProperty.getIndependentComponents();
const useLabelOutline = model.labelOutlineProperties.length > 0;
const slabTh = model.renderable.getSlabThickness();
const slabType = model.renderable.getSlabType();
const slabTrap = model.renderable.getSlabTrapezoidIntegration();
// has the render pass shader replacement changed? Two options
let needRebuild = false;
if (!model.currentRenderPass && model.lastRenderPassShaderReplacement || model.currentRenderPass && model.currentRenderPass.getShaderReplacement() !== model.lastRenderPassShaderReplacement) {
needRebuild = true;
}
const numValidInputs = model.currentValidInputs?.length ?? 0;
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) {
model.lastHaveSeenDepthRequest = model.haveSeenDepthRequest;
model.lastNumberOfComponents = model.numberOfComponents;
model.lastMultiTexturePerVolumeEnabled = model.multiTexturePerVolumeEnabled;
model.lastIndependentComponents = iComp;
model.lastUseLabelOutline = useLabelOutline;
model.lastNumValidInputs = numValidInputs;
model.lastSlabThickness = slabTh;
model.lastSlabType = slabType;
model.lastSlabTrapezoidIntegration = slabTrap;
return true;
}
return false;
};
publicAPI.getShaderTemplate = (shaders, ren, actor) => {
shaders.Vertex = vtkImageResliceMapperVS;
shaders.Fragment = vtkImageResliceMapperFS;
shaders.Geometry = '';
};
publicAPI.replaceShaderValues = (shaders, ren, actor) => {
publicAPI.replaceShaderTCoord(shaders, ren, actor);
publicAPI.replaceShaderPositionVC(shaders, ren, actor);
if (model.haveSeenDepthRequest) {
let FSSource = shaders.Fragment;
FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::ZBuffer::Dec', 'uniform int depthRequest;').result;
FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::ZBuffer::Impl', ['if (depthRequest == 1) {', 'float iz = floor(gl_FragCoord.z*65535.0 + 0.1);', 'float rf = floor(iz/256.0)/255.0;', 'float gf = mod(iz,256.0)/255.0;', 'gl_FragData[0] = vec4(rf, gf, 0.0, 1.0); }']).result;
shaders.Fragment = FSSource;
}
publicAPI.replaceShaderCoincidentOffset(shaders, ren, actor);
};
// Helper to generate shader code for compositing multiple inputs
// Some inputs may be labelmaps (with outline), others may be background images
// labelmapInputs: array of input indices that are labelmaps (e.g., [0, 2])
// totalInputs: total number of inputs (1-4)
function generateMultiInputCompositeShader(labelmapInputs, totalInputs) {
const rgba = ['r', 'g', 'b', 'a'];
const allInputs = Array.from({
length: totalInputs
}, (_, i) => i);
const backgroundInputs = allInputs.filter(i => !labelmapInputs.includes(i));
// Generate texture coordinate lines for labelmap inputs
const texCoordLines = labelmapInputs.map(i => `vec3 labelTexCoord${i} = (WCTCMatrix${i} * vec4(fragWorldPos, 1.0)).xyz;`).join('\n ');
// Build texture sampling conditional for neighbor checking
const textureSampling = (() => {
if (labelmapInputs.length === 0) return '';
const conditions = labelmapInputs.map((inputIdx, arrayIdx) => {
if (arrayIdx === 0) {
return `(labelInputIdx == ${arrayIdx}) ? texture(volumeTexture[${inputIdx}], neighborTexCoord).r`;
}
return ` : (labelInputIdx == ${arrayIdx}) ? texture(volumeTexture[${inputIdx}], neighborTexCoord).r`;
});
return `float neighborLabel = ${conditions.join('')} : 0.0;`;
})();
// Process backgrounds first, then labelmaps on top
const orderedInputs = [...backgroundInputs, ...labelmapInputs];
const processInputs = orderedInputs.map(inputIdx => {
const isLabelmap = labelmapInputs.includes(inputIdx);
const labelArrayIdx = labelmapInputs.indexOf(inputIdx);
if (isLabelmap) {
return `
// Process input ${inputIdx} as labelmap
{
float labelValue = tvalue.${rgba[inputIdx]};
int segmentIndex = int(labelValue * 255.0);
if (segmentIndex > 0) {
float textureCoordinate = float(segmentIndex - 1) / labelOutlineTextureWidth;
float labelmapRow = (float(${labelArrayIdx}) + 0.5) / numLabelmaps;
float thicknessValue = texture2D(labelOutlineThicknessTexture, vec2(textureCoordinate, labelmapRow)).r;
float labelOutlineOpacityValue = texture2D(labelOutlineOpacityTexture, vec2(textureCoordinate, labelmapRow)).r;
int actualThickness = int(thicknessValue * 255.0);
vec3 currentLabelTC = labelTexCoord${inputIdx};
vec3 currentTexelSize = texelSize${inputIdx};
vec3 currentTangent1 = outlineTangent1_${inputIdx};
vec3 currentTangent2 = outlineTangent2_${inputIdx};
bool pixelOnBorder = false;
int labelInputIdx = ${labelArrayIdx};
for (int i = -actualThickness; i <= actualThickness; i++) {
for (int j = -actualThickness; j <= actualThickness; j++) {
if (i == 0 && j == 0) continue;
vec3 neighborTexCoord = currentLabelTC + float(i) * currentTangent1 * currentTexelSize + float(j) * currentTangent2 * currentTexelSize;
if (any(greaterThan(neighborTexCoord, vec3(1.0))) || any(lessThan(neighborTexCoord, vec3(0.0)))) {
pixelOnBorder = true;
break;
}
${textureSampling}
if (neighborLabel != labelValue) {
pixelOnBorder = true;
break;
}
}
if (pixelOnBorder) break;
}
if (pixelOnBorder) {
convergentColor.rgb = mix(convergentColor.rgb, tcolor${inputIdx}.rgb, labelOutlineOpacityValue);
convergentColor.a = max(convergentColor.a, labelOutlineOpacityValue);
} else if (compWeight${inputIdx} > 0.0) {
float fillAlpha = compWeight${inputIdx} * opacity;
convergentColor.rgb = mix(convergentColor.rgb, tcolor${inputIdx}.rgb, fillAlpha);
convergentColor.a = max(convergentColor.a, fillAlpha);
}
}
}`;
}
return `
// Process input ${inputIdx} as background image
{
float bgAlpha = compWeight${inputIdx} * opacity;
convergentColor.rgb = mix(convergentColor.rgb, tcolor${inputIdx}.rgb, bgAlpha);
convergentColor.a = max(convergentColor.a, bgAlpha);
}`;
}).join('\n ');
const labelDesc = labelmapInputs.length > 0 ? `labelmaps at input${labelmapInputs.length > 1 ? 's' : ''} ${labelmapInputs.join(', ')}` : 'no labelmaps';
const bgDesc = backgroundInputs.length > 0 ? `background at input${backgroundInputs.length > 1 ? 's' : ''} ${backgroundInputs.join(', ')}` : 'no background';
return splitStringOnEnter(`
// Multi-texture mode: ${labelDesc}, ${bgDesc}
vec4 convergentColor = vec4(0.0, 0.0, 0.0, 0.0);
// Compute labelmap texture coordinates
${texCoordLines}
// Process each input in order
${processInputs}
gl_FragData[0] = convergentColor;
`);
}
publicAPI.replaceShaderTCoord = (shaders, ren, actor) => {
let VSSource = shaders.Vertex;
const GSSource = shaders.Geometry;
let FSSource = shaders.Fragment;
const useLabelOutline = model.labelOutlineProperties.length > 0;
const slabThickness = model.renderable.getSlabThickness();
VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Dec', []).result;
VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::TCoord::Impl', []).result;
const tNumComp = model.numberOfComponents;
const firstActorPropertyForIComps = actor.getProperty(model.currentValidInputs[0].inputIndex);
const iComps = firstActorPropertyForIComps.getIndependentComponents();
const numInputs = model.scalarTextures.length;
let tcoordFSDec = [`uniform highp sampler3D volumeTexture[${numInputs}];`,
// color shift and scale
'uniform float cshift0;', 'uniform float cscale0;',
// pwf shift and scale
'uniform float pwfshift0;', 'uniform float pwfscale0;',
// color and pwf textures
'uniform sampler2D colorTexture1;', 'uniform sampler2D pwfTexture1;',
// opacity
'uniform float opacity;',
// background color
'uniform vec4 backgroundColor;'];
// Add per-input WCTCMatrix uniforms
for (let i = 0; i < numInputs; i++) {
tcoordFSDec.push(`uniform mat4 WCTCMatrix${i};`);
}
// Add label outline uniforms if enabled
if (useLabelOutline) {
tcoordFSDec = tcoordFSDec.concat(['uniform sampler2D labelOutlineThicknessTexture;', 'uniform sampler2D labelOutlineOpacityTexture;', 'uniform float labelOutlineTextureWidth;', 'uniform float numLabelmaps;']);
// Add per-input tangent vectors and texelSize
for (let i = 0; i < numInputs; i++) {
tcoordFSDec.push(`uniform vec3 outlineTangent1_${i};`);
tcoordFSDec.push(`uniform vec3 outlineTangent2_${i};`);
tcoordFSDec.push(`uniform vec3 texelSize${i};`);
}
}
// Function to sample texture - takes world position, computes per-input texture coords
tcoordFSDec.push('vec4 rawSampleTexture(vec3 worldPos) {');
if (!model.multiTexturePerVolumeEnabled) {
tcoordFSDec.push('vec3 tc0 = (WCTCMatrix0 * vec4(worldPos, 1.0)).xyz;', 'return texture(volumeTexture[0], tc0);', '}');
} else {
tcoordFSDec.push('vec4 rawSample;');
for (let component = 0; component < numInputs; ++component) {
tcoordFSDec.push(`vec3 tc${component} = (WCTCMatrix${component} * vec4(worldPos, 1.0)).xyz;`, `rawSample[${component}] = texture(volumeTexture[${component}], tc${component})[0];`);
}
tcoordFSDec.push('return rawSample;', '}');
}
if (iComps) {
for (let comp = 1; comp < tNumComp; comp++) {
tcoordFSDec = tcoordFSDec.concat([
// color shift and scale
`uniform float cshift${comp};`, `uniform float cscale${comp};`,
// weighting shift and scale
`uniform float pwfshift${comp};`, `uniform float pwfscale${comp};`]);
}
// the heights defined below are the locations
// for the up to four components of the tfuns
// the tfuns have a height of 2XnumComps pixels so the
// values are computed to hit the middle of the two rows
// for that component
switch (tNumComp) {
case 1:
tcoordFSDec = tcoordFSDec.concat(['uniform float mix0;', '#define height0 0.5']);
break;
case 2:
tcoordFSDec = tcoordFSDec.concat(['uniform float mix0;', 'uniform float mix1;', '#define height0 0.25', '#define height1 0.75']);
break;
case 3:
tcoordFSDec = tcoordFSDec.concat(['uniform float mix0;', 'uniform float mix1;', 'uniform float mix2;', '#define height0 0.17', '#define height1 0.5', '#define height2 0.83']);
break;
case 4:
tcoordFSDec = tcoordFSDec.concat(['uniform float mix0;', 'uniform float mix1;', 'uniform float mix2;', 'uniform float mix3;', '#define height0 0.125', '#define height1 0.375', '#define height2 0.625', '#define height3 0.875']);
break;
default:
vtkErrorMacro('Unsupported number of independent coordinates.');
}
}
if (slabThickness > 0.0) {
tcoordFSDec = tcoordFSDec.concat(['uniform vec3 spacing;', 'uniform float slabThickness;', 'uniform int slabType;', 'uniform int slabTrapezoid;', 'uniform vec3 vboScaling;']);
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;', '}']);
}
FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::TCoord::Dec', tcoordFSDec).result;
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);'];
if (slabThickness > 0.0) {
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