@acransac/vtk.js
Version:
Visualization Toolkit for the Web
349 lines (291 loc) • 10.5 kB
JavaScript
import macro from 'vtk.js/Sources/macro';
import { mat4 } from 'gl-matrix';
import vtkWebGPUBufferManager from 'vtk.js/Sources/Rendering/WebGPU/BufferManager';
import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
const { vtkDebugMacro } = macro;
const { BufferUsage } = vtkWebGPUBufferManager;
const vtkWebGPURendererUBOCode = `
[[block]] struct renderVals
{
[[offset(0)]] WCDCMatrix : mat4x4<f32>;
[[offset(64)]] WCVCMatrix : mat4x4<f32>;
[[offset(128)]] VCDCMatrix : mat4x4<f32>;
[[offset(192)]] WCVCNormals : mat4x4<f32>;
};
[[binding(0), group(0)]] var<uniform> rendererUBO : renderVals;
`;
const vtkWebGPURenderUBOSize = 256 / 4;
// ----------------------------------------------------------------------------
// vtkWebGPURenderer methods
// ----------------------------------------------------------------------------
/* eslint-disable no-bitwise */
function vtkWebGPURenderer(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkWebGPURenderer');
// Builds myself.
publicAPI.buildPass = (prepass) => {
if (prepass) {
if (!model.renderable) {
return;
}
// make sure we have a camera
if (!model.renderable.isActiveCameraCreated()) {
model.renderable.resetCamera();
}
publicAPI.updateLights();
publicAPI.prepareNodes();
publicAPI.addMissingNode(model.renderable.getActiveCamera());
publicAPI.addMissingNodes(model.renderable.getViewPropsWithNestedProps());
publicAPI.removeUnusedNodes();
}
};
publicAPI.getUBOCode = () => vtkWebGPURendererUBOCode;
publicAPI.updateLights = () => {
let count = 0;
const lights = model.renderable.getLightsByReference();
for (let index = 0; index < lights.length; ++index) {
if (lights[index].getSwitch() > 0.0) {
count++;
}
}
if (!count) {
vtkDebugMacro('No lights are on, creating one.');
model.renderable.createLight();
}
return count;
};
// register pipeline callbacks from a mapper
publicAPI.registerPipelineCallback = (pipeline, cb) => {
// if there is a matching pipeline just add the cb
for (let i = 0; i < model.pipelineCallbacks.length; i++) {
if (model.pipelineCallbacks[i].pipeline === pipeline) {
model.pipelineCallbacks[i].callbacks.push(cb);
return;
}
}
model.pipelineCallbacks.push({ pipeline, callbacks: [cb] });
};
publicAPI.updateUBO = () => {
const device = model.parent.getDevice();
let needSend = false;
// make sure the data is up to date
// has the camera changed?
const cam = model.renderable.getActiveCamera();
const utime = model.UBOUpdateTime.getMTime();
if (
model.parent.getMTime() > utime ||
publicAPI.getMTime() > utime ||
cam.getMTime() > utime ||
model.renderable.getMTime() > utime
) {
const aspectRatio = publicAPI.getAspectRatio();
const mat = cam.getCompositeProjectionMatrix(aspectRatio, -1, 1);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
model.UBOData[i * 4 + j] = mat[j * 4 + i];
}
}
model.UBOData[0] = mat[0];
model.UBOData[4] = mat[1];
model.UBOData[8] = mat[2];
model.UBOData[12] = mat[3];
model.UBOData[1] = mat[4];
model.UBOData[5] = mat[5];
model.UBOData[9] = mat[6];
model.UBOData[13] = mat[7];
model.UBOData[2] = 0.5 * mat[8] + 0.5 * mat[12];
model.UBOData[6] = 0.5 * mat[9] + 0.5 * mat[13];
model.UBOData[10] = 0.5 * mat[10] + 0.5 * mat[14];
model.UBOData[14] = 0.5 * mat[11] + 0.5 * mat[15];
model.UBOData[3] = mat[12];
model.UBOData[7] = mat[13];
model.UBOData[11] = mat[14];
model.UBOData[15] = mat[15];
mat4.copy(model.tmpMat4, cam.getViewMatrix());
// zero out translation
model.tmpMat4[3] = 0.0;
model.tmpMat4[7] = 0.0;
model.tmpMat4[11] = 0.0;
mat4.invert(model.tmpMat4, model.tmpMat4);
model.UBOData.set(model.tmpMat4, 48);
// console.log(JSON.stringify(model.UBOData));
model.UBOUpdateTime.modified();
needSend = true;
}
// make sure the buffer is created
if (!model.UBO) {
const req = {
address: model.UBOData,
time: 0,
usage: BufferUsage.UniformArray,
};
model.UBO = device.getBufferManager().getBuffer(req);
model.UBOBindGroup = device.getHandle().createBindGroup({
layout: device.getRendererBindGroupLayout(),
entries: [
{
binding: 0,
resource: {
buffer: model.UBO.getHandle(),
},
},
],
});
needSend = false;
}
// send data down if needed
if (needSend) {
device
.getHandle()
.queue.writeBuffer(
model.UBO.getHandle(),
0,
model.UBOData.buffer,
model.UBOData.byteOffset,
model.UBOData.byteLength
);
}
};
// Renders myself
publicAPI.opaquePass = (prepass) => {
if (prepass) {
const renDesc = model.parent.getSwapChain().getRenderPassDescription();
if (!model.renderable.getTransparent()) {
const background = model.renderable.getBackgroundByReference();
renDesc.colorAttachments[0].loadValue = background;
} else {
renDesc.colorAttachments[0].loadValue = [0.0, 0.0, 0.0, 0.0];
}
if (!model.renderable.getPreserveDepthBuffer()) {
renDesc.depthStencilAttachment.depthLoadValue = 1.0;
renDesc.depthStencilAttachment.depthStoreOp = 'store';
} else {
// todo need to handle this case
// renDesc.depthStencilAttachment.depthStoreOp = 'store';
}
// clear last pipelines
model.pipelineCallbacks = [];
model.renderPass = model.parent
.getCommandEncoder()
.beginRenderPass(renDesc);
publicAPI.updateUBO();
} else {
// loop over registered pipelines
for (let i = 0; i < model.pipelineCallbacks.length; i++) {
const pStruct = model.pipelineCallbacks[i];
const pl = pStruct.pipeline;
pl.bind(model.renderPass);
model.renderPass.setBindGroup(0, model.UBOBindGroup);
// bind our BindGroup
// set viewport
const tsize = publicAPI.getTiledSizeAndOrigin();
model.renderPass.setViewport(
tsize.lowerLeftU,
tsize.lowerLeftV,
tsize.usize,
tsize.vsize,
0.0,
1.0
);
// set scissor
model.renderPass.setScissorRect(
tsize.lowerLeftU,
tsize.lowerLeftV,
tsize.usize,
tsize.vsize
);
// renderPass.setPipeline(renderPipeline);
for (let cb = 0; cb < pStruct.callbacks.length; cb++) {
pStruct.callbacks[cb](pl);
}
}
model.renderPass.endPass();
}
};
publicAPI.getAspectRatio = () => {
const size = model.parent.getSizeByReference();
const viewport = model.renderable.getViewportByReference();
return (
(size[0] * (viewport[2] - viewport[0])) /
((viewport[3] - viewport[1]) * size[1])
);
};
publicAPI.getTiledSizeAndOrigin = () => {
const vport = model.renderable.getViewportByReference();
// if there is no window assume 0 1
const tileViewPort = [0.0, 0.0, 1.0, 1.0];
// find the lower left corner of the viewport, taking into account the
// lower left boundary of this tile
const vpu = vtkMath.clampValue(vport[0] - tileViewPort[0], 0.0, 1.0);
const vpv = vtkMath.clampValue(vport[1] - tileViewPort[1], 0.0, 1.0);
// store the result as a pixel value
const ndvp = model.parent.normalizedDisplayToDisplay(vpu, vpv);
const lowerLeftU = Math.round(ndvp[0]);
const lowerLeftV = Math.round(ndvp[1]);
// find the upper right corner of the viewport, taking into account the
// lower left boundary of this tile
let vpu2 = vtkMath.clampValue(vport[2] - tileViewPort[0], 0.0, 1.0);
let vpv2 = vtkMath.clampValue(vport[3] - tileViewPort[1], 0.0, 1.0);
// also watch for the upper right boundary of the tile
if (vpu2 > tileViewPort[2] - tileViewPort[0]) {
vpu2 = tileViewPort[2] - tileViewPort[0];
}
if (vpv2 > tileViewPort[3] - tileViewPort[1]) {
vpv2 = tileViewPort[3] - tileViewPort[1];
}
const ndvp2 = model.parent.normalizedDisplayToDisplay(vpu2, vpv2);
// now compute the size of the intersection of the viewport with the
// current tile
let usize = Math.round(ndvp2[0]) - lowerLeftU;
let vsize = Math.round(ndvp2[1]) - lowerLeftV;
if (usize < 0) {
usize = 0;
}
if (vsize < 0) {
vsize = 0;
}
return { usize, vsize, lowerLeftU, lowerLeftV };
};
publicAPI.clear = () => {
// const ts = publicAPI.getTiledSizeAndOrigin();
// gl.enable(gl.SCISSOR_TEST);
// gl.scissor(ts.lowerLeftU, ts.lowerLeftV, ts.usize, ts.vsize);
// gl.viewport(ts.lowerLeftU, ts.lowerLeftV, ts.usize, ts.vsize);
// gl.clear(clearMask);
// gl.enable(gl.DEPTH_TEST);
/* eslint-enable no-bitwise */
};
publicAPI.releaseGraphicsResources = () => {
if (model.selector !== null) {
model.selector.releaseGraphicsResources();
}
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
context: null,
selector: null,
UBOData: null,
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkViewNode.extend(publicAPI, model, initialValues);
model.UBOData = new Float32Array(vtkWebGPURenderUBOSize);
model.UBOUpdateTime = {};
macro.obj(model.UBOUpdateTime);
model.tmpMat4 = mat4.create();
// Build VTK API
macro.get(publicAPI, model, ['renderPass']);
macro.setGet(publicAPI, model, ['selector']);
// Object methods
vtkWebGPURenderer(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend, 'vtkWebGPURenderer');
// ----------------------------------------------------------------------------
export default { newInstance, extend };