UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

663 lines (606 loc) 22.7 kB
import { m as macro } from '../../macros2.js'; import vtkPolyData from '../../Common/DataModel/PolyData.js'; import vtkProperty from '../Core/Property.js'; import vtkRenderPass from '../SceneGraph/RenderPass.js'; import vtkWebGPUBufferManager from './BufferManager.js'; import vtkWebGPUSimpleMapper from './SimpleMapper.js'; import vtkWebGPURenderEncoder from './RenderEncoder.js'; import vtkWebGPUShaderCache from './ShaderCache.js'; import vtkWebGPUTexture from './Texture.js'; import vtkWebGPUUniformBuffer from './UniformBuffer.js'; import vtkWebGPUFullScreenQuad from './FullScreenQuad.js'; import vtkWebGPUVolumePassFSQ from './VolumePassFSQ.js'; import { f as distance2BetweenPoints } from '../../Common/Core/Math/index.js'; const { Representation } = vtkProperty; const { BufferUsage, PrimitiveTypes } = vtkWebGPUBufferManager; // The volume rendering pass consists of two sub passes. The first // (depthRange) renders polygonal cubes for the volumes to compute min and // max bounds in depth for the image. This is then fed into the second pass // (final) which actually does the raycasting between those bounds sampling // the volumes along the way. So the first pass tends to be very fast whicle // the second is where most of the work is done. // given x then y then z ordering // // 2-----3 // / | / | // 6-----7 | // | | | | // | 0-----1 // |/ |/ // 4-----5 // const cubeFaceTriangles = [[0, 4, 6], [0, 6, 2], [1, 3, 7], [1, 7, 5], [0, 5, 4], [0, 1, 5], [2, 6, 7], [2, 7, 3], [0, 3, 1], [0, 2, 3], [4, 5, 7], [4, 7, 6]]; const DepthBoundsFS = ` //VTK::Renderer::Dec //VTK::Select::Dec //VTK::VolumePass::Dec //VTK::TCoord::Dec //VTK::RenderEncoder::Dec //VTK::Mapper::Dec //VTK::IOStructs::Dec @fragment fn main( //VTK::IOStructs::Input ) //VTK::IOStructs::Output { var output : fragmentOutput; //VTK::Select::Impl //VTK::TCoord::Impl //VTK::VolumePass::Impl // use the maximum (closest) of the current value and the zbuffer // the blend func will then take the min to find the farthest stop value var stopval: f32 = max(input.fragPos.z, textureLoad(opaquePassDepthTexture, vec2<i32>(i32(input.fragPos.x), i32(input.fragPos.y)), 0)); //VTK::RenderEncoder::Impl return output; } `; const volumeCopyFragTemplate = ` //VTK::Renderer::Dec //VTK::Mapper::Dec //VTK::TCoord::Dec //VTK::RenderEncoder::Dec //VTK::IOStructs::Dec @fragment fn main( //VTK::IOStructs::Input ) //VTK::IOStructs::Output { var output: fragmentOutput; var computedColor: vec4<f32> = textureSample(volumePassColorTexture, volumePassColorTextureSampler, mapperUBO.tscale*input.tcoordVS); //VTK::RenderEncoder::Impl return output; } `; /* eslint-disable no-undef */ /* eslint-disable no-bitwise */ // ---------------------------------------------------------------------------- function vtkWebGPUVolumePass(publicAPI, model) { // Set our className model.classHierarchy.push('vtkWebGPUVolumePass'); // create the required textures, encoders, FSQ etc publicAPI.initialize = viewNode => { if (!model._clearEncoder) { publicAPI.createClearEncoder(viewNode); } if (!model._mergeEncoder) { publicAPI.createMergeEncoder(viewNode); } if (!model._copyEncoder) { publicAPI.createCopyEncoder(viewNode); } if (!model._depthRangeEncoder) { publicAPI.createDepthRangeEncoder(viewNode); } if (!model.fullScreenQuad) { model.fullScreenQuad = vtkWebGPUVolumePassFSQ.newInstance(); model.fullScreenQuad.setDevice(viewNode.getDevice()); model.fullScreenQuad.setTextureViews([...model._depthRangeEncoder.getColorTextureViews()]); } if (!model._volumeCopyQuad) { model._volumeCopyQuad = vtkWebGPUFullScreenQuad.newInstance(); model._volumeCopyQuad.setPipelineHash('volpassfsq'); model._volumeCopyQuad.setDevice(viewNode.getDevice()); model._volumeCopyQuad.setFragmentShaderTemplate(volumeCopyFragTemplate); model._copyUBO = vtkWebGPUUniformBuffer.newInstance({ label: 'mapperUBO' }); model._copyUBO.addEntry('tscale', 'vec2<f32>'); model._volumeCopyQuad.setUBO(model._copyUBO); model._volumeCopyQuad.setTextureViews([model._colorTextureView]); } }; publicAPI.traverse = (renNode, viewNode) => { if (model.deleted) { return; } // we just render our delegates in order model._currentParent = viewNode; // create stuff we need publicAPI.initialize(viewNode); // determine if we are rendering a small size publicAPI.computeTiming(viewNode); // first render the boxes to generate a min max depth // map for all the volumes publicAPI.renderDepthBounds(renNode, viewNode); // always mark true model._firstGroup = true; const device = viewNode.getDevice(); // determine how many volumes we can render at a time. We subtract // 4 because we use know we use textures for min, max, ofun and tfun const maxVolumes = device.getHandle().limits.maxSampledTexturesPerShaderStage - 4; // if we have to make multiple passes then break the volumes up into groups // rendered from farthest to closest if (model.volumes.length > maxVolumes) { const cameraPos = renNode.getRenderable().getActiveCamera().getPosition(); // sort from back to front based on volume centroid const distances = []; for (let v = 0; v < model.volumes.length; v++) { const bounds = model.volumes[v].getRenderable().getBounds(); const centroid = [0.5 * (bounds[1] + bounds[0]), 0.5 * (bounds[3] + bounds[2]), 0.5 * (bounds[5] + bounds[4])]; distances[v] = distance2BetweenPoints(centroid, cameraPos); } // sort by distance const volumeOrder = [...Array(model.volumes.length).keys()]; volumeOrder.sort((a, b) => distances[b] - distances[a]); // render in chunks back to front let volumesToRender = []; // start with smallest chunk so that the last (closest) chunk // has a full maxVolumes; let chunkSize = volumeOrder.length % maxVolumes; for (let v = 0; v < volumeOrder.length; v++) { volumesToRender.push(model.volumes[volumeOrder[v]]); if (volumesToRender.length >= chunkSize) { publicAPI.rayCastPass(viewNode, renNode, volumesToRender); volumesToRender = []; chunkSize = maxVolumes; model._firstGroup = false; } } } else { // if not rendering in chunks then just draw all of them at once publicAPI.rayCastPass(viewNode, renNode, model.volumes); } // copy back to the original color buffer // final composite model._volumeCopyQuad.setWebGPURenderer(renNode); if (model._useSmallViewport) { const width = model._colorTextureView.getTexture().getWidth(); const height = model._colorTextureView.getTexture().getHeight(); model._copyUBO.setArray('tscale', [model._smallViewportWidth / width, model._smallViewportHeight / height]); } else { model._copyUBO.setArray('tscale', [1.0, 1.0]); } model._copyUBO.sendIfNeeded(device); model._copyEncoder.setColorTextureView(0, model.colorTextureView); model._copyEncoder.attachTextureViews(); model._copyEncoder.begin(viewNode.getCommandEncoder()); renNode.scissorAndViewport(model._copyEncoder); model._volumeCopyQuad.prepareAndDraw(model._copyEncoder); model._copyEncoder.end(); }; // unsubscribe from our listeners publicAPI.delete = macro.chain(() => { if (model._animationRateSubscription) { model._animationRateSubscription.unsubscribe(); model._animationRateSubscription = null; } }, publicAPI.delete); publicAPI.computeTiming = viewNode => { const rwi = viewNode.getRenderable().getInteractor(); if (model._lastScale == null) { const firstMapper = model.volumes[0].getRenderable().getMapper(); model._lastScale = firstMapper.getInitialInteractionScale() || 1.0; } model._useSmallViewport = false; if (rwi.isAnimating() && model._lastScale > 1.5) { model._useSmallViewport = true; } model._colorTexture.resize(viewNode.getCanvas().width, viewNode.getCanvas().height); if (!model._animationRateSubscription) { // when the animation frame rate changes recompute the scale factor model._animationRateSubscription = rwi.onAnimationFrameRateUpdate(() => { const firstMapper = model.volumes[0].getRenderable().getMapper(); if (firstMapper.getAutoAdjustSampleDistances()) { const frate = rwi.getRecentAnimationFrameRate(); const targetScale = model._lastScale * rwi.getDesiredUpdateRate() / frate; model._lastScale = targetScale; // clamp scale to some reasonable values. // Below 1.5 we will just be using full resolution as that is close enough // Above 400 seems like a lot so we limit to that 1/20th per axis if (model._lastScale > 400) { model._lastScale = 400; } } else { model._lastScale = firstMapper.getImageSampleDistance() * firstMapper.getImageSampleDistance(); } if (model._lastScale < 1.5) { model._lastScale = 1.5; } }); } }; publicAPI.rayCastPass = (viewNode, renNode, volumes) => { const encoder = model._firstGroup ? model._clearEncoder : model._mergeEncoder; encoder.attachTextureViews(); encoder.begin(viewNode.getCommandEncoder()); let width = model._colorTextureView.getTexture().getWidth(); let height = model._colorTextureView.getTexture().getHeight(); if (model._useSmallViewport) { const canvas = viewNode.getCanvas(); const scaleFactor = 1 / Math.sqrt(model._lastScale); model._smallViewportWidth = Math.ceil(scaleFactor * canvas.width); model._smallViewportHeight = Math.ceil(scaleFactor * canvas.height); width = model._smallViewportWidth; height = model._smallViewportHeight; } encoder.getHandle().setViewport(0, 0, width, height, 0.0, 1.0); // set scissor encoder.getHandle().setScissorRect(0, 0, width, height); model.fullScreenQuad.setWebGPURenderer(renNode); model.fullScreenQuad.setVolumes(volumes); model.fullScreenQuad.prepareAndDraw(encoder); encoder.end(); }; publicAPI.renderDepthBounds = (renNode, viewNode) => { publicAPI.updateDepthPolyData(renNode); const pd = model._boundsPoly; const points = pd.getPoints(); const cells = pd.getPolys(); let buffRequest = { hash: `vp${cells.getMTime()}`, usage: BufferUsage.Index, cells, numberOfPoints: points.getNumberOfPoints(), primitiveType: PrimitiveTypes.Triangles, representation: Representation.SURFACE }; const indexBuffer = viewNode.getDevice().getBufferManager().getBuffer(buffRequest); model._mapper.getVertexInput().setIndexBuffer(indexBuffer); // points buffRequest = { usage: BufferUsage.PointArray, format: 'float32x4', hash: `vp${points.getMTime()}${cells.getMTime()}`, dataArray: points, indexBuffer, packExtra: true }; const buff = viewNode.getDevice().getBufferManager().getBuffer(buffRequest); model._mapper.getVertexInput().addBuffer(buff, ['vertexBC']); model._mapper.setNumberOfVertices(buff.getSizeInBytes() / buff.getStrideInBytes()); publicAPI.drawDepthRange(renNode, viewNode); }; publicAPI.updateDepthPolyData = renNode => { // check mtimes first let update = false; for (let i = 0; i < model.volumes.length; i++) { const mtime = model.volumes[i].getMTime(); if (!model._lastMTimes[i] || mtime !== model._lastMTimes[i]) { update = true; model._lastMTimes[i] = mtime; } } // also check stabilized time const stime = renNode.getStabilizedTime(); if (model._lastMTimes.length <= model.volumes.length || stime !== model._lastMTimes[model.volumes.length]) { update = true; model._lastMTimes[model.volumes.length] = stime; } // if no need to update then return if (!update) { return; } // rebuild const center = renNode.getStabilizedCenterByReference(); const numPts = model.volumes.length * 8; const points = new Float64Array(numPts * 3); const numTris = model.volumes.length * 12; const polys = new Uint16Array(numTris * 4); // add points and cells for (let i = 0; i < model.volumes.length; i++) { model.volumes[i].getBoundingCubePoints(points, i * 24); let cellIdx = i * 12 * 4; const offset = i * 8; for (let t = 0; t < 12; t++) { polys[cellIdx++] = 3; polys[cellIdx++] = offset + cubeFaceTriangles[t][0]; polys[cellIdx++] = offset + cubeFaceTriangles[t][1]; polys[cellIdx++] = offset + cubeFaceTriangles[t][2]; } } for (let p = 0; p < points.length; p += 3) { points[p] -= center[0]; points[p + 1] -= center[1]; points[p + 2] -= center[2]; } model._boundsPoly.getPoints().setData(points, 3); model._boundsPoly.getPoints().modified(); model._boundsPoly.getPolys().setData(polys, 1); model._boundsPoly.getPolys().modified(); model._boundsPoly.modified(); }; publicAPI.drawDepthRange = (renNode, viewNode) => { // copy current depth buffer to model._depthRangeTexture.resizeToMatch(model.colorTextureView.getTexture()); model._depthRangeTexture2.resizeToMatch(model.colorTextureView.getTexture()); model._depthRangeEncoder.attachTextureViews(); publicAPI.setCurrentOperation('volumeDepthRangePass'); renNode.setRenderEncoder(model._depthRangeEncoder); renNode.volumeDepthRangePass(true); model._mapper.setWebGPURenderer(renNode); model._mapper.prepareToDraw(model._depthRangeEncoder); model._mapper.registerDrawCallback(model._depthRangeEncoder); renNode.volumeDepthRangePass(false); }; publicAPI.createDepthRangeEncoder = viewNode => { const device = viewNode.getDevice(); model._depthRangeEncoder = vtkWebGPURenderEncoder.newInstance({ label: 'VolumePass DepthRange' }); model._depthRangeEncoder.setPipelineHash('volr'); model._depthRangeEncoder.setReplaceShaderCodeFunction(pipeline => { const fDesc = pipeline.getShaderDescription('fragment'); fDesc.addOutput('vec4<f32>', 'outColor1'); fDesc.addOutput('vec4<f32>', 'outColor2'); let code = fDesc.getCode(); code = vtkWebGPUShaderCache.substitute(code, '//VTK::RenderEncoder::Impl', ['output.outColor1 = vec4<f32>(input.fragPos.z, 0.0, 0.0, 0.0);', 'output.outColor2 = vec4<f32>(stopval, 0.0, 0.0, 0.0);']).result; fDesc.setCode(code); }); model._depthRangeEncoder.setDescription({ colorAttachments: [{ view: null, clearValue: [0.0, 0.0, 0.0, 0.0], loadOp: 'clear', storeOp: 'store' }, { view: null, clearValue: [1.0, 1.0, 1.0, 1.0], loadOp: 'clear', storeOp: 'store' }] }); model._depthRangeEncoder.setPipelineSettings({ primitive: { cullMode: 'none' }, fragment: { targets: [{ format: 'r16float', blend: { color: { srcFactor: 'one', dstFactor: 'one', operation: 'max' }, alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'max' } } }, { format: 'r16float', blend: { color: { srcFactor: 'one', dstFactor: 'one', operation: 'min' }, alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'min' } } }] } }); // and the textures it needs model._depthRangeTexture = vtkWebGPUTexture.newInstance({ label: 'volumePassMaxDepth' }); model._depthRangeTexture.create(device, { width: viewNode.getCanvas().width, height: viewNode.getCanvas().height, format: 'r16float', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); const maxView = model._depthRangeTexture.createView('maxTexture'); model._depthRangeEncoder.setColorTextureView(0, maxView); model._depthRangeTexture2 = vtkWebGPUTexture.newInstance({ label: 'volumePassDepthMin' }); model._depthRangeTexture2.create(device, { width: viewNode.getCanvas().width, height: viewNode.getCanvas().height, format: 'r16float', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); const minView = model._depthRangeTexture2.createView('minTexture'); model._depthRangeEncoder.setColorTextureView(1, minView); model._mapper.setDevice(viewNode.getDevice()); model._mapper.setTextureViews([model.depthTextureView]); }; publicAPI.createClearEncoder = viewNode => { model._colorTexture = vtkWebGPUTexture.newInstance({ label: 'volumePassColor' }); model._colorTexture.create(viewNode.getDevice(), { width: viewNode.getCanvas().width, height: viewNode.getCanvas().height, format: 'bgra8unorm', /* eslint-disable no-undef */ /* eslint-disable no-bitwise */ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); model._colorTextureView = model._colorTexture.createView('volumePassColorTexture'); model._colorTextureView.addSampler(viewNode.getDevice(), { minFilter: 'linear', magFilter: 'linear' }); model._clearEncoder = vtkWebGPURenderEncoder.newInstance({ label: 'VolumePass Clear' }); model._clearEncoder.setColorTextureView(0, model._colorTextureView); model._clearEncoder.setDescription({ colorAttachments: [{ view: null, clearValue: [0.0, 0.0, 0.0, 0.0], loadOp: 'clear', storeOp: 'store' }] }); model._clearEncoder.setPipelineHash('volpf'); model._clearEncoder.setPipelineSettings({ primitive: { cullMode: 'none' }, fragment: { targets: [{ format: 'bgra8unorm', blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } } }] } }); }; publicAPI.createCopyEncoder = viewNode => { model._copyEncoder = vtkWebGPURenderEncoder.newInstance({ label: 'volumePassCopy' }); model._copyEncoder.setDescription({ colorAttachments: [{ view: null, loadOp: 'load', storeOp: 'store' }] }); model._copyEncoder.setPipelineHash('volcopypf'); model._copyEncoder.setPipelineSettings({ primitive: { cullMode: 'none' }, fragment: { targets: [{ format: 'rgba16float', blend: { color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } } }] } }); }; publicAPI.createMergeEncoder = viewNode => { model._mergeEncoder = vtkWebGPURenderEncoder.newInstance({ label: 'volumePassMerge' }); model._mergeEncoder.setColorTextureView(0, model._colorTextureView); model._mergeEncoder.setDescription({ colorAttachments: [{ view: null, loadOp: 'load', storeOp: 'store' }] }); model._mergeEncoder.setReplaceShaderCodeFunction(pipeline => { const fDesc = pipeline.getShaderDescription('fragment'); fDesc.addOutput('vec4<f32>', 'outColor'); let code = fDesc.getCode(); code = vtkWebGPUShaderCache.substitute(code, '//VTK::RenderEncoder::Impl', ['output.outColor = vec4<f32>(computedColor.rgb, computedColor.a);']).result; fDesc.setCode(code); }); model._mergeEncoder.setPipelineHash('volpf'); model._mergeEncoder.setPipelineSettings({ primitive: { cullMode: 'none' }, fragment: { targets: [{ format: 'bgra8unorm', blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } } }] } }); }; // 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; } } }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { colorTextureView: null, depthTextureView: null, volumes: null }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); // Build VTK API vtkRenderPass.extend(publicAPI, model, initialValues); model._mapper = vtkWebGPUSimpleMapper.newInstance(); model._mapper.setFragmentShaderTemplate(DepthBoundsFS); model._mapper.getShaderReplacements().set('replaceShaderVolumePass', (hash, pipeline, vertexInput) => { const fDesc = pipeline.getShaderDescription('fragment'); fDesc.addBuiltinInput('vec4<f32>', '@builtin(position) fragPos'); }); model._boundsPoly = vtkPolyData.newInstance(); model._lastMTimes = []; macro.setGet(publicAPI, model, ['colorTextureView', 'depthTextureView']); // Object methods vtkWebGPUVolumePass(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkWebGPUVolumePass'); // ---------------------------------------------------------------------------- var vtkWebGPUVolumePass$1 = { newInstance, extend }; export { vtkWebGPUVolumePass$1 as default, extend, newInstance };