@kitware/vtk.js
Version:
Visualization Toolkit for the Web
518 lines (467 loc) • 19.8 kB
JavaScript
import { mat4, vec3 } from 'gl-matrix';
import { n as newInstance$1, o as obj, g as get, k as getArray, e as setGet, c as macro } from '../../macros2.js';
import { r as radiansFromDegrees } from '../../Common/Core/Math/index.js';
import vtkViewNode from '../SceneGraph/ViewNode.js';
import vtkWebGPUBindGroup from './BindGroup.js';
import vtkWebGPUFullScreenQuad from './FullScreenQuad.js';
import vtkWebGPUStorageBuffer from './StorageBuffer.js';
import vtkWebGPUUniformBuffer from './UniformBuffer.js';
import { registerOverride } from './ViewNodeFactory.js';
const {
vtkDebugMacro
} = macro;
const clearFragColorTemplate = `
//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> = mapperUBO.BackgroundColor;
//VTK::RenderEncoder::Impl
return output;
}
`;
const clearFragTextureTemplate = `
fn vecToRectCoord(dir: vec3<f32>) -> vec2<f32> {
var tau: f32 = 6.28318530718;
var pi: f32 = 3.14159265359;
var out: vec2<f32> = vec2<f32>(0.0);
out.x = atan2(dir.z, dir.x) / tau;
out.x += 0.5;
var phix: f32 = length(vec2(dir.x, dir.z));
out.y = atan2(dir.y, phix) / pi + 0.5;
return out;
}
//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 tcoord: vec4<f32> = vec4<f32>(input.vertexVC.xy, -1, 1);
var V: vec4<f32> = normalize(mapperUBO.FSQMatrix * tcoord); // vec2<f32>((input.tcoordVS.x - 0.5) * 2, -(input.tcoordVS.y - 0.5) * 2);
// textureSampleLevel gets rid of some ugly artifacts
var background = textureSampleLevel(EnvironmentTexture, EnvironmentTextureSampler, vecToRectCoord(V.xyz), 0.0);
var computedColor: vec4<f32> = vec4<f32>(background.rgb, 1);
//VTK::RenderEncoder::Impl
return output;
}
`;
const _fsqClearMat4 = new Float64Array(16);
const _tNormalMat4 = new Float64Array(16);
// Light type index gives either 0, 1, or 2 which indicates what type of light there is.
// While technically, there are only spot and directional lights, within the CellArrayMapper
// there is a third, positional light. It is technically just a variant of a spot light with
// a cone angle of 90 or above, however certain calculations can be skipped if it is treated
// separately.
// The mappings are shown below:
// 0 -> positional light
// 1 -> directional light
// 2 -> spot light
function getLightTypeIndex(light) {
if (light.getPositional()) {
if (light.getConeAngle() >= 90) {
return 0;
}
return 2;
}
return 1;
}
// ----------------------------------------------------------------------------
// 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;
}
model.camera = model.renderable.getActiveCamera();
publicAPI.updateLights();
publicAPI.prepareNodes();
publicAPI.addMissingNode(model.camera);
publicAPI.addMissingNodes(model.renderable.getViewPropsWithNestedProps());
publicAPI.removeUnusedNodes();
model.webgpuCamera = publicAPI.getViewNodeFor(model.camera, model.webgpuCamera);
publicAPI.updateStabilizedMatrix();
}
};
publicAPI.updateStabilizedMatrix = () => {
// This method is designed to help with floating point
// issues when rendering datasets that push the limits of
// resolutions on float.
//
// One of the most common cases is when the dataset is located far
// away from the origin relative to the clipping range we are looking
// at. For that case we want to perform the floating point sensitive
// multiplications on the CPU in double. To this end we want the
// vertex rendering ops to look something like
//
// Compute shifted points and load those into the VBO
// pointCoordsSC = WorldToStabilizedMatrix * pointCoords;
//
// In the vertex shader do the following
// positionVC = StabilizedToDeviceMatrix * ModelToStabilizedMatrix*vertexIn;
//
// We use two matrices because it is expensive to change the
// WorldToStabilized matrix as we have to reupload all pointCoords
// So that matrix (MCSCMatrix) is fairly static, the Stabilized to
// Device matrix is the one that gets updated every time the camera
// changes.
//
// The basic idea is that we should translate the data so that
// when the center of the view frustum moves a lot
// we recenter it. The center of the view frustum is roughly
// camPos + dirOfProj*(far + near)*0.5
const clipRange = model.camera.getClippingRange();
const pos = model.camera.getPositionByReference();
const dop = model.camera.getDirectionOfProjectionByReference();
const center = [];
const offset = [];
vec3.scale(offset, dop, 0.5 * (clipRange[0] + clipRange[1]));
vec3.add(center, pos, offset);
vec3.sub(offset, center, model.stabilizedCenter);
const length = vec3.len(offset);
if (length / (clipRange[1] - clipRange[0]) > model.recenterThreshold) {
model.stabilizedCenter = center;
model.stabilizedTime.modified();
}
};
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;
};
publicAPI.updateUBO = () => {
// make sure the data is up to date
// has the camera changed?
const utime = model.UBO.getSendTime();
if (model._parent.getMTime() > utime || publicAPI.getMTime() > utime || model.camera.getMTime() > utime || model.renderable.getMTime() > utime) {
const keyMats = model.webgpuCamera.getKeyMatrices(publicAPI);
model.UBO.setArray('WCVCMatrix', keyMats.wcvc);
model.UBO.setArray('SCPCMatrix', keyMats.scpc);
model.UBO.setArray('PCSCMatrix', keyMats.pcsc);
model.UBO.setArray('SCVCMatrix', keyMats.scvc);
model.UBO.setArray('VCPCMatrix', keyMats.vcpc);
model.UBO.setArray('WCVCNormals', keyMats.normalMatrix);
model.UBO.setValue('LightCount', model.renderable.getLights().length);
model.UBO.setValue('MaxEnvironmentMipLevel', model.renderable.getEnvironmentTexture()?.getMipLevel());
model.UBO.setValue('BackgroundDiffuseStrength', model.renderable.getEnvironmentTextureDiffuseStrength());
model.UBO.setValue('BackgroundSpecularStrength', model.renderable.getEnvironmentTextureSpecularStrength());
const tsize = publicAPI.getYInvertedTiledSizeAndOrigin();
model.UBO.setArray('viewportSize', [tsize.usize, tsize.vsize]);
model.UBO.setValue('cameraParallel', model.camera.getParallelProjection());
const device = model._parent.getDevice();
model.UBO.sendIfNeeded(device);
}
};
publicAPI.updateSSBO = () => {
const lights = model.renderable.getLights();
const keyMats = model.webgpuCamera.getKeyMatrices(publicAPI);
let lightTimeString = `${model.renderable.getMTime()}`;
for (let i = 0; i < lights.length; i++) {
lightTimeString += lights[i].getMTime();
}
if (lightTimeString !== model.lightTimeString) {
const lightPosArray = new Float32Array(lights.length * 4);
const lightDirArray = new Float32Array(lights.length * 4);
const lightColorArray = new Float32Array(lights.length * 4);
const lightTypeArray = new Float32Array(lights.length * 4);
for (let i = 0; i < lights.length; i++) {
const offset = i * 4;
// Position
const viewCoordinatePosition = lights[i].getPosition();
vec3.transformMat4(viewCoordinatePosition, viewCoordinatePosition, keyMats.wcvc);
// viewCoordinatePosition
lightPosArray[offset] = viewCoordinatePosition[0];
lightPosArray[offset + 1] = viewCoordinatePosition[1];
lightPosArray[offset + 2] = viewCoordinatePosition[2];
lightPosArray[offset + 3] = 0;
// Rotation (All are negative to correct for -Z being forward)
lightDirArray[offset] = -lights[i].getDirection()[0];
lightDirArray[offset + 1] = -lights[i].getDirection()[1];
lightDirArray[offset + 2] = -lights[i].getDirection()[2];
lightDirArray[offset + 3] = 0;
// Color
lightColorArray[offset] = lights[i].getColor()[0];
lightColorArray[offset + 1] = lights[i].getColor()[1];
lightColorArray[offset + 2] = lights[i].getColor()[2];
lightColorArray[offset + 3] = lights[i].getIntensity() * 5; // arbitrary multiplication to fix the dullness of low value PBR lights
// Type
lightTypeArray[offset] = getLightTypeIndex(lights[i]); // Type
lightTypeArray[offset + 1] = Math.cos(radiansFromDegrees(lights[i].getConeAngle())); // Inner Phi, should probably do some check on these to make sure they dont excede limits
lightTypeArray[offset + 2] = Math.cos(radiansFromDegrees(lights[i].getConeAngle() + lights[i].getConeFalloff())); // Outer Phi
lightTypeArray[offset + 3] = 0;
}
// Im not sure how correct this is, but this is what the example does
// https://kitware.github.io/vtk-js/api/Rendering_WebGPU_VolumePassFSQ.html
model.SSBO.clearData();
model.SSBO.setNumberOfInstances(lights.length);
model.SSBO.addEntry('LightPos', 'vec4<f32>'); // Position
model.SSBO.addEntry('LightDir', 'vec4<f32>'); // Direction
model.SSBO.addEntry('LightColor', 'vec4<f32>'); // Color (r, g, b, intensity)
model.SSBO.addEntry('LightData', 'vec4<f32>'); // Other data (type, etc, etc, etc)
model.SSBO.setAllInstancesFromArray('LightPos', lightPosArray);
model.SSBO.setAllInstancesFromArray('LightDir', lightDirArray);
model.SSBO.setAllInstancesFromArray('LightColor', lightColorArray);
model.SSBO.setAllInstancesFromArray('LightData', lightTypeArray);
const device = model._parent.getDevice();
model.SSBO.send(device);
}
model.lightTimeString = lightTimeString;
};
publicAPI.scissorAndViewport = encoder => {
const tsize = publicAPI.getYInvertedTiledSizeAndOrigin();
encoder.getHandle().setViewport(tsize.lowerLeftU, tsize.lowerLeftV, tsize.usize, tsize.vsize, 0.0, 1.0);
// set scissor
encoder.getHandle().setScissorRect(tsize.lowerLeftU, tsize.lowerLeftV, tsize.usize, tsize.vsize);
};
publicAPI.bindUBO = renderEncoder => {
renderEncoder.activateBindGroup(model.bindGroup);
};
// Renders myself
publicAPI.opaquePass = prepass => {
if (prepass) {
model.renderEncoder.begin(model._parent.getCommandEncoder());
publicAPI.updateUBO();
publicAPI.updateSSBO();
} else {
publicAPI.scissorAndViewport(model.renderEncoder);
publicAPI.clear();
model.renderEncoder.end();
}
};
publicAPI.clear = () => {
if (model.renderable.getTransparent() || model.suppressClear) {
return;
}
const device = model._parent.getDevice();
// Normal Solid Color
if (!model.clearFSQ) {
model.clearFSQ = vtkWebGPUFullScreenQuad.newInstance();
model.clearFSQ.setDevice(device);
model.clearFSQ.setPipelineHash('clearfsq');
model.clearFSQ.setFragmentShaderTemplate(clearFragColorTemplate);
const ubo = vtkWebGPUUniformBuffer.newInstance({
label: 'mapperUBO'
});
ubo.addEntry('FSQMatrix', 'mat4x4<f32>');
ubo.addEntry('BackgroundColor', 'vec4<f32>');
model.clearFSQ.setUBO(ubo);
model.backgroundTex = model.renderable.getEnvironmentTexture();
}
// Textured Background
if (model.clearFSQ.getPipelineHash() !== 'clearfsqwithtexture' && model.renderable.getUseEnvironmentTextureAsBackground() && model.backgroundTex?.getImageLoaded()) {
model.clearFSQ.setFragmentShaderTemplate(clearFragTextureTemplate);
const ubo = vtkWebGPUUniformBuffer.newInstance({
label: 'mapperUBO'
});
ubo.addEntry('FSQMatrix', 'mat4x4<f32>');
ubo.addEntry('BackgroundColor', 'vec4<f32>');
model.clearFSQ.setUBO(ubo);
const environmentTextureHash = device.getTextureManager().getTextureForVTKTexture(model.backgroundTex, 'EnvironmentTexture');
if (environmentTextureHash.getReady()) {
const tview = environmentTextureHash.createView(`EnvironmentTexture`);
model.clearFSQ.setTextureViews([tview]);
model.backgroundTexLoaded = true;
const interpolate = model.backgroundTex.getInterpolate() ? 'linear' : 'nearest';
tview.addSampler(device, {
addressModeU: 'repeat',
addressModeV: 'clamp-to-edge',
addressModeW: 'repeat',
minFilter: interpolate,
magFilter: interpolate,
mipmapFilter: 'linear'
});
}
model.clearFSQ.setPipelineHash('clearfsqwithtexture');
} else if (model.clearFSQ.getPipelineHash() === 'clearfsqwithtexture' && !model.renderable.getUseEnvironmentTextureAsBackground()) {
// In case the mode is changed at runtime
model.clearFSQ = vtkWebGPUFullScreenQuad.newInstance();
model.clearFSQ.setDevice(device);
model.clearFSQ.setPipelineHash('clearfsq');
model.clearFSQ.setFragmentShaderTemplate(clearFragColorTemplate);
const ubo = vtkWebGPUUniformBuffer.newInstance({
label: 'mapperUBO'
});
ubo.addEntry('FSQMatrix', 'mat4x4<f32>');
ubo.addEntry('BackgroundColor', 'vec4<f32>');
model.clearFSQ.setUBO(ubo);
}
const keyMats = model.webgpuCamera.getKeyMatrices(publicAPI);
const background = model.renderable.getBackgroundByReference();
model.clearFSQ.getUBO().setArray('BackgroundColor', background);
mat4.transpose(_tNormalMat4, keyMats.normalMatrix);
mat4.mul(_fsqClearMat4, keyMats.scvc, keyMats.pcsc);
mat4.mul(_fsqClearMat4, _tNormalMat4, _fsqClearMat4);
model.clearFSQ.getUBO().setArray('FSQMatrix', _fsqClearMat4);
model.clearFSQ.getUBO().sendIfNeeded(device);
model.clearFSQ.prepareAndDraw(model.renderEncoder);
};
publicAPI.translucentPass = prepass => {
if (prepass) {
model.renderEncoder.begin(model._parent.getCommandEncoder());
} else {
publicAPI.scissorAndViewport(model.renderEncoder);
model.renderEncoder.end();
}
};
publicAPI.volumeDepthRangePass = prepass => {
if (prepass) {
model.renderEncoder.begin(model._parent.getCommandEncoder());
} else {
publicAPI.scissorAndViewport(model.renderEncoder);
model.renderEncoder.end();
}
};
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.convertToOpenGLDepth = val => model.webgpuCamera.convertToOpenGLDepth(val);
publicAPI.getYInvertedTiledSizeAndOrigin = () => {
const res = publicAPI.getTiledSizeAndOrigin();
const size = model._parent.getSizeByReference();
res.lowerLeftV = size[1] - res.vsize - res.lowerLeftV;
return res;
};
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 = vport[0] - tileViewPort[0];
const vpv = vport[1] - tileViewPort[1];
// 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
const vpu2 = vport[2] - tileViewPort[0];
const vpv2 = vport[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.getPropFromID = id => {
for (let i = 0; i < model.children.length; i++) {
const res = model.children[i].getPropID ? model.children[i].getPropID() : -1;
if (res === id) {
return model.children[i];
}
}
return null;
};
publicAPI.getStabilizedTime = () => model.stabilizedTime.getMTime();
publicAPI.releaseGraphicsResources = () => {
if (model.selector !== null) {
model.selector.releaseGraphicsResources();
}
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
bindGroup: null,
selector: null,
renderEncoder: null,
recenterThreshold: 20.0,
suppressClear: false,
stabilizedCenter: [0.0, 0.0, 0.0]
};
// ----------------------------------------------------------------------------
function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkViewNode.extend(publicAPI, model, initialValues);
// UBO
model.UBO = vtkWebGPUUniformBuffer.newInstance({
label: 'rendererUBO'
});
model.UBO.addEntry('WCVCMatrix', 'mat4x4<f32>');
model.UBO.addEntry('SCPCMatrix', 'mat4x4<f32>');
model.UBO.addEntry('PCSCMatrix', 'mat4x4<f32>');
model.UBO.addEntry('SCVCMatrix', 'mat4x4<f32>');
model.UBO.addEntry('VCPCMatrix', 'mat4x4<f32>');
model.UBO.addEntry('WCVCNormals', 'mat4x4<f32>');
model.UBO.addEntry('viewportSize', 'vec2<f32>');
model.UBO.addEntry('LightCount', 'i32');
model.UBO.addEntry('MaxEnvironmentMipLevel', 'f32');
model.UBO.addEntry('BackgroundDiffuseStrength', 'f32');
model.UBO.addEntry('BackgroundSpecularStrength', 'f32');
model.UBO.addEntry('cameraParallel', 'u32');
// SSBO (Light data)
model.SSBO = vtkWebGPUStorageBuffer.newInstance({
label: 'rendererLightSSBO'
});
model.lightTimeString = '';
model.bindGroup = vtkWebGPUBindGroup.newInstance({
label: 'rendererBG'
});
model.bindGroup.setBindables([model.UBO, model.SSBO]);
model.tmpMat4 = mat4.identity(new Float64Array(16));
model.stabilizedTime = {};
obj(model.stabilizedTime, {
mtime: 0
});
// Build VTK API
get(publicAPI, model, ['bindGroup', 'stabilizedTime']);
getArray(publicAPI, model, ['stabilizedCenter']);
setGet(publicAPI, model, ['renderEncoder', 'selector', 'suppressClear', 'UBO']);
// Object methods
vtkWebGPURenderer(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = newInstance$1(extend, 'vtkWebGPURenderer');
// ----------------------------------------------------------------------------
var index = {
newInstance,
extend
};
// Register ourself to WebGPU backend if imported
registerOverride('vtkRenderer', newInstance);
export { index as default, extend, newInstance };