import * as THREE from 'three/webgpu';
import { Glyph } from './glyph';
import { ZincObject } from './zincObject';
import { updateWorldMatrixFromAncestors } from '../utilities';
import { JSONLoader } from '../loaders/JSONLoader';
import {
createGlyphTransformCompute,
dispatchAndReadbackGlyphTransform,
isGlyphComputeSupported,
readbackGlyphTransform,
} from '../tsl/glyphTransform';
import { createGlyphInstancedMaterial } from '../tsl/glyphRenderMaterial';
/**
* This is a container of {@link Glyph} and their graphical properties
* including transformations, colors, number of time steps, duration of animations
* and group name. Please note that all glyphs in the glyphset share the same geometry
* however they may have different transformations.
*
* @class
* @author Alan Wu
* @return {Glyphset}
*/
const Glyphset = function () {
ZincObject.call(this);
const glyphList = [];
let axis1s = undefined;
let axis2s = undefined;
let axis3s = undefined;
let positions = undefined;
//Glyph geometry loaded after dispose is discarded
let disposed = false;
let scales = undefined;
let colors = undefined;
let labels = undefined;
let numberOfTimeSteps = 0;
let numberOfVertices = 0;
let baseSize = [0, 0, 0];
let offset = [0, 0, 0];
let labelsOn = false;
let scaleFactors = [0, 0, 0];
let repeat_mode = 'NONE';
this.ready = false;
let morphColours = false;
let morphVertices = false;
this.isGlyphset = true;
let _transformMatrix = new THREE.Matrix4();
const _bot_colour = new THREE.Color();
const _top_colour = new THREE.Color();
const _boundingBox1 = new THREE.Box3();
const _boundingBox2 = new THREE.Box3();
const _boundingBox3 = new THREE.Box3();
const _points = [];
const _current_positions = [];
const _current_axis1s = [];
const _current_axis2s = [];
const _current_axis3s = [];
const _current_scales = [];
const _current_colors = [];
const _glyph_axis_array = [];
this.globalScale = 1;
let glyphCompute = undefined;
let glyphComputePending = false;
// True once a full resync (dispatchGlyphCompute) has completed and nothing
// has invalidated it since; false during active playback and while a resync
// is in flight.
let boundsAreExact = true;
let renderBuffersInitialized = false;
let wasAnimating = false;
// Conservative bounding box unioned across every keyframe's exact
let allFramesBoundingBox = undefined;
// Debounces the accurate GPU->CPU readback (dispatchGlyphCompute) behind
// setMorphTime() - e.g. a downstream slider firing many calls per second
// while being dragged. The fast, readback-free GPU-only update still
// happens on every call (see updateMorphGlyphsets), so visuals stay live;
// Only the costlier readback (bounding box/raycast/export accuracy) waits
// for ACCURATE_RESYNC_DEBOUNCE_MS of quiet.
let accurateResyncTimer = undefined;
const ACCURATE_RESYNC_DEBOUNCE_MS = 500;
//Set to false when the static (non morphing) transformation needs to be
//re-applied, e.g. after a scale change.
let staticTransformationApplied = false;
for (let i = 0; i < 8; i++) {
_points[i] = new THREE.Vector3();
}
/**
* Writes a resolve_glyph_axes() result tuple [point, axis1, axis2, axis3]
* into a THREE.Matrix4's .elements, matching the column-major
* axis1/axis2/axis3/point layout instanceMatrix expects.
*/
const writeTransformMatrix = (matrix, arrayTuple) => {
matrix.elements[0] = arrayTuple[1][0];
matrix.elements[1] = arrayTuple[1][1];
matrix.elements[2] = arrayTuple[1][2];
matrix.elements[3] = 0.0;
matrix.elements[4] = arrayTuple[2][0];
matrix.elements[5] = arrayTuple[2][1];
matrix.elements[6] = arrayTuple[2][2];
matrix.elements[7] = 0.0;
matrix.elements[8] = arrayTuple[3][0];
matrix.elements[9] = arrayTuple[3][1];
matrix.elements[10] = arrayTuple[3][2];
matrix.elements[11] = 0.0;
matrix.elements[12] = arrayTuple[0][0];
matrix.elements[13] = arrayTuple[0][1];
matrix.elements[14] = arrayTuple[0][2];
matrix.elements[15] = 1.0;
};
/**
* Flattens a { "0": [x,y,z,...], "1": [...], ... } per-keyframe object
* (as used for axis1/axis2/axis3/positions/scale) into one contiguous
* [time][record][xyz] buffer for the GPU compute path.
*/
const flattenVec3Frames = (framesObj, steps, baseCount) => {
const out = new Float32Array(steps * baseCount * 3);
for (let t = 0; t < steps; t++) {
const frame = framesObj[t.toString()];
if (frame) {
out.set(frame, t * baseCount * 3);
}
}
return out;
};
/**
* Same as flattenVec3Frames but for the colours object, whose per-frame
* values are one packed hex int per glyph rather than 3 raw components -
* decode each into RGB floats up front so the compute shader only ever
* deals with plain vec3s.
*/
const flattenColorFrames = (framesObj, steps, baseCount) => {
const out = new Float32Array(steps * baseCount * 3);
const tmpColor = new THREE.Color();
for (let t = 0; t < steps; t++) {
const frame = framesObj[t.toString()];
if (frame) {
for (let i = 0; i < baseCount; i++) {
tmpColor.setHex(frame[i]);
const o = (t * baseCount + i) * 3;
out[o] = tmpColor.r;
out[o + 1] = tmpColor.g;
out[o + 2] = tmpColor.b;
}
}
}
return out;
};
/**
* Copy glyphset data into this glyphset then load the glyph's geoemtry
* with the provided glyphURL. FinishCallback will be called once
* glyph is loaded.
*
* @param {Array} glyphsetData - contains the informations about the glyphs.
* @param {String} glyphURL - URL to the geometry which will be applied to all
* all the glyphs in the glyphset once loaded.
* @param {Function} finishCallback - User's function to be called once glyph's
* geometry is loaded.
*/
this.load = (glyphsetData, glyphURL, finishCallback, isInline, displayLabels) => {
disposed = false;
axis1s = glyphsetData.axis1;
axis2s = glyphsetData.axis2;
axis3s = glyphsetData.axis3;
positions = glyphsetData.positions;
scales = glyphsetData.scale;
colors = glyphsetData.colors;
labels = glyphsetData.label;
morphColours = glyphsetData.metadata.MorphColours;
morphVertices = glyphsetData.metadata.MorphVertices;
numberOfTimeSteps = glyphsetData.metadata.number_of_time_steps;
repeat_mode = glyphsetData.metadata.repeat_mode;
numberOfVertices = glyphsetData.metadata.number_of_vertices;
if (repeat_mode == 'AXES_2D' || repeat_mode == 'MIRROR')
numberOfVertices = numberOfVertices * 2;
else if (repeat_mode == 'AXES_3D') numberOfVertices = numberOfVertices * 3;
baseSize = glyphsetData.metadata.base_size;
offset = glyphsetData.metadata.offset;
scaleFactors = glyphsetData.metadata.scale_factors;
glyphCompute = undefined;
//The CPU path is used when the backend does not support GPU compute
if (
morphVertices &&
numberOfTimeSteps > 1 &&
positions &&
positions['0'] &&
isGlyphComputeSupported()
) {
const baseCount = positions['0'].length / 3;
try {
glyphCompute = createGlyphTransformCompute({
baseCount,
outputCount: numberOfVertices,
repeat_mode,
positionsData: flattenVec3Frames(positions, numberOfTimeSteps, baseCount),
axis1Data: flattenVec3Frames(axis1s, numberOfTimeSteps, baseCount),
axis2Data: flattenVec3Frames(axis2s, numberOfTimeSteps, baseCount),
axis3Data: flattenVec3Frames(axis3s, numberOfTimeSteps, baseCount),
scaleData: flattenVec3Frames(scales, numberOfTimeSteps, baseCount),
colorData:
morphColours && colors
? flattenColorFrames(colors, numberOfTimeSteps, baseCount)
: undefined,
baseSize,
offset,
scaleFactors,
globalScale: this.globalScale,
});
} catch (err) {
console.error(
'Failed to set up GPU glyph transform compute, falling back to CPU animation.',
err,
);
glyphCompute = undefined;
}
}
const loader = new JSONLoader();
this.geometry = new THREE.BufferGeometry();
const instancedMesh = new THREE.InstancedMesh(this.geometry, undefined, numberOfVertices);
this.setMorph(instancedMesh);
if (isInline) {
var object = loader.parse(glyphURL);
meshloader(finishCallback, displayLabels)(object.geometry, object.materials);
object.geometry.dispose();
} else {
loader.crossOrigin = 'Anonymous';
loader.load(glyphURL, meshloader(finishCallback, displayLabels));
}
};
/**
* Calculate the actual transformation value that can be applied
* to the transformation matrix.
*
* @returns {Array}
*/
const resolve_glyph_axes = (point, axis1, axis2, axis3, scale, return_arrays) => {
if (repeat_mode == 'NONE' || repeat_mode == 'MIRROR') {
let axis_scale = [0.0, 0.0, 0.0];
let final_axis1 = [0.0, 0.0, 0.0];
let final_axis2 = [0.0, 0.0, 0.0];
let final_axis3 = [0.0, 0.0, 0.0];
let final_point = [0.0, 0.0, 0.0];
const mirrored_axis1 = [0.0, 0.0, 0.0];
const mirrored_axis2 = [0.0, 0.0, 0.0];
const mirrored_axis3 = [0.0, 0.0, 0.0];
const mirrored_point = [0.0, 0.0, 0.0];
for (var j = 0; j < 3; j++) {
var sign = scale[j] < 0.0 ? -1.0 : 1.0;
axis_scale[j] = (sign * baseSize[j] + scale[j] * scaleFactors[j]) * this.globalScale;
}
for (var j = 0; j < 3; j++) {
final_axis1[j] = axis1[j] * axis_scale[0];
final_axis2[j] = axis2[j] * axis_scale[1];
final_axis3[j] = axis3[j] * axis_scale[2];
final_point[j] =
point[j] +
offset[0] * final_axis1[j] +
offset[1] * final_axis2[j] +
offset[2] * final_axis3[j];
if (repeat_mode == 'MIRROR') {
mirrored_axis1[j] = -final_axis1[j];
mirrored_axis2[j] = -final_axis2[j];
mirrored_axis3[j] = -final_axis3[j];
mirrored_point[j] = final_point[j];
if (scale[0] < 0.0) {
// shift glyph origin to end of axis1
final_point[j] -= final_axis1[j];
mirrored_point[j] -= mirrored_axis1[j];
}
}
}
/* if required, reverse axis3 to maintain right-handed coordinate system */
if (
0.0 >
final_axis3[0] * (final_axis1[1] * final_axis2[2] - final_axis1[2] * final_axis2[1]) +
final_axis3[1] * (final_axis1[2] * final_axis2[0] - final_axis1[0] * final_axis2[2]) +
final_axis3[2] * (final_axis1[0] * final_axis2[1] - final_axis1[1] * final_axis2[0])
) {
final_axis3[0] = -final_axis3[0];
final_axis3[1] = -final_axis3[1];
final_axis3[2] = -final_axis3[2];
}
return_arrays[0] = [final_point, final_axis1, final_axis2, final_axis3];
if (repeat_mode == 'MIRROR') {
if (
0.0 >
mirrored_axis3[0] *
(mirrored_axis1[1] * mirrored_axis2[2] - mirrored_axis1[2] * mirrored_axis2[1]) +
mirrored_axis3[1] *
(mirrored_axis1[2] * mirrored_axis2[0] - mirrored_axis1[0] * mirrored_axis2[2]) +
mirrored_axis3[2] *
(mirrored_axis1[0] * mirrored_axis2[1] - mirrored_axis1[1] * mirrored_axis2[0])
) {
mirrored_axis3[0] = -mirrored_axis3[0];
mirrored_axis3[1] = -mirrored_axis3[1];
mirrored_axis3[2] = -mirrored_axis3[2];
}
return_arrays[1] = [mirrored_point, mirrored_axis1, mirrored_axis2, mirrored_axis3];
}
} else if (repeat_mode == 'AXES_2D' || repeat_mode == 'AXES_3D') {
let axis_scale = [0.0, 0.0, 0.0];
let final_point = [0.0, 0.0, 0.0];
for (var j = 0; j < 3; j++) {
var sign = scale[j] < 0.0 ? -1.0 : 1.0;
axis_scale[j] = (sign * baseSize[0] + scale[j] * scaleFactors[0]) * this.globalScale;
}
for (var j = 0; j < 3; j++) {
final_point[j] =
point[j] +
offset[0] * axis_scale[0] * axis1[j] +
offset[1] * axis_scale[1] * axis2[j] +
offset[2] * axis_scale[2] * axis3[j];
}
const number_of_glyphs = repeat_mode == 'AXES_2D' ? 2 : 3;
for (let k = 0; k < number_of_glyphs; k++) {
let use_axis1, use_axis2;
const use_scale = scale[k];
let final_axis1 = [0.0, 0.0, 0.0];
let final_axis2 = [0.0, 0.0, 0.0];
let final_axis3 = [0.0, 0.0, 0.0];
if (k == 0) {
use_axis1 = axis1;
use_axis2 = axis2;
} else if (k == 1) {
use_axis1 = axis2;
use_axis2 = repeat_mode == 'AXES_2D' ? axis1 : axis3;
} else // if (k == 2)
{
use_axis1 = axis3;
use_axis2 = axis1;
}
const final_scale1 = (baseSize[0] + use_scale * scaleFactors[0]) * this.globalScale;
final_axis1[0] = use_axis1[0] * final_scale1;
final_axis1[1] = use_axis1[1] * final_scale1;
final_axis1[2] = use_axis1[2] * final_scale1;
final_axis3[0] = final_axis1[1] * use_axis2[2] - use_axis2[1] * final_axis1[2];
final_axis3[1] = final_axis1[2] * use_axis2[0] - use_axis2[2] * final_axis1[0];
final_axis3[2] = final_axis1[0] * use_axis2[1] - final_axis1[1] * use_axis2[0];
let magnitude = Math.sqrt(
final_axis3[0] * final_axis3[0] +
final_axis3[1] * final_axis3[1] +
final_axis3[2] * final_axis3[2],
);
if (0.0 < magnitude) {
let scaling =
((baseSize[2] + use_scale * scaleFactors[2]) * this.globalScale) / magnitude;
if (repeat_mode == 'AXES_2D' && k > 0) {
scaling *= -1.0;
}
final_axis3[0] *= scaling;
final_axis3[1] *= scaling;
final_axis3[2] *= scaling;
}
final_axis2[0] = final_axis3[1] * final_axis1[2] - final_axis1[1] * final_axis3[2];
final_axis2[1] = final_axis3[2] * final_axis1[0] - final_axis1[2] * final_axis3[0];
final_axis2[2] = final_axis3[0] * final_axis1[1] - final_axis3[1] * final_axis1[0];
magnitude = Math.sqrt(
final_axis2[0] * final_axis2[0] +
final_axis2[1] * final_axis2[1] +
final_axis2[2] * final_axis2[2],
);
if (0.0 < magnitude) {
var scaling =
((baseSize[1] + use_scale * scaleFactors[1]) * this.globalScale) / magnitude;
final_axis2[0] *= scaling;
final_axis2[1] *= scaling;
final_axis2[2] *= scaling;
}
return_arrays[k] = [final_point, final_axis1, final_axis2, final_axis3];
}
}
return return_arrays;
};
/**
* Update transformation for each of the glyph in this glyphset.
*/
const updateGlyphsetTransformation = (
current_positions,
current_axis1s,
current_axis2s,
current_axis3s,
current_scales,
) => {
let numberOfGlyphs = 1;
if (repeat_mode == 'AXES_2D' || repeat_mode == 'MIRROR') numberOfGlyphs = 2;
else if (repeat_mode == 'AXES_3D') numberOfGlyphs = 3;
const numberOfPositions = current_positions.length / 3;
let current_glyph_index = 0;
_glyph_axis_array.length = numberOfGlyphs;
for (let i = 0; i < numberOfPositions; i++) {
const current_index = i * 3;
const current_position = [
current_positions[current_index],
current_positions[current_index + 1],
current_positions[current_index + 2],
];
const current_axis1 = [
current_axis1s[current_index],
current_axis1s[current_index + 1],
current_axis1s[current_index + 2],
];
const current_axis2 = [
current_axis2s[current_index],
current_axis2s[current_index + 1],
current_axis2s[current_index + 2],
];
const current_axis3 = [
current_axis3s[current_index],
current_axis3s[current_index + 1],
current_axis3s[current_index + 2],
];
const current_scale = [
current_scales[current_index],
current_scales[current_index + 1],
current_scales[current_index + 2],
];
const arrays = resolve_glyph_axes(
current_position,
current_axis1,
current_axis2,
current_axis3,
current_scale,
_glyph_axis_array,
);
if (arrays.length == numberOfGlyphs) {
for (let j = 0; j < numberOfGlyphs; j++) {
writeTransformMatrix(_transformMatrix, arrays[j]);
this.morph.setMatrixAt(current_glyph_index, _transformMatrix);
const glyph = glyphList[current_glyph_index];
if (glyph) {
glyph.setTransformation(arrays[j][0], arrays[j][1], arrays[j][2], arrays[j][3]);
}
current_glyph_index++;
}
}
}
this.morph.instanceMatrix.needsUpdate = true;
this.boundingBoxUpdateRequired = true;
this.morph.computeBoundingSphere();
};
/**
* Update colour for each of the glyph in this glyphset.
*
* @param {Array} current_colors - one hex value per raw (pre-repeat_mode-
* expansion) glyph record.
* @param {Boolean} labelsOnly - when true, only glyph labels are
* recoloured - instanceColor is left untouched (not even created). Used
* for the initial load-time colour of GPU-compute-driven glyphsets (see
* createGlyphs()): instanceColor must stay uncreated there (see
* applyGlyphComputeResult's comment on the NodeMaterial double-multiply
* this avoids), but labels aren't part of the compute-buffer render path
* at all and would otherwise show no colour until the first accurate
* resync.
*/
const updateGlyphsetHexColors = (current_colors, labelsOnly) => {
let numberOfGlyphs = 1;
if (repeat_mode == 'AXES_2D' || repeat_mode == 'MIRROR') numberOfGlyphs = 2;
else if (repeat_mode == 'AXES_3D') numberOfGlyphs = 3;
const numberOfColours = current_colors.length;
let current_glyph_index = 0;
for (let i = 0; i < numberOfColours; i++) {
const hex_values = current_colors[i];
for (let j = 0; j < numberOfGlyphs; j++) {
_bot_colour.setHex(hex_values);
if (!labelsOnly) {
this.morph.setColorAt(current_glyph_index, _bot_colour);
}
const glyph = glyphList[current_glyph_index];
if (glyph) glyph.setColour(_bot_colour);
current_glyph_index++;
}
}
if (!labelsOnly) {
this.morph.instanceColor.needsUpdate = true;
}
};
/**
* Writes back a GPU glyph-transform compute readback (see
* dispatchGlyphCompute) into instanceMatrix/instanceColor - i.e. exactly
* the data updateGlyphsetTransformation()/updateGlyphsetHexColors() write
* on the CPU path, so bounding box/raycast/getClosestVertex keep working
* completely unchanged downstream.
*/
const applyGlyphComputeResult = (result) => {
this.requestRender();
const matrixArray = this.morph.instanceMatrix.array;
const updateLabels = this.canShowLabel();
for (let i = 0; i < numberOfVertices; i++) {
const o4 = i * 4;
const o16 = i * 16;
matrixArray[o16] = result.axis1[o4];
matrixArray[o16 + 1] = result.axis1[o4 + 1];
matrixArray[o16 + 2] = result.axis1[o4 + 2];
matrixArray[o16 + 3] = 0;
matrixArray[o16 + 4] = result.axis2[o4];
matrixArray[o16 + 5] = result.axis2[o4 + 1];
matrixArray[o16 + 6] = result.axis2[o4 + 2];
matrixArray[o16 + 7] = 0;
matrixArray[o16 + 8] = result.axis3[o4];
matrixArray[o16 + 9] = result.axis3[o4 + 1];
matrixArray[o16 + 10] = result.axis3[o4 + 2];
matrixArray[o16 + 11] = 0;
matrixArray[o16 + 12] = result.position[o4];
matrixArray[o16 + 13] = result.position[o4 + 1];
matrixArray[o16 + 14] = result.position[o4 + 2];
matrixArray[o16 + 15] = 1;
if (updateLabels) {
const glyph = glyphList[i];
if (glyph) {
glyph.setTransformation(
[result.position[o4], result.position[o4 + 1], result.position[o4 + 2]],
[result.axis1[o4], result.axis1[o4 + 1], result.axis1[o4 + 2]],
[result.axis2[o4], result.axis2[o4 + 1], result.axis2[o4 + 2]],
[result.axis3[o4], result.axis3[o4 + 1], result.axis3[o4 + 2]],
);
}
}
}
this.morph.instanceMatrix.needsUpdate = true;
this.boundingBoxUpdateRequired = true;
this.morph.computeBoundingSphere();
if (result.color) {
const colorArray = this.morph.instanceColor ? this.morph.instanceColor.array : undefined;
for (let i = 0; i < numberOfVertices; i++) {
const o4 = i * 4;
if (colorArray) {
const o3 = i * 3;
colorArray[o3] = result.color[o4];
colorArray[o3 + 1] = result.color[o4 + 1];
colorArray[o3 + 2] = result.color[o4 + 2];
}
if (updateLabels) {
const glyph = glyphList[i];
if (glyph) {
_bot_colour.setRGB(result.color[o4], result.color[o4 + 1], result.color[o4 + 2]);
glyph.setColour(_bot_colour);
}
}
}
if (colorArray) {
this.morph.instanceColor.needsUpdate = true;
}
}
};
/**
* Kicks off (at most one in flight at a time) an async GPU compute +
* readback for the current bottom/top frame + proportion. If a previous
* dispatch is still resolving, this frame's update is skipped rather than
* queued - the next call to updateMorphGlyphsets() will dispatch with
* whatever bottom/top/proportion is current *then*, so playback can't
* fall further and further behind, at the cost of the displayed state
* lagging the true target time by about one frame while animating.
*/
const dispatchGlyphCompute = (bottom_frame, top_frame, proportion) => {
if (glyphComputePending) return;
const renderer = this.region?.getScene?.()?.getRenderer?.();
if (!renderer) return;
glyphCompute.uniforms.bottomFrame.value = bottom_frame;
glyphCompute.uniforms.topFrame.value = top_frame;
glyphCompute.uniforms.proportion.value = proportion;
glyphCompute.uniforms.globalScale.value = this.globalScale;
glyphComputePending = true;
boundsAreExact = false;
dispatchAndReadbackGlyphTransform(renderer, glyphCompute)
.then((result) => {
glyphComputePending = false;
applyGlyphComputeResult(result);
boundsAreExact = true;
// Restore the stock InstancedMesh.raycast (instanceMatrix is now
// exact again) by dropping the no-op instance override, if present.
delete this.morph.raycast;
})
.catch((err) => {
glyphComputePending = false;
console.error('Glyph transform compute readback failed.', err);
});
};
/**
* The fast per-frame path used while actively playing: only dispatches
* the compute pass (updating the GPU buffers the render material reads
* directly - see ../tsl/glyphRenderMaterial.js) with no CPU readback at
* all, bouding box will not be updated.
*/
const dispatchGlyphComputeFast = (bottom_frame, top_frame, proportion) => {
this.requestRender();
const renderer = this.region?.getScene?.()?.getRenderer?.();
if (!renderer) return false;
glyphCompute.uniforms.bottomFrame.value = bottom_frame;
glyphCompute.uniforms.topFrame.value = top_frame;
glyphCompute.uniforms.proportion.value = proportion;
glyphCompute.uniforms.globalScale.value = this.globalScale;
renderer.compute(glyphCompute.compute);
boundsAreExact = false;
this.morph.raycast = () => {};
renderBuffersInitialized = true;
return true;
};
/**
* The readback half of the debounced accurate resync (see
* scheduleAccurateResync): unlike dispatchGlyphCompute(), this does NOT
* call renderer.compute() first - by the time this fires, the buffers
* already hold the right result.
*/
const readbackGlyphComputeOnly = () => {
if (glyphComputePending) return;
const renderer = this.region?.getScene?.()?.getRenderer?.();
if (!renderer) return;
glyphComputePending = true;
readbackGlyphTransform(renderer, glyphCompute)
.then((result) => {
glyphComputePending = false;
applyGlyphComputeResult(result);
boundsAreExact = true;
delete this.morph.raycast;
})
.catch((err) => {
glyphComputePending = false;
console.error('Glyph transform compute readback failed.', err);
});
};
const cancelScheduledAccurateResync = () => {
if (accurateResyncTimer !== undefined) {
clearTimeout(accurateResyncTimer);
accurateResyncTimer = undefined;
}
};
/**
* (Re)schedules a single accurate resync ACCURATE_RESYNC_DEBOUNCE_MS from
* now, cancelling any previously scheduled one.
*/
const scheduleAccurateResync = () => {
cancelScheduledAccurateResync();
accurateResyncTimer = setTimeout(() => {
accurateResyncTimer = undefined;
readbackGlyphComputeOnly();
}, ACCURATE_RESYNC_DEBOUNCE_MS);
};
/**
* Update the current states of the glyphs in this glyphset, this includes transformation and
* colour for each of them. This is called when glyphset and glyphs are initialised and whenever
* the internal time has been updated.
*
* @param {Boolean} debounceAccurateResync - When true the costlier GPU->CPU readback
* (bounding box/raycast/ export accuracy - see dispatchGlyphCompute) is debounced
* until updates have been quiet for ACCURATE_RESYNC_DEBOUNCE_MS. When false/omitted
* (e.g. the render loop's animation-just-stopped resync), the readback happens
* immediately, as before.
*/
const updateMorphGlyphsets = (debounceAccurateResync) => {
const current_time = (this.inbuildTime / this.duration) * (numberOfTimeSteps - 1);
const bottom_frame = Math.floor(current_time);
const proportion = 1 - (current_time - bottom_frame);
const top_frame = Math.ceil(current_time);
if (morphVertices && glyphCompute) {
if (debounceAccurateResync) {
dispatchGlyphComputeFast(bottom_frame, top_frame, proportion);
scheduleAccurateResync();
} else {
cancelScheduledAccurateResync();
// GPU path: transform (and colour, if morphColours) for this frame -
// handles both updateGlyphsetTransformation() and
// updateGlyphsetHexColors() below in one dispatch.
dispatchGlyphCompute(bottom_frame, top_frame, proportion);
}
return;
}
let current_positions = _current_positions;
let current_axis1s = _current_axis1s;
let current_axis2s = _current_axis2s;
let current_axis3s = _current_axis3s;
let current_scales = _current_scales;
let current_colors = _current_colors;
if (morphVertices) {
const bottom_positions = positions[bottom_frame.toString()];
const top_positions = positions[top_frame.toString()];
const bottom_axis1 = axis1s[bottom_frame.toString()];
const top_axis1 = axis1s[top_frame.toString()];
const bottom_axis2 = axis2s[bottom_frame.toString()];
const top_axis2 = axis2s[top_frame.toString()];
const bottom_axis3 = axis3s[bottom_frame.toString()];
const top_axis3 = axis3s[top_frame.toString()];
const bottom_scale = scales[bottom_frame.toString()];
const top_scale = scales[top_frame.toString()];
_current_positions.length = bottom_positions.length;
_current_axis1s.length = bottom_positions.length;
_current_axis2s.length = bottom_positions.length;
_current_axis3s.length = bottom_positions.length;
_current_scales.length = bottom_positions.length;
for (let i = 0; i < bottom_positions.length; i++) {
current_positions[i] =
proportion * bottom_positions[i] + (1.0 - proportion) * top_positions[i];
current_axis1s[i] = proportion * bottom_axis1[i] + (1.0 - proportion) * top_axis1[i];
current_axis2s[i] = proportion * bottom_axis2[i] + (1.0 - proportion) * top_axis2[i];
current_axis3s[i] = proportion * bottom_axis3[i] + (1.0 - proportion) * top_axis3[i];
current_scales[i] = proportion * bottom_scale[i] + (1.0 - proportion) * top_scale[i];
}
} else {
current_positions = positions['0'];
current_axis1s = axis1s['0'];
current_axis2s = axis2s['0'];
current_axis3s = axis3s['0'];
current_scales = scales['0'];
}
//Colour only morphing does not change the transformation, skip it
//unless it is out of date.
if (morphVertices || !staticTransformationApplied) {
updateGlyphsetTransformation(
current_positions,
current_axis1s,
current_axis2s,
current_axis3s,
current_scales,
);
staticTransformationApplied = !morphVertices;
}
if (colors != undefined) {
if (morphColours) {
const bottom_colors = colors[bottom_frame.toString()];
const top_colors = colors[top_frame.toString()];
current_colors.length = bottom_colors.length;
for (let i = 0; i < bottom_colors.length; i++) {
_bot_colour.setHex(bottom_colors[i]);
_top_colour.setHex(top_colors[i]);
_bot_colour.setRGB(
_bot_colour.r * proportion + _top_colour.r * (1 - proportion),
_bot_colour.g * proportion + _top_colour.g * (1 - proportion),
_bot_colour.b * proportion + _top_colour.b * (1 - proportion),
);
current_colors[i] = _bot_colour.getHex();
}
} else {
current_colors = colors['0'];
}
updateGlyphsetHexColors(current_colors, false);
}
};
/**
* Check if the glyph transforms are animated on the GPU.
*
* @return {Boolean}
*/
this.isUsingGPUCompute = () => {
return glyphCompute !== undefined;
};
/**
* Get the assigned label for instance at index
*/
this.getLabel = (index) => {
if (labels && labels.length > index) {
return labels[index];
}
return undefined;
};
this.isLabelDisplayed = () => {
return labelsOn;
};
/**
* Check whether label can be shown
*/
this.canShowLabel = () => {
return glyphList?.length && (labels?.length || this.groupName);
};
/**
* Display the label of the glyphs in the glyphset.
*/
this.showLabel = () => {
this.requestRender();
if (glyphList?.length) {
labelsOn = true;
for (let i = 0; i < glyphList.length; i++) {
glyphList[i].showLabel(this.morph.material ? this.morph.material.color : undefined);
}
}
};
/**
* Hide label with the choosen colour.
*/
this.hideLabel = () => {
this.requestRender();
for (let i = 0; i < glyphList.length; i++) {
glyphList[i].hideLabel();
}
labelsOn = false;
};
/**
* Computes a single Box3 that conservatively contains every keyframe's
* exact per-instance transform of the base glyph geometry, for use as
* getBoundingBox()'s answer while boundsAreExact is false (see its
* declaration above for the caveat on this not being a formally proven
* bound). One-time cost at load, reusing resolve_glyph_axes() the same
* way updateGlyphsetTransformation() does, just for every keyframe
* instead of just the current one, unioned together.
*/
const computeAllFramesBoundingBox = () => {
const localBox = new THREE.Box3().setFromBufferAttribute(this.geometry.attributes.position);
const tempMatrix = new THREE.Matrix4();
const tempBox = new THREE.Box3();
const tempArray = [];
const result = new THREE.Box3();
let first = true;
for (let t = 0; t < numberOfTimeSteps; t++) {
const tPositions = positions[t.toString()];
const tAxis1 = axis1s[t.toString()];
const tAxis2 = axis2s[t.toString()];
const tAxis3 = axis3s[t.toString()];
const tScales = scales[t.toString()];
if (!tPositions) continue;
const numberOfPositions = tPositions.length / 3;
for (let i = 0; i < numberOfPositions; i++) {
const idx = i * 3;
const point = [tPositions[idx], tPositions[idx + 1], tPositions[idx + 2]];
const a1 = [tAxis1[idx], tAxis1[idx + 1], tAxis1[idx + 2]];
const a2 = [tAxis2[idx], tAxis2[idx + 1], tAxis2[idx + 2]];
const a3 = [tAxis3[idx], tAxis3[idx + 1], tAxis3[idx + 2]];
const sc = [tScales[idx], tScales[idx + 1], tScales[idx + 2]];
const arrays = resolve_glyph_axes(point, a1, a2, a3, sc, tempArray);
for (let j = 0; j < arrays.length; j++) {
writeTransformMatrix(tempMatrix, arrays[j]);
tempBox.copy(localBox).applyMatrix4(tempMatrix);
if (first) {
result.copy(tempBox);
first = false;
} else {
result.union(tempBox);
}
}
}
}
return first ? undefined : result;
};
/**
* Create the glyphs in the glyphset.
*
* @param {Boolean} displayLabels -Flag to determine either the labels should be display or not.
*/
const createGlyphs = (displayLabels) => {
for (let i = 0; i < numberOfVertices; i++) {
const glyph = new Glyph(undefined, undefined, i, this);
let label = labels ? labels[i] : undefined;
label = label ? label : this.groupName;
if (label) {
glyph.setLabel(label);
}
if (numberOfTimeSteps > 0) {
glyph.setFrustumCulled(false);
}
glyphList[i] = glyph;
this.morph.add(glyph.getGroup());
}
//Only display labels if the label list is available
if (labels && labels.length > 0 && displayLabels) {
this.showLabel();
}
//Update the transformation of the glyphs.
updateGlyphsetTransformation(
positions['0'],
axis1s['0'],
axis2s['0'],
axis3s['0'],
scales['0'],
);
staticTransformationApplied = true;
if (colors != undefined) {
updateGlyphsetHexColors(colors['0'], !!(glyphCompute && glyphCompute.outputs.color));
}
if (glyphCompute) {
allFramesBoundingBox = computeAllFramesBoundingBox();
}
this.ready = true;
};
/**
* Add a custom {@link Glyph} to this {@link Glyphset}.
*
* @param {Glyph} Glyph to be added.
*/
this.addCustomGlyph = (glyph) => {
this.requestRender();
if (glyph.isGlyph) glyphList.push(glyph);
this.ready = true;
this.boundingBoxUpdateRequired = true;
};
/**
* Add a THREE.Mesh object to be displayed as glyph in this {@link Glyphset}.
*
* @param {THREE.Mesh} Mesh to be added.
* @param {Number} id of the mesh.
*/
this.addMeshAsGlyph = (mesh, id) => {
this.requestRender();
if (mesh.isMesh) {
const glyph = new Glyph(undefined, undefined, id, this);
glyph.fromMesh(mesh);
glyphList.push(glyph);
this.morph.add(glyph.getGroup());
this.ready = true;
this.boundingBoxUpdateRequired = true;
return glyph;
}
return undefined;
};
/**
* A function which iterates through the list of glyphs and call the callback
* function with the glyph as the argument.
*
* @param {Function} callbackFunction - Callback function with the glyph
* as an argument.
*/
this.forEachGlyph = (callbackFunction) => {
for (let i = 0; i < glyphList.length; i++) {
callbackFunction(glyphList[i]);
}
};
var meshloader = (finishCallback, displayLabels) => {
return (geometry, materials) => {
if (disposed) {
geometry.dispose();
return;
}
this.geometry.copy(geometry);
this.geometry.computeBoundingSphere();
this.geometry.computeBoundingBox();
if (materials && materials[0]) this.morph.material = materials[0];
if (glyphCompute) {
this.morph.material = createGlyphInstancedMaterial(this.morph.material, glyphCompute);
}
createGlyphs(displayLabels);
this.morph.name = this.groupName;
this.morph.userData = this;
this.setMorph(this.morph);
geometry.dispose();
if (finishCallback != undefined && typeof finishCallback == 'function') finishCallback(this);
};
};
/**
* Get the index of the closest vertex to centroid.
*/
this.getClosestVertexIndex = function () {
let closestIndex = -1;
if (this.morph && this.ready) {
this.getBoundingBox().getCenter(this._v1);
let current_positions = positions['0'];
const numberOfPositions = current_positions.length / 3;
let distance = -1;
let currentDistance = 0;
for (let i = 0; i < numberOfPositions; i++) {
const current_index = i * 3;
this._v2.set(
current_positions[current_index],
current_positions[current_index + 1],
current_positions[current_index + 2],
);
currentDistance = this._v1.distanceTo(this._v2);
if (distance == -1) {
distance = currentDistance;
closestIndex = i;
} else if (distance > currentDistance) {
distance = currentDistance;
closestIndex = i;
}
}
}
return closestIndex;
};
/**
* Get the closest vertex to centroid.
*/
this.getClosestVertex = function () {
if (this.closestVertexIndex == -1) {
this.closestVertexIndex = this.getClosestVertexIndex();
}
if (this.closestVertexIndex >= 0) {
/*
if (glyphList && glyphList[this.closestVertexIndex]) {
glyphList[this.closestVertexIndex].getBoundingBox().getCenter(position);
}
*/
if (this.morph) {
let position = new THREE.Vector3();
this.morph.getMatrixAt(this.closestVertexIndex, _transformMatrix);
position.setFromMatrixPosition(_transformMatrix);
return position;
}
}
return undefined;
};
/**
* Get the bounding box for the whole set of glyphs.
*
* @return {Three.Box3};
*/
this.getBoundingBox = () => {
if (this.morph && this.ready && this.morph.visible) {
if (glyphCompute && !boundsAreExact && allFramesBoundingBox) {
this.cachedBoundingBox.copy(allFramesBoundingBox);
this.cachedBoundingBox.applyMatrix4(updateWorldMatrixFromAncestors(this.morph));
return this.cachedBoundingBox;
}
if (this.boundingBoxUpdateRequired) {
_boundingBox1.setFromBufferAttribute(this.morph.geometry.attributes.position);
for (let i = 0; i < numberOfVertices; i++) {
this.morph.getMatrixAt(i, _transformMatrix);
_boundingBox2.copy(_boundingBox1).applyMatrix4(_transformMatrix);
if (i == 0) {
_boundingBox3.copy(_boundingBox2);
} else {
_boundingBox3.union(_boundingBox2);
}
}
if (_boundingBox3) {
this.cachedBoundingBox.copy(_boundingBox3);
this.cachedBoundingBox.applyMatrix4(updateWorldMatrixFromAncestors(this.morph));
this.boundingBoxUpdateRequired = false;
} else return undefined;
}
return this.cachedBoundingBox;
}
return undefined;
};
/**
* Set the local time of this glyphset.
*
* @param {Number} time - Can be any value between 0 to duration.
*/
this.setMorphTime = (time) => {
this.requestRender();
if (time > this.duration) this.inbuildTime = this.duration;
else if (0 > time) this.inbuildTime = 0;
else this.inbuildTime = time;
if (morphColours || morphVertices) {
updateMorphGlyphsets(true);
if (morphVertices) this.markerUpdateRequired = true;
}
};
/**
* Check if the glyphset is time varying.
*
* @return {Boolean}
*/
this.isTimeVarying = () => {
if ((this.ready === false || numberOfTimeSteps > 0) && (morphColours || morphVertices))
return true;
return false;
};
/**
* Get the current inbuild time of the
*
* @return {Number}
*/
this.getCurrentTime = () => {
return this.inbuildTime;
};
/**
* Exporters (see sceneExporter.js/GLTFExporter.js) read
* THREE.InstancedMesh.instanceColor directly off this.morph - which
* createGlyphs() intentionally leaves null whenever colour is
* glyphCompute-driven. No-op for glyphsets whose colour isn't glyphCompute-driven
* (instanceColor there is already accurate, ordinary CPU-driven state).
*
* @return {Promise}
*/
this.prepareColorForExport = async () => {
if (!(glyphCompute && glyphCompute.outputs.color)) return;
const renderer = this.region?.getScene?.()?.getRenderer?.();
if (!renderer) return;
renderer.compute(glyphCompute.compute);
const colorResult = (await readbackGlyphTransform(renderer, glyphCompute)).color;
// setColorAt(0, ...) is only here to lazily allocate instanceColor the
// same way the stock InstancedMesh API would; the loop below overwrites
// every instance (including 0) with the real snapshot right after.
this.morph.setColorAt(0, _bot_colour);
const colorArray = this.morph.instanceColor.array;
for (let i = 0; i < numberOfVertices; i++) {
const o4 = i * 4;
const o3 = i * 3;
colorArray[o3] = colorResult[o4];
colorArray[o3 + 1] = colorResult[o4 + 1];
colorArray[o3 + 2] = colorResult[o4 + 2];
}
this.morph.instanceColor.needsUpdate = true;
};
/**
* Undoes prepareColorForExport() - see its comment for why this matters:
* without it, the next live render would permanently pick up the stale
* instanceColor multiply bug this whole design avoids.
*/
this.clearColorExportState = () => {
if (glyphCompute && glyphCompute.outputs.color && this.morph.instanceColor) {
this.morph.instanceColor = null;
if (this.morph.material) this.morph.material.needsUpdate = true;
}
};
/**
* Set the objects scale.
*
* @return {THREE.Box3}.
*/
this.setScaleAll = function (scale) {
this.requestRender();
this.globalScale = scale;
staticTransformationApplied = false;
updateMorphGlyphsets(false);
};
/**
* Clear this glyphset and its list of glyphs which will release them from the memory.
*/
this.dispose = () => {
disposed = true;
for (let i = glyphList.length - 1; i >= 0; i--) {
glyphList[i].dispose();
}
if (this.geometry) this.geometry.dispose();
if (this.morph) this.morph.material.dispose();
cancelScheduledAccurateResync();
axis1s = undefined;
axis2s = undefined;
axis3s = undefined;
positions = undefined;
scales = undefined;
colors = undefined;
glyphCompute = undefined;
glyphComputePending = false;
boundsAreExact = true;
renderBuffersInitialized = false;
wasAnimating = false;
allFramesBoundingBox = undefined;
this.ready = false;
this.groupName = undefined;
};
/**
* Update the glyphsets if required the render.
*/
this.render = (delta, playAnimation, cameraControls, options) => {
if (glyphCompute && !renderBuffersInitialized) {
const renderer = this.region?.getScene?.()?.getRenderer?.();
if (renderer) {
renderer.compute(glyphCompute.compute);
renderBuffersInitialized = true;
this.requestRender();
}
}
if (playAnimation == true) {
let targetTime = this.inbuildTime + delta;
if (targetTime > this.duration) targetTime = targetTime - this.duration;
this.inbuildTime = targetTime;
if (morphVertices && glyphCompute) {
// Fast GPU-only path: update the render buffers every frame with
// no CPU readback. Colour (if morphColours) rides along in the
// same compute pass/buffers - see glyphRenderMaterial.js's colorNode.
// Also drop any accurate resync a prior setMorphTime() call (e.g. a
// downstream slider) left debounced-and-pending - it would apply a
// now-stale frame once it fired mid-animation otherwise.
cancelScheduledAccurateResync();
const current_time = (this.inbuildTime / this.duration) * (numberOfTimeSteps - 1);
const bottom_frame = Math.floor(current_time);
const proportion = 1 - (current_time - bottom_frame);
const top_frame = Math.ceil(current_time);
dispatchGlyphComputeFast(bottom_frame, top_frame, proportion);
} else if (morphColours || morphVertices) {
updateMorphGlyphsets(false);
}
wasAnimating = true;
} else {
if (wasAnimating && glyphCompute) {
// Just stopped: one accurate resync so bounding-box/raycast are
// exact again for as long as playback stays paused.
updateMorphGlyphsets(false);
}
wasAnimating = false;
}
this.updateMarker(playAnimation, options);
};
};
Glyphset.prototype = Object.create(ZincObject.prototype);
export { Glyphset };