@acransac/vtk.js
Version:
Visualization Toolkit for the Web
544 lines (482 loc) • 16.5 kB
JavaScript
import * as macro from 'vtk.js/Sources/macro';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkWebGPUBuffer from 'vtk.js/Sources/Rendering/WebGPU/Buffer';
import vtkProperty from 'vtk.js/Sources/Rendering/Core/Property';
import Constants from './Constants';
const { BufferUsage, PrimitiveTypes } = Constants;
const { Representation } = vtkProperty;
const { vtkDebugMacro } = macro;
// the webgpu constants all show up as undefined
/* eslint-disable no-undef */
// const { ObjectType } = Constants;
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Static API
// ----------------------------------------------------------------------------
export const STATIC = {};
function requestMatches(req1, req2) {
if (req1.time !== req2.time) return false;
if (req1.address !== req2.address) return false;
if (req1.format !== req2.format) return false;
if (req1.usage !== req2.usage) return false;
if (req1.hash !== req2.hash) return false;
return true;
}
const cellCounters = {
// easy, every input point becomes an output point
anythingToPoints(numPoints, cellPts) {
return numPoints;
},
linesToWireframe(numPoints, cellPts) {
if (numPoints > 1) {
return (numPoints - 1) * 2;
}
return 0;
},
polysToWireframe(numPoints, cellPts) {
if (numPoints > 2) {
return numPoints * 2;
}
return 0;
},
stripsToWireframe(numPoints, cellPts) {
if (numPoints > 2) {
return numPoints * 4 - 6;
}
return 0;
},
polysToSurface(npts, cellPts) {
if (npts > 2) {
return (npts - 2) * 3;
}
return 0;
},
stripsToSurface(npts, cellPts, offset) {
if (numPoints > 2) {
return (npts - 2) * 3;
}
return 0;
},
};
function getPrimitiveName(primType) {
switch (primType) {
case PrimitiveTypes.Points:
return 'points';
case PrimitiveTypes.Lines:
return 'lines';
case PrimitiveTypes.Triangles:
return 'polys';
case PrimitiveTypes.TriangleStrips:
return 'strips';
default:
return '';
}
}
function getOutputSize(cellArray, representation, inRepName) {
let countFunc = null;
if (representation === Representation.POINTS || inRepName === 'points') {
countFunc = cellCounters.anythingToPoints;
} else if (
representation === Representation.WIREFRAME ||
inRepName === 'lines'
) {
countFunc = cellCounters[`${inRepName}ToWireframe`];
} else {
countFunc = cellCounters[`${inRepName}ToSurface`];
}
const array = cellArray.getData();
const size = array.length;
let caboCount = 0;
for (let index = 0; index < size; ) {
caboCount += countFunc(array[index], array);
index += array[index] + 1;
}
return caboCount;
}
function packArray(
cellArray,
primType,
representation,
inArray,
outputType,
options
) {
const result = { elementCount: 0, blockSize: 0, stride: 0 };
if (!cellArray.getData() || !cellArray.getData().length) {
return result;
}
const shift = options.shift ? options.shift : 0;
const scale = options.scale ? options.scale : 1;
const packExtra = Object.prototype.hasOwnProperty.call(options, 'packExtra')
? options.packExtra
: false;
const pointData = inArray.getData();
let addAPoint;
const cellBuilders = {
// easy, every input point becomes an output point
anythingToPoints(numPoints, cellPts, offset, cellId) {
for (let i = 0; i < numPoints; ++i) {
addAPoint(cellPts[offset + i], cellId);
}
},
linesToWireframe(numPoints, cellPts, offset, cellId) {
// for lines we add a bunch of segments
for (let i = 0; i < numPoints - 1; ++i) {
addAPoint(cellPts[offset + i], cellId);
addAPoint(cellPts[offset + i + 1], cellId);
}
},
polysToWireframe(numPoints, cellPts, offset, cellId) {
// for polys we add a bunch of segments and close it
if (numPoints > 2) {
for (let i = 0; i < numPoints; ++i) {
addAPoint(cellPts[offset + i], cellId);
addAPoint(cellPts[offset + ((i + 1) % numPoints)], cellId);
}
}
},
stripsToWireframe(numPoints, cellPts, offset, cellId) {
if (numPoints > 2) {
// for strips we add a bunch of segments and close it
for (let i = 0; i < numPoints - 1; ++i) {
addAPoint(cellPts[offset + i], cellId);
addAPoint(cellPts[offset + i + 1], cellId);
}
for (let i = 0; i < numPoints - 2; i++) {
addAPoint(cellPts[offset + i], cellId);
addAPoint(cellPts[offset + i + 2], cellId);
}
}
},
polysToSurface(npts, cellPts, offset, cellId) {
for (let i = 0; i < npts - 2; i++) {
addAPoint(cellPts[offset + 0], cellId);
addAPoint(cellPts[offset + i + 1], cellId);
addAPoint(cellPts[offset + i + 2], cellId);
}
},
stripsToSurface(npts, cellPts, offset, cellId) {
for (let i = 0; i < npts - 2; i++) {
addAPoint(cellPts[offset + i], cellId);
addAPoint(cellPts[offset + i + 1 + (i % 2)], cellId);
addAPoint(cellPts[offset + i + 1 + ((i + 1) % 2)], cellId);
}
},
};
const inRepName = getPrimitiveName(primType);
let func = null;
if (
representation === Representation.POINTS ||
primType === PrimitiveTypes.Points
) {
func = cellBuilders.anythingToPoints;
} else if (
representation === Representation.WIREFRAME ||
primType === PrimitiveTypes.Lines
) {
func = cellBuilders[`${inRepName}ToWireframe`];
} else {
func = cellBuilders[`${inRepName}ToSurface`];
}
const array = cellArray.getData();
const size = array.length;
const caboCount = getOutputSize(cellArray, representation, inRepName);
let vboidx = 0;
const numComp = inArray.getNumberOfComponents();
const packedVBO = new window[outputType](
caboCount * (numComp + (packExtra ? 1 : 0))
);
// pick the right function based on point versus cell data
let getData = (ptId, cellId) => pointData[ptId];
if (options.cellData) {
getData = (ptId, cellId) => pointData[cellId];
console.log('has celldata');
}
// add data based on number of components
if (numComp === 1) {
addAPoint = function addAPointFunc(i, cellid) {
packedVBO[vboidx++] = scale * getData(i, cellid) + shift;
};
} else if (numComp === 2) {
addAPoint = function addAPointFunc(i, cellid) {
packedVBO[vboidx++] = scale * getData(i * 2, cellid * 2) + shift;
packedVBO[vboidx++] = scale * getData(i * 2 + 1, cellid * 2 + 1) + shift;
};
} else if (numComp === 3 && !packExtra) {
addAPoint = function addAPointFunc(i, cellid) {
packedVBO[vboidx++] = scale * getData(i * 3, cellid * 3) + shift;
packedVBO[vboidx++] = scale * getData(i * 3 + 1, cellid * 3 + 1) + shift;
packedVBO[vboidx++] = scale * getData(i * 3 + 2, cellid * 3 + 2) + shift;
};
} else if (numComp === 3 && packExtra) {
addAPoint = function addAPointFunc(i, cellid) {
packedVBO[vboidx++] = scale * getData(i * 3, cellid * 3) + shift;
packedVBO[vboidx++] = scale * getData(i * 3 + 1, cellid * 3 + 1) + shift;
packedVBO[vboidx++] = scale * getData(i * 3 + 2, cellid * 3 + 2) + shift;
packedVBO[vboidx++] = scale * 1.0 + shift;
};
} else if (numComp === 4) {
addAPoint = function addAPointFunc(i, cellid) {
packedVBO[vboidx++] = scale * getData(i * 4, cellid * 4) + shift;
packedVBO[vboidx++] = scale * getData(i * 4 + 1, cellid * 4 + 1) + shift;
packedVBO[vboidx++] = scale * getData(i * 4 + 2, cellid * 4 + 2) + shift;
packedVBO[vboidx++] = scale * getData(i * 4 + 3, cellid * 4 + 3) + shift;
};
}
let cellId = options.cellOffset;
for (let index = 0; index < size; ) {
func(array[index], array, index + 1, cellId);
index += array[index] + 1;
cellId++;
}
result.address = packedVBO;
result.elementCount = caboCount;
return result;
}
function getNormal(pointData, i0, i1, i2) {
const v1 = [
pointData[i2 * 3] - pointData[i1 * 3],
pointData[i2 * 3 + 1] - pointData[i1 * 3 + 1],
pointData[i2 * 3 + 2] - pointData[i1 * 3 + 2],
];
const v2 = [
pointData[i0 * 3] - pointData[i1 * 3],
pointData[i0 * 3 + 1] - pointData[i1 * 3 + 1],
pointData[i0 * 3 + 2] - pointData[i1 * 3 + 2],
];
const result = [];
vtkMath.cross(v1, v2, result);
vtkMath.normalize(result);
return result;
}
function generateNormals(cellArray, primType, representation, inArray) {
if (!cellArray.getData() || !cellArray.getData().length) {
return null;
}
const pointData = inArray.getData();
let addAPoint;
const cellBuilders = {
polysToPoints(numPoints, cellPts, offset) {
const normal = getNormal(
pointData,
cellPts[offset],
cellPts[offset + 1],
cellPts[offset + 2]
);
for (let i = 0; i < numPoints; ++i) {
addAPoint(normal);
}
},
polysToWireframe(numPoints, cellPts, offset) {
// for polys we add a bunch of segments and close it
// compute the normal
const normal = getNormal(
pointData,
cellPts[offset],
cellPts[offset + 1],
cellPts[offset + 2]
);
for (let i = 0; i < numPoints; ++i) {
addAPoint(normal);
addAPoint(normal);
}
},
polysToSurface(npts, cellPts, offset) {
if (npts < 3) {
// ignore degenerate triangles
vtkDebugMacro('skipping degenerate triangle');
} else {
// compute the normal
const normal = getNormal(
pointData,
cellPts[offset],
cellPts[offset + 1],
cellPts[offset + 2]
);
for (let i = 0; i < npts - 2; i++) {
addAPoint(normal);
addAPoint(normal);
addAPoint(normal);
}
}
},
};
const primName = getPrimitiveName(primType);
let func = null;
if (representation === Representation.POINTS) {
func = cellBuilders[`${primName}ToPoints`];
} else if (representation === Representation.WIREFRAME) {
func = cellBuilders[`${primName}ToWireframe`];
} else {
func = cellBuilders[`${primName}ToSurface`];
}
const caboCount = getOutputSize(cellArray, representation, primName);
let vboidx = 0;
const packedVBO = new Int8Array(caboCount * 4);
addAPoint = function addAPointFunc(normal) {
packedVBO[vboidx++] = 127 * normal[0];
packedVBO[vboidx++] = 127 * normal[1];
packedVBO[vboidx++] = 127 * normal[2];
packedVBO[vboidx++] = 127;
};
const array = cellArray.getData();
const size = array.length;
for (let index = 0; index < size; ) {
func(array[index], array, index + 1);
index += array[index] + 1;
}
return packedVBO;
}
function getStrideFromFormat(format) {
if (!format) return 0;
let numComp = 1;
if (format.substring(format.length - 2) === 'x4') numComp = 4;
if (format.substring(format.length - 2) === 'x3') numComp = 3;
if (format.substring(format.length - 2) === 'x2') numComp = 2;
let typeSize = 4;
if (numComp > 1) {
if (format.substring(format.length - 3, format.length - 1) === '8x') {
typeSize = 1;
}
if (format.substring(format.length - 4, format.length - 1) === '16x') {
typeSize = 2;
}
} else {
if (format.substring(format.length - 1) === '8') typeSize = 1;
if (format.substring(format.length - 2) === '16') typeSize = 2;
}
return numComp * typeSize;
}
function getArrayTypeFromFormat(format) {
if (!format) return null;
if (format.substring(0, 7) === 'float32') return 'Float32Array';
if (format.substring(0, 6) === 'snorm8') return 'Int8Array';
if (format.substring(0, 6) === 'unorm8') return 'Uint8Array';
return '';
}
// ----------------------------------------------------------------------------
// vtkWebGPUBufferManager methods
// ----------------------------------------------------------------------------
function vtkWebGPUBufferManager(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkWebGPUBufferManager');
// we cache based on the passed in source, when the source is
// garbage collected then the cache entry is removed. If a source
// is not provided then the buffer is NOT cached and you are on your own
// if you want to share it etc
publicAPI.getBuffer = (req) => {
// if a dataArray is provided set the address
if (req.dataArray) {
req.address = req.dataArray.getData();
}
if (req.source) {
// if a matching buffer already exists then return it
if (model.buffers.has(req.source)) {
const dabuffers = model.buffers.get(req.source);
for (let i = 0; i < dabuffers.length; i++) {
if (requestMatches(dabuffers[i].request, req)) {
return dabuffers[i].buffer;
}
}
}
}
// create one
const buffer = vtkWebGPUBuffer.newInstance();
buffer.setDevice(model.device);
const stride = getStrideFromFormat(req.format);
const arrayType = getArrayTypeFromFormat(req.format);
let gpuUsage = null;
// handle uniform buffers
if (req.usage === BufferUsage.UniformArray) {
/* eslint-disable no-bitwise */
gpuUsage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
/* eslint-enable no-bitwise */
buffer.createAndWrite(req.address, gpuUsage);
}
// handle textures
if (req.usage === BufferUsage.Texture) {
/* eslint-disable no-bitwise */
gpuUsage = GPUBufferUsage.COPY_SRC;
/* eslint-enable no-bitwise */
buffer.createAndWrite(req.address, gpuUsage);
buffer.setArrayInformation([{ offset: 0, format: req.format }]);
}
// handle point data
if (req.usage === BufferUsage.PointArray) {
gpuUsage = GPUBufferUsage.VERTEX;
const result = packArray(
req.cells,
req.primitiveType,
req.representation,
req.dataArray,
arrayType,
{
packExtra: req.packExtra,
shift: req.shift,
scale: req.scale,
cellData: req.cellData,
cellOffset: req.cellOffset,
}
);
// console.log(result);
buffer.createAndWrite(result.address, gpuUsage);
buffer.setStrideInBytes(stride);
buffer.setArrayInformation([{ offset: 0, format: req.format }]);
}
// handle normals from points, snorm8x4
if (req.usage === BufferUsage.NormalsFromPoints) {
gpuUsage = GPUBufferUsage.VERTEX;
const normals = generateNormals(
req.cells,
req.primitiveType,
req.representation,
req.dataArray
);
buffer.createAndWrite(normals, gpuUsage);
buffer.setStrideInBytes(stride);
buffer.setArrayInformation([{ offset: 0, format: req.format }]);
}
buffer.setSourceTime(req.time);
// cache the buffer if we have a dataArray.
// We create a new req that only has the 4 fields required for
// a comparison to avoid GC cycles
if (req.source) {
if (!model.buffers.has(req.source)) {
model.buffers.set(req.source, []);
}
const dabuffers = model.buffers.get(req.dataArray);
dabuffers.push({
request: {
time: req.time,
address: req.address,
format: req.format,
usage: req.usage,
},
buffer,
});
}
return buffer;
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
device: null,
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Object methods
macro.obj(publicAPI, model);
// this is a cache, and a cache with GC pretty much means WeakMap
model.buffers = new WeakMap();
macro.setGet(publicAPI, model, ['device']);
vtkWebGPUBufferManager(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend);
// ----------------------------------------------------------------------------
export default { newInstance, extend, ...STATIC, ...Constants };