@kitware/vtk.js
Version:
Visualization Toolkit for the Web
589 lines (535 loc) • 20.7 kB
JavaScript
import { m as macro } from '../../macros2.js';
import { registerViewConstructor } from '../Core/RenderWindow.js';
import vtkForwardPass from './ForwardPass.js';
import vtkWebGPUBuffer from './Buffer.js';
import vtkWebGPUDevice from './Device.js';
import vtkWebGPUHardwareSelector from './HardwareSelector.js';
import vtkWebGPUViewNodeFactory, { registerOverride } from './ViewNodeFactory.js';
import vtkRenderPass from '../SceneGraph/RenderPass.js';
import vtkRenderWindowViewNode from '../SceneGraph/RenderWindowViewNode.js';
import HalfFloat from '../../Common/Core/HalfFloat.js';
const {
vtkErrorMacro
} = macro;
// const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
const SCREENSHOT_PLACEHOLDER = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%'
};
// ----------------------------------------------------------------------------
// vtkWebGPURenderWindow methods
// ----------------------------------------------------------------------------
function vtkWebGPURenderWindow(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkWebGPURenderWindow');
publicAPI.getViewNodeFactory = () => model.myFactory;
// Auto update style
const previousSize = [0, 0];
function updateWindow() {
// Canvas size
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();
}
}
// ImageStream size
if (model.viewStream) {
// If same size that's a NoOp
model.viewStream.setSize(model.size[0], model.size[1]);
}
// Offscreen ?
model.canvas.style.display = model.useOffScreen ? 'none' : 'block';
// Cursor type
if (model.el) {
model.el.style.cursor = model.cursorVisibility ? model.cursor : 'none';
}
// Invalidate cached DOM container size
model.containerSize = null;
}
publicAPI.onModified(updateWindow);
publicAPI.recreateSwapChain = () => {
if (model.context) {
model.context.unconfigure();
model.presentationFormat = navigator.gpu.getPreferredCanvasFormat(model.adapter);
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
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();
// Builds myself.
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.traverseRenderers = (renPass) => {
// // iterate over renderers
// const numlayers = publicAPI.getRenderable().getNumberOfLayers();
// const renderers = publicAPI.getChildren();
// for (let i = 0; i < numlayers; i++) {
// for (let index = 0; index < renderers.length; index++) {
// const renNode = renderers[index];
// const ren = publicAPI.getRenderable().getRenderers()[index];
// if (ren.getDraw() && ren.getLayer() === i) {
// renNode.traverse(renPass);
// }
// }
// }
// };
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');
}
// Remove canvas from previous container
model.el.removeChild(model.canvas);
// If the renderer has previously added
// a background image, remove it from the DOM.
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 the renderer is set to use a background
// image, attach it to the DOM.
if (model.useBackgroundImage) {
model.el.appendChild(model.bgImage);
}
}
// Trigger modified()
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 () => {
// Get a GPU device to render with
model.adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
});
if (model.deleted) {
return;
}
// console.log([...model.adapter.features]);
model.device = vtkWebGPUDevice.newInstance();
model.device.initialize(await model.adapter.requestDevice({
requiredLimits: {
maxBufferSize: model.adapter.limits.maxBufferSize,
maxStorageBufferBindingSize: model.adapter.limits.maxStorageBufferBindingSize,
maxUniformBufferBindingSize: model.adapter.limits.maxUniformBufferBindingSize
}
}));
if (model.deleted) {
model.device = null;
return;
}
// model.device.getHandle().lost.then((info) => {
// console.log(`${info.message}`);
// publicAPI.releaseGraphicsResources();
// });
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;
// Add or remove the background image from the
// DOM as specified.
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) {
// Copy current canvas to not modify the original
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);
// temporaryCanvas.putImageData(imageData, 0, 0);
temporaryContext.putImageData(imageData, 0, 0);
// Get current client rect to place canvas
const mainBoundingClientRect = model.canvas.getBoundingClientRect();
const renderWindow = model.renderable;
const renderers = renderWindow.getRenderers();
renderers.forEach(renderer => {
const viewProps = renderer.getViewProps();
viewProps.forEach(viewProp => {
// Check if the prop has a container that should have canvas
if (viewProp.getContainer) {
const container = viewProp.getContainer();
const canvasList = container.getElementsByTagName('canvas');
// Go throughout all canvas and copy it into temporary main 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) {
// resize the main canvas back to its original size and show it
model.size = model._screenshot.originalSize;
// process the resize
publicAPI.modified();
// restore the saved camera parameters, if applicable
if (model._screenshot.cameras) {
model._screenshot.cameras.forEach(({
restoreParamsFn,
arg
}) => restoreParamsFn(arg));
}
// Trigger a render at the original size
publicAPI.traverseAllPasses();
// Remove and clean up the placeholder, revealing the original
model.el.removeChild(model._screenshot.placeHolder);
model._screenshot.placeHolder.remove();
model._screenshot = null;
}
resolve(imageURL);
} else {
// Create a placeholder image overlay while we resize and render
const tmpImg = document.createElement('img');
tmpImg.style = SCREENSHOT_PLACEHOLDER;
tmpImg.src = imageURL;
model._screenshot.placeHolder = model.el.appendChild(tmpImg);
// hide the main canvas
model.canvas.style.display = 'none';
// remember the main canvas original size, then resize it
model._screenshot.originalSize = model.size;
model.size = model._screenshot.size;
model._screenshot.size = null;
// process the resize
publicAPI.modified();
if (resetCamera) {
const isUserResetCamera = resetCamera !== true;
// If resetCamera was requested, we first save camera parameters
// from all the renderers, so we can restore them later
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,
// "clone" the params so we don't keep refs to properties
arg: JSON.parse(JSON.stringify(params))
};
});
// Perform the resetCamera() on each renderer only after capturing
// the params from all active cameras, in case there happen to be
// linked cameras among the renderers.
model._screenshot.cameras.forEach(({
resetCameraFn,
resetCameraArgs
}) => resetCameraFn(resetCameraArgs));
}
// Trigger a render at the custom size
publicAPI.traverseAllPasses();
}
});
});
};
publicAPI.traverseAllPasses = () => {
if (model.deleted) {
return;
}
// if we are not initialized then we call initialize
// which is async so we will not actually get a render
// so we queue up another traverse for when we are initialized
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) {
// Force background to be transparent + render
const mainRenderer = model.renderable.getRenderers()[0];
mainRenderer.getBackgroundByReference()[3] = 0;
// Enable display of the background image
publicAPI.setUseBackgroundImage(true);
// Bind to remote stream
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();
// as this is async we really don't want to store things in
// the class as multiple calls may start before resolving
// so anything specific to this request gets put into the
// result object (by value in most cases)
const result = {
width: texture.getWidth(),
height: texture.getHeight()
};
// must be a multiple of 256 bytes, so 32 texels with rgba16
result.colorBufferWidth = 32 * Math.floor((result.width + 31) / 32);
result.colorBufferSizeInBytes = result.colorBufferWidth * result.height * 8;
const colorBuffer = vtkWebGPUBuffer.newInstance();
colorBuffer.setDevice(device);
/* eslint-disable no-bitwise */
/* eslint-disable no-undef */
colorBuffer.create(result.colorBufferSizeInBytes, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
/* eslint-enable no-bitwise */
/* eslint-enable no-undef */
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);
/* eslint-disable no-undef */
const cLoad = colorBuffer.mapAsync(GPUMapMode.READ);
await cLoad;
/* eslint-enable no-undef */
result.colorValues = new Uint16Array(colorBuffer.getMappedRange().slice());
colorBuffer.unmap();
// repack the array
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);
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
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
};
// ----------------------------------------------------------------------------
function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Create internal instances
model.canvas = document.createElement('canvas');
model.canvas.style.width = '100%';
// Create internal bgImage
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';
// Inheritance
vtkRenderWindowViewNode.extend(publicAPI, model, initialValues);
model.myFactory = vtkWebGPUViewNodeFactory.newInstance();
// setup default forward pass rendering
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');
// Build VTK API
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');
// Object methods
vtkWebGPURenderWindow(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkWebGPURenderWindow');
// ----------------------------------------------------------------------------
// Register API specific RenderWindow implementation
// ----------------------------------------------------------------------------
registerViewConstructor('WebGPU', newInstance);
// ----------------------------------------------------------------------------
var vtkRenderWindow = {
newInstance,
extend
};
// Register ourself to WebGPU backend if imported
registerOverride('vtkRenderWindow', newInstance);
export { vtkRenderWindow as default, extend, newInstance };