import macro from 'vtk.js/Sources/macros'; import { registerViewConstructor } from 'vtk.js/Sources/Rendering/Core/RenderWindow'; import vtkForwardPass from 'vtk.js/Sources/Rendering/WebGPU/ForwardPass'; import vtkWebGPUBuffer from 'vtk.js/Sources/Rendering/WebGPU/Buffer'; import vtkWebGPUDevice from 'vtk.js/Sources/Rendering/WebGPU/Device'; import vtkWebGPUHardwareSelector from 'vtk.js/Sources/Rendering/WebGPU/HardwareSelector'; import vtkWebGPUViewNodeFactory, { registerOverride, } from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory'; import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass'; import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode'; import HalfFloat from 'vtk.js/Sources/Common/Core/HalfFloat';
const { vtkErrorMacro } = macro;
const SCREENSHOT_PLACEHOLDER = { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', };
function vtkWebGPURenderWindow(publicAPI, model) { model.classHierarchy.push('vtkWebGPURenderWindow');
publicAPI.getViewNodeFactory = () => model.myFactory;
const previousSize = [0, 0]; function updateWindow() { if (model.renderable) { if ( model.size[0] !== previousSize[0] || model.size[1] !== previousSize[1] ) { previousSize[0] = model.size[0]; previousSize[1] = model.size[1]; model.canvas.setAttribute('width', model.size[0]); model.canvas.setAttribute('height', model.size[1]); publicAPI.recreateSwapChain(); } }
if (model.viewStream) { model.viewStream.setSize(model.size[0], model.size[1]); }
model.canvas.style.display = model.useOffScreen ? 'none' : 'block';
if (model.el) { model.el.style.cursor = model.cursorVisibility ? model.cursor : 'none'; }
model.containerSize = null; } publicAPI.onModified(updateWindow);
publicAPI.recreateSwapChain = () => { if (model.context) { model.context.unconfigure(); model.presentationFormat = navigator.gpu.getPreferredCanvasFormat( model.adapter );
model.context.configure({ device: model.device.getHandle(), format: model.presentationFormat, alphaMode: 'premultiplied', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, width: model.size[0], height: model.size[1], }); model._configured = true; } };
publicAPI.getCurrentTexture = () => model.context.getCurrentTexture();
publicAPI.buildPass = (prepass) => { if (prepass) { if (!model.renderable) { return; }
publicAPI.prepareNodes(); publicAPI.addMissingNodes(model.renderable.getRenderersByReference()); publicAPI.removeUnusedNodes();
publicAPI.initialize(); } else if (model.initialized) { if (!model._configured) { publicAPI.recreateSwapChain(); } model.commandEncoder = model.device.createCommandEncoder(); } };
publicAPI.initialize = () => { if (!model.initializing) { model.initializing = true; if (!navigator.gpu) { vtkErrorMacro('WebGPU is not enabled.'); return; }
publicAPI.create3DContextAsync().then(() => { model.initialized = true; if (model.deleted) { return; } publicAPI.invokeInitialized(); }); } };
publicAPI.setContainer = (el) => { if (model.el && model.el !== el) { if (model.canvas.parentNode !== model.el) { vtkErrorMacro('Error: canvas parent node does not match container'); }
model.el.removeChild(model.canvas);
if (model.el.contains(model.bgImage)) { model.el.removeChild(model.bgImage); } }
if (model.el !== el) { model.el = el; if (model.el) { model.el.appendChild(model.canvas);
if (model.useBackgroundImage) { model.el.appendChild(model.bgImage); } }
publicAPI.modified(); } };
publicAPI.getContainer = () => model.el;
publicAPI.getContainerSize = () => { if (!model.containerSize && model.el) { const { width, height } = model.el.getBoundingClientRect(); model.containerSize = [width, height]; } return model.containerSize || model.size; };
publicAPI.getFramebufferSize = () => model.size;
publicAPI.create3DContextAsync = async () => { model.adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance', }); if (model.deleted) { return; } model.device = vtkWebGPUDevice.newInstance(); model.device.initialize(await model.adapter.requestDevice()); if (model.deleted) { model.device = null; return; } model.context = model.canvas.getContext('webgpu'); };
publicAPI.releaseGraphicsResources = () => { const rp = vtkRenderPass.newInstance(); rp.setCurrentOperation('Release'); rp.traverse(publicAPI, null); model.adapter = null; model.device = null; model.context = null; model.initialized = false; model.initializing = false; };
publicAPI.setBackgroundImage = (img) => { model.bgImage.src = img.src; };
publicAPI.setUseBackgroundImage = (value) => { model.useBackgroundImage = value;
if (model.useBackgroundImage && !model.el.contains(model.bgImage)) { model.el.appendChild(model.bgImage); } else if (!model.useBackgroundImage && model.el.contains(model.bgImage)) { model.el.removeChild(model.bgImage); } };
async function getCanvasDataURL(format = model.imageFormat) { const temporaryCanvas = document.createElement('canvas'); const temporaryContext = temporaryCanvas.getContext('2d'); temporaryCanvas.width = model.canvas.width; temporaryCanvas.height = model.canvas.height;
const result = await publicAPI.getPixelsAsync(); const imageData = new ImageData( result.colorValues, result.width, result.height ); temporaryContext.putImageData(imageData, 0, 0);
const mainBoundingClientRect = model.canvas.getBoundingClientRect();
const renderWindow = model.renderable; const renderers = renderWindow.getRenderers(); renderers.forEach((renderer) => { const viewProps = renderer.getViewProps(); viewProps.forEach((viewProp) => { if (viewProp.getContainer) { const container = viewProp.getContainer(); const canvasList = container.getElementsByTagName('canvas'); for (let i = 0; i < canvasList.length; i++) { const currentCanvas = canvasList[i]; const boundingClientRect = currentCanvas.getBoundingClientRect(); const newXPosition = boundingClientRect.x - mainBoundingClientRect.x; const newYPosition = boundingClientRect.y - mainBoundingClientRect.y; temporaryContext.drawImage( currentCanvas, newXPosition, newYPosition ); } } }); });
const screenshot = temporaryCanvas.toDataURL(format); temporaryCanvas.remove(); publicAPI.invokeImageReady(screenshot); }
publicAPI.captureNextImage = ( format = 'image/png', { resetCamera = false, size = null, scale = 1 } = {} ) => { if (model.deleted) { return null; } model.imageFormat = format; const previous = model.notifyStartCaptureImage; model.notifyStartCaptureImage = true;
model._screenshot = { size: !!size || scale !== 1 ? size || model.size.map((val) => val * scale) : null, };
return new Promise((resolve, reject) => { const subscription = publicAPI.onImageReady((imageURL) => { if (model._screenshot.size === null) { model.notifyStartCaptureImage = previous; subscription.unsubscribe(); if (model._screenshot.placeHolder) { model.size = model._screenshot.originalSize;
publicAPI.modified();
if (model._screenshot.cameras) { model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) => restoreParamsFn(arg) ); }
publicAPI.traverseAllPasses();
model.el.removeChild(model._screenshot.placeHolder); model._screenshot.placeHolder.remove(); model._screenshot = null; } resolve(imageURL); } else { const tmpImg = document.createElement('img'); tmpImg.style = SCREENSHOT_PLACEHOLDER; tmpImg.src = imageURL; model._screenshot.placeHolder = model.el.appendChild(tmpImg);
model.canvas.style.display = 'none';
model._screenshot.originalSize = model.size; model.size = model._screenshot.size; model._screenshot.size = null;
publicAPI.modified();
if (resetCamera) { const isUserResetCamera = resetCamera !== true;
model._screenshot.cameras = model.renderable .getRenderers() .map((renderer) => { const camera = renderer.getActiveCamera(); const params = camera.get( 'focalPoint', 'position', 'parallelScale' );
return { resetCameraArgs: isUserResetCamera ? { renderer } : undefined, resetCameraFn: isUserResetCamera ? resetCamera : renderer.resetCamera, restoreParamsFn: camera.set, arg: JSON.parse(JSON.stringify(params)), }; });
model._screenshot.cameras.forEach( ({ resetCameraFn, resetCameraArgs }) => resetCameraFn(resetCameraArgs) ); }
publicAPI.traverseAllPasses(); } }); }); };
publicAPI.traverseAllPasses = () => { if (model.deleted) { return; } if (!model.initialized) { publicAPI.initialize(); const subscription = publicAPI.onInitialized(() => { subscription.unsubscribe(); publicAPI.traverseAllPasses(); }); } else { if (model.renderPasses) { for (let index = 0; index < model.renderPasses.length; ++index) { model.renderPasses[index].traverse(publicAPI, null); } } if (model.commandEncoder) { model.device.submitCommandEncoder(model.commandEncoder); model.commandEncoder = null; if (model.notifyStartCaptureImage) { model.device.onSubmittedWorkDone().then(() => { getCanvasDataURL(); }); } } } };
publicAPI.setViewStream = (stream) => { if (model.viewStream === stream) { return false; } if (model.subscription) { model.subscription.unsubscribe(); model.subscription = null; } model.viewStream = stream; if (model.viewStream) { const mainRenderer = model.renderable.getRenderers()[0]; mainRenderer.getBackgroundByReference()[3] = 0;
publicAPI.setUseBackgroundImage(true);
model.subscription = model.viewStream.onImageReady((e) => publicAPI.setBackgroundImage(e.image) ); model.viewStream.setSize(model.size[0], model.size[1]); model.viewStream.invalidateCache(); model.viewStream.render();
publicAPI.modified(); } return true; };
publicAPI.getUniquePropID = () => model.nextPropID++;
publicAPI.getPropFromID = (id) => { for (let i = 0; i < model.children.length; i++) { const res = model.children[i].getPropFromID(id); if (res !== null) { return res; } } return null; };
publicAPI.getPixelsAsync = async () => { const device = model.device; const texture = model.renderPasses[0].getOpaquePass().getColorTexture();
const result = { width: texture.getWidth(), height: texture.getHeight(), };
result.colorBufferWidth = 32 * Math.floor((result.width + 31) / 32); result.colorBufferSizeInBytes = result.colorBufferWidth * result.height * 8; const colorBuffer = vtkWebGPUBuffer.newInstance(); colorBuffer.setDevice(device); colorBuffer.create( result.colorBufferSizeInBytes, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST );
const cmdEnc = model.device.createCommandEncoder(); cmdEnc.copyTextureToBuffer( { texture: texture.getHandle(), }, { buffer: colorBuffer.getHandle(), bytesPerRow: 8 * result.colorBufferWidth, rowsPerImage: result.height, }, { width: result.width, height: result.height, depthOrArrayLayers: 1, } ); device.submitCommandEncoder(cmdEnc);
const cLoad = colorBuffer.mapAsync(GPUMapMode.READ); await cLoad;
result.colorValues = new Uint16Array(colorBuffer.getMappedRange().slice()); colorBuffer.unmap(); const tmparray = new Uint8ClampedArray(result.height * result.width * 4); for (let y = 0; y < result.height; y++) { for (let x = 0; x < result.width; x++) { const doffset = (y * result.width + x) * 4; const soffset = (y * result.colorBufferWidth + x) * 4; tmparray[doffset] = 255.0 * HalfFloat.fromHalf(result.colorValues[soffset]); tmparray[doffset + 1] = 255.0 * HalfFloat.fromHalf(result.colorValues[soffset + 1]); tmparray[doffset + 2] = 255.0 * HalfFloat.fromHalf(result.colorValues[soffset + 2]); tmparray[doffset + 3] = 255.0 * HalfFloat.fromHalf(result.colorValues[soffset + 3]); } } result.colorValues = tmparray; return result; };
publicAPI.createSelector = () => { const ret = vtkWebGPUHardwareSelector.newInstance(); ret.setWebGPURenderWindow(publicAPI); return ret; };
const superSetSize = publicAPI.setSize; publicAPI.setSize = (width, height) => { const modified = superSetSize(width, height); if (modified) { publicAPI.invokeWindowResizeEvent({ width, height }); } return modified; };
publicAPI.delete = macro.chain(publicAPI.delete, publicAPI.setViewStream); }
const DEFAULT_VALUES = { initialized: false, context: null, adapter: null, device: null, canvas: null, cursorVisibility: true, cursor: 'pointer', containerSize: null, renderPasses: [], notifyStartCaptureImage: false, imageFormat: 'image/png', useOffScreen: false, useBackgroundImage: false, nextPropID: 1, xrSupported: false, presentationFormat: null, };
export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues);
model.canvas = document.createElement('canvas'); model.canvas.style.width = '100%';
model.bgImage = new Image(); model.bgImage.style.position = 'absolute'; model.bgImage.style.left = '0'; model.bgImage.style.top = '0'; model.bgImage.style.width = '100%'; model.bgImage.style.height = '100%'; model.bgImage.style.zIndex = '-1';
vtkRenderWindowViewNode.extend(publicAPI, model, initialValues);
model.myFactory = vtkWebGPUViewNodeFactory.newInstance();
model.renderPasses[0] = vtkForwardPass.newInstance();
if (!model.selector) { model.selector = vtkWebGPUHardwareSelector.newInstance(); model.selector.setWebGPURenderWindow(publicAPI); }
macro.event(publicAPI, model, 'imageReady'); macro.event(publicAPI, model, 'initialized');
macro.get(publicAPI, model, [ 'commandEncoder', 'device', 'presentationFormat', 'useBackgroundImage', 'xrSupported', ]);
macro.setGet(publicAPI, model, [ 'initialized', 'context', 'canvas', 'device', 'renderPasses', 'notifyStartCaptureImage', 'cursor', 'useOffScreen', ]);
macro.setGetArray(publicAPI, model, ['size'], 2); macro.event(publicAPI, model, 'windowResizeEvent');
vtkWebGPURenderWindow(publicAPI, model); }
export const newInstance = macro.newInstance(extend, 'vtkWebGPURenderWindow');
registerViewConstructor('WebGPU', newInstance);
export default { newInstance, extend, };
registerOverride('vtkRenderWindow', newInstance);
|