UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

884 lines (779 loc) 34.1 kB
import { m as macro } from '../../macros2.js'; import { mat4 } from 'gl-matrix'; import vtkWebGPUFullScreenQuad from './FullScreenQuad.js'; import vtkWebGPUUniformBuffer from './UniformBuffer.js'; import vtkWebGPUShaderCache from './ShaderCache.js'; import vtkWebGPUStorageBuffer from './StorageBuffer.js'; import vtkWebGPUSampler from './Sampler.js'; import vtkWebGPUTypes from './Types.js'; import { BlendMode } from '../Core/VolumeMapper/Constants.js'; const volFragTemplate = ` //VTK::Renderer::Dec //VTK::Mapper::Dec //VTK::TCoord::Dec //VTK::Volume::TraverseDec //VTK::RenderEncoder::Dec //VTK::IOStructs::Dec fn getTextureValue(vTex: texture_3d<f32>, tpos: vec4<f32>) -> f32 { // todo multicomponent support return textureSampleLevel(vTex, clampSampler, tpos.xyz, 0.0).r; } fn getGradient(vTex: texture_3d<f32>, tpos: vec4<f32>, vNum: i32, scalar: f32) -> vec4<f32> { var result: vec4<f32>; var tstep: vec4<f32> = volumeSSBO.values[vNum].tstep; result.x = getTextureValue(vTex, tpos + vec4<f32>(tstep.x, 0.0, 0.0, 1.0)) - scalar; result.y = getTextureValue(vTex, tpos + vec4<f32>(0.0, tstep.y, 0.0, 1.0)) - scalar; result.z = getTextureValue(vTex, tpos + vec4<f32>(0.0, 0.0, tstep.z, 1.0)) - scalar; result.w = 0.0; // divide by spacing as that is our delta result = result / volumeSSBO.values[vNum].spacing; // now we have a gradient in unit tcoords var grad: f32 = length(result.xyz); if (grad > 0.0) { // rotate to View Coords, needed for lighting and shading var nMat: mat4x4<f32> = rendererUBO.SCVCMatrix * volumeSSBO.values[vNum].planeNormals; result = nMat * result; result = result / length(result); } // store gradient magnitude in .w result.w = grad; return result; } fn processVolume(vTex: texture_3d<f32>, vNum: i32, cNum: i32, posSC: vec4<f32>, tfunRows: f32) -> vec4<f32> { var outColor: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 0.0); // convert to tcoords and reject if outside the volume var tpos: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*posSC; if (tpos.x < 0.0 || tpos.y < 0.0 || tpos.z < 0.0 || tpos.x > 1.0 || tpos.y > 1.0 || tpos.z > 1.0) { return outColor; } var scalar: f32 = getTextureValue(vTex, tpos); var coord: vec2<f32> = vec2<f32>(scalar * componentSSBO.values[cNum].cScale + componentSSBO.values[cNum].cShift, (0.5 + 2.0 * f32(vNum)) / tfunRows); var color: vec4<f32> = textureSampleLevel(tfunTexture, clampSampler, coord, 0.0); var gofactor: f32 = 1.0; var normal: vec4<f32> = vec4<f32>(0.0,0.0,0.0,0.0); if (componentSSBO.values[cNum].gomin < 1.0 || volumeSSBO.values[vNum].shade[0] > 0.0) { normal = getGradient(vTex, tpos, vNum, scalar); if (componentSSBO.values[cNum].gomin < 1.0) { gofactor = clamp(normal.a*componentSSBO.values[cNum].goScale + componentSSBO.values[cNum].goShift, componentSSBO.values[cNum].gomin, componentSSBO.values[cNum].gomax); } } coord.x = (scalar * componentSSBO.values[cNum].oScale + componentSSBO.values[cNum].oShift); var opacity: f32 = textureSampleLevel(ofunTexture, clampSampler, coord, 0.0).r; if (volumeSSBO.values[vNum].shade[0] > 0.0) { color = color*abs(normal.z); } outColor = vec4<f32>(color.rgb, gofactor * opacity); return outColor; } // adjust the start and end point of a raycast such that it intersects the unit cube. // This function is used to take a raycast starting point and step vector // and numSteps and return the startijng and ending steps for intersecting the // unit cube. Recall for a 3D texture, the unit cube is the range of texture coordsinates // that have valid values. So this funtion can be used to take a ray in texture coordinates // and bound it to intersecting the texture. // fn adjustBounds(tpos: vec4<f32>, tstep: vec4<f32>, numSteps: f32) -> vec2<f32> { var result: vec2<f32> = vec2<f32>(0.0, numSteps); var tpos2: vec4<f32> = tpos + tstep*numSteps; // move tpos to the start of the volume var adjust: f32 = min( max(tpos.x/tstep.x, (tpos.x - 1.0)/tstep.x), min( max((tpos.y - 1.0)/tstep.y, tpos.y/tstep.y), max((tpos.z - 1.0)/tstep.z, tpos.z/tstep.z))); if (adjust < 0.0) { result.x = result.x - adjust; } // adjust length to the end adjust = max( min(tpos2.x/tstep.x, (tpos2.x - 1.0)/tstep.x), max( min((tpos2.y - 1.0)/tstep.y, tpos2.y/tstep.y), min((tpos2.z - 1.0)/tstep.z, tpos2.z/tstep.z))); if (adjust > 0.0) { result.y = result.y - adjust; } return result; } fn getSimpleColor(scalar: f32, vNum: i32, cNum: i32) -> vec4<f32> { // how many rows (tfuns) do we have in our tfunTexture var tfunRows: f32 = f32(textureDimensions(tfunTexture).y); var coord: vec2<f32> = vec2<f32>(scalar * componentSSBO.values[cNum].cScale + componentSSBO.values[cNum].cShift, (0.5 + 2.0 * f32(vNum)) / tfunRows); var color: vec4<f32> = textureSampleLevel(tfunTexture, clampSampler, coord, 0.0); coord.x = (scalar * componentSSBO.values[cNum].oScale + componentSSBO.values[cNum].oShift); var opacity: f32 = textureSampleLevel(ofunTexture, clampSampler, coord, 0.0).r; return vec4<f32>(color.rgb, opacity); } fn traverseMax(vTex: texture_3d<f32>, vNum: i32, cNum: i32, rayLengthSC: f32, minPosSC: vec4<f32>, rayStepSC: vec4<f32>) { // convert to tcoords and reject if outside the volume var numSteps: f32 = rayLengthSC/mapperUBO.SampleDistance; var tpos: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*minPosSC; var tpos2: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*(minPosSC + rayStepSC); var tstep: vec4<f32> = tpos2 - tpos; var rayBounds: vec2<f32> = adjustBounds(tpos, tstep, numSteps); // did we hit anything if (rayBounds.x >= rayBounds.y) { traverseVals[vNum] = vec4<f32>(0.0,0.0,0.0,0.0); return; } tpos = tpos + tstep*rayBounds.x; var curDist: f32 = rayBounds.x; var maxVal: f32 = -1.0e37; loop { var scalar: f32 = getTextureValue(vTex, tpos); if (scalar > maxVal) { maxVal = scalar; } // increment position curDist = curDist + 1.0; tpos = tpos + tstep; // check if we have reached a terminating condition if (curDist > rayBounds.y) { break; } } // process to get the color and opacity traverseVals[vNum] = getSimpleColor(maxVal, vNum, cNum); } fn traverseMin(vTex: texture_3d<f32>, vNum: i32, cNum: i32, rayLengthSC: f32, minPosSC: vec4<f32>, rayStepSC: vec4<f32>) { // convert to tcoords and reject if outside the volume var numSteps: f32 = rayLengthSC/mapperUBO.SampleDistance; var tpos: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*minPosSC; var tpos2: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*(minPosSC + rayStepSC); var tstep: vec4<f32> = tpos2 - tpos; var rayBounds: vec2<f32> = adjustBounds(tpos, tstep, numSteps); // did we hit anything if (rayBounds.x >= rayBounds.y) { traverseVals[vNum] = vec4<f32>(0.0,0.0,0.0,0.0); return; } tpos = tpos + tstep*rayBounds.x; var curDist: f32 = rayBounds.x; var minVal: f32 = 1.0e37; loop { var scalar: f32 = getTextureValue(vTex, tpos); if (scalar < minVal) { minVal = scalar; } // increment position curDist = curDist + 1.0; tpos = tpos + tstep; // check if we have reached a terminating condition if (curDist > rayBounds.y) { break; } } // process to get the color and opacity traverseVals[vNum] = getSimpleColor(minVal, vNum, cNum); } fn traverseAverage(vTex: texture_3d<f32>, vNum: i32, cNum: i32, rayLengthSC: f32, minPosSC: vec4<f32>, rayStepSC: vec4<f32>) { // convert to tcoords and reject if outside the volume var numSteps: f32 = rayLengthSC/mapperUBO.SampleDistance; var tpos: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*minPosSC; var tpos2: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*(minPosSC + rayStepSC); var tstep: vec4<f32> = tpos2 - tpos; var rayBounds: vec2<f32> = adjustBounds(tpos, tstep, numSteps); // did we hit anything if (rayBounds.x >= rayBounds.y) { traverseVals[vNum] = vec4<f32>(0.0,0.0,0.0,0.0); return; } let ipRange: vec4<f32> = volumeSSBO.values[vNum].ipScalarRange; tpos = tpos + tstep*rayBounds.x; var curDist: f32 = rayBounds.x; var avgVal: f32 = 0.0; var sampleCount: f32 = 0.0; loop { var sample: f32 = getTextureValue(vTex, tpos); // right now leave filtering off until WebGL changes get merged // if (ipRange.z == 0.0 || sample >= ipRange.x && sample <= ipRange.y) // { avgVal = avgVal + sample; sampleCount = sampleCount + 1.0; // } // increment position curDist = curDist + 1.0; tpos = tpos + tstep; // check if we have reached a terminating condition if (curDist > rayBounds.y) { break; } } if (sampleCount <= 0.0) { traverseVals[vNum] = vec4<f32>(0.0,0.0,0.0,0.0); } // process to get the color and opacity traverseVals[vNum] = getSimpleColor(avgVal/sampleCount, vNum, cNum); } fn traverseAdditive(vTex: texture_3d<f32>, vNum: i32, cNum: i32, rayLengthSC: f32, minPosSC: vec4<f32>, rayStepSC: vec4<f32>) { // convert to tcoords and reject if outside the volume var numSteps: f32 = rayLengthSC/mapperUBO.SampleDistance; var tpos: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*minPosSC; var tpos2: vec4<f32> = volumeSSBO.values[vNum].SCTCMatrix*(minPosSC + rayStepSC); var tstep: vec4<f32> = tpos2 - tpos; var rayBounds: vec2<f32> = adjustBounds(tpos, tstep, numSteps); // did we hit anything if (rayBounds.x >= rayBounds.y) { traverseVals[vNum] = vec4<f32>(0.0,0.0,0.0,0.0); return; } let ipRange: vec4<f32> = volumeSSBO.values[vNum].ipScalarRange; tpos = tpos + tstep*rayBounds.x; var curDist: f32 = rayBounds.x; var sumVal: f32 = 0.0; loop { var sample: f32 = getTextureValue(vTex, tpos); // right now leave filtering off until WebGL changes get merged // if (ipRange.z == 0.0 || sample >= ipRange.x && sample <= ipRange.y) // { sumVal = sumVal + sample; // } // increment position curDist = curDist + 1.0; tpos = tpos + tstep; // check if we have reached a terminating condition if (curDist > rayBounds.y) { break; } } // process to get the color and opacity traverseVals[vNum] = getSimpleColor(sumVal, vNum, cNum); } fn composite(rayLengthSC: f32, minPosSC: vec4<f32>, rayStepSC: vec4<f32>) -> vec4<f32> { // initial ray position is at the beginning var rayPosSC: vec4<f32> = minPosSC; // how many rows (tfuns) do we have in our tfunTexture var tfunRows: f32 = f32(textureDimensions(tfunTexture).y); var curDist: f32 = 0.0; var computedColor: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 0.0); var sampleColor: vec4<f32>; //VTK::Volume::TraverseCalls loop { // for each volume, sample and accumulate color //VTK::Volume::CompositeCalls // increment position curDist = curDist + mapperUBO.SampleDistance; rayPosSC = rayPosSC + rayStepSC; // check if we have reached a terminating condition if (curDist > rayLengthSC) { break; } if (computedColor.a > 0.98) { break; } } return computedColor; } @fragment fn main( //VTK::IOStructs::Input ) //VTK::IOStructs::Output { var output: fragmentOutput; var rayMax: f32 = textureSampleLevel(maxTexture, clampSampler, input.tcoordVS, 0.0).r; var rayMin: f32 = textureSampleLevel(minTexture, clampSampler, input.tcoordVS, 0.0).r; // discard empty rays if (rayMax <= rayMin) { discard; } else { // compute start and end ray positions in view coordinates var minPosSC: vec4<f32> = rendererUBO.PCSCMatrix*vec4<f32>(2.0 * input.tcoordVS.x - 1.0, 1.0 - 2.0 * input.tcoordVS.y, rayMax, 1.0); minPosSC = minPosSC * (1.0 / minPosSC.w); var maxPosSC: vec4<f32> = rendererUBO.PCSCMatrix*vec4<f32>(2.0 * input.tcoordVS.x - 1.0, 1.0 - 2.0 * input.tcoordVS.y, rayMin, 1.0); maxPosSC = maxPosSC * (1.0 / maxPosSC.w); var rayLengthSC: f32 = distance(minPosSC.xyz, maxPosSC.xyz); var rayStepSC: vec4<f32> = (maxPosSC - minPosSC)*(mapperUBO.SampleDistance/rayLengthSC); rayStepSC.w = 0.0; var computedColor: vec4<f32>; //VTK::Volume::Loop //VTK::RenderEncoder::Impl } return output; } `; const tmpMat4 = new Float64Array(16); const tmp2Mat4 = new Float64Array(16); // ---------------------------------------------------------------------------- // vtkWebGPUVolumePassFSQ methods // ---------------------------------------------------------------------------- function vtkWebGPUVolumePassFSQ(publicAPI, model) { // Set our className model.classHierarchy.push('vtkWebGPUVolumePassFSQ'); publicAPI.replaceShaderPosition = (hash, pipeline, vertexInput) => { const vDesc = pipeline.getShaderDescription('vertex'); vDesc.addBuiltinOutput('vec4<f32>', '@builtin(position) Position'); let code = vDesc.getCode(); code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', ['output.tcoordVS = vec2<f32>(vertexBC.x * 0.5 + 0.5, 1.0 - vertexBC.y * 0.5 - 0.5);', 'output.Position = vec4<f32>(vertexBC, 1.0);']).result; vDesc.setCode(code); const fDesc = pipeline.getShaderDescription('fragment'); fDesc.addBuiltinInput('vec4<f32>', '@builtin(position) fragPos'); }; model.shaderReplacements.set('replaceShaderPosition', publicAPI.replaceShaderPosition); publicAPI.replaceShaderVolume = (hash, pipeline, vertexInput) => { const fDesc = pipeline.getShaderDescription('fragment'); let code = fDesc.getCode(); const compositeCalls = []; const traverseCalls = []; for (let i = 0; i < model.volumes.length; i++) { // todo pass rowPos const blendMode = model.volumes[i].getRenderable().getMapper().getBlendMode(); if (blendMode === BlendMode.COMPOSITE_BLEND) { compositeCalls.push(` sampleColor = processVolume(volTexture${i}, ${i}, ${model.rowStarts[i]}, rayPosSC, tfunRows);`); compositeCalls.push(` computedColor = vec4<f32>( sampleColor.a * sampleColor.rgb * (1.0 - computedColor.a) + computedColor.rgb, (1.0 - computedColor.a)*sampleColor.a + computedColor.a);`); } else { traverseCalls.push(` sampleColor = traverseVals[${i}];`); traverseCalls.push(` computedColor = vec4<f32>( sampleColor.a * sampleColor.rgb * (1.0 - computedColor.a) + computedColor.rgb, (1.0 - computedColor.a)*sampleColor.a + computedColor.a);`); } } code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::CompositeCalls', compositeCalls).result; code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::TraverseCalls', traverseCalls).result; code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::TraverseDec', [`var<private> traverseVals: array<vec4<f32>,${model.volumes.length}>;`]).result; // call the full and partial methods as needed let compositeWhileTraversing = false; for (let vidx = 0; vidx < model.volumes.length; vidx++) { const blendMode = model.volumes[vidx].getRenderable().getMapper().getBlendMode(); if (blendMode === BlendMode.COMPOSITE_BLEND) { compositeWhileTraversing = true; } else if (blendMode === BlendMode.MAXIMUM_INTENSITY_BLEND) { code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::Loop', [` traverseMax(volTexture${vidx}, ${vidx}, ${vidx}, rayLengthSC, minPosSC, rayStepSC);`, ` computedColor = traverseVals[${vidx}];`, '//VTK::Volume::Loop']).result; } else if (blendMode === BlendMode.MINIMUM_INTENSITY_BLEND) { code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::Loop', [` traverseMin(volTexture${vidx}, ${vidx}, ${vidx}, rayLengthSC, minPosSC, rayStepSC);`, ` computedColor = traverseVals[${vidx}];`, '//VTK::Volume::Loop']).result; } else if (blendMode === BlendMode.AVERAGE_INTENSITY_BLEND) { code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::Loop', [` traverseAverage(volTexture${vidx}, ${vidx}, ${vidx}, rayLengthSC, minPosSC, rayStepSC);`, ` computedColor = traverseVals[${vidx}];`, '//VTK::Volume::Loop']).result; } else if (blendMode === BlendMode.ADDITIVE_INTENSITY_BLEND) { code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::Loop', [` traverseAdditive(volTexture${vidx}, ${vidx}, ${vidx}, rayLengthSC, minPosSC, rayStepSC);`, ` computedColor = traverseVals[${vidx}];`, '//VTK::Volume::Loop']).result; } } if (compositeWhileTraversing) { code = vtkWebGPUShaderCache.substitute(code, '//VTK::Volume::Loop', [' computedColor = composite(rayLengthSC, minPosSC, rayStepSC);']).result; } fDesc.setCode(code); }; model.shaderReplacements.set('replaceShaderVolume', publicAPI.replaceShaderVolume); publicAPI.updateLUTImage = device => { // depends on // - volumes array (length and values) - mtime // - tfun arrays - renderable/property mtime let mtime = publicAPI.getMTime(); for (let i = 0; i < model.volumes.length; i++) { const vol = model.volumes[i].getRenderable(); const image = vol.getMapper().getInputData(); mtime = Math.max(mtime, vol.getMTime(), image.getMTime()); } if (mtime < model.lutBuildTime.getMTime()) { return; } // first determine how large the image should be model.numRows = 0; model.rowStarts = []; for (let vidx = 0; vidx < model.volumes.length; vidx++) { model.rowStarts.push(model.numRows); const webgpuvol = model.volumes[vidx]; const actor = webgpuvol.getRenderable(); const volMapr = actor.getMapper(); const vprop = actor.getProperty(); const image = volMapr.getInputData(); const scalars = image.getPointData() && image.getPointData().getScalars(); const numComp = scalars.getNumberOfComponents(); const iComps = vprop.getIndependentComponents(); const numIComps = iComps ? numComp : 1; model.numRows += numIComps; } // allocate the image array const colorArray = new Uint8ClampedArray(model.numRows * 2 * model.rowLength * 4); const opacityArray = new Float32Array(model.numRows * 2 * model.rowLength); let imgRow = 0; const tmpTable = new Float32Array(model.rowLength * 3); const rowLength = model.rowLength; for (let vidx = 0; vidx < model.volumes.length; vidx++) { const webgpuvol = model.volumes[vidx]; const actor = webgpuvol.getRenderable(); const volMapr = actor.getMapper(); const vprop = actor.getProperty(); const image = volMapr.getInputData(); const scalars = image.getPointData() && image.getPointData().getScalars(); const numComp = scalars.getNumberOfComponents(); const iComps = vprop.getIndependentComponents(); const numIComps = iComps ? numComp : 1; for (let c = 0; c < numIComps; ++c) { const cfun = vprop.getRGBTransferFunction(c); const cRange = cfun.getRange(); cfun.getTable(cRange[0], cRange[1], rowLength, tmpTable, 1); let ioffset = imgRow * rowLength * 4; for (let i = 0; i < rowLength; ++i) { colorArray[ioffset + i * 4] = 255.0 * tmpTable[i * 3]; colorArray[ioffset + i * 4 + 1] = 255.0 * tmpTable[i * 3 + 1]; colorArray[ioffset + i * 4 + 2] = 255.0 * tmpTable[i * 3 + 2]; colorArray[ioffset + i * 4 + 3] = 255.0; for (let co = 0; co < 4; co++) { colorArray[ioffset + (rowLength + i) * 4 + co] = colorArray[ioffset + i * 4 + co]; } } const ofun = vprop.getScalarOpacity(c); const opacityFactor = model.sampleDist / vprop.getScalarOpacityUnitDistance(c); const oRange = ofun.getRange(); ofun.getTable(oRange[0], oRange[1], rowLength, tmpTable, 1); // adjust for sample distance etc ioffset = imgRow * rowLength; for (let i = 0; i < rowLength; ++i) { opacityArray[ioffset + i] = 1.0 - (1.0 - tmpTable[i]) ** opacityFactor; opacityArray[ioffset + i + rowLength] = opacityArray[ioffset + i]; } imgRow += 2; } } { const treq = { nativeArray: colorArray, width: model.rowLength, height: model.numRows * 2, depth: 1, format: 'rgba8unorm' }; const newTex = device.getTextureManager().getTexture(treq); const tview = newTex.createView('tfunTexture'); model.textureViews[2] = tview; } { const treq = { nativeArray: opacityArray, width: model.rowLength, height: model.numRows * 2, depth: 1, format: 'r16float' }; const newTex = device.getTextureManager().getTexture(treq); const tview = newTex.createView('ofunTexture'); model.textureViews[3] = tview; } model.lutBuildTime.modified(); }; publicAPI.updateSSBO = device => { // if any of // - color or opacity tfun ranges changed - volume Mtime // - any volume matrix changed - volume MTime // - stabilized center changed - ren.stabilizedMTime // - any volume's input data worldtoindex or dimensions changed - input's mtime // let mtime = Math.max(publicAPI.getMTime(), model.WebGPURenderer.getStabilizedTime()); for (let i = 0; i < model.volumes.length; i++) { const vol = model.volumes[i].getRenderable(); const volMapr = vol.getMapper(); const image = volMapr.getInputData(); mtime = Math.max(mtime, vol.getMTime(), image.getMTime(), volMapr.getMTime()); } if (mtime < model.SSBO.getSendTime()) { return; } // create the volumeSBBO const center = model.WebGPURenderer.getStabilizedCenterByReference(); model.SSBO.clearData(); model.SSBO.setNumberOfInstances(model.volumes.length); // create SCTC matrices SC -> world -> model -> index -> tcoord // // when doing coord conversions from A to C recall // the order is mat4.mult(AtoC, BtoC, AtoB); // const marray = new Float64Array(model.volumes.length * 16); const vPlaneArray = new Float64Array(model.volumes.length * 16); const tstepArray = new Float64Array(model.volumes.length * 4); const shadeArray = new Float64Array(model.volumes.length * 4); const spacingArray = new Float64Array(model.volumes.length * 4); const ipScalarRangeArray = new Float64Array(model.volumes.length * 4); for (let vidx = 0; vidx < model.volumes.length; vidx++) { const webgpuvol = model.volumes[vidx]; const actor = webgpuvol.getRenderable(); const volMapr = actor.getMapper(); const image = volMapr.getInputData(); mat4.identity(tmpMat4); mat4.translate(tmpMat4, tmpMat4, center); // tmpMat4 is now SC->World const mcwcmat = actor.getMatrix(); mat4.transpose(tmp2Mat4, mcwcmat); mat4.invert(tmp2Mat4, tmp2Mat4); // tmp2Mat4 is now world to model mat4.multiply(tmpMat4, tmp2Mat4, tmpMat4); // tmp4Mat is now SC->Model // the method on the data is world to index but the volume is in // model coordinates so really in this context it is model to index const modelToIndex = image.getWorldToIndex(); mat4.multiply(tmpMat4, modelToIndex, tmpMat4); // tmpMat4 is now SC -> Index const dims = image.getDimensions(); mat4.identity(tmp2Mat4); mat4.scale(tmp2Mat4, tmp2Mat4, [1.0 / dims[0], 1.0 / dims[1], 1.0 / dims[2]]); mat4.multiply(tmpMat4, tmp2Mat4, tmpMat4); // tmpMat4 is now SC -> Tcoord for (let j = 0; j < 16; j++) { marray[vidx * 16 + j] = tmpMat4[j]; } mat4.invert(tmpMat4, tmpMat4); // now it is Tcoord To SC for (let j = 0; j < 4; j++) { vPlaneArray[vidx * 16 + j * 4] = tmpMat4[j * 4]; vPlaneArray[vidx * 16 + j * 4 + 1] = tmpMat4[j * 4 + 1]; vPlaneArray[vidx * 16 + j * 4 + 2] = tmpMat4[j * 4 + 2]; vPlaneArray[vidx * 16 + j * 4 + 3] = 0.0; } tstepArray[vidx * 4] = 1.0 / dims[0]; tstepArray[vidx * 4 + 1] = 1.0 / dims[1]; tstepArray[vidx * 4 + 2] = 1.0 / dims[2]; tstepArray[vidx * 4 + 3] = 1.0; shadeArray[vidx * 4] = actor.getProperty().getShade() ? 1.0 : 0.0; const spacing = image.getSpacing(); spacingArray[vidx * 4] = spacing[0]; spacingArray[vidx * 4 + 1] = spacing[1]; spacingArray[vidx * 4 + 2] = spacing[2]; spacingArray[vidx * 4 + 3] = 1.0; // handle filteringMode const tScale = model.textureViews[vidx + 4].getTexture().getScale(); const ipScalarRange = actor.getProperty().getIpScalarRange(); ipScalarRangeArray[vidx * 4] = ipScalarRange[0] / tScale; ipScalarRangeArray[vidx * 4 + 1] = ipScalarRange[1] / tScale; ipScalarRangeArray[vidx * 4 + 2] = actor.getProperty().getFilterMode(); } model.SSBO.addEntry('SCTCMatrix', 'mat4x4<f32>'); model.SSBO.addEntry('planeNormals', 'mat4x4<f32>'); model.SSBO.addEntry('shade', 'vec4<f32>'); model.SSBO.addEntry('tstep', 'vec4<f32>'); model.SSBO.addEntry('spacing', 'vec4<f32>'); model.SSBO.addEntry('ipScalarRange', 'vec4<f32>'); model.SSBO.setAllInstancesFromArray('SCTCMatrix', marray); model.SSBO.setAllInstancesFromArray('planeNormals', vPlaneArray); model.SSBO.setAllInstancesFromArray('shade', shadeArray); model.SSBO.setAllInstancesFromArray('tstep', tstepArray); model.SSBO.setAllInstancesFromArray('spacing', spacingArray); model.SSBO.setAllInstancesFromArray('ipScalarRange', ipScalarRangeArray); model.SSBO.send(device); // now create the componentSSBO model.componentSSBO.clearData(); model.componentSSBO.setNumberOfInstances(model.numRows); const cScaleArray = new Float64Array(model.numRows); const cShiftArray = new Float64Array(model.numRows); const oScaleArray = new Float64Array(model.numRows); const oShiftArray = new Float64Array(model.numRows); const gominArray = new Float64Array(model.numRows); const gomaxArray = new Float64Array(model.numRows); const goshiftArray = new Float64Array(model.numRows); const goscaleArray = new Float64Array(model.numRows); let rowIdx = 0; for (let vidx = 0; vidx < model.volumes.length; vidx++) { const webgpuvol = model.volumes[vidx]; const actor = webgpuvol.getRenderable(); const volMapr = actor.getMapper(); const vprop = actor.getProperty(); const image = volMapr.getInputData(); const scalars = image.getPointData() && image.getPointData().getScalars(); const numComp = scalars.getNumberOfComponents(); const iComps = vprop.getIndependentComponents(); // const numIComps = iComps ? numComp : 1; // half float? const tformat = model.textureViews[vidx + 4].getTexture().getFormat(); const tDetails = vtkWebGPUTypes.getDetailsFromTextureFormat(tformat); const halfFloat = tDetails.elementSize === 2 && tDetails.sampleType === 'float'; const volInfo = { scale: [255.0], offset: [0.0] }; if (halfFloat) { volInfo.scale[0] = 1.0; } // three levels of shift scale combined into one // for performance in the fragment shader for (let compIdx = 0; compIdx < numComp; compIdx++) { const target = iComps ? compIdx : 0; const sscale = volInfo.scale[compIdx]; const ofun = vprop.getScalarOpacity(target); const oRange = ofun.getRange(); const oscale = sscale / (oRange[1] - oRange[0]); const oshift = (volInfo.offset[compIdx] - oRange[0]) / (oRange[1] - oRange[0]); oShiftArray[rowIdx] = oshift; oScaleArray[rowIdx] = oscale; const cfun = vprop.getRGBTransferFunction(target); const cRange = cfun.getRange(); cShiftArray[rowIdx] = (volInfo.offset[compIdx] - cRange[0]) / (cRange[1] - cRange[0]); cScaleArray[rowIdx] = sscale / (cRange[1] - cRange[0]); // todo sscale for dependent should be based off of the A channel? // not target (which is 0 in that case) const useGO = vprop.getUseGradientOpacity(target); if (useGO) { const gomin = vprop.getGradientOpacityMinimumOpacity(target); const gomax = vprop.getGradientOpacityMaximumOpacity(target); gominArray[rowIdx] = gomin; gomaxArray[rowIdx] = gomax; const goRange = [vprop.getGradientOpacityMinimumValue(target), vprop.getGradientOpacityMaximumValue(target)]; goscaleArray[rowIdx] = sscale * (gomax - gomin) / (goRange[1] - goRange[0]); goshiftArray[rowIdx] = -goRange[0] * (gomax - gomin) / (goRange[1] - goRange[0]) + gomin; } else { gominArray[rowIdx] = 1.0; gomaxArray[rowIdx] = 1.0; goscaleArray[rowIdx] = 0.0; goshiftArray[rowIdx] = 1.0; } rowIdx++; } } model.componentSSBO.addEntry('cScale', 'f32'); model.componentSSBO.addEntry('cShift', 'f32'); model.componentSSBO.addEntry('oScale', 'f32'); model.componentSSBO.addEntry('oShift', 'f32'); model.componentSSBO.addEntry('goShift', 'f32'); model.componentSSBO.addEntry('goScale', 'f32'); model.componentSSBO.addEntry('gomin', 'f32'); model.componentSSBO.addEntry('gomax', 'f32'); model.componentSSBO.setAllInstancesFromArray('cScale', cScaleArray); model.componentSSBO.setAllInstancesFromArray('cShift', cShiftArray); model.componentSSBO.setAllInstancesFromArray('oScale', oScaleArray); model.componentSSBO.setAllInstancesFromArray('oShift', oShiftArray); model.componentSSBO.setAllInstancesFromArray('goScale', goscaleArray); model.componentSSBO.setAllInstancesFromArray('goShift', goshiftArray); model.componentSSBO.setAllInstancesFromArray('gomin', gominArray); model.componentSSBO.setAllInstancesFromArray('gomax', gomaxArray); model.componentSSBO.send(device); }; const superClassUpdateBuffers = publicAPI.updateBuffers; publicAPI.updateBuffers = () => { superClassUpdateBuffers(); // compute the min step size let sampleDist = model.volumes[0].getRenderable().getMapper().getSampleDistance(); for (let i = 0; i < model.volumes.length; i++) { const vol = model.volumes[i]; const volMapr = vol.getRenderable().getMapper(); const sd = volMapr.getSampleDistance(); if (sd < sampleDist) { sampleDist = sd; } } if (model.sampleDist !== sampleDist) { model.sampleDist = sampleDist; model.UBO.setValue('SampleDistance', sampleDist); model.UBO.sendIfNeeded(model.device); } // add in 3d volume textures for (let vidx = 0; vidx < model.volumes.length; vidx++) { const webgpuvol = model.volumes[vidx]; const actor = webgpuvol.getRenderable(); const volMapr = actor.getMapper(); const image = volMapr.getInputData(); const newTex = model.device.getTextureManager().getTextureForImageData(image); if (!model.textureViews[vidx + 4] || model.textureViews[vidx + 4].getTexture() !== newTex) { const tview = newTex.createView(`volTexture${vidx}`); model.textureViews[vidx + 4] = tview; } } // clear any old leftovers if (model.volumes.length < model.lastVolumeLength) { // we may have gaps in the array right now so no splice for (let i = model.volumes.length; i < model.lastVolumeLength; i++) { model.textureViews.pop(); } } model.lastVolumeLength = model.volumes.length; publicAPI.updateLUTImage(model.device); publicAPI.updateSSBO(model.device); if (!model.clampSampler) { model.clampSampler = vtkWebGPUSampler.newInstance({ label: 'clampSampler' }); model.clampSampler.create(model.device, { minFilter: 'linear', magFilter: 'linear' }); } }; publicAPI.computePipelineHash = () => { model.pipelineHash = 'volfsq'; for (let vidx = 0; vidx < model.volumes.length; vidx++) { const blendMode = model.volumes[vidx].getRenderable().getMapper().getBlendMode(); model.pipelineHash += `${blendMode}`; } }; // marks modified when needed publicAPI.setVolumes = val => { if (!model.volumes || model.volumes.length !== val.length) { model.volumes = [...val]; publicAPI.modified(); return; } for (let i = 0; i < val.length; i++) { if (val[i] !== model.volumes[i]) { model.volumes = [...val]; publicAPI.modified(); return; } } }; const superclassGetBindables = publicAPI.getBindables; publicAPI.getBindables = () => { const bindables = superclassGetBindables(); bindables.push(model.componentSSBO); bindables.push(model.clampSampler); return bindables; }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { volumes: null, rowLength: 1024, lastVolumeLength: 0 }; // ---------------------------------------------------------------------------- function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues); // Inheritance vtkWebGPUFullScreenQuad.extend(publicAPI, model, initialValues); model.fragmentShaderTemplate = volFragTemplate; model.UBO = vtkWebGPUUniformBuffer.newInstance({ label: 'mapperUBO' }); model.UBO.addEntry('SampleDistance', 'f32'); model.SSBO = vtkWebGPUStorageBuffer.newInstance({ label: 'volumeSSBO' }); model.componentSSBO = vtkWebGPUStorageBuffer.newInstance({ label: 'componentSSBO' }); model.lutBuildTime = {}; macro.obj(model.lutBuildTime, { mtime: 0 }); // Object methods vtkWebGPUVolumePassFSQ(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkWebGPUVolumePassFSQ'); // ---------------------------------------------------------------------------- var vtkWebGPUVolumePassFSQ$1 = { newInstance, extend }; export { vtkWebGPUVolumePassFSQ$1 as default, extend, newInstance };