zincjs 2.4.1-beta.5 → 2.4.1-beta.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zincjs",
3
- "version": "2.4.1-beta.5",
3
+ "version": "2.4.1-beta.7",
4
4
  "description": "ZincJS (Web-based-Zinc-Visualisation)",
5
5
  "type": "module",
6
6
  "main": "build/zinc.js",
@@ -3,7 +3,8 @@ import { Glyph } from './glyph';
3
3
  import { ZincObject } from './zincObject';
4
4
  import { updateWorldMatrixFromAncestors } from '../utilities';
5
5
  import { JSONLoader } from '../loaders/JSONLoader';
6
- import { createGlyphTransformCompute, dispatchAndReadbackGlyphTransform, readbackGlyphTransform } from '../tsl/glyphTransform';
6
+ import { createGlyphTransformCompute, dispatchAndReadbackGlyphTransform, isGlyphComputeSupported,
7
+ readbackGlyphTransform } from '../tsl/glyphTransform';
7
8
  import { createGlyphInstancedMaterial } from '../tsl/glyphRenderMaterial';
8
9
 
9
10
  /**
@@ -176,7 +177,9 @@ const Glyphset = function () {
176
177
  offset = glyphsetData.metadata.offset;
177
178
  scaleFactors = glyphsetData.metadata.scale_factors;
178
179
  glyphCompute = undefined;
179
- if (morphVertices && numberOfTimeSteps > 1 && positions && positions["0"]) {
180
+ //The CPU path is used when the backend does not support GPU compute
181
+ if (morphVertices && numberOfTimeSteps > 1 && positions && positions["0"] &&
182
+ isGlyphComputeSupported()) {
180
183
  const baseCount = positions["0"].length / 3;
181
184
  try {
182
185
  glyphCompute = createGlyphTransformCompute({
@@ -699,6 +702,15 @@ const Glyphset = function () {
699
702
  }
700
703
  };
701
704
 
705
+ /**
706
+ * Check if the glyph transforms are animated on the GPU.
707
+ *
708
+ * @return {Boolean}
709
+ */
710
+ this.isUsingGPUCompute = () => {
711
+ return glyphCompute !== undefined;
712
+ }
713
+
702
714
  /**
703
715
  * Get the assigned label for instance at index
704
716
  */
@@ -1042,8 +1054,7 @@ const Glyphset = function () {
1042
1054
  const renderer = this.region?.getScene?.()?.getRenderer?.();
1043
1055
  if (!renderer) return;
1044
1056
  renderer.compute(glyphCompute.compute);
1045
- const colorResult = new Float32Array(
1046
- await renderer.getArrayBufferAsync(glyphCompute.outputs.color.value));
1057
+ const colorResult = (await readbackGlyphTransform(renderer, glyphCompute)).color;
1047
1058
  // setColorAt(0, ...) is only here to lazily allocate instanceColor the
1048
1059
  // same way the stock InstancedMesh API would; the loop below overwrites
1049
1060
  // every instance (including 0) with the real snapshot right after.
package/src/renderer.js CHANGED
@@ -3,6 +3,7 @@ import { ResizeSensor } from 'css-element-queries';
3
3
  import { Scene } from './scene';
4
4
  import { onRenderRequest, offRenderRequest } from './renderRequests';
5
5
  import { setTextPixelsPerUnit } from './textSprite';
6
+ import { setGlyphComputeSupported } from './tsl/glyphTransform';
6
7
  /**
7
8
  * Create a Zinc 3D renderer in the container provided.
8
9
  * The primary function of a Zinc 3D renderer is to display the current
@@ -152,10 +153,10 @@ const Renderer = function (containerIn) {
152
153
  //WebGPURenderer otherwise requests a device with the default
153
154
  //maxTextureArrayLayers (256 on many adapters), which is too low for
154
155
  //some currnetly sypported scaffold, and the default
155
- //maxStorageBuffersPerShaderStage (8 on many adapters), which is too
156
- //low for compute passes like glyphset.js's GPU glyph-transform (11
157
- //storage buffers). We will request the highest possible on the device
158
- //for both.
156
+ //maxStorageBuffersPerShaderStage (8 on many adapters). The glyph
157
+ //transform compute pass only needs two storage buffers, the higher
158
+ //limit is kept as headroom. We will request the highest possible on
159
+ //the device for both.
159
160
  if (parameters.device === undefined && parameters.requiredLimits === undefined &&
160
161
  typeof navigator !== 'undefined' && navigator.gpu) {
161
162
  try {
@@ -173,6 +174,9 @@ const Renderer = function (containerIn) {
173
174
  }
174
175
  renderer = new THREE.WebGPURenderer(parameters);
175
176
  await renderer.init();
177
+ //GPU glyph animation requires the WebGPU backend, the WebGL fallback
178
+ //cannot index storage buffers freely.
179
+ setGlyphComputeSupported(renderer.backend?.isWebGPUBackend === true);
176
180
 
177
181
  //renderer = new THREE.WebGLRenderer(parameters);
178
182
  if (container !== undefined) {
@@ -7,6 +7,7 @@ import {
7
7
  int,
8
8
  float,
9
9
  vec3,
10
+ vec4,
10
11
  uniform,
11
12
  mix,
12
13
  select,
@@ -28,6 +29,9 @@ const repeatModeToInt = (repeat_mode) => {
28
29
  }
29
30
  }
30
31
 
32
+ //Order of the vec4 fields of each glyph instance in the output buffer
33
+ const OUTPUT_FIELDS = { position: 0, axis1: 1, axis2: 2, axis3: 3, color: 4 };
34
+
31
35
  const multiplierForRepeatMode = (repeat_mode) => {
32
36
  if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR") return 2;
33
37
  if (repeat_mode == "AXES_3D") return 3;
@@ -66,9 +70,10 @@ const multiplierForRepeatMode = (repeat_mode) => {
66
70
  * @param {Array<number>} params.scaleFactors
67
71
  * @returns {Object} { uniforms, compute, outputs } - update
68
72
  * uniforms.bottomFrame/topFrame/proportion.value then pass `compute` to
69
- * renderer.compute()/computeAsync(); read results back from
70
- * outputs.position/axis1/axis2/axis3/color (each a StorageBufferNode -
71
- * the raw attribute to read back is `outputs.position.value` etc).
73
+ * renderer.compute()/computeAsync(); read results back with
74
+ * readbackGlyphTransform(). outputs.buffer is the interleaved storage
75
+ * buffer (outputs.stride vec4 per instance), outputs.position/axis1/
76
+ * axis2/axis3/color provide element(index) accessors for materials.
72
77
  */
73
78
  function createGlyphTransformCompute({
74
79
  baseCount,
@@ -87,18 +92,38 @@ function createGlyphTransformCompute({
87
92
  }) {
88
93
  const multiplier = multiplierForRepeatMode(repeat_mode);
89
94
 
90
- const positionsBuffer = attributeArray(positionsData, 'vec3');
91
- const axis1Buffer = attributeArray(axis1Data, 'vec3');
92
- const axis2Buffer = attributeArray(axis2Data, 'vec3');
93
- const axis3Buffer = attributeArray(axis3Data, 'vec3');
94
- const scaleBuffer = attributeArray(scaleData, 'vec3');
95
- const colorBuffer = colorData ? attributeArray(colorData, 'vec3') : undefined;
96
-
97
- const outPosition = attributeArray(outputCount, 'vec3');
98
- const outAxis1 = attributeArray(outputCount, 'vec3');
99
- const outAxis2 = attributeArray(outputCount, 'vec3');
100
- const outAxis3 = attributeArray(outputCount, 'vec3');
101
- const outColor = colorData ? attributeArray(outputCount, 'vec3') : undefined;
95
+ // All inputs are interleaved into a single storage buffer and all outputs
96
+ // into another, one vec4 per field. Separate buffers per field needed up
97
+ // to 11 storage buffers in the compute stage, more than some devices
98
+ // support (e.g. 10 on iOS). vec4 also avoids three.js padding vec3
99
+ // storage data into an extra copy.
100
+ const hasColor = !!colorData;
101
+ const inputSources = [positionsData, axis1Data, axis2Data, axis3Data, scaleData];
102
+ if (hasColor) inputSources.push(colorData);
103
+ const inputStride = inputSources.length;
104
+ const outputStride = hasColor ? OUTPUT_FIELDS.color + 1 : OUTPUT_FIELDS.axis3 + 1;
105
+ const recordCount = positionsData.length / 3;
106
+ const inputData = new Float32Array(recordCount * inputStride * 4);
107
+ for (let record = 0; record < recordCount; record++) {
108
+ for (let field = 0; field < inputStride; field++) {
109
+ const source = inputSources[field];
110
+ const target = (record * inputStride + field) * 4;
111
+ inputData[target] = source[record * 3];
112
+ inputData[target + 1] = source[record * 3 + 1];
113
+ inputData[target + 2] = source[record * 3 + 2];
114
+ }
115
+ }
116
+ const inputBuffer = attributeArray(inputData, 'vec4');
117
+ const outputBuffer = attributeArray(outputCount * outputStride, 'vec4');
118
+
119
+ const readInput = (recordIdx, field) =>
120
+ inputBuffer.element(recordIdx.mul(int(inputStride)).add(int(field))).xyz;
121
+ const writeOutput = (outIdx, field, value) =>
122
+ outputBuffer.element(outIdx.mul(int(outputStride)).add(int(field))).assign(vec4(value, 0));
123
+ //Accessor used by the render material to read an output field
124
+ const outputField = (field) => ({
125
+ element: (idx) => outputBuffer.element(int(idx).mul(int(outputStride)).add(int(field))).xyz,
126
+ });
102
127
 
103
128
  const uniforms = {
104
129
  bottomFrame: uniform(0, 'int'),
@@ -121,11 +146,11 @@ function createGlyphTransformCompute({
121
146
  const topIdx = uniforms.topFrame.mul(int(baseCount)).add(inputIdx);
122
147
 
123
148
  // Matches updateMorphGlyphsets(): bottom*proportion + top*(1-proportion).
124
- const point = mix(positionsBuffer.element(topIdx), positionsBuffer.element(bottomIdx), uniforms.proportion);
125
- const axis1 = mix(axis1Buffer.element(topIdx), axis1Buffer.element(bottomIdx), uniforms.proportion);
126
- const axis2 = mix(axis2Buffer.element(topIdx), axis2Buffer.element(bottomIdx), uniforms.proportion);
127
- const axis3 = mix(axis3Buffer.element(topIdx), axis3Buffer.element(bottomIdx), uniforms.proportion);
128
- const scale = mix(scaleBuffer.element(topIdx), scaleBuffer.element(bottomIdx), uniforms.proportion);
149
+ const point = mix(readInput(topIdx, 0), readInput(bottomIdx, 0), uniforms.proportion);
150
+ const axis1 = mix(readInput(topIdx, 1), readInput(bottomIdx, 1), uniforms.proportion);
151
+ const axis2 = mix(readInput(topIdx, 2), readInput(bottomIdx, 2), uniforms.proportion);
152
+ const axis3 = mix(readInput(topIdx, 3), readInput(bottomIdx, 3), uniforms.proportion);
153
+ const scale = mix(readInput(topIdx, 4), readInput(bottomIdx, 4), uniforms.proportion);
129
154
 
130
155
  // NOTE: each branch below assigns straight into the output storage
131
156
  // buffer elements (outPosition.element(outIdx).assign(...) etc.),
@@ -175,10 +200,10 @@ function createGlyphTransformCompute({
175
200
  a3.assign(a3.negate());
176
201
  });
177
202
 
178
- outPosition.element(outIdx).assign(p);
179
- outAxis1.element(outIdx).assign(a1);
180
- outAxis2.element(outIdx).assign(a2);
181
- outAxis3.element(outIdx).assign(a3);
203
+ writeOutput(outIdx, OUTPUT_FIELDS.position, p);
204
+ writeOutput(outIdx, OUTPUT_FIELDS.axis1, a1);
205
+ writeOutput(outIdx, OUTPUT_FIELDS.axis2, a2);
206
+ writeOutput(outIdx, OUTPUT_FIELDS.axis3, a3);
182
207
 
183
208
  }).Else(() => {
184
209
 
@@ -220,16 +245,16 @@ function createGlyphTransformCompute({
220
245
  .div(max(mag2, 1e-8));
221
246
  const finalAxis2 = select(mag2.greaterThan(0), axis2Raw.mul(scaling2), axis2Raw);
222
247
 
223
- outPosition.element(outIdx).assign(finalPoint);
224
- outAxis1.element(outIdx).assign(finalAxis1);
225
- outAxis2.element(outIdx).assign(finalAxis2);
226
- outAxis3.element(outIdx).assign(finalAxis3);
248
+ writeOutput(outIdx, OUTPUT_FIELDS.position, finalPoint);
249
+ writeOutput(outIdx, OUTPUT_FIELDS.axis1, finalAxis1);
250
+ writeOutput(outIdx, OUTPUT_FIELDS.axis2, finalAxis2);
251
+ writeOutput(outIdx, OUTPUT_FIELDS.axis3, finalAxis3);
227
252
 
228
253
  });
229
254
 
230
- if (colorBuffer) {
231
- const color = mix(colorBuffer.element(topIdx), colorBuffer.element(bottomIdx), uniforms.proportion);
232
- outColor.element(outIdx).assign(color);
255
+ if (hasColor) {
256
+ const color = mix(readInput(topIdx, 5), readInput(bottomIdx, 5), uniforms.proportion);
257
+ writeOutput(outIdx, OUTPUT_FIELDS.color, color);
233
258
  }
234
259
 
235
260
  } )().compute( outputCount );
@@ -238,11 +263,14 @@ function createGlyphTransformCompute({
238
263
  uniforms,
239
264
  compute: computeFn,
240
265
  outputs: {
241
- position: outPosition,
242
- axis1: outAxis1,
243
- axis2: outAxis2,
244
- axis3: outAxis3,
245
- color: outColor,
266
+ buffer: outputBuffer,
267
+ stride: outputStride,
268
+ count: outputCount,
269
+ position: outputField(OUTPUT_FIELDS.position),
270
+ axis1: outputField(OUTPUT_FIELDS.axis1),
271
+ axis2: outputField(OUTPUT_FIELDS.axis2),
272
+ axis3: outputField(OUTPUT_FIELDS.axis3),
273
+ color: hasColor ? outputField(OUTPUT_FIELDS.color) : undefined,
246
274
  },
247
275
  };
248
276
  }
@@ -264,27 +292,30 @@ function createGlyphTransformCompute({
264
292
  * @param {THREE.WebGPURenderer} renderer
265
293
  * @param {Object} glyphCompute - return value of createGlyphTransformCompute().
266
294
  * @returns {Promise<Object>} { position, axis1, axis2, axis3, color } -
267
- * plain Float32Arrays, one vec3 per output glyph instance (color only
268
- * present when the glyphset has colours).
295
+ * plain Float32Arrays with a 4 float stride per output glyph instance
296
+ * (color only present when the glyphset has colours).
269
297
  */
270
298
  async function readbackGlyphTransform(renderer, glyphCompute) {
271
- const { position, axis1, axis2, axis3, color } = glyphCompute.outputs;
272
- // Read sequentially rather than Promise.all-ing these - concurrent
273
- // getArrayBufferAsync() calls against the same device have been observed
274
- // to crash the (Node-only, test-time) WebGPU native backend; one at a
275
- // time is the safe pattern here.
276
- const positionResult = new Float32Array(await renderer.getArrayBufferAsync(position.value));
277
- const axis1Result = new Float32Array(await renderer.getArrayBufferAsync(axis1.value));
278
- const axis2Result = new Float32Array(await renderer.getArrayBufferAsync(axis2.value));
279
- const axis3Result = new Float32Array(await renderer.getArrayBufferAsync(axis3.value));
280
- const colorResult = color ? new Float32Array(await renderer.getArrayBufferAsync(color.value)) : undefined;
281
-
299
+ const { buffer, stride, count, color } = glyphCompute.outputs;
300
+ //A single readback of the interleaved output buffer
301
+ const data = new Float32Array(await renderer.getArrayBufferAsync(buffer.value));
302
+ //Split into one array per field, each with a 4 float stride per instance
303
+ const extract = (field) => {
304
+ const result = new Float32Array(count * 4);
305
+ for (let i = 0; i < count; i++) {
306
+ const source = (i * stride + field) * 4;
307
+ result[i * 4] = data[source];
308
+ result[i * 4 + 1] = data[source + 1];
309
+ result[i * 4 + 2] = data[source + 2];
310
+ }
311
+ return result;
312
+ };
282
313
  return {
283
- position: positionResult,
284
- axis1: axis1Result,
285
- axis2: axis2Result,
286
- axis3: axis3Result,
287
- color: colorResult,
314
+ position: extract(OUTPUT_FIELDS.position),
315
+ axis1: extract(OUTPUT_FIELDS.axis1),
316
+ axis2: extract(OUTPUT_FIELDS.axis2),
317
+ axis3: extract(OUTPUT_FIELDS.axis3),
318
+ color: color ? extract(OUTPUT_FIELDS.color) : undefined,
288
319
  };
289
320
  }
290
321
 
@@ -303,4 +334,25 @@ async function dispatchAndReadbackGlyphTransform(renderer, glyphCompute) {
303
334
  return readbackGlyphTransform(renderer, glyphCompute);
304
335
  }
305
336
 
306
- export { createGlyphTransformCompute, dispatchAndReadbackGlyphTransform, readbackGlyphTransform, REPEAT_MODE };
337
+ /*
338
+ * The GPU glyph transforms read storage buffers at arbitrary indices, which
339
+ * the WebGL fallback backend of WebGPURenderer does not support (it turns
340
+ * them into plain vertex attributes). The renderer sets this when it is
341
+ * initialised, glyphsets use the CPU animation path when it is false.
342
+ */
343
+ let glyphComputeSupported = true;
344
+
345
+ const setGlyphComputeSupported = (flag) => {
346
+ glyphComputeSupported = flag ? true : false;
347
+ }
348
+
349
+ const isGlyphComputeSupported = () => glyphComputeSupported;
350
+
351
+ export {
352
+ createGlyphTransformCompute,
353
+ dispatchAndReadbackGlyphTransform,
354
+ isGlyphComputeSupported,
355
+ readbackGlyphTransform,
356
+ setGlyphComputeSupported,
357
+ REPEAT_MODE,
358
+ };