zincjs 2.4.1-beta.2 → 2.4.1-beta.4

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.
@@ -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 };
@@ -2,7 +2,9 @@ import * as THREE from 'three/webgpu';
2
2
  import { Fn, attribute, materialColor, materialOpacity, mix, uniform, vec4 } from 'three/tsl';
3
3
 
4
4
  //Maps a classic material's constructor name to its NodeMaterial
5
- //equivalent, since only NodeMaterial exposes colorNode.
5
+ //equivalent, since only NodeMaterial exposes colorNode/positionNode.
6
+ //Exported for reuse anywhere else a classic material needs upgrading to a
7
+ //NodeMaterial to attach a custom node to (see glyphRenderMaterial.js).
6
8
  const NODE_MATERIAL_CLASSES = {
7
9
  MeshBasicMaterial: THREE.MeshBasicNodeMaterial,
8
10
  MeshLambertMaterial: THREE.MeshLambertNodeMaterial,
@@ -16,15 +18,6 @@ const NODE_MATERIAL_CLASSES = {
16
18
  * targets {@link updateMorphColorAttribute} (in utilities.js) keeps
17
19
  * published as the 'morphColor0'/'morphColor1' geometry attributes -
18
20
  * whichever two morph targets currently have non-zero influence.
19
- *
20
- * This replaces the legacy `material.onBeforeCompile = augmentMorphColor`
21
- * approach, which patched raw GLSL into the classic WebGL shader and has no
22
- * effect once a material is compiled through the WebGPU/TSL pipeline.
23
- * three.js's own built-in WebGPU morph target system packs morph colour
24
- * data into its morph texture (see three's Morph.js) but its blend loop
25
- * only ever applies the result to position/normal, never colour, so this
26
- * has to be done by hand.
27
- *
28
21
  * Converts `material` to its NodeMaterial equivalent first if it isn't one
29
22
  * already, since only NodeMaterial exposes `colorNode`.
30
23
  *
@@ -47,9 +40,6 @@ const applyMorphColorNode = (material) => {
47
40
 
48
41
  const morphColorMix = uniform(0);
49
42
 
50
- //materialColor already folds in material.color and, if present, .map -
51
- //multiplying it by the blended per vertex morph colour keeps that
52
- //compositing instead of replacing it outright.
53
43
  target.colorNode = Fn(() => {
54
44
  const colorA = attribute('morphColor0', 'vec3');
55
45
  const colorB = attribute('morphColor1', 'vec3');
@@ -62,4 +52,4 @@ const applyMorphColorNode = (material) => {
62
52
  return target;
63
53
  };
64
54
 
65
- export { applyMorphColorNode };
55
+ export { applyMorphColorNode, NODE_MATERIAL_CLASSES };
@@ -11,6 +11,7 @@ import {
11
11
  If,
12
12
  varying,
13
13
  float,
14
+ sRGBTransferEOTF,
14
15
  } from 'three/tsl';
15
16
 
16
17
  export function createWebGPUMaterial() {
@@ -111,7 +112,9 @@ placeholderTexture.needsUpdate = true;
111
112
  // Contrast calculations
112
113
  const contrastedColor = brightenedColor.sub(vec3(0.5)).mul(uniforms.contrast).add(vec3(0.5));
113
114
 
114
- return vec4(contrastedColor, 1.0);
115
+ // WebGPURenderer always applies an sRGB OETF (linear -> sRGB) encode to
116
+ // the final composited frame, we are applying an sRGB -> linear here to cancel out the effect.
117
+ return vec4(sRGBTransferEOTF(contrastedColor), 1.0);
115
118
  });
116
119
 
117
120
  // Assign to NodeMaterial slots
package/vite.config.js CHANGED
@@ -27,6 +27,7 @@ export default defineConfig({
27
27
  'three/tsl': 'THREE',
28
28
  'three/webgpu': 'THREE',
29
29
  'three-spritetext': 'SpriteText',
30
+ 'three/examples/jsm/exporters/GLTFExporter': 'GLTFExporter',
30
31
  'three/examples/jsm/loaders/GLTFLoader': 'GLTFLoader',
31
32
  'fflate': 'fflate'
32
33
  }