@acransac/vtk.js
Version:
Visualization Toolkit for the Web
545 lines (463 loc) • 16.7 kB
JavaScript
import macro from 'vtk.js/Sources/macro';
import { registerViewConstructor } from 'vtk.js/Sources/Rendering/Core/RenderWindow';
import vtkForwardPass from 'vtk.js/Sources/Rendering/WebGPU/ForwardPass';
import vtkWebGPUDevice from 'vtk.js/Sources/Rendering/WebGPU/Device';
import vtkWebGPUSwapChain from 'vtk.js/Sources/Rendering/WebGPU/SwapChain';
import vtkWebGPUViewNodeFactory from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory';
import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass';
import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode';
// import vtkOpenGLTextureUnitManager from 'vtk.js/Sources/Rendering/OpenGL/TextureUnitManager';
// import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';
const { vtkErrorMacro } = macro;
// const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
// ----------------------------------------------------------------------------
// vtkWebGPURenderWindow methods
// ----------------------------------------------------------------------------
function vtkWebGPURenderWindow(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkWebGPURenderWindow');
// 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 = () => {
model.swapChain.releaseGraphicsResources();
if (!model.swapChain.getCreated()) {
model.swapChain.create(model.device, publicAPI);
// model.commandBufferIndexes.clear();
// model.commandBufferIndexes.resize(this->Swapchain->GetMaximumFramesInFlight(), -1);
}
};
publicAPI.getPipeline = (hash) => {
if (hash in model.pipelines) {
return model.pipelines[hash];
}
return null;
};
publicAPI.createPipeline = (hash, pipeline) => {
pipeline.initialize(publicAPI);
model.pipelines[hash] = pipeline;
};
// Builds myself.
publicAPI.buildPass = (prepass) => {
if (prepass) {
if (!model.renderable) {
return;
}
publicAPI.prepareNodes();
publicAPI.addMissingNodes(model.renderable.getRenderersByReference());
publicAPI.removeUnusedNodes();
publicAPI.initialize();
// model.children.forEach((child) => {
// child.setOpenGLRenderWindow(publicAPI);
// });
} else if (model.initialized) {
if (!model.swapChain.getCreated()) {
model.swapChain.create(model.device, publicAPI);
}
model.commandEncoder = model.device.createCommandEncoder();
}
};
publicAPI.initialize = () => {
if (!model.initializing) {
publicAPI.create3DContext(); // async
}
};
publicAPI.makeCurrent = () => {
model.context.makeCurrent();
};
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 = () => {
if (model.activeFramebuffer) {
return model.activeFramebuffer.getSize();
}
return model.size;
};
publicAPI.isInViewport = (x, y, viewport) => {
const vCoords = viewport.getViewportByReference();
const size = publicAPI.getFramebufferSize();
if (
vCoords[0] * size[0] <= x &&
vCoords[2] * size[0] >= x &&
vCoords[1] * size[1] <= y &&
vCoords[3] * size[1] >= y
) {
return true;
}
return false;
};
publicAPI.getViewportSize = (viewport) => {
const vCoords = viewport.getViewportByReference();
const size = publicAPI.getFramebufferSize();
return [
(vCoords[2] - vCoords[0]) * size[0],
(vCoords[3] - vCoords[1]) * size[1],
];
};
publicAPI.getViewportCenter = (viewport) => {
const size = publicAPI.getViewportSize(viewport);
return [size[0] * 0.5, size[1] * 0.5];
};
publicAPI.displayToNormalizedDisplay = (x, y, z) => {
const size = publicAPI.getFramebufferSize();
return [x / size[0], y / size[1], z];
};
publicAPI.normalizedDisplayToDisplay = (x, y, z) => {
const size = publicAPI.getFramebufferSize();
return [x * size[0], y * size[1], z];
};
publicAPI.worldToView = (x, y, z, renderer) => renderer.worldToView(x, y, z);
publicAPI.viewToWorld = (x, y, z, renderer) => renderer.viewToWorld(x, y, z);
publicAPI.worldToDisplay = (x, y, z, renderer) => {
const val = renderer.worldToView(x, y, z);
const dims = publicAPI.getViewportSize(renderer);
const val2 = renderer.viewToProjection(
val[0],
val[1],
val[2],
dims[0] / dims[1]
);
const val3 = renderer.projectionToNormalizedDisplay(
val2[0],
val2[1],
val2[2]
);
return publicAPI.normalizedDisplayToDisplay(val3[0], val3[1], val3[2]);
};
publicAPI.displayToWorld = (x, y, z, renderer) => {
const val = publicAPI.displayToNormalizedDisplay(x, y, z);
const val2 = renderer.normalizedDisplayToProjection(val[0], val[1], val[2]);
const dims = publicAPI.getViewportSize(renderer);
const val3 = renderer.projectionToView(
val2[0],
val2[1],
val2[2],
dims[0] / dims[1]
);
return renderer.viewToWorld(val3[0], val3[1], val3[2]);
};
publicAPI.normalizedDisplayToViewport = (x, y, z, renderer) => {
let vCoords = renderer.getViewportByReference();
vCoords = publicAPI.normalizedDisplayToDisplay(vCoords[0], vCoords[1], 0.0);
const coords = publicAPI.normalizedDisplayToDisplay(x, y, z);
return [coords[0] - vCoords[0] - 0.5, coords[1] - vCoords[1] - 0.5, z];
};
publicAPI.viewportToNormalizedViewport = (x, y, z, renderer) => {
const size = publicAPI.getViewportSize(renderer);
if (size && size[0] !== 0 && size[1] !== 0) {
return [x / (size[0] - 1.0), y / (size[1] - 1.0), z];
}
return [x, y, z];
};
publicAPI.normalizedViewportToViewport = (x, y, z) => {
const size = publicAPI.getFramebufferSize();
return [x * (size[0] - 1.0), y * (size[1] - 1.0), z];
};
publicAPI.displayToLocalDisplay = (x, y, z) => {
const size = publicAPI.getFramebufferSize();
return [x, size[1] - y - 1, z];
};
publicAPI.viewportToNormalizedDisplay = (x, y, z, renderer) => {
let vCoords = renderer.getViewportByReference();
vCoords = publicAPI.normalizedDisplayToDisplay(vCoords[0], vCoords[1], 0.0);
const x2 = x + vCoords[0] + 0.5;
const y2 = y + vCoords[1] + 0.5;
return publicAPI.displayToNormalizedDisplay(x2, y2, z);
};
publicAPI.getPixelData = (x1, y1, x2, y2) => {
const pixels = new Uint8Array((x2 - x1 + 1) * (y2 - y1 + 1) * 4);
model.context.readPixels(
x1,
y1,
x2 - x1 + 1,
y2 - y1 + 1,
model.context.RGBA,
model.context.UNSIGNED_BYTE,
pixels
);
return pixels;
};
publicAPI.create3DContext = () => {
model.initializing = true;
(async () => {
if (!navigator.gpu) {
vtkErrorMacro('WebGPU is not enabled.');
return;
}
// Get a GPU device to render with
model.adapter = await navigator.gpu.requestAdapter();
model.device = vtkWebGPUDevice.newInstance();
model.device.initialize(await model.adapter.requestDevice());
model.context = model.canvas.getContext('gpupresent');
model.initialized = true;
})();
};
publicAPI.restoreContext = () => {
const rp = vtkRenderPass.newInstance();
rp.setCurrentOperation('Release');
rp.traverse(publicAPI, null);
};
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);
}
};
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;
temporaryContext.drawImage(model.canvas, 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') => {
if (model.deleted) {
return null;
}
model.imageFormat = format;
const previous = model.notifyStartCaptureImage;
model.notifyStartCaptureImage = true;
return new Promise((resolve, reject) => {
const subscription = publicAPI.onImageReady((imageURL) => {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
resolve(imageURL);
});
});
};
publicAPI.traverseAllPasses = () => {
if (!model.initialized) {
publicAPI.initialize();
} 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) {
vtkErrorMacro('Getting image');
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.delete = macro.chain(publicAPI.delete, publicAPI.setViewStream);
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
initialized: false,
context: null,
adapter: null,
device: null,
canvas: null,
swapChain: null,
pipelines: null,
cullFaceEnabled: false,
depthMaskEnabled: true,
size: [300, 300],
cursorVisibility: true,
cursor: 'pointer',
textureUnitManager: null,
textureResourceIds: null,
containerSize: null,
renderPasses: [],
notifyStartCaptureImage: false,
activeFramebuffer: null,
imageFormat: 'image/png',
useOffScreen: false,
useBackgroundImage: false,
};
// ----------------------------------------------------------------------------
export 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';
model.swapChain = vtkWebGPUSwapChain.newInstance();
model.pipelines = {};
// Inheritance
vtkViewNode.extend(publicAPI, model, initialValues);
model.myFactory = vtkWebGPUViewNodeFactory.newInstance();
/* eslint-disable no-use-before-define */
model.myFactory.registerOverride('vtkRenderWindow', newInstance);
/* eslint-enable no-use-before-define */
// setup default forward pass rendering
model.renderPasses[0] = vtkForwardPass.newInstance();
macro.event(publicAPI, model, 'imageReady');
// Build VTK API
macro.get(publicAPI, model, [
'swapChain',
'commandEncoder',
'device',
'textureUnitManager',
'useBackgroundImage',
]);
macro.setGet(publicAPI, model, [
'initialized',
'context',
'canvas',
'device',
'renderPasses',
'notifyStartCaptureImage',
'cursor',
'useOffScreen',
// might want to make this not call modified as
// we change the active framebuffer a lot. Or maybe
// only mark modified if the size or depth
// of the buffer has changed
'activeFramebuffer',
]);
macro.setGetArray(publicAPI, model, ['size'], 2);
// Object methods
vtkWebGPURenderWindow(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend, 'vtkWebGPURenderWindow');
// ----------------------------------------------------------------------------
// Register API specific RenderWindow implementation
// ----------------------------------------------------------------------------
registerViewConstructor('WebGPU', newInstance);
// ----------------------------------------------------------------------------
export default {
newInstance,
extend,
};