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
|
@@ -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 '../
|
|
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.
|
|
30
|
-
|
|
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.
|
|
39
|
-
if (relative) {
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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 '../
|
|
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
|
|
137
|
-
//on
|
|
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 = {
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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 './
|
|
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
|
|
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
|
/**
|
package/src/sceneExporter.js
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 };
|