CellArrayBufferObject

WebGL build ArrayBufferObjects based on CellArrays

The vtkCellArrayBufferObject is designed to build array buffers that do not rely on
an index buffer but rather just a glDrawArrays. They are built based on cellArrays
that get passed from a PolyDataMapper

createVBO = (cellArray, inRep, outRep, points, normals, tcoords, colors)

The main entry point, builds the array buffer based on the supplied arguments

elementCount

How many elements are in this buffer. Generally you would call DrawArrays
with this number.

stride

The stride in bytes between one element and the next

vertexOffset

The offset in bytes from the start of an element and the vertex data

normalOffset

The offset in bytes from the start of an element and the normal data

tCoordOffset

The offset in bytes from the start of an element and the texture coordinate data

tCoordComponents

The number of components in a texture coordinate, typically 2

colorOffset

The offset in bytes from the start of an element and the color data

colorComponents

The number of components in a color value typically 4

getCoordShift = ()

getCoordScale = ()

getInverseShiftAndScaleMatrix = ()

Get the shift and scale vectors computed by createVBO(), as well as the inverse transform to apply to the rendering transform.

getCoordShiftAndScaleEnabled = ()

Get a boolean indicating whether the shift and scale vectors are currently being applied to the coordinates.
createVBO() only applies shift and scale if one of the following conditions are met:

  • If the data is far from the origin relative to its size
  • If the size is huge when not far from the origin
  • If data is a point, but far from the origin

This is necessary as OpenGL ES 2.0 does not support double precision data types.

Source

index.js
import { mat4, quat, vec3 } from 'gl-matrix';

import macro from 'vtk.js/Sources/macros';
import vtkBufferObject from 'vtk.js/Sources/Rendering/OpenGL/BufferObject';
import { ObjectType } from 'vtk.js/Sources/Rendering/OpenGL/BufferObject/Constants';
import { Representation } from 'vtk.js/Sources/Rendering/Core/Property/Constants';

const { vtkErrorMacro } = macro;

// ----------------------------------------------------------------------------
// Static functions
// ----------------------------------------------------------------------------

function computeInverseShiftAndScaleMatrix(coordShift, coordScale) {
const inverseScale = new Float64Array(3);
vec3.inverse(inverseScale, coordScale);

const matrix = new Float64Array(16);
mat4.fromRotationTranslationScale(
matrix,
quat.create(),
coordShift,
inverseScale
);

return matrix;
}

function shouldApplyCoordShiftAndScale(coordShift, coordScale) {
if (coordShift === null || coordScale === null) {
return false;
}

return !(
vec3.exactEquals(coordShift, [0, 0, 0]) &&
vec3.exactEquals(coordScale, [1, 1, 1])
);
}

// ----------------------------------------------------------------------------
// vtkOpenGLCellArrayBufferObject methods
// ----------------------------------------------------------------------------

function vtkOpenGLCellArrayBufferObject(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkOpenGLCellArrayBufferObject');

publicAPI.setType(ObjectType.ARRAY_BUFFER);

publicAPI.createVBO = (
cellArray,
inRep,
outRep,
options,
selectionMaps = null
) => {
if (!cellArray.getData() || !cellArray.getData().length) {
model.elementCount = 0;
return 0;
}

// Figure out how big each block will be, currently 6 or 7 floats.
model.blockSize = 3;
model.vertexOffset = 0;
model.normalOffset = 0;
model.tCoordOffset = 0;
model.tCoordComponents = 0;
model.colorComponents = 0;
model.colorOffset = 0;
model.customData = [];

const pointData = options.points.getData();
let normalData = null;
let tcoordData = null;
let colorData = null;

const colorComponents = options.colors
? options.colors.getNumberOfComponents()
: 0;
const textureComponents = options.tcoords
? options.tcoords.getNumberOfComponents()
: 0;

// the values of 4 below are because floats are 4 bytes

if (options.normals) {
model.normalOffset = 4 * model.blockSize;
model.blockSize += 3;
normalData = options.normals.getData();
}

if (options.customAttributes) {
options.customAttributes.forEach((a) => {
if (a) {
model.customData.push({
data: a.getData(),
offset: 4 * model.blockSize,
components: a.getNumberOfComponents(),
name: a.getName(),
});
model.blockSize += a.getNumberOfComponents();
}
});
}

if (options.tcoords) {
model.tCoordOffset = 4 * model.blockSize;
model.tCoordComponents = textureComponents;
model.blockSize += textureComponents;
tcoordData = options.tcoords.getData();
}

if (options.colors) {
model.colorComponents = options.colors.getNumberOfComponents();
model.colorOffset = 0;
colorData = options.colors.getData();
if (!model.colorBO) {
model.colorBO = vtkBufferObject.newInstance();
}
model.colorBO.setOpenGLRenderWindow(model._openGLRenderWindow);
} else {
model.colorBO = null;
}
model.stride = 4 * model.blockSize;

let pointIdx = 0;
let normalIdx = 0;
let tcoordIdx = 0;
let colorIdx = 0;
let custIdx = 0;
let cellCount = 0;
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, cellIdx) {
// for lines we add a bunch of segments
for (let i = 0; i < numPoints - 1; ++i) {
addAPoint(cellPts[offset + i], cellIdx);
addAPoint(cellPts[offset + i + 1], cellIdx);
}
},
polysToWireframe(numPoints, cellPts, offset, cellIdx) {
// 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], cellIdx);
addAPoint(cellPts[offset + ((i + 1) % numPoints)], cellIdx);
}
}
},
stripsToWireframe(numPoints, cellPts, offset, cellIdx) {
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], cellIdx);
addAPoint(cellPts[offset + i + 1], cellIdx);
}
for (let i = 0; i < numPoints - 2; i++) {
addAPoint(cellPts[offset + i], cellIdx);
addAPoint(cellPts[offset + i + 2], cellIdx);
}
}
},
polysToSurface(npts, cellPts, offset, cellIdx) {
for (let i = 0; i < npts - 2; i++) {
addAPoint(cellPts[offset + 0], cellIdx);
addAPoint(cellPts[offset + i + 1], cellIdx);
addAPoint(cellPts[offset + i + 2], cellIdx);
}
},
stripsToSurface(npts, cellPts, offset, cellIdx) {
for (let i = 0; i < npts - 2; i++) {
addAPoint(cellPts[offset + i], cellIdx);
addAPoint(cellPts[offset + i + 1 + (i % 2)], cellIdx);
addAPoint(cellPts[offset + i + 1 + ((i + 1) % 2)], cellIdx);
}
},
};

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 (npts > 2) {
return (npts - 2) * 3;
}
return 0;
},
};

let func = null;
let countFunc = null;
if (outRep === Representation.POINTS || inRep === 'verts') {
func = cellBuilders.anythingToPoints;
countFunc = cellCounters.anythingToPoints;
} else if (outRep === Representation.WIREFRAME || inRep === 'lines') {
func = cellBuilders[`${inRep}ToWireframe`];
countFunc = cellCounters[`${inRep}ToWireframe`];
} else {
func = cellBuilders[`${inRep}ToSurface`];
countFunc = cellCounters[`${inRep}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;
}

let packedUCVBO = null;
const packedVBO = new Float32Array(caboCount * model.blockSize);
if (colorData) {
packedUCVBO = new Uint8Array(caboCount * 4);
}
let vboidx = 0;
let ucidx = 0;

// Find out if shift scale should be used
// Compute squares of diagonal size and distance from the origin
let diagSq = 0.0;
let distSq = 0.0;
for (let i = 0; i < 3; ++i) {
const range = options.points.getRange(i);

const delta = range[1] - range[0];
diagSq += delta * delta;

const distShift = 0.5 * (range[1] + range[0]);
distSq += distShift * distShift;
}

const useShiftAndScale =
diagSq > 0 &&
(Math.abs(distSq) / diagSq > 1.0e6 || // If data is far from the origin relative to its size
Math.abs(Math.log10(diagSq)) > 3.0 || // If the size is huge when not far from the origin
(diagSq === 0 && distSq > 1.0e6)); // If data is a point, but far from the origin

if (useShiftAndScale) {
// Compute shift and scale vectors
const coordShift = new Float64Array(3);
const coordScale = new Float64Array(3);
for (let i = 0; i < 3; ++i) {
const range = options.points.getRange(i);
const delta = range[1] - range[0];

coordShift[i] = 0.5 * (range[1] + range[0]);
coordScale[i] = delta > 0 ? 1.0 / delta : 1.0;
}
publicAPI.setCoordShiftAndScale(coordShift, coordScale);
} else if (model.coordShiftAndScaleEnabled === true) {
// Make sure to reset
publicAPI.setCoordShiftAndScale(null, null);
}

// Initialize the structures used to keep track of point ids and cell ids for selectors
if (selectionMaps) {
if (!selectionMaps.points && !selectionMaps.cells) {
selectionMaps.points = new Int32Array(caboCount);
selectionMaps.cells = new Int32Array(caboCount);
} else {
const newPoints = new Int32Array(
caboCount + selectionMaps.points.length
);
newPoints.set(selectionMaps.points);
selectionMaps.points = newPoints;
const newCells = new Int32Array(caboCount + selectionMaps.cells.length);
newCells.set(selectionMaps.cells);
selectionMaps.cells = newCells;
}
}

let pointCount = options.vertexOffset;
addAPoint = function addAPointFunc(pointId, cellId) {
// Keep track of original point and cell ids, for selection
if (selectionMaps) {
selectionMaps.points[pointCount] = pointId;
selectionMaps.cells[pointCount] = cellCount + options.cellOffset;
}
++pointCount;

// Vertices
pointIdx = pointId * 3;

if (!model.coordShiftAndScaleEnabled) {
packedVBO[vboidx++] = pointData[pointIdx++];
packedVBO[vboidx++] = pointData[pointIdx++];
packedVBO[vboidx++] = pointData[pointIdx++];
} else {
// Apply shift and scale
packedVBO[vboidx++] =
(pointData[pointIdx++] - model.coordShift[0]) * model.coordScale[0];
packedVBO[vboidx++] =
(pointData[pointIdx++] - model.coordShift[1]) * model.coordScale[1];
packedVBO[vboidx++] =
(pointData[pointIdx++] - model.coordShift[2]) * model.coordScale[2];
}

if (normalData !== null) {
if (options.haveCellNormals) {
normalIdx = (cellCount + options.cellOffset) * 3;
} else {
normalIdx = pointId * 3;
}
packedVBO[vboidx++] = normalData[normalIdx++];
packedVBO[vboidx++] = normalData[normalIdx++];
packedVBO[vboidx++] = normalData[normalIdx++];
}

model.customData.forEach((attr) => {
custIdx = pointId * attr.components;
for (let j = 0; j < attr.components; ++j) {
packedVBO[vboidx++] = attr.data[custIdx++];
}
});

if (tcoordData !== null) {
if (options.useTCoordsPerCell) {
tcoordIdx = cellId * textureComponents;
} else {
tcoordIdx = pointId * textureComponents;
}
for (let j = 0; j < textureComponents; ++j) {
packedVBO[vboidx++] = tcoordData[tcoordIdx++];
}
}

if (colorData !== null) {
if (options.haveCellScalars) {
colorIdx = (cellCount + options.cellOffset) * colorComponents;
} else {
colorIdx = pointId * colorComponents;
}
packedUCVBO[ucidx++] = colorData[colorIdx++];
packedUCVBO[ucidx++] = colorData[colorIdx++];
packedUCVBO[ucidx++] = colorData[colorIdx++];
packedUCVBO[ucidx++] =
colorComponents === 4 ? colorData[colorIdx++] : 255;
}
};

// Browse the cell array: the index is at the beginning of a cell
// The value of 'array' at the position 'index' is the number of points in the cell
for (let index = 0; index < size; index += array[index] + 1, cellCount++) {
func(array[index], array, index + 1, cellCount + options.cellOffset);
}
model.elementCount = caboCount;
publicAPI.upload(packedVBO, ObjectType.ARRAY_BUFFER);
if (model.colorBO) {
model.colorBOStride = 4;
model.colorBO.upload(packedUCVBO, ObjectType.ARRAY_BUFFER);
}
return cellCount;
};

publicAPI.setCoordShiftAndScale = (coordShift, coordScale) => {
if (
coordShift !== null &&
(coordShift.constructor !== Float64Array || coordShift.length !== 3)
) {
vtkErrorMacro('Wrong type for coordShift, expected vec3 or null');
return;
}

if (
coordScale !== null &&
(coordScale.constructor !== Float64Array || coordScale.length !== 3)
) {
vtkErrorMacro('Wrong type for coordScale, expected vec3 or null');
return;
}

if (
model.coordShift === null ||
coordShift === null ||
!vec3.equals(coordShift, model.coordShift)
) {
model.coordShift = coordShift;
}

if (
model.coordScale === null ||
coordScale === null ||
!vec3.equals(coordScale, model.coordScale)
) {
model.coordScale = coordScale;
}

model.coordShiftAndScaleEnabled = shouldApplyCoordShiftAndScale(
model.coordShift,
model.coordScale
);

if (model.coordShiftAndScaleEnabled) {
model.inverseShiftAndScaleMatrix = computeInverseShiftAndScaleMatrix(
model.coordShift,
model.coordScale
);
} else {
model.inverseShiftAndScaleMatrix = null;
}
};
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
elementCount: 0,
stride: 0,
colorBOStride: 0,
vertexOffset: 0,
normalOffset: 0,
tCoordOffset: 0,
tCoordComponents: 0,
colorOffset: 0,
colorComponents: 0,
tcoordBO: null,
customData: [],
coordShift: null,
coordScale: null,
coordShiftAndScaleEnabled: false,
inverseShiftAndScaleMatrix: null,
};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

// Inheritance
vtkBufferObject.extend(publicAPI, model, initialValues);

macro.setGet(publicAPI, model, [
'colorBO',
'elementCount',
'stride',
'colorBOStride',
'vertexOffset',
'normalOffset',
'tCoordOffset',
'tCoordComponents',
'colorOffset',
'colorComponents',
'customData',
]);

macro.get(publicAPI, model, [
'coordShift',
'coordScale',
'coordShiftAndScaleEnabled',
'inverseShiftAndScaleMatrix',
]);

// Object specific methods
vtkOpenGLCellArrayBufferObject(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(extend);

// ----------------------------------------------------------------------------

export default { newInstance, extend };