zincjs 2.4.1-beta.2 → 2.4.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/zinc.js +3 -3
- package/build/zinc.js.map +1 -1
- package/package.json +1 -1
- package/src/loaders/GLTFToZincJSLoader.js +56 -2
- package/src/loaders/niftiReader.js +2 -2
- package/src/primitives/geometry.js +1 -1
- package/src/primitives/glyph.js +18 -4
- package/src/primitives/glyphset.js +460 -38
- package/src/primitives/lines.js +1 -1
- package/src/primitives/pointset.js +13 -22
- package/src/primitives/textureSlides.js +3 -1
- package/src/region.js +9 -0
- package/src/renderer.js +25 -6
- package/src/scene.js +26 -2
- package/src/sceneExporter.js +94 -12
- package/src/tsl/glyphRenderMaterial.js +53 -0
- package/src/tsl/glyphTransform.js +306 -0
- package/src/{tls → tsl}/morphColorMaterial.js +4 -14
- package/src/{tls → tsl}/textureSlides.js +4 -1
- package/vite.config.js +1 -0
- package/src/three/GLTFExporter.js +0 -2501
- /package/src/{tls → tsl}/pointsMaterial.js +0 -0
|
@@ -2,6 +2,8 @@ import * as THREE from 'three/webgpu';
|
|
|
2
2
|
import { Glyph } from './glyph';
|
|
3
3
|
import { ZincObject } from './zincObject';
|
|
4
4
|
import { JSONLoader } from '../loaders/JSONLoader';
|
|
5
|
+
import { createGlyphTransformCompute, dispatchAndReadbackGlyphTransform, readbackGlyphTransform } from '../tsl/glyphTransform';
|
|
6
|
+
import { createGlyphInstancedMaterial } from '../tsl/glyphRenderMaterial';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* This is a container of {@link Glyph} and their graphical properties
|
|
@@ -49,10 +51,92 @@ const Glyphset = function () {
|
|
|
49
51
|
const _current_colors = [];
|
|
50
52
|
const _glyph_axis_array = [];
|
|
51
53
|
this.globalScale = 1;
|
|
54
|
+
let glyphCompute = undefined;
|
|
55
|
+
let glyphComputePending = false;
|
|
56
|
+
// True once a full resync (dispatchGlyphCompute) has completed and nothing
|
|
57
|
+
// has invalidated it since; false during active playback and while a resync
|
|
58
|
+
// is in flight.
|
|
59
|
+
let boundsAreExact = true;
|
|
60
|
+
let renderBuffersInitialized = false;
|
|
61
|
+
let wasAnimating = false;
|
|
62
|
+
// Conservative bounding box unioned across every keyframe's exact
|
|
63
|
+
let allFramesBoundingBox = undefined;
|
|
64
|
+
// Debounces the accurate GPU->CPU readback (dispatchGlyphCompute) behind
|
|
65
|
+
// setMorphTime() - e.g. a downstream slider firing many calls per second
|
|
66
|
+
// while being dragged. The fast, readback-free GPU-only update still
|
|
67
|
+
// happens on every call (see updateMorphGlyphsets), so visuals stay live;
|
|
68
|
+
// Only the costlier readback (bounding box/raycast/export accuracy) waits
|
|
69
|
+
// for ACCURATE_RESYNC_DEBOUNCE_MS of quiet.
|
|
70
|
+
let accurateResyncTimer = undefined;
|
|
71
|
+
const ACCURATE_RESYNC_DEBOUNCE_MS = 500;
|
|
52
72
|
for (let i = 0; i < 8; i++) {
|
|
53
73
|
_points[i] = new THREE.Vector3();
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Writes a resolve_glyph_axes() result tuple [point, axis1, axis2, axis3]
|
|
78
|
+
* into a THREE.Matrix4's .elements, matching the column-major
|
|
79
|
+
* axis1/axis2/axis3/point layout instanceMatrix expects.
|
|
80
|
+
*/
|
|
81
|
+
const writeTransformMatrix = (matrix, arrayTuple) => {
|
|
82
|
+
matrix.elements[0] = arrayTuple[1][0];
|
|
83
|
+
matrix.elements[1] = arrayTuple[1][1];
|
|
84
|
+
matrix.elements[2] = arrayTuple[1][2];
|
|
85
|
+
matrix.elements[3] = 0.0;
|
|
86
|
+
matrix.elements[4] = arrayTuple[2][0];
|
|
87
|
+
matrix.elements[5] = arrayTuple[2][1];
|
|
88
|
+
matrix.elements[6] = arrayTuple[2][2];
|
|
89
|
+
matrix.elements[7] = 0.0;
|
|
90
|
+
matrix.elements[8] = arrayTuple[3][0];
|
|
91
|
+
matrix.elements[9] = arrayTuple[3][1];
|
|
92
|
+
matrix.elements[10] = arrayTuple[3][2];
|
|
93
|
+
matrix.elements[11] = 0.0;
|
|
94
|
+
matrix.elements[12] = arrayTuple[0][0];
|
|
95
|
+
matrix.elements[13] = arrayTuple[0][1];
|
|
96
|
+
matrix.elements[14] = arrayTuple[0][2];
|
|
97
|
+
matrix.elements[15] = 1.0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Flattens a { "0": [x,y,z,...], "1": [...], ... } per-keyframe object
|
|
102
|
+
* (as used for axis1/axis2/axis3/positions/scale) into one contiguous
|
|
103
|
+
* [time][record][xyz] buffer for the GPU compute path.
|
|
104
|
+
*/
|
|
105
|
+
const flattenVec3Frames = (framesObj, steps, baseCount) => {
|
|
106
|
+
const out = new Float32Array(steps * baseCount * 3);
|
|
107
|
+
for (let t = 0; t < steps; t++) {
|
|
108
|
+
const frame = framesObj[t.toString()];
|
|
109
|
+
if (frame) {
|
|
110
|
+
out.set(frame, t * baseCount * 3);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Same as flattenVec3Frames but for the colours object, whose per-frame
|
|
118
|
+
* values are one packed hex int per glyph rather than 3 raw components -
|
|
119
|
+
* decode each into RGB floats up front so the compute shader only ever
|
|
120
|
+
* deals with plain vec3s.
|
|
121
|
+
*/
|
|
122
|
+
const flattenColorFrames = (framesObj, steps, baseCount) => {
|
|
123
|
+
const out = new Float32Array(steps * baseCount * 3);
|
|
124
|
+
const tmpColor = new THREE.Color();
|
|
125
|
+
for (let t = 0; t < steps; t++) {
|
|
126
|
+
const frame = framesObj[t.toString()];
|
|
127
|
+
if (frame) {
|
|
128
|
+
for (let i = 0; i < baseCount; i++) {
|
|
129
|
+
tmpColor.setHex(frame[i]);
|
|
130
|
+
const o = (t * baseCount + i) * 3;
|
|
131
|
+
out[o] = tmpColor.r;
|
|
132
|
+
out[o + 1] = tmpColor.g;
|
|
133
|
+
out[o + 2] = tmpColor.b;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
56
140
|
/**
|
|
57
141
|
* Copy glyphset data into this glyphset then load the glyph's geoemtry
|
|
58
142
|
* with the provided glyphURL. FinishCallback will be called once
|
|
@@ -84,6 +168,30 @@ const Glyphset = function () {
|
|
|
84
168
|
baseSize = glyphsetData.metadata.base_size;
|
|
85
169
|
offset = glyphsetData.metadata.offset;
|
|
86
170
|
scaleFactors = glyphsetData.metadata.scale_factors;
|
|
171
|
+
glyphCompute = undefined;
|
|
172
|
+
if (morphVertices && numberOfTimeSteps > 1 && positions && positions["0"]) {
|
|
173
|
+
const baseCount = positions["0"].length / 3;
|
|
174
|
+
try {
|
|
175
|
+
glyphCompute = createGlyphTransformCompute({
|
|
176
|
+
baseCount,
|
|
177
|
+
outputCount: numberOfVertices,
|
|
178
|
+
repeat_mode,
|
|
179
|
+
positionsData: flattenVec3Frames(positions, numberOfTimeSteps, baseCount),
|
|
180
|
+
axis1Data: flattenVec3Frames(axis1s, numberOfTimeSteps, baseCount),
|
|
181
|
+
axis2Data: flattenVec3Frames(axis2s, numberOfTimeSteps, baseCount),
|
|
182
|
+
axis3Data: flattenVec3Frames(axis3s, numberOfTimeSteps, baseCount),
|
|
183
|
+
scaleData: flattenVec3Frames(scales, numberOfTimeSteps, baseCount),
|
|
184
|
+
colorData: (morphColours && colors) ? flattenColorFrames(colors, numberOfTimeSteps, baseCount) : undefined,
|
|
185
|
+
baseSize,
|
|
186
|
+
offset,
|
|
187
|
+
scaleFactors,
|
|
188
|
+
globalScale: this.globalScale,
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error("Failed to set up GPU glyph transform compute, falling back to CPU animation.", err);
|
|
192
|
+
glyphCompute = undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
87
195
|
const loader = new JSONLoader();
|
|
88
196
|
this.geometry = new THREE.BufferGeometry();
|
|
89
197
|
const instancedMesh = new THREE.InstancedMesh(this.geometry, undefined, numberOfVertices);
|
|
@@ -180,7 +288,7 @@ const Glyphset = function () {
|
|
|
180
288
|
+ offset[1] * axis_scale[1] * axis2[j]
|
|
181
289
|
+ offset[2] * axis_scale[2] * axis3[j];
|
|
182
290
|
}
|
|
183
|
-
const number_of_glyphs = (
|
|
291
|
+
const number_of_glyphs = (repeat_mode == "AXES_2D") ? 2 : 3;
|
|
184
292
|
for (let k = 0; k < number_of_glyphs; k++) {
|
|
185
293
|
let use_axis1, use_axis2;
|
|
186
294
|
const use_scale = scale[k];
|
|
@@ -193,7 +301,7 @@ const Glyphset = function () {
|
|
|
193
301
|
}
|
|
194
302
|
else if (k == 1) {
|
|
195
303
|
use_axis1 = axis2;
|
|
196
|
-
use_axis2 = (
|
|
304
|
+
use_axis2 = (repeat_mode == "AXES_2D") ? axis1 : axis3;
|
|
197
305
|
}
|
|
198
306
|
else // if (k == 2)
|
|
199
307
|
{
|
|
@@ -268,22 +376,7 @@ const Glyphset = function () {
|
|
|
268
376
|
current_axis3, current_scale, _glyph_axis_array);
|
|
269
377
|
if (arrays.length == numberOfGlyphs) {
|
|
270
378
|
for (let j = 0; j < numberOfGlyphs; j++) {
|
|
271
|
-
_transformMatrix
|
|
272
|
-
_transformMatrix.elements[1] = arrays[j][1][1];
|
|
273
|
-
_transformMatrix.elements[2] = arrays[j][1][2];
|
|
274
|
-
_transformMatrix.elements[3] = 0.0;
|
|
275
|
-
_transformMatrix.elements[4] = arrays[j][2][0];
|
|
276
|
-
_transformMatrix.elements[5] = arrays[j][2][1];
|
|
277
|
-
_transformMatrix.elements[6] = arrays[j][2][2];
|
|
278
|
-
_transformMatrix.elements[7] = 0.0;
|
|
279
|
-
_transformMatrix.elements[8] = arrays[j][3][0];
|
|
280
|
-
_transformMatrix.elements[9] = arrays[j][3][1];
|
|
281
|
-
_transformMatrix.elements[10] = arrays[j][3][2];
|
|
282
|
-
_transformMatrix.elements[11] = 0.0;
|
|
283
|
-
_transformMatrix.elements[12] = arrays[j][0][0];
|
|
284
|
-
_transformMatrix.elements[13] = arrays[j][0][1];
|
|
285
|
-
_transformMatrix.elements[14] = arrays[j][0][2];
|
|
286
|
-
_transformMatrix.elements[15] = 1.0;
|
|
379
|
+
writeTransformMatrix(_transformMatrix, arrays[j]);
|
|
287
380
|
this.morph.setMatrixAt(current_glyph_index, _transformMatrix);
|
|
288
381
|
const glyph = glyphList[current_glyph_index];
|
|
289
382
|
if (glyph) {
|
|
@@ -301,8 +394,19 @@ const Glyphset = function () {
|
|
|
301
394
|
|
|
302
395
|
/**
|
|
303
396
|
* Update colour for each of the glyph in this glyphset.
|
|
397
|
+
*
|
|
398
|
+
* @param {Array} current_colors - one hex value per raw (pre-repeat_mode-
|
|
399
|
+
* expansion) glyph record.
|
|
400
|
+
* @param {Boolean} labelsOnly - when true, only glyph labels are
|
|
401
|
+
* recoloured - instanceColor is left untouched (not even created). Used
|
|
402
|
+
* for the initial load-time colour of GPU-compute-driven glyphsets (see
|
|
403
|
+
* createGlyphs()): instanceColor must stay uncreated there (see
|
|
404
|
+
* applyGlyphComputeResult's comment on the NodeMaterial double-multiply
|
|
405
|
+
* this avoids), but labels aren't part of the compute-buffer render path
|
|
406
|
+
* at all and would otherwise show no colour until the first accurate
|
|
407
|
+
* resync.
|
|
304
408
|
*/
|
|
305
|
-
const updateGlyphsetHexColors = current_colors => {
|
|
409
|
+
const updateGlyphsetHexColors = (current_colors, labelsOnly) => {
|
|
306
410
|
let numberOfGlyphs = 1;
|
|
307
411
|
if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR")
|
|
308
412
|
numberOfGlyphs = 2;
|
|
@@ -314,22 +418,213 @@ const Glyphset = function () {
|
|
|
314
418
|
const hex_values = current_colors[i];
|
|
315
419
|
for (let j = 0; j < numberOfGlyphs; j++) {
|
|
316
420
|
_bot_colour.setHex(hex_values)
|
|
317
|
-
|
|
421
|
+
if (!labelsOnly) {
|
|
422
|
+
this.morph.setColorAt(current_glyph_index, _bot_colour);
|
|
423
|
+
}
|
|
318
424
|
const glyph = glyphList[current_glyph_index];
|
|
319
425
|
if (glyph)
|
|
320
426
|
glyph.setColour(_bot_colour);
|
|
321
427
|
current_glyph_index++;
|
|
322
428
|
}
|
|
323
429
|
}
|
|
324
|
-
|
|
430
|
+
if (!labelsOnly) {
|
|
431
|
+
this.morph.instanceColor.needsUpdate = true;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Writes back a GPU glyph-transform compute readback (see
|
|
437
|
+
* dispatchGlyphCompute) into instanceMatrix/instanceColor - i.e. exactly
|
|
438
|
+
* the data updateGlyphsetTransformation()/updateGlyphsetHexColors() write
|
|
439
|
+
* on the CPU path, so bounding box/raycast/getClosestVertex keep working
|
|
440
|
+
* completely unchanged downstream.
|
|
441
|
+
*/
|
|
442
|
+
const applyGlyphComputeResult = (result) => {
|
|
443
|
+
const matrixArray = this.morph.instanceMatrix.array;
|
|
444
|
+
const updateLabels = this.canShowLabel();
|
|
445
|
+
for (let i = 0; i < numberOfVertices; i++) {
|
|
446
|
+
const o4 = i * 4;
|
|
447
|
+
const o16 = i * 16;
|
|
448
|
+
matrixArray[o16] = result.axis1[o4];
|
|
449
|
+
matrixArray[o16 + 1] = result.axis1[o4 + 1];
|
|
450
|
+
matrixArray[o16 + 2] = result.axis1[o4 + 2];
|
|
451
|
+
matrixArray[o16 + 3] = 0;
|
|
452
|
+
matrixArray[o16 + 4] = result.axis2[o4];
|
|
453
|
+
matrixArray[o16 + 5] = result.axis2[o4 + 1];
|
|
454
|
+
matrixArray[o16 + 6] = result.axis2[o4 + 2];
|
|
455
|
+
matrixArray[o16 + 7] = 0;
|
|
456
|
+
matrixArray[o16 + 8] = result.axis3[o4];
|
|
457
|
+
matrixArray[o16 + 9] = result.axis3[o4 + 1];
|
|
458
|
+
matrixArray[o16 + 10] = result.axis3[o4 + 2];
|
|
459
|
+
matrixArray[o16 + 11] = 0;
|
|
460
|
+
matrixArray[o16 + 12] = result.position[o4];
|
|
461
|
+
matrixArray[o16 + 13] = result.position[o4 + 1];
|
|
462
|
+
matrixArray[o16 + 14] = result.position[o4 + 2];
|
|
463
|
+
matrixArray[o16 + 15] = 1;
|
|
464
|
+
if (updateLabels) {
|
|
465
|
+
const glyph = glyphList[i];
|
|
466
|
+
if (glyph) {
|
|
467
|
+
glyph.setTransformation(
|
|
468
|
+
[result.position[o4], result.position[o4 + 1], result.position[o4 + 2]],
|
|
469
|
+
[result.axis1[o4], result.axis1[o4 + 1], result.axis1[o4 + 2]],
|
|
470
|
+
[result.axis2[o4], result.axis2[o4 + 1], result.axis2[o4 + 2]],
|
|
471
|
+
[result.axis3[o4], result.axis3[o4 + 1], result.axis3[o4 + 2]],
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
this.morph.instanceMatrix.needsUpdate = true;
|
|
477
|
+
this.boundingBoxUpdateRequired = true;
|
|
478
|
+
this.morph.computeBoundingSphere();
|
|
479
|
+
|
|
480
|
+
if (result.color) {
|
|
481
|
+
const colorArray = this.morph.instanceColor ? this.morph.instanceColor.array : undefined;
|
|
482
|
+
for (let i = 0; i < numberOfVertices; i++) {
|
|
483
|
+
const o4 = i * 4;
|
|
484
|
+
if (colorArray) {
|
|
485
|
+
const o3 = i * 3;
|
|
486
|
+
colorArray[o3] = result.color[o4];
|
|
487
|
+
colorArray[o3 + 1] = result.color[o4 + 1];
|
|
488
|
+
colorArray[o3 + 2] = result.color[o4 + 2];
|
|
489
|
+
}
|
|
490
|
+
if (updateLabels) {
|
|
491
|
+
const glyph = glyphList[i];
|
|
492
|
+
if (glyph) {
|
|
493
|
+
_bot_colour.setRGB(result.color[o4], result.color[o4 + 1], result.color[o4 + 2]);
|
|
494
|
+
glyph.setColour(_bot_colour);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (colorArray) {
|
|
499
|
+
this.morph.instanceColor.needsUpdate = true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Kicks off (at most one in flight at a time) an async GPU compute +
|
|
506
|
+
* readback for the current bottom/top frame + proportion. If a previous
|
|
507
|
+
* dispatch is still resolving, this frame's update is skipped rather than
|
|
508
|
+
* queued - the next call to updateMorphGlyphsets() will dispatch with
|
|
509
|
+
* whatever bottom/top/proportion is current *then*, so playback can't
|
|
510
|
+
* fall further and further behind, at the cost of the displayed state
|
|
511
|
+
* lagging the true target time by about one frame while animating.
|
|
512
|
+
*/
|
|
513
|
+
const dispatchGlyphCompute = (bottom_frame, top_frame, proportion) => {
|
|
514
|
+
if (glyphComputePending) return;
|
|
515
|
+
const renderer = this.region?.getScene?.()?.getRenderer?.();
|
|
516
|
+
if (!renderer) return;
|
|
517
|
+
glyphCompute.uniforms.bottomFrame.value = bottom_frame;
|
|
518
|
+
glyphCompute.uniforms.topFrame.value = top_frame;
|
|
519
|
+
glyphCompute.uniforms.proportion.value = proportion;
|
|
520
|
+
glyphCompute.uniforms.globalScale.value = this.globalScale;
|
|
521
|
+
glyphComputePending = true;
|
|
522
|
+
boundsAreExact = false;
|
|
523
|
+
dispatchAndReadbackGlyphTransform(renderer, glyphCompute).then((result) => {
|
|
524
|
+
glyphComputePending = false;
|
|
525
|
+
applyGlyphComputeResult(result);
|
|
526
|
+
boundsAreExact = true;
|
|
527
|
+
// Restore the stock InstancedMesh.raycast (instanceMatrix is now
|
|
528
|
+
// exact again) by dropping the no-op instance override, if present.
|
|
529
|
+
delete this.morph.raycast;
|
|
530
|
+
}).catch((err) => {
|
|
531
|
+
glyphComputePending = false;
|
|
532
|
+
console.error("Glyph transform compute readback failed.", err);
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The fast per-frame path used while actively playing: only dispatches
|
|
538
|
+
* the compute pass (updating the GPU buffers the render material reads
|
|
539
|
+
* directly - see ../tsl/glyphRenderMaterial.js) with no CPU readback at
|
|
540
|
+
* all, bouding box will not be updated.
|
|
541
|
+
*/
|
|
542
|
+
const dispatchGlyphComputeFast = (bottom_frame, top_frame, proportion) => {
|
|
543
|
+
const renderer = this.region?.getScene?.()?.getRenderer?.();
|
|
544
|
+
if (!renderer) return false;
|
|
545
|
+
glyphCompute.uniforms.bottomFrame.value = bottom_frame;
|
|
546
|
+
glyphCompute.uniforms.topFrame.value = top_frame;
|
|
547
|
+
glyphCompute.uniforms.proportion.value = proportion;
|
|
548
|
+
glyphCompute.uniforms.globalScale.value = this.globalScale;
|
|
549
|
+
renderer.compute(glyphCompute.compute);
|
|
550
|
+
boundsAreExact = false;
|
|
551
|
+
this.morph.raycast = () => {};
|
|
552
|
+
renderBuffersInitialized = true;
|
|
553
|
+
return true;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The readback half of the debounced accurate resync (see
|
|
558
|
+
* scheduleAccurateResync): unlike dispatchGlyphCompute(), this does NOT
|
|
559
|
+
* call renderer.compute() first - by the time this fires, the buffers
|
|
560
|
+
* already hold the right result.
|
|
561
|
+
*/
|
|
562
|
+
const readbackGlyphComputeOnly = () => {
|
|
563
|
+
if (glyphComputePending) return;
|
|
564
|
+
const renderer = this.region?.getScene?.()?.getRenderer?.();
|
|
565
|
+
if (!renderer) return;
|
|
566
|
+
glyphComputePending = true;
|
|
567
|
+
readbackGlyphTransform(renderer, glyphCompute).then((result) => {
|
|
568
|
+
glyphComputePending = false;
|
|
569
|
+
applyGlyphComputeResult(result);
|
|
570
|
+
boundsAreExact = true;
|
|
571
|
+
delete this.morph.raycast;
|
|
572
|
+
}).catch((err) => {
|
|
573
|
+
glyphComputePending = false;
|
|
574
|
+
console.error("Glyph transform compute readback failed.", err);
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const cancelScheduledAccurateResync = () => {
|
|
579
|
+
if (accurateResyncTimer !== undefined) {
|
|
580
|
+
clearTimeout(accurateResyncTimer);
|
|
581
|
+
accurateResyncTimer = undefined;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* (Re)schedules a single accurate resync ACCURATE_RESYNC_DEBOUNCE_MS from
|
|
587
|
+
* now, cancelling any previously scheduled one.
|
|
588
|
+
*/
|
|
589
|
+
const scheduleAccurateResync = () => {
|
|
590
|
+
cancelScheduledAccurateResync();
|
|
591
|
+
accurateResyncTimer = setTimeout(() => {
|
|
592
|
+
accurateResyncTimer = undefined;
|
|
593
|
+
readbackGlyphComputeOnly();
|
|
594
|
+
}, ACCURATE_RESYNC_DEBOUNCE_MS);
|
|
325
595
|
};
|
|
326
596
|
|
|
327
597
|
/**
|
|
328
598
|
* Update the current states of the glyphs in this glyphset, this includes transformation and
|
|
329
599
|
* colour for each of them. This is called when glyphset and glyphs are initialised and whenever
|
|
330
600
|
* the internal time has been updated.
|
|
601
|
+
*
|
|
602
|
+
* @param {Boolean} debounceAccurateResync - When true the costlier GPU->CPU readback
|
|
603
|
+
* (bounding box/raycast/ export accuracy - see dispatchGlyphCompute) is debounced
|
|
604
|
+
* until updates have been quiet for ACCURATE_RESYNC_DEBOUNCE_MS. When false/omitted
|
|
605
|
+
* (e.g. the render loop's animation-just-stopped resync), the readback happens
|
|
606
|
+
* immediately, as before.
|
|
331
607
|
*/
|
|
332
|
-
const updateMorphGlyphsets = () => {
|
|
608
|
+
const updateMorphGlyphsets = (debounceAccurateResync) => {
|
|
609
|
+
const current_time = this.inbuildTime / this.duration * (numberOfTimeSteps - 1);
|
|
610
|
+
const bottom_frame = Math.floor(current_time);
|
|
611
|
+
const proportion = 1 - (current_time - bottom_frame);
|
|
612
|
+
const top_frame = Math.ceil(current_time);
|
|
613
|
+
|
|
614
|
+
if (morphVertices && glyphCompute) {
|
|
615
|
+
if (debounceAccurateResync) {
|
|
616
|
+
dispatchGlyphComputeFast(bottom_frame, top_frame, proportion);
|
|
617
|
+
scheduleAccurateResync();
|
|
618
|
+
} else {
|
|
619
|
+
cancelScheduledAccurateResync();
|
|
620
|
+
// GPU path: transform (and colour, if morphColours) for this frame -
|
|
621
|
+
// handles both updateGlyphsetTransformation() and
|
|
622
|
+
// updateGlyphsetHexColors() below in one dispatch.
|
|
623
|
+
dispatchGlyphCompute(bottom_frame, top_frame, proportion);
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
333
628
|
let current_positions = _current_positions;
|
|
334
629
|
let current_axis1s = _current_axis1s;
|
|
335
630
|
let current_axis2s = _current_axis2s;
|
|
@@ -337,10 +632,6 @@ const Glyphset = function () {
|
|
|
337
632
|
let current_scales = _current_scales;
|
|
338
633
|
let current_colors = _current_colors;
|
|
339
634
|
|
|
340
|
-
const current_time = this.inbuildTime / this.duration * (numberOfTimeSteps - 1);
|
|
341
|
-
const bottom_frame = Math.floor(current_time);
|
|
342
|
-
const proportion = 1 - (current_time - bottom_frame);
|
|
343
|
-
const top_frame = Math.ceil(current_time);
|
|
344
635
|
if (morphVertices) {
|
|
345
636
|
const bottom_positions = positions[bottom_frame.toString()];
|
|
346
637
|
const top_positions = positions[top_frame.toString()];
|
|
@@ -387,15 +678,10 @@ const Glyphset = function () {
|
|
|
387
678
|
_bot_colour.b * proportion + _top_colour.b * (1 - proportion));
|
|
388
679
|
current_colors[i] = _bot_colour.getHex();
|
|
389
680
|
}
|
|
390
|
-
/*
|
|
391
|
-
for (var i = 0; i < bottom_colors.length; i++) {
|
|
392
|
-
current_colors.push(proportion * bottom_colors[i] + (1.0 - proportion) * top_colors[i]);
|
|
393
|
-
}
|
|
394
|
-
*/
|
|
395
681
|
} else {
|
|
396
682
|
current_colors = colors["0"];
|
|
397
683
|
}
|
|
398
|
-
updateGlyphsetHexColors(current_colors);
|
|
684
|
+
updateGlyphsetHexColors(current_colors, false);
|
|
399
685
|
}
|
|
400
686
|
};
|
|
401
687
|
|
|
@@ -442,6 +728,53 @@ const Glyphset = function () {
|
|
|
442
728
|
labelsOn = false;
|
|
443
729
|
}
|
|
444
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Computes a single Box3 that conservatively contains every keyframe's
|
|
733
|
+
* exact per-instance transform of the base glyph geometry, for use as
|
|
734
|
+
* getBoundingBox()'s answer while boundsAreExact is false (see its
|
|
735
|
+
* declaration above for the caveat on this not being a formally proven
|
|
736
|
+
* bound). One-time cost at load, reusing resolve_glyph_axes() the same
|
|
737
|
+
* way updateGlyphsetTransformation() does, just for every keyframe
|
|
738
|
+
* instead of just the current one, unioned together.
|
|
739
|
+
*/
|
|
740
|
+
const computeAllFramesBoundingBox = () => {
|
|
741
|
+
const localBox = new THREE.Box3().setFromBufferAttribute(this.geometry.attributes.position);
|
|
742
|
+
const tempMatrix = new THREE.Matrix4();
|
|
743
|
+
const tempBox = new THREE.Box3();
|
|
744
|
+
const tempArray = [];
|
|
745
|
+
const result = new THREE.Box3();
|
|
746
|
+
let first = true;
|
|
747
|
+
for (let t = 0; t < numberOfTimeSteps; t++) {
|
|
748
|
+
const tPositions = positions[t.toString()];
|
|
749
|
+
const tAxis1 = axis1s[t.toString()];
|
|
750
|
+
const tAxis2 = axis2s[t.toString()];
|
|
751
|
+
const tAxis3 = axis3s[t.toString()];
|
|
752
|
+
const tScales = scales[t.toString()];
|
|
753
|
+
if (!tPositions) continue;
|
|
754
|
+
const numberOfPositions = tPositions.length / 3;
|
|
755
|
+
for (let i = 0; i < numberOfPositions; i++) {
|
|
756
|
+
const idx = i * 3;
|
|
757
|
+
const point = [tPositions[idx], tPositions[idx + 1], tPositions[idx + 2]];
|
|
758
|
+
const a1 = [tAxis1[idx], tAxis1[idx + 1], tAxis1[idx + 2]];
|
|
759
|
+
const a2 = [tAxis2[idx], tAxis2[idx + 1], tAxis2[idx + 2]];
|
|
760
|
+
const a3 = [tAxis3[idx], tAxis3[idx + 1], tAxis3[idx + 2]];
|
|
761
|
+
const sc = [tScales[idx], tScales[idx + 1], tScales[idx + 2]];
|
|
762
|
+
const arrays = resolve_glyph_axes(point, a1, a2, a3, sc, tempArray);
|
|
763
|
+
for (let j = 0; j < arrays.length; j++) {
|
|
764
|
+
writeTransformMatrix(tempMatrix, arrays[j]);
|
|
765
|
+
tempBox.copy(localBox).applyMatrix4(tempMatrix);
|
|
766
|
+
if (first) {
|
|
767
|
+
result.copy(tempBox);
|
|
768
|
+
first = false;
|
|
769
|
+
} else {
|
|
770
|
+
result.union(tempBox);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return first ? undefined : result;
|
|
776
|
+
}
|
|
777
|
+
|
|
445
778
|
/**
|
|
446
779
|
* Create the glyphs in the glyphset.
|
|
447
780
|
*
|
|
@@ -468,9 +801,11 @@ const Glyphset = function () {
|
|
|
468
801
|
//Update the transformation of the glyphs.
|
|
469
802
|
updateGlyphsetTransformation(positions["0"], axis1s["0"],
|
|
470
803
|
axis2s["0"], axis3s["0"], scales["0"]);
|
|
471
|
-
//Update the color of the glyphs.
|
|
472
804
|
if (colors != undefined) {
|
|
473
|
-
updateGlyphsetHexColors(colors["0"]);
|
|
805
|
+
updateGlyphsetHexColors(colors["0"], !!(glyphCompute && glyphCompute.outputs.color));
|
|
806
|
+
}
|
|
807
|
+
if (glyphCompute) {
|
|
808
|
+
allFramesBoundingBox = computeAllFramesBoundingBox();
|
|
474
809
|
}
|
|
475
810
|
this.ready = true;
|
|
476
811
|
};
|
|
@@ -526,6 +861,9 @@ const Glyphset = function () {
|
|
|
526
861
|
this.geometry.computeBoundingBox();
|
|
527
862
|
if (materials && materials[0])
|
|
528
863
|
this.morph.material = materials[0];
|
|
864
|
+
if (glyphCompute) {
|
|
865
|
+
this.morph.material = createGlyphInstancedMaterial(this.morph.material, glyphCompute);
|
|
866
|
+
}
|
|
529
867
|
createGlyphs(displayLabels);
|
|
530
868
|
this.morph.name = this.groupName;
|
|
531
869
|
this.morph.userData = this;
|
|
@@ -597,6 +935,12 @@ const Glyphset = function () {
|
|
|
597
935
|
*/
|
|
598
936
|
this.getBoundingBox = () => {
|
|
599
937
|
if (this.morph && this.ready && this.morph.visible) {
|
|
938
|
+
if (glyphCompute && !boundsAreExact && allFramesBoundingBox) {
|
|
939
|
+
this.cachedBoundingBox.copy(allFramesBoundingBox);
|
|
940
|
+
this.morph.updateWorldMatrix(true, true);
|
|
941
|
+
this.cachedBoundingBox.applyMatrix4(this.morph.matrixWorld);
|
|
942
|
+
return this.cachedBoundingBox;
|
|
943
|
+
}
|
|
600
944
|
if (this.boundingBoxUpdateRequired) {
|
|
601
945
|
_boundingBox1.setFromBufferAttribute(
|
|
602
946
|
this.morph.geometry.attributes.position);
|
|
@@ -635,7 +979,7 @@ const Glyphset = function () {
|
|
|
635
979
|
else
|
|
636
980
|
this.inbuildTime = time;
|
|
637
981
|
if (morphColours || morphVertices) {
|
|
638
|
-
updateMorphGlyphsets();
|
|
982
|
+
updateMorphGlyphsets(true);
|
|
639
983
|
if (morphVertices)
|
|
640
984
|
this.markerUpdateRequired = true;
|
|
641
985
|
}
|
|
@@ -662,6 +1006,49 @@ const Glyphset = function () {
|
|
|
662
1006
|
return this.inbuildTime;
|
|
663
1007
|
}
|
|
664
1008
|
|
|
1009
|
+
/**
|
|
1010
|
+
* Exporters (see sceneExporter.js/GLTFExporter.js) read
|
|
1011
|
+
* THREE.InstancedMesh.instanceColor directly off this.morph - which
|
|
1012
|
+
* createGlyphs() intentionally leaves null whenever colour is
|
|
1013
|
+
* glyphCompute-driven. No-op for glyphsets whose colour isn't glyphCompute-driven
|
|
1014
|
+
* (instanceColor there is already accurate, ordinary CPU-driven state).
|
|
1015
|
+
*
|
|
1016
|
+
* @return {Promise}
|
|
1017
|
+
*/
|
|
1018
|
+
this.prepareColorForExport = async () => {
|
|
1019
|
+
if (!(glyphCompute && glyphCompute.outputs.color)) return;
|
|
1020
|
+
const renderer = this.region?.getScene?.()?.getRenderer?.();
|
|
1021
|
+
if (!renderer) return;
|
|
1022
|
+
renderer.compute(glyphCompute.compute);
|
|
1023
|
+
const colorResult = new Float32Array(
|
|
1024
|
+
await renderer.getArrayBufferAsync(glyphCompute.outputs.color.value));
|
|
1025
|
+
// setColorAt(0, ...) is only here to lazily allocate instanceColor the
|
|
1026
|
+
// same way the stock InstancedMesh API would; the loop below overwrites
|
|
1027
|
+
// every instance (including 0) with the real snapshot right after.
|
|
1028
|
+
this.morph.setColorAt(0, _bot_colour);
|
|
1029
|
+
const colorArray = this.morph.instanceColor.array;
|
|
1030
|
+
for (let i = 0; i < numberOfVertices; i++) {
|
|
1031
|
+
const o4 = i * 4;
|
|
1032
|
+
const o3 = i * 3;
|
|
1033
|
+
colorArray[o3] = colorResult[o4];
|
|
1034
|
+
colorArray[o3 + 1] = colorResult[o4 + 1];
|
|
1035
|
+
colorArray[o3 + 2] = colorResult[o4 + 2];
|
|
1036
|
+
}
|
|
1037
|
+
this.morph.instanceColor.needsUpdate = true;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Undoes prepareColorForExport() - see its comment for why this matters:
|
|
1042
|
+
* without it, the next live render would permanently pick up the stale
|
|
1043
|
+
* instanceColor multiply bug this whole design avoids.
|
|
1044
|
+
*/
|
|
1045
|
+
this.clearColorExportState = () => {
|
|
1046
|
+
if (glyphCompute && glyphCompute.outputs.color && this.morph.instanceColor) {
|
|
1047
|
+
this.morph.instanceColor = null;
|
|
1048
|
+
if (this.morph.material) this.morph.material.needsUpdate = true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
665
1052
|
/**
|
|
666
1053
|
* Set the objects scale.
|
|
667
1054
|
*
|
|
@@ -669,7 +1056,7 @@ const Glyphset = function () {
|
|
|
669
1056
|
*/
|
|
670
1057
|
this.setScaleAll = function(scale) {
|
|
671
1058
|
this.globalScale = scale;
|
|
672
|
-
updateMorphGlyphsets();
|
|
1059
|
+
updateMorphGlyphsets(false);
|
|
673
1060
|
}
|
|
674
1061
|
|
|
675
1062
|
/**
|
|
@@ -683,12 +1070,19 @@ const Glyphset = function () {
|
|
|
683
1070
|
this.geometry.dispose();
|
|
684
1071
|
if (this.morph)
|
|
685
1072
|
this.morph.material.dispose();
|
|
1073
|
+
cancelScheduledAccurateResync();
|
|
686
1074
|
axis1s = undefined;
|
|
687
1075
|
axis2s = undefined;
|
|
688
1076
|
axis3s = undefined;
|
|
689
1077
|
positions = undefined;
|
|
690
1078
|
scales = undefined;
|
|
691
1079
|
colors = undefined;
|
|
1080
|
+
glyphCompute = undefined;
|
|
1081
|
+
glyphComputePending = false;
|
|
1082
|
+
boundsAreExact = true;
|
|
1083
|
+
renderBuffersInitialized = false;
|
|
1084
|
+
wasAnimating = false;
|
|
1085
|
+
allFramesBoundingBox = undefined;
|
|
692
1086
|
this.ready = false;
|
|
693
1087
|
this.groupName = undefined;
|
|
694
1088
|
}
|
|
@@ -697,14 +1091,42 @@ const Glyphset = function () {
|
|
|
697
1091
|
* Update the glyphsets if required the render.
|
|
698
1092
|
*/
|
|
699
1093
|
this.render = (delta, playAnimation, options) => {
|
|
1094
|
+
if (glyphCompute && !renderBuffersInitialized) {
|
|
1095
|
+
const renderer = this.region?.getScene?.()?.getRenderer?.();
|
|
1096
|
+
if (renderer) {
|
|
1097
|
+
renderer.compute(glyphCompute.compute);
|
|
1098
|
+
renderBuffersInitialized = true;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
700
1101
|
if (playAnimation == true) {
|
|
701
1102
|
let targetTime = this.inbuildTime + delta;
|
|
702
1103
|
if (targetTime > this.duration)
|
|
703
1104
|
targetTime = targetTime - this.duration;
|
|
704
1105
|
this.inbuildTime = targetTime;
|
|
705
|
-
if (
|
|
706
|
-
|
|
1106
|
+
if (morphVertices && glyphCompute) {
|
|
1107
|
+
// Fast GPU-only path: update the render buffers every frame with
|
|
1108
|
+
// no CPU readback. Colour (if morphColours) rides along in the
|
|
1109
|
+
// same compute pass/buffers - see glyphRenderMaterial.js's colorNode.
|
|
1110
|
+
// Also drop any accurate resync a prior setMorphTime() call (e.g. a
|
|
1111
|
+
// downstream slider) left debounced-and-pending - it would apply a
|
|
1112
|
+
// now-stale frame once it fired mid-animation otherwise.
|
|
1113
|
+
cancelScheduledAccurateResync();
|
|
1114
|
+
const current_time = this.inbuildTime / this.duration * (numberOfTimeSteps - 1);
|
|
1115
|
+
const bottom_frame = Math.floor(current_time);
|
|
1116
|
+
const proportion = 1 - (current_time - bottom_frame);
|
|
1117
|
+
const top_frame = Math.ceil(current_time);
|
|
1118
|
+
dispatchGlyphComputeFast(bottom_frame, top_frame, proportion);
|
|
1119
|
+
} else if (morphColours || morphVertices) {
|
|
1120
|
+
updateMorphGlyphsets(false);
|
|
1121
|
+
}
|
|
1122
|
+
wasAnimating = true;
|
|
1123
|
+
} else {
|
|
1124
|
+
if (wasAnimating && glyphCompute) {
|
|
1125
|
+
// Just stopped: one accurate resync so bounding-box/raycast are
|
|
1126
|
+
// exact again for as long as playback stays paused.
|
|
1127
|
+
updateMorphGlyphsets(false);
|
|
707
1128
|
}
|
|
1129
|
+
wasAnimating = false;
|
|
708
1130
|
}
|
|
709
1131
|
this.updateMarker(playAnimation, options);
|
|
710
1132
|
}
|
package/src/primitives/lines.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { toBufferGeometry } from '../utilities';
|
|
3
|
-
import { applyMorphColorNode } from '../
|
|
3
|
+
import { applyMorphColorNode } from '../tsl/morphColorMaterial';
|
|
4
4
|
import { LineSegments } from '../three/line/LineSegments';
|
|
5
5
|
import { ZincObject } from './zincObject';
|
|
6
6
|
|