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.
@@ -3,7 +3,7 @@ import { Label } from './label';
3
3
  import { InstancedPoints } from '../three/InstancedPoints';
4
4
  import { ZincObject } from './zincObject';
5
5
  import { getCircularTexture, toBufferGeometry } from '../utilities';
6
- import { createInstancedPointsMaterial } from '../tls/pointsMaterial';
6
+ import { createInstancedPointsMaterial } from '../tsl/pointsMaterial';
7
7
 
8
8
  //Maximum number of points a programmatically built (addPoints) Pointset can
9
9
  //hold - the InstancedMesh backing it is allocated once with this capacity.
@@ -26,28 +26,26 @@ const createPointQuadGeometry = (capacity) => {
26
26
 
27
27
  /**
28
28
  * Extract the per time step, flat [x0, y0, z0, x1, y1, z1, ...] position
29
- * arrays from a (possibly time varying) BufferGeometry. Index 0 is always
30
- * the base frame, subsequent entries come from `morphAttributes.position`.
29
+ * arrays from a (possibly time varying) BufferGeometry.
30
+
31
31
  */
32
32
  const extractPositionFrames = (geometry, localTimeEnabled) => {
33
33
  const positionAttribute = geometry.getAttribute('position');
34
- const frames = [Float32Array.from(positionAttribute.array)];
35
34
  const morphPositions = geometry.morphAttributes.position;
36
35
  if (localTimeEnabled && morphPositions && morphPositions.length > 0) {
37
36
  const relative = geometry.morphTargetsRelative;
38
- morphPositions.forEach((attribute) => {
39
- if (relative) {
40
- const frame = new Float32Array(positionAttribute.array.length);
41
- for (let i = 0; i < frame.length; i++) {
42
- frame[i] = positionAttribute.array[i] + attribute.array[i];
43
- }
44
- frames.push(frame);
45
- } else {
46
- frames.push(Float32Array.from(attribute.array));
37
+ return morphPositions.map((attribute) => {
38
+ if (!relative) {
39
+ return Float32Array.from(attribute.array);
47
40
  }
41
+ const frame = new Float32Array(positionAttribute.array.length);
42
+ for (let i = 0; i < frame.length; i++) {
43
+ frame[i] = positionAttribute.array[i] + attribute.array[i];
44
+ }
45
+ return frame;
48
46
  });
49
47
  }
50
- return frames;
48
+ return [Float32Array.from(positionAttribute.array)];
51
49
  }
52
50
 
53
51
  /**
@@ -59,14 +57,7 @@ const extractPositionFrames = (geometry, localTimeEnabled) => {
59
57
  const extractColorFrames = (geometry, localMorphColour) => {
60
58
  const morphColors = geometry.morphAttributes.color;
61
59
  if (localMorphColour && morphColors && morphColors.length > 0) {
62
- const colorAttribute = geometry.getAttribute('color');
63
- const frames = [];
64
- frames.push(colorAttribute ? Float32Array.from(colorAttribute.array) :
65
- new Float32Array(morphColors[0].array.length));
66
- morphColors.forEach((attribute) => {
67
- frames.push(Float32Array.from(attribute.array));
68
- });
69
- return frames;
60
+ return morphColors.map((attribute) => Float32Array.from(attribute.array));
70
61
  }
71
62
  return undefined;
72
63
  }
@@ -1,6 +1,6 @@
1
1
  import * as THREE from 'three/webgpu';
2
2
  //import * as shader from '../shaders/textureSlide.js';
3
- import { createWebGPUMaterial } from '../tls/textureSlides.js';
3
+ import { createWebGPUMaterial } from '../tsl/textureSlides.js';
4
4
  import { TexturePrimitive } from './texturePrimitive';
5
5
 
6
6
 
@@ -18,6 +18,8 @@ const cloneData3DTexture = (sourceTex) => {
18
18
  targetTex.wrapS = sourceTex.wrapS;
19
19
  targetTex.wrapT = sourceTex.wrapT;
20
20
  targetTex.wrapR = sourceTex.wrapR;
21
+ targetTex.colorSpace = sourceTex.colorSpace;
22
+ targetTex.anisotropy = sourceTex.anisotropy;
21
23
  targetTex.needsUpdate = true;
22
24
  return targetTex;
23
25
  }
package/src/region.js CHANGED
@@ -125,6 +125,15 @@ let Region = function (parentIn, sceneIn) {
125
125
  return parent;
126
126
  }
127
127
 
128
+ /**
129
+ * Get the {@link Scene} this region belongs to.
130
+ *
131
+ * @return {Scene}
132
+ */
133
+ this.getScene = () => {
134
+ return scene;
135
+ }
136
+
128
137
  /**
129
138
  * Get the array of each hierarachy from the root region to this region.
130
139
  *
package/src/renderer.js CHANGED
@@ -133,14 +133,20 @@ const Renderer = function (containerIn) {
133
133
  }
134
134
  //WebGPURenderer otherwise requests a device with the default
135
135
  //maxTextureArrayLayers (256 on many adapters), which is too low for
136
- //some currnetly sypported scaffold. We will request the highest possible
137
- //on the device
136
+ //some currnetly sypported scaffold, and the default
137
+ //maxStorageBuffersPerShaderStage (8 on many adapters), which is too
138
+ //low for compute passes like glyphset.js's GPU glyph-transform (11
139
+ //storage buffers). We will request the highest possible on the device
140
+ //for both.
138
141
  if (parameters.device === undefined && parameters.requiredLimits === undefined &&
139
142
  typeof navigator !== 'undefined' && navigator.gpu) {
140
143
  try {
141
144
  const adapter = await navigator.gpu.requestAdapter({ powerPreference: parameters.powerPreference });
142
145
  if (adapter) {
143
- parameters.requiredLimits = { maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers };
146
+ parameters.requiredLimits = {
147
+ maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers,
148
+ maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage,
149
+ };
144
150
  }
145
151
  } catch (err) {
146
152
  //Fall through and let WebGPURenderer's own adapter/device request
@@ -719,10 +725,23 @@ const Renderer = function (containerIn) {
719
725
  }
720
726
  }
721
727
 
728
+ /**
729
+ * Check if the renderer is running on the WebGL 2 fallback backend.
730
+ * WebGPURenderer never uses WebGL 1, so this is false when WebGPU is used.
731
+ *
732
+ * @return {Boolean}
733
+ */
722
734
  this.isWebGL2 = () => {
723
- if (renderer)
724
- return renderer.capabilities.isWebGL2;
725
- return false;
735
+ return renderer?.backend?.isWebGLBackend === true;
736
+ }
737
+
738
+ /**
739
+ * Check if the renderer is running on the WebGPU backend.
740
+ *
741
+ * @return {Boolean}
742
+ */
743
+ this.isWebGPU = () => {
744
+ return renderer?.backend?.isWebGPUBackend === true;
726
745
  }
727
746
  };
728
747
 
package/src/scene.js CHANGED
@@ -9,7 +9,7 @@ import { SceneExporter } from './sceneExporter';
9
9
  import { SceneLoader } from './sceneLoader';
10
10
  import { Viewport } from './controls';
11
11
  import { createPointQuadGeometry } from './primitives/pointset';
12
- import { createInstancedPointsMaterial } from './tls/pointsMaterial';
12
+ import { createInstancedPointsMaterial } from './tsl/pointsMaterial';
13
13
  import {
14
14
  createBufferGeometry,
15
15
  createNewSpriteText,
@@ -988,8 +988,32 @@ const Scene = function (containerIn, rendererIn) {
988
988
  return undefined;
989
989
  }
990
990
 
991
+ /**
992
+ * Check if the renderer is running on the WebGL 2 fallback backend.
993
+ *
994
+ * @return {Boolean}
995
+ */
991
996
  this.isWebGL2 = () => {
992
- return rendererIn.isWebGL2();
997
+ return rendererIn?.backend?.isWebGLBackend === true;
998
+ }
999
+
1000
+ /**
1001
+ * Check if the renderer is running on the WebGPU backend.
1002
+ *
1003
+ * @return {Boolean}
1004
+ */
1005
+ this.isWebGPU = () => {
1006
+ return rendererIn?.backend?.isWebGPUBackend === true;
1007
+ }
1008
+
1009
+ /**
1010
+ * Get the THREE.WebGPURenderer this scene is being drawn with, for code
1011
+ * that needs to dispatch its own GPU work (e.g. a TSL compute pass).
1012
+ *
1013
+ * @return {THREE.WebGPURenderer}
1014
+ */
1015
+ this.getRenderer = () => {
1016
+ return rendererIn;
993
1017
  }
994
1018
 
995
1019
  /**
@@ -1,4 +1,49 @@
1
- import { GLTFExporter } from './three/GLTFExporter';
1
+ import { BufferAttribute } from 'three/webgpu';
2
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter';
3
+
4
+ /*
5
+ * GLTFExporter only writes POSITION and NORMAL morph targets. glTF allows
6
+ * COLOR_n morph targets as well (GLTFLoader reads them), so add them to the
7
+ * targets the exporter has already written. Like other glTF morph targets
8
+ * they are stored relative to the base attribute.
9
+ */
10
+ class GLTFMorphColourPlugin {
11
+ constructor(writer) {
12
+ this.writer = writer;
13
+ this.name = 'ZINC_morph_colour';
14
+ }
15
+
16
+ writeMesh(mesh, meshDef) {
17
+ const geometry = mesh.geometry;
18
+ const morphColours = geometry && geometry.morphAttributes ?
19
+ geometry.morphAttributes.color : undefined;
20
+ const baseColour = geometry ? geometry.attributes.color : undefined;
21
+ const primitive = meshDef.primitives ? meshDef.primitives[0] : undefined;
22
+ if (!morphColours || !baseColour || !primitive || !primitive.targets)
23
+ return;
24
+ // All primitives of a mesh share the same targets array.
25
+ const targets = primitive.targets;
26
+ const itemSize = baseColour.itemSize;
27
+ const count = baseColour.count;
28
+ for (let i = 0; i < targets.length && i < morphColours.length; i++) {
29
+ const attribute = morphColours[i];
30
+ if (attribute.itemSize !== itemSize || attribute.count < count) {
31
+ console.warn('GLTFExporter: Colour morph target does not match the base colour, skipped.');
32
+ return;
33
+ }
34
+ const array = new Float32Array(count * itemSize);
35
+ for (let j = 0; j < count; j++) {
36
+ for (let a = 0; a < itemSize; a++) {
37
+ const value = attribute.getComponent(j, a);
38
+ array[j * itemSize + a] = geometry.morphTargetsRelative ?
39
+ value : value - baseColour.getComponent(j, a);
40
+ }
41
+ }
42
+ targets[i].COLOR_0 = this.writer.processAccessor(
43
+ new BufferAttribute(array, itemSize), geometry);
44
+ }
45
+ }
46
+ }
2
47
 
3
48
  /**
4
49
  * Provides an object which uses for exporting the scene
@@ -10,22 +55,59 @@ import { GLTFExporter } from './three/GLTFExporter';
10
55
  const SceneExporter = function (sceneIn) {
11
56
  const scene = sceneIn;
12
57
 
13
- this.exportGLTF = (binary) => {
58
+ // Matches the exporter's onlyVisible pruning - animations targeting a
59
+ // hidden node would otherwise produce channels with no target node.
60
+ const isVisible = (object) => {
61
+ for (let current = object; current; current = current.parent) {
62
+ if (current.visible === false) return false;
63
+ }
64
+ return true;
65
+ };
66
+
67
+ this.exportGLTF = async (binary) => {
14
68
  const rootRegion = scene.getRootRegion();
15
69
  const zincObjects = rootRegion.getAllObjects(true);
16
70
  const animations = [];
17
- zincObjects.forEach(zincObject => {
18
- if (zincObject.animationClip) {
19
- animations.push({clip: zincObject.animationClip[0], mesh: zincObject.getMorph()});
71
+ const glyphsetsToRestore = [];
72
+ for (const zincObject of zincObjects) {
73
+ if (zincObject.animationClip && zincObject.animationClip[0]) {
74
+ const mesh = zincObject.getMorph();
75
+ if (mesh && isVisible(mesh)) {
76
+ // The exporter resolves tracks against the scene root, but morph
77
+ // clips target '' (the mesh itself), so retarget them by uuid.
78
+ const clip = zincObject.animationClip[0].clone();
79
+ clip.tracks.forEach(track => { track.name = mesh.uuid + track.name; });
80
+ animations.push(clip);
81
+ }
82
+ }
83
+ // Glyphsets with GPU-compute-driven colour intentionally keep
84
+ // instanceColor null during normal rendering (see glyphset.js's
85
+ // prepareColorForExport() for why) - populate it just for this
86
+ // export, then undo it again afterwards.
87
+ if (zincObject.isGlyphset && zincObject.prepareColorForExport) {
88
+ await zincObject.prepareColorForExport();
89
+ glyphsetsToRestore.push(zincObject);
20
90
  }
21
- });
91
+ }
22
92
  const exporter = new GLTFExporter();
23
- const options = { binary, animations };
24
- return new Promise((resolve, reject) => {
25
- exporter.parse( scene.getThreeJSScene(), function ( gltf ) {
26
- resolve(gltf);
27
- }, options );
28
- });
93
+ exporter.register(writer => new GLTFMorphColourPlugin(writer));
94
+ const options = { binary, animations, onlyVisible: true };
95
+ try {
96
+ return await new Promise((resolve, reject) => {
97
+ exporter.parse(
98
+ scene.getThreeJSScene(),
99
+ function ( gltf ) {
100
+ resolve(gltf);
101
+ },
102
+ function ( error ) {
103
+ console.error("Unable to export GLTF.", error);
104
+ reject(error);
105
+ },
106
+ options );
107
+ });
108
+ } finally {
109
+ glyphsetsToRestore.forEach(zincObject => zincObject.clearColorExportState());
110
+ }
29
111
  }
30
112
  }
31
113
 
@@ -0,0 +1,53 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { Fn, instanceIndex, positionGeometry, materialColor, materialOpacity, vec4 } from 'three/tsl';
3
+ import { NODE_MATERIAL_CLASSES } from './morphColorMaterial';
4
+
5
+ /**
6
+ * Builds a NodeMaterial that renders each glyph instance's transform and
7
+ * colour read directly from a glyphCompute's (see ../tsl/glyphTransform.js)
8
+ * GPU output buffers, via a custom positionNode/colorNode - i.e. no
9
+ * instanceMatrix/instanceColor CPU sync is involved in producing what gets
10
+ * drawn. This is what lets Glyphset render every frame of an animation
11
+ * straight from the compute pass's output with zero GPU->CPU readback.
12
+ *
13
+ * Per NodeMaterial.setupPosition(), setting `positionNode` fully replaces
14
+ * (not composes with) the automatic per-instance instanceMatrix transform a
15
+ * stock THREE.InstancedMesh would otherwise apply - so instanceMatrix is
16
+ * simply irrelevant to rendering once this is set, no double-transform risk.
17
+ * (instanceMatrix is still kept in sync separately, when paused, purely so
18
+ * Glyphset's existing bounding-box/raycast code - which does need a
19
+ * CPU-side transform to read - keeps working unmodified.)
20
+ *
21
+ * @param {THREE.Material} sourceMaterial - the material JSONLoader produced
22
+ * for this glyph geometry; converted to its NodeMaterial equivalent.
23
+ * @param {Object} glyphCompute - return value of createGlyphTransformCompute().
24
+ * @returns {THREE.NodeMaterial}
25
+ */
26
+ function createGlyphInstancedMaterial(sourceMaterial, glyphCompute) {
27
+ let material = sourceMaterial;
28
+ if (!material || !material.isNodeMaterial) {
29
+ const NodeMaterialClass = (sourceMaterial && NODE_MATERIAL_CLASSES[sourceMaterial.type]) || THREE.MeshPhongNodeMaterial;
30
+ material = new NodeMaterialClass();
31
+ if (sourceMaterial) material.copy(sourceMaterial);
32
+ }
33
+
34
+ const { position, axis1, axis2, axis3, color } = glyphCompute.outputs;
35
+
36
+ material.positionNode = Fn(() => {
37
+ const idx = instanceIndex;
38
+ return position.element(idx)
39
+ .add(axis1.element(idx).mul(positionGeometry.x))
40
+ .add(axis2.element(idx).mul(positionGeometry.y))
41
+ .add(axis3.element(idx).mul(positionGeometry.z));
42
+ })();
43
+
44
+ if (color) {
45
+ material.colorNode = Fn(() => {
46
+ return vec4(color.element(instanceIndex).mul(materialColor), materialOpacity);
47
+ })();
48
+ }
49
+
50
+ return material;
51
+ }
52
+
53
+ export { createGlyphInstancedMaterial };
@@ -0,0 +1,306 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import {
3
+ Fn,
4
+ If,
5
+ attributeArray,
6
+ instanceIndex,
7
+ int,
8
+ float,
9
+ vec3,
10
+ uniform,
11
+ mix,
12
+ select,
13
+ max,
14
+ } from 'three/tsl';
15
+
16
+ /**
17
+ * Mirrors the string repeat_mode values used throughout glyphset.js as small
18
+ * integers, since TSL uniforms/comparisons work with numeric types.
19
+ */
20
+ const REPEAT_MODE = { NONE: 0, MIRROR: 1, AXES_2D: 2, AXES_3D: 3 };
21
+
22
+ const repeatModeToInt = (repeat_mode) => {
23
+ switch (repeat_mode) {
24
+ case "MIRROR": return REPEAT_MODE.MIRROR;
25
+ case "AXES_2D": return REPEAT_MODE.AXES_2D;
26
+ case "AXES_3D": return REPEAT_MODE.AXES_3D;
27
+ default: return REPEAT_MODE.NONE;
28
+ }
29
+ }
30
+
31
+ const multiplierForRepeatMode = (repeat_mode) => {
32
+ if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR") return 2;
33
+ if (repeat_mode == "AXES_3D") return 3;
34
+ return 1;
35
+ }
36
+
37
+ /**
38
+ * GPU port of Glyphset's resolve_glyph_axes() + the per-frame keyframe
39
+ * interpolation from updateMorphGlyphsets(). This is only used for the
40
+ * recurring, per-frame time-varying animation path - the one-time,
41
+ * load-time glyph transform/colour computation still runs on the CPU (see
42
+ * glyphset.js).
43
+ *
44
+ * baseSize/offset/scaleFactors/globalScale/repeat_mode are fixed for the
45
+ * lifetime of a glyphset, so they're baked in as uniforms once here rather
46
+ * than re-passed on every dispatch; bottomFrame/topFrame/proportion are the
47
+ * only per-frame-changing uniforms (see uniforms.bottomFrame etc. on the
48
+ * returned object).
49
+ *
50
+ * @param {Object} params
51
+ * @param {number} params.baseCount - number of raw (pre-repeat_mode-expansion)
52
+ * glyph records per keyframe (positions.length/3 for a single frame).
53
+ * @param {number} params.numberOfTimeSteps - number of keyframes.
54
+ * @param {number} params.outputCount - final instance count after
55
+ * repeat_mode expansion (glyphset.js's numberOfVertices).
56
+ * @param {string} params.repeat_mode - "NONE" | "MIRROR" | "AXES_2D" | "AXES_3D".
57
+ * @param {Float32Array} params.positionsData - flattened [time][record][xyz].
58
+ * @param {Float32Array} params.axis1Data
59
+ * @param {Float32Array} params.axis2Data
60
+ * @param {Float32Array} params.axis3Data
61
+ * @param {Float32Array} params.scaleData
62
+ * @param {Float32Array} [params.colorData] - flattened [time][record][rgb],
63
+ * omitted when the glyphset has no colours.
64
+ * @param {Array<number>} params.baseSize
65
+ * @param {Array<number>} params.offset
66
+ * @param {Array<number>} params.scaleFactors
67
+ * @returns {Object} { uniforms, compute, outputs } - update
68
+ * 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).
72
+ */
73
+ function createGlyphTransformCompute({
74
+ baseCount,
75
+ outputCount,
76
+ repeat_mode,
77
+ positionsData,
78
+ axis1Data,
79
+ axis2Data,
80
+ axis3Data,
81
+ scaleData,
82
+ colorData,
83
+ baseSize,
84
+ offset,
85
+ scaleFactors,
86
+ globalScale,
87
+ }) {
88
+ const multiplier = multiplierForRepeatMode(repeat_mode);
89
+
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;
102
+
103
+ const uniforms = {
104
+ bottomFrame: uniform(0, 'int'),
105
+ topFrame: uniform(0, 'int'),
106
+ proportion: uniform(1),
107
+ repeatMode: uniform(repeatModeToInt(repeat_mode), 'int'),
108
+ baseSize: uniform(new THREE.Vector3(...baseSize)),
109
+ offset: uniform(new THREE.Vector3(...offset)),
110
+ scaleFactors: uniform(new THREE.Vector3(...scaleFactors)),
111
+ globalScale: uniform(globalScale),
112
+ };
113
+
114
+ const computeFn = Fn(() => {
115
+
116
+ const outIdx = int(instanceIndex);
117
+ const inputIdx = outIdx.div(int(multiplier));
118
+ const copyIdx = outIdx.mod(int(multiplier));
119
+
120
+ const bottomIdx = uniforms.bottomFrame.mul(int(baseCount)).add(inputIdx);
121
+ const topIdx = uniforms.topFrame.mul(int(baseCount)).add(inputIdx);
122
+
123
+ // 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);
129
+
130
+ // NOTE: each branch below assigns straight into the output storage
131
+ // buffer elements (outPosition.element(outIdx).assign(...) etc.),
132
+ // rather than into a shared vec3(0).toVar() declared before the
133
+ // If/Else and read back afterwards - that pattern (var declared
134
+ // outside an If().Else(), written in both branches, read after)
135
+ // reliably crashes this three.js version's WGSL codegen (verified via
136
+ // a native crash bisected down to exactly that shape in
137
+ // test/glyphTransformCompute.test.js's development). Writing directly
138
+ // to the output buffer inside each branch avoids it.
139
+
140
+ const isNoneOrMirror = uniforms.repeatMode.equal(int(REPEAT_MODE.NONE))
141
+ .or(uniforms.repeatMode.equal(int(REPEAT_MODE.MIRROR)));
142
+
143
+ If(isNoneOrMirror, () => {
144
+
145
+ // resolve_glyph_axes(): NONE / MIRROR branch.
146
+ const signVec = vec3(
147
+ select(scale.x.lessThan(0), float(-1), float(1)),
148
+ select(scale.y.lessThan(0), float(-1), float(1)),
149
+ select(scale.z.lessThan(0), float(-1), float(1)),
150
+ );
151
+ const axisScale = signVec.mul(uniforms.baseSize).add(scale.mul(uniforms.scaleFactors)).mul(uniforms.globalScale);
152
+ const baseAxis1 = axis1.mul(axisScale.x);
153
+ const baseAxis2 = axis2.mul(axisScale.y);
154
+ const baseAxis3 = axis3.mul(axisScale.z);
155
+ const basePoint = point
156
+ .add(baseAxis1.mul(uniforms.offset.x))
157
+ .add(baseAxis2.mul(uniforms.offset.y))
158
+ .add(baseAxis3.mul(uniforms.offset.z));
159
+
160
+ const isMirrorCopy = uniforms.repeatMode.equal(int(REPEAT_MODE.MIRROR)).and(copyIdx.equal(int(1)));
161
+
162
+ const a1 = select(isMirrorCopy, baseAxis1.negate(), baseAxis1).toVar();
163
+ const a2 = select(isMirrorCopy, baseAxis2.negate(), baseAxis2).toVar();
164
+ const a3 = select(isMirrorCopy, baseAxis3.negate(), baseAxis3).toVar();
165
+ const p = basePoint.toVar();
166
+
167
+ If(uniforms.repeatMode.equal(int(REPEAT_MODE.MIRROR)).and(scale.x.lessThan(0)), () => {
168
+ // shift glyph origin to end of axis1
169
+ p.subAssign(a1);
170
+ });
171
+
172
+ // reverse axis3 if required to maintain a right-handed coordinate system
173
+ const triple = a3.dot(a1.cross(a2));
174
+ If(triple.lessThan(0), () => {
175
+ a3.assign(a3.negate());
176
+ });
177
+
178
+ outPosition.element(outIdx).assign(p);
179
+ outAxis1.element(outIdx).assign(a1);
180
+ outAxis2.element(outIdx).assign(a2);
181
+ outAxis3.element(outIdx).assign(a3);
182
+
183
+ }).Else(() => {
184
+
185
+ // resolve_glyph_axes(): AXES_2D / AXES_3D branch.
186
+ const signVec = vec3(
187
+ select(scale.x.lessThan(0), float(-1), float(1)),
188
+ select(scale.y.lessThan(0), float(-1), float(1)),
189
+ select(scale.z.lessThan(0), float(-1), float(1)),
190
+ );
191
+ const axisScale = signVec.mul(uniforms.baseSize.x).add(scale.mul(uniforms.scaleFactors.x)).mul(uniforms.globalScale);
192
+ const finalPoint = point
193
+ .add(axis1.mul(axisScale.x).mul(uniforms.offset.x))
194
+ .add(axis2.mul(axisScale.y).mul(uniforms.offset.y))
195
+ .add(axis3.mul(axisScale.z).mul(uniforms.offset.z));
196
+
197
+ const useScale = select(copyIdx.equal(int(0)), scale.x,
198
+ select(copyIdx.equal(int(1)), scale.y, scale.z));
199
+
200
+ const isAxes2D = uniforms.repeatMode.equal(int(REPEAT_MODE.AXES_2D));
201
+
202
+ const useAxis1 = select(copyIdx.equal(int(0)), axis1,
203
+ select(copyIdx.equal(int(1)), axis2, axis3));
204
+ const useAxis2 = select(copyIdx.equal(int(0)), axis2,
205
+ select(copyIdx.equal(int(1)), select(isAxes2D, axis1, axis3), axis1));
206
+
207
+ const finalScale1 = uniforms.baseSize.x.add(useScale.mul(uniforms.scaleFactors.x)).mul(uniforms.globalScale);
208
+ const finalAxis1 = useAxis1.mul(finalScale1);
209
+
210
+ const axis3Raw = finalAxis1.cross(useAxis2);
211
+ const mag1 = axis3Raw.length();
212
+ let scaling1 = uniforms.baseSize.z.add(useScale.mul(uniforms.scaleFactors.z)).mul(uniforms.globalScale)
213
+ .div(max(mag1, 1e-8));
214
+ scaling1 = select(isAxes2D.and(copyIdx.greaterThan(int(0))), scaling1.mul(-1), scaling1);
215
+ const finalAxis3 = select(mag1.greaterThan(0), axis3Raw.mul(scaling1), axis3Raw);
216
+
217
+ const axis2Raw = finalAxis3.cross(finalAxis1);
218
+ const mag2 = axis2Raw.length();
219
+ const scaling2 = uniforms.baseSize.y.add(useScale.mul(uniforms.scaleFactors.y)).mul(uniforms.globalScale)
220
+ .div(max(mag2, 1e-8));
221
+ const finalAxis2 = select(mag2.greaterThan(0), axis2Raw.mul(scaling2), axis2Raw);
222
+
223
+ outPosition.element(outIdx).assign(finalPoint);
224
+ outAxis1.element(outIdx).assign(finalAxis1);
225
+ outAxis2.element(outIdx).assign(finalAxis2);
226
+ outAxis3.element(outIdx).assign(finalAxis3);
227
+
228
+ });
229
+
230
+ if (colorBuffer) {
231
+ const color = mix(colorBuffer.element(topIdx), colorBuffer.element(bottomIdx), uniforms.proportion);
232
+ outColor.element(outIdx).assign(color);
233
+ }
234
+
235
+ } )().compute( outputCount );
236
+
237
+ return {
238
+ uniforms,
239
+ compute: computeFn,
240
+ outputs: {
241
+ position: outPosition,
242
+ axis1: outAxis1,
243
+ axis2: outAxis2,
244
+ axis3: outAxis3,
245
+ color: outColor,
246
+ },
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Reads a glyph-transform compute pass's output buffers back to the CPU,
252
+ * without dispatching a new renderer.compute() call first - i.e. assumes
253
+ * the buffers already hold the desired result from a previous compute()
254
+ * dispatch (see dispatchAndReadbackGlyphTransform, or a bare
255
+ * renderer.compute(glyphCompute.compute) call). Split out from
256
+ * dispatchAndReadbackGlyphTransform so a caller that already dispatched
257
+ * recently (e.g. Glyphset's debounced accurate resync, which reuses the
258
+ * buffers its own immediately-preceding fast/no-readback dispatch just
259
+ * wrote) doesn't have to pay for - or risk - a second, redundant
260
+ * renderer.compute() call right on top of the first: back-to-back compute
261
+ * dispatches in close succession have been observed to reliably crash the
262
+ * (Node-only, test-time) WebGPU native backend.
263
+ *
264
+ * @param {THREE.WebGPURenderer} renderer
265
+ * @param {Object} glyphCompute - return value of createGlyphTransformCompute().
266
+ * @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).
269
+ */
270
+ 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
+
282
+ return {
283
+ position: positionResult,
284
+ axis1: axis1Result,
285
+ axis2: axis2Result,
286
+ axis3: axis3Result,
287
+ color: colorResult,
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Dispatches a glyph-transform compute pass built by
293
+ * createGlyphTransformCompute() and reads its output buffers back to the
294
+ * CPU. Update `glyphCompute.uniforms.bottomFrame/topFrame/proportion(/
295
+ * globalScale).value` before calling this.
296
+ *
297
+ * @param {THREE.WebGPURenderer} renderer
298
+ * @param {Object} glyphCompute - return value of createGlyphTransformCompute().
299
+ * @returns {Promise<Object>} see readbackGlyphTransform().
300
+ */
301
+ async function dispatchAndReadbackGlyphTransform(renderer, glyphCompute) {
302
+ renderer.compute(glyphCompute.compute);
303
+ return readbackGlyphTransform(renderer, glyphCompute);
304
+ }
305
+
306
+ export { createGlyphTransformCompute, dispatchAndReadbackGlyphTransform, readbackGlyphTransform, REPEAT_MODE };