sparcoon 0.7.3 → 0.9.1
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/README.md +124 -76
- package/dist/artifact/FXArtifact.d.ts +120 -0
- package/dist/artifact/FXArtifact.js +1 -0
- package/dist/coreLayout.d.ts +22 -0
- package/dist/coreLayout.js +32 -0
- package/dist/editor/index.d.ts +8 -0
- package/dist/editor/index.js +17 -0
- package/dist/effect/FXEffect.d.ts +77 -0
- package/dist/effect/FXEffect.js +454 -0
- package/dist/effect/FXEffectSampling.Internal.d.ts +24 -0
- package/dist/effect/FXEffectSampling.Internal.js +165 -0
- package/dist/effect/FXEffectSpec.d.ts +115 -0
- package/dist/effect/FXEffectSpec.js +1 -0
- package/dist/emitter/FXEmitter.Internal.d.ts +23 -58
- package/dist/emitter/FXEmitter.Internal.js +13 -29
- package/dist/emitter/FXEmitter.d.ts +54 -46
- package/dist/emitter/FXEmitter.js +357 -186
- package/dist/emitter/FXFromArtifactsOptions.d.ts +13 -0
- package/dist/emitter/FXFromArtifactsOptions.js +1 -0
- package/dist/emitter/FXObjectMotion.Internal.d.ts +33 -0
- package/dist/emitter/FXObjectMotion.Internal.js +87 -0
- package/dist/emitter/FXSimulationHolder.d.ts +1 -0
- package/dist/emitter/FXSimulationHolder.js +43 -0
- package/dist/index.d.ts +11 -39
- package/dist/index.js +13 -36
- package/dist/instancedParticle/FXInstancedParticle.d.ts +1 -22
- package/dist/instancedParticle/FXInstancedParticle.js +145 -85
- package/dist/instancedParticle/glTypeInfo.d.ts +5 -0
- package/dist/instancedParticle/glTypeInfo.js +3 -0
- package/dist/instancedParticle/primitiveGeometry.d.ts +10 -0
- package/dist/instancedParticle/primitiveGeometry.js +30 -0
- package/dist/miscellaneous/asserts.d.ts +1 -18
- package/dist/miscellaneous/asserts.js +21 -28
- package/dist/miscellaneous/fxMath.d.ts +26 -0
- package/dist/miscellaneous/fxMath.js +261 -0
- package/dist/miscellaneous/math.d.ts +2 -7
- package/dist/miscellaneous/math.js +8 -9
- package/dist/miscellaneous/texture/fxDataTexture.d.ts +26 -0
- package/dist/miscellaneous/texture/fxDataTexture.js +20 -0
- package/dist/render/FXArtifactMaterial.Internal.d.ts +16 -0
- package/dist/render/FXArtifactMaterial.Internal.js +944 -0
- package/dist/render/FXArtifactMaterial.d.ts +1 -0
- package/dist/render/FXArtifactMaterial.js +79 -0
- package/dist/render/FXMaterialOptions.d.ts +27 -0
- package/dist/render/FXMaterialOptions.js +5 -0
- package/dist/render/FXMesh.d.ts +47 -0
- package/dist/render/FXMesh.js +99 -0
- package/dist/render/FXMeshMaterial.d.ts +40 -0
- package/dist/render/FXMeshMaterial.js +77 -0
- package/dist/world/FXWorld.d.ts +52 -0
- package/dist/world/FXWorld.js +104 -0
- package/package.json +49 -29
- package/dist/_virtual/2d.js +0 -3
- package/dist/_virtual/3d.js +0 -3
- package/dist/_virtual/4d.js +0 -3
- package/dist/_virtual/mod.js +0 -5
- package/dist/_virtual/mod2.js +0 -3
- package/dist/emitter/behavior/FXBehavior.d.ts +0 -24
- package/dist/emitter/behavior/FXBehavior.js +0 -10
- package/dist/emitter/behavior/FXBehaviorDirectionalForce.d.ts +0 -23
- package/dist/emitter/behavior/FXBehaviorDirectionalForce.js +0 -72
- package/dist/emitter/behavior/FXBehaviorPointForce.d.ts +0 -36
- package/dist/emitter/behavior/FXBehaviorPointForce.js +0 -94
- package/dist/emitter/behavior/FXBehaviorScaleOverLife.d.ts +0 -24
- package/dist/emitter/behavior/FXBehaviorScaleOverLife.js +0 -55
- package/dist/emitter/behavior/FXBehaviorTorqueDamping.d.ts +0 -20
- package/dist/emitter/behavior/FXBehaviorTorqueDamping.js +0 -48
- package/dist/emitter/behavior/FXBehaviorTorqueNoise.d.ts +0 -25
- package/dist/emitter/behavior/FXBehaviorTorqueNoise.js +0 -67
- package/dist/emitter/behavior/FXBehaviorTorqueOverLife.d.ts +0 -19
- package/dist/emitter/behavior/FXBehaviorTorqueOverLife.js +0 -41
- package/dist/emitter/behavior/FXBehaviorVelocityDamping.d.ts +0 -20
- package/dist/emitter/behavior/FXBehaviorVelocityDamping.js +0 -50
- package/dist/emitter/behavior/FXBehaviorVelocityNoise.d.ts +0 -25
- package/dist/emitter/behavior/FXBehaviorVelocityNoise.js +0 -76
- package/dist/emitter/behavior/FXBehaviorVelocityOverLife.d.ts +0 -22
- package/dist/emitter/behavior/FXBehaviorVelocityOverLife.js +0 -55
- package/dist/emitter/spawn/FXSpawn.d.ts +0 -11
- package/dist/emitter/spawn/FXSpawn.js +0 -10
- package/dist/emitter/spawn/FXSpawnBox.d.ts +0 -18
- package/dist/emitter/spawn/FXSpawnBox.js +0 -50
- package/dist/emitter/spawn/FXSpawnLifetime.d.ts +0 -17
- package/dist/emitter/spawn/FXSpawnLifetime.js +0 -44
- package/dist/emitter/spawn/FXSpawnOffset.d.ts +0 -21
- package/dist/emitter/spawn/FXSpawnOffset.js +0 -49
- package/dist/emitter/spawn/FXSpawnPoint.d.ts +0 -18
- package/dist/emitter/spawn/FXSpawnPoint.js +0 -46
- package/dist/emitter/spawn/FXSpawnRotation.d.ts +0 -17
- package/dist/emitter/spawn/FXSpawnRotation.js +0 -42
- package/dist/emitter/spawn/FXSpawnScale.d.ts +0 -22
- package/dist/emitter/spawn/FXSpawnScale.js +0 -56
- package/dist/emitter/spawn/FXSpawnSphere.d.ts +0 -36
- package/dist/emitter/spawn/FXSpawnSphere.js +0 -87
- package/dist/emitter/spawn/FXSpawnTorque.d.ts +0 -17
- package/dist/emitter/spawn/FXSpawnTorque.js +0 -42
- package/dist/emitter/spawn/FXSpawnVelocity.d.ts +0 -29
- package/dist/emitter/spawn/FXSpawnVelocity.js +0 -91
- package/dist/instancedParticle/FXInstancedParticle.Internal.d.ts +0 -2
- package/dist/instancedParticle/shared.d.ts +0 -19
- package/dist/instancedParticle/shared.js +0 -116
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +0 -24
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +0 -275
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.d.ts +0 -75
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +0 -63
- package/dist/materials/FXMaterial/FXBlending.Internal.d.ts +0 -24
- package/dist/materials/FXMaterial/FXBlending.Internal.js +0 -64
- package/dist/materials/FXMaterial/FXDepthMaterial.Internal.d.ts +0 -4
- package/dist/materials/FXMaterial/FXDepthMaterial.Internal.js +0 -144
- package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.d.ts +0 -4
- package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +0 -175
- package/dist/materials/FXMaterial/FXMaterial.d.ts +0 -70
- package/dist/materials/FXMaterial/FXMaterial.js +0 -51
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.d.ts +0 -7
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.js +0 -103
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.d.ts +0 -7
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.js +0 -21
- package/dist/miscellaneous/color/FXColor.Internal.d.ts +0 -7
- package/dist/miscellaneous/color/FXColor.Internal.js +0 -42
- package/dist/miscellaneous/color/FXColor.d.ts +0 -234
- package/dist/miscellaneous/color/FXColor.js +0 -636
- package/dist/miscellaneous/curve/FXCurve1D.Internal.d.ts +0 -20
- package/dist/miscellaneous/curve/FXCurve1D.d.ts +0 -17
- package/dist/miscellaneous/curve/FXCurve1D.js +0 -94
- package/dist/miscellaneous/miscellaneous.d.ts +0 -34
- package/dist/miscellaneous/miscellaneous.js +0 -85
- package/dist/miscellaneous/texture/FXTextureView.Internal.d.ts +0 -49
- package/dist/miscellaneous/texture/FXTextureView.Internal.js +0 -6
- package/dist/miscellaneous/texture/FXTextureView.d.ts +0 -72
- package/dist/miscellaneous/texture/FXTextureView.js +0 -234
- package/dist/miscellaneous/webglCapabilities.d.ts +0 -1
- package/dist/miscellaneous/webglCapabilities.js +0 -45
- package/dist/node_modules/fast-simplex-noise/lib/2d.js +0 -93
- package/dist/node_modules/fast-simplex-noise/lib/3d.js +0 -142
- package/dist/node_modules/fast-simplex-noise/lib/4d.js +0 -196
- package/dist/node_modules/fast-simplex-noise/lib/mod.js +0 -24
- package/dist/nodes/FXNode.d.ts +0 -5
- package/dist/nodes/FXNode.js +0 -10
- package/dist/nodes/blending/FXNodeBlending.d.ts +0 -6
- package/dist/nodes/blending/FXNodeBlending.js +0 -9
- package/dist/nodes/blending/FXNodeLightnessBlendingMask.d.ts +0 -19
- package/dist/nodes/blending/FXNodeLightnessBlendingMask.js +0 -65
- package/dist/nodes/color/FXNodeColor.d.ts +0 -6
- package/dist/nodes/color/FXNodeColor.js +0 -9
- package/dist/nodes/color/FXNodeColorOverLife.d.ts +0 -22
- package/dist/nodes/color/FXNodeColorOverLife.js +0 -64
- package/dist/nodes/color/FXNodeSphericalClip.d.ts +0 -21
- package/dist/nodes/color/FXNodeSphericalClip.js +0 -54
- package/dist/nodes/normal/FXNodeFlatNormal.d.ts +0 -6
- package/dist/nodes/normal/FXNodeFlatNormal.js +0 -16
- package/dist/nodes/normal/FXNodeNormal.d.ts +0 -6
- package/dist/nodes/normal/FXNodeNormal.js +0 -9
- package/dist/nodes/normal/FXNodeSphericalNormal.d.ts +0 -6
- package/dist/nodes/normal/FXNodeSphericalNormal.js +0 -23
- package/dist/nodes/texture/FXNodeAnimatedTexture.d.ts +0 -52
- package/dist/nodes/texture/FXNodeAnimatedTexture.js +0 -151
- package/dist/nodes/texture/FXNodeStaticTexture.d.ts +0 -20
- package/dist/nodes/texture/FXNodeStaticTexture.js +0 -67
- package/dist/nodes/texture/FXNodeTexture.d.ts +0 -6
- package/dist/nodes/texture/FXNodeTexture.js +0 -9
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { Group, Mesh, Quaternion, Vector3, } from "three";
|
|
2
|
+
import { FXEmitter } from "../emitter/FXEmitter.js";
|
|
3
|
+
import { FXObjectMotionTracker } from "../emitter/FXObjectMotion.Internal.js";
|
|
4
|
+
import { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
|
|
5
|
+
import { FXMeshMaterial } from "../render/FXMeshMaterial.js";
|
|
6
|
+
import { FXWorld } from "../world/FXWorld.js";
|
|
7
|
+
import { frameOfTime, normalizeQuaternion, sampleTracks, sampleTransform, timeOfFrame, } from "./FXEffectSampling.Internal.js";
|
|
8
|
+
const VECTOR_COMPONENTS = ["x", "y", "z", "w"];
|
|
9
|
+
const PARAMETER_UNIFORM_PREFIX = "u_param_";
|
|
10
|
+
const PARAMETER_BINDING_PREFIX = "b_param_";
|
|
11
|
+
/**
|
|
12
|
+
* Writes `transform` onto `object`, skipping any channel in `liveChannels` - a "fake" channel the
|
|
13
|
+
* editor excludes from the exported keyframe data, left for the consumer to drive directly through
|
|
14
|
+
* the returned runtime object's own `position`/`quaternion`/`scale`.
|
|
15
|
+
*/
|
|
16
|
+
function applyTransform(object, transform, liveChannels) {
|
|
17
|
+
if (liveChannels?.includes("position") !== true) {
|
|
18
|
+
object.position.set(transform.position[0], transform.position[1], transform.position[2]);
|
|
19
|
+
}
|
|
20
|
+
if (liveChannels?.includes("rotation") !== true) {
|
|
21
|
+
const quaternion = normalizeQuaternion(transform.rotation);
|
|
22
|
+
object.quaternion.set(quaternion[0], quaternion[1], quaternion[2], quaternion[3]);
|
|
23
|
+
}
|
|
24
|
+
if (liveChannels?.includes("scale") !== true) {
|
|
25
|
+
object.scale.set(transform.scale[0], transform.scale[1], transform.scale[2]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Materializes a possibly-readonly keyframe value into the mutable shape the value writers expect. */
|
|
29
|
+
function materializeValue(value) {
|
|
30
|
+
return typeof value === "number" ? value : [...value];
|
|
31
|
+
}
|
|
32
|
+
// Duck-typed, not `instanceof`: the app's `three` and this package's peer-dep `three` can be two
|
|
33
|
+
// separate module instances (e.g. hoisted differently across a workspace), which would make
|
|
34
|
+
// `instanceof Texture`/`instanceof BufferGeometry` false for a perfectly good asset. Every `three`
|
|
35
|
+
// object of these kinds carries this own-property flag regardless of which module instance built it.
|
|
36
|
+
function isTexture(asset) {
|
|
37
|
+
return asset.isTexture === true;
|
|
38
|
+
}
|
|
39
|
+
function isBufferGeometry(asset) {
|
|
40
|
+
return asset.isBufferGeometry === true;
|
|
41
|
+
}
|
|
42
|
+
function texturesForSlots(slots, assets) {
|
|
43
|
+
const textures = {};
|
|
44
|
+
for (const name of slots) {
|
|
45
|
+
const asset = assets[name];
|
|
46
|
+
if (isTexture(asset)) {
|
|
47
|
+
textures[PARAMETER_UNIFORM_PREFIX + name] = asset;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return textures;
|
|
51
|
+
}
|
|
52
|
+
/** Every `BufferGeometry`-typed entry in `assets`, for {@link resolveGeometrySource}'s lookup - a
|
|
53
|
+
* custom geometry slot is a single name, not a per-shader-uniform slot list like textures. */
|
|
54
|
+
function geometriesFromAssets(assets) {
|
|
55
|
+
const geometries = {};
|
|
56
|
+
for (const name in assets) {
|
|
57
|
+
const asset = assets[name];
|
|
58
|
+
if (isBufferGeometry(asset)) {
|
|
59
|
+
geometries[name] = asset;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return geometries;
|
|
63
|
+
}
|
|
64
|
+
/** Writes one parameter's sampled value into whichever render uniform / behavior bindings it declares. */
|
|
65
|
+
function writeParameterSlots(render, behavior, name, value, uniforms, bindings) {
|
|
66
|
+
const uniformKey = PARAMETER_UNIFORM_PREFIX + name;
|
|
67
|
+
if (uniformKey in render.uniforms) {
|
|
68
|
+
uniforms[uniformKey] = value;
|
|
69
|
+
}
|
|
70
|
+
const bindingKey = PARAMETER_BINDING_PREFIX + name;
|
|
71
|
+
if (bindingKey in behavior.bindings) {
|
|
72
|
+
bindings[bindingKey] = typeof value === "number" ? value : (value[0] ?? 0);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const vector = typeof value === "number" ? [value] : value;
|
|
76
|
+
for (let i = 0; i < VECTOR_COMPONENTS.length; i += 1) {
|
|
77
|
+
const key = bindingKey + "_" + VECTOR_COMPONENTS[i];
|
|
78
|
+
if (key in behavior.bindings) {
|
|
79
|
+
bindings[key] = vector[i] ?? 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Restores one parameter's baked baseline (a parameter that stopped being driven this frame). */
|
|
84
|
+
function writeParameterBaseline(render, behavior, name, uniforms, bindings) {
|
|
85
|
+
const uniformKey = PARAMETER_UNIFORM_PREFIX + name;
|
|
86
|
+
const uniform = render.uniforms[uniformKey];
|
|
87
|
+
// Types say `value` is defined once `"value" in uniform`, but guard the runtime
|
|
88
|
+
// `{ external, value: undefined }` case so we never bake an undefined baseline.
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
90
|
+
if (uniform !== undefined && "value" in uniform && uniform.value !== undefined) {
|
|
91
|
+
uniforms[uniformKey] = uniform.value;
|
|
92
|
+
}
|
|
93
|
+
const bindingKey = PARAMETER_BINDING_PREFIX + name;
|
|
94
|
+
for (const key in behavior.bindings) {
|
|
95
|
+
if (key === bindingKey || key.startsWith(bindingKey + "_")) {
|
|
96
|
+
bindings[key] = behavior.bindings[key].value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// A VFX mesh has no behavior, so its value drive addresses render uniforms only; this stand-in
|
|
101
|
+
// makes the shared slot writer a no-op on the binding side.
|
|
102
|
+
const NO_BEHAVIOR_BINDINGS = { bindings: {} };
|
|
103
|
+
/**
|
|
104
|
+
* Runtime for a whole exported Sparcoon project: a `Group` that builds every emitter + VFX mesh
|
|
105
|
+
* from precompiled artifacts and drives the shared timeline. Add it to your scene, call `play()`,
|
|
106
|
+
* and `update(deltaSeconds)` each frame. Lit graphs read the scene's lights, so a lit effect needs
|
|
107
|
+
* a light probe + directional light in the host scene (studio lighting is not part of the export).
|
|
108
|
+
* An editor-emitted module supplies only the data (`FXEffectSpec` + a scene name) and a thin named
|
|
109
|
+
* subclass; this base class is identical for every export.
|
|
110
|
+
*
|
|
111
|
+
* The timeline drive mirrors the editor's own transport: burst/play events fire across the
|
|
112
|
+
* continuous swept interval, while value + transform tracks are frame-stepped onto the `fps` grid.
|
|
113
|
+
*/
|
|
114
|
+
export class FXEffect extends Group {
|
|
115
|
+
constructor(specification, assets, options = {}) {
|
|
116
|
+
super();
|
|
117
|
+
this.emitters = [];
|
|
118
|
+
this.meshMaterials = [];
|
|
119
|
+
this.meshes = [];
|
|
120
|
+
// World-space velocity/angular-velocity tracker per mesh, parallel to `meshes`.
|
|
121
|
+
this.meshMotionTrackers = [];
|
|
122
|
+
this.emitterDriven = [];
|
|
123
|
+
this.meshDriven = [];
|
|
124
|
+
// Name -> index, for the live-update API (getEmitter/getMesh/setEmitterParam/setMeshParam). A
|
|
125
|
+
// name the editor duplicated across entities resolves to the last one built (Map.set overwrites).
|
|
126
|
+
this.emitterIndexByName = new Map();
|
|
127
|
+
this.meshIndexByName = new Map();
|
|
128
|
+
this.time = 0;
|
|
129
|
+
this.lastTime = 0;
|
|
130
|
+
this.lastFrame = -1;
|
|
131
|
+
this.meshClock = 0;
|
|
132
|
+
this.playing = false;
|
|
133
|
+
this.disposed = false;
|
|
134
|
+
// Reused decompose scratch for the per-mesh motion diff (avoids a per-tick allocation); scale
|
|
135
|
+
// is discarded.
|
|
136
|
+
this.meshPositionScratch = new Vector3();
|
|
137
|
+
this.meshQuaternionScratch = new Quaternion();
|
|
138
|
+
this.meshScaleScratch = new Vector3();
|
|
139
|
+
this.specification = specification;
|
|
140
|
+
this.world = options.world ?? FXWorld.getDefault();
|
|
141
|
+
applyTransform(this, specification.transform);
|
|
142
|
+
const geometries = geometriesFromAssets(assets);
|
|
143
|
+
for (const emitterSpec of specification.emitters) {
|
|
144
|
+
const textures = texturesForSlots(emitterSpec.externalSlots, assets);
|
|
145
|
+
const emitter = FXEmitter.fromArtifacts(emitterSpec.render, emitterSpec.behavior, {
|
|
146
|
+
textures,
|
|
147
|
+
geometries,
|
|
148
|
+
expectedCapacity: emitterSpec.expectedCapacity,
|
|
149
|
+
castShadow: emitterSpec.castShadow ?? false,
|
|
150
|
+
receiveShadow: emitterSpec.receiveShadow ?? false,
|
|
151
|
+
}, this.world);
|
|
152
|
+
if (options.camera !== undefined && emitterSpec.sortInterval > 0) {
|
|
153
|
+
emitter.sortCamera = options.camera;
|
|
154
|
+
emitter.sortFraction = 1 / emitterSpec.sortInterval;
|
|
155
|
+
}
|
|
156
|
+
applyTransform(emitter, emitterSpec.transform);
|
|
157
|
+
this.add(emitter);
|
|
158
|
+
this.emitterIndexByName.set(emitterSpec.name, this.emitters.length);
|
|
159
|
+
this.emitters.push(emitter);
|
|
160
|
+
this.emitterDriven.push(new Set());
|
|
161
|
+
}
|
|
162
|
+
for (const meshSpec of specification.meshes) {
|
|
163
|
+
const textures = texturesForSlots(meshSpec.externalSlots, assets);
|
|
164
|
+
const material = new FXMeshMaterial(meshSpec.render, textures);
|
|
165
|
+
const mesh = new Mesh(resolveGeometrySource(meshSpec.geometry, geometries), material.buildThreeMaterial());
|
|
166
|
+
if (meshSpec.receiveShadow ?? false) {
|
|
167
|
+
mesh.receiveShadow = true;
|
|
168
|
+
}
|
|
169
|
+
if (meshSpec.castShadow ?? false) {
|
|
170
|
+
mesh.castShadow = true;
|
|
171
|
+
mesh.customDepthMaterial = material.buildThreeDepthMaterial();
|
|
172
|
+
}
|
|
173
|
+
applyTransform(mesh, meshSpec.transform);
|
|
174
|
+
this.add(mesh);
|
|
175
|
+
this.meshIndexByName.set(meshSpec.name, this.meshes.length);
|
|
176
|
+
this.meshes.push(mesh);
|
|
177
|
+
this.meshMaterials.push(material);
|
|
178
|
+
this.meshMotionTrackers.push(new FXObjectMotionTracker());
|
|
179
|
+
this.meshDriven.push(new Set());
|
|
180
|
+
}
|
|
181
|
+
// Pose + value drive at the timeline start, so the effect shows its authored frame-0 state
|
|
182
|
+
// before playback begins.
|
|
183
|
+
this.driveFrame(0);
|
|
184
|
+
// Self-subscribe: from here the world drives this effect every frame; dispose() unsubscribes.
|
|
185
|
+
this.world.registerEffect(this);
|
|
186
|
+
}
|
|
187
|
+
/** Restarts the timeline from the top: clears live particles and begins playback. */
|
|
188
|
+
play() {
|
|
189
|
+
if (this.disposed) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.resetTransport();
|
|
193
|
+
this.playing = true;
|
|
194
|
+
this.driveFrame(0);
|
|
195
|
+
}
|
|
196
|
+
/** Halts playback and clears live particles, returning to the authored frame-0 state. */
|
|
197
|
+
stop() {
|
|
198
|
+
if (this.disposed) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.playing = false;
|
|
202
|
+
this.resetTransport();
|
|
203
|
+
this.driveFrame(0);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* @internal Advances this effect one frame; driven by {@link FXWorld.update}. Drives only the
|
|
207
|
+
* timeline and the meshes - the world ticks the shared particle pool separately, exactly once.
|
|
208
|
+
* A stopped effect stays frozen.
|
|
209
|
+
*/
|
|
210
|
+
advanceFrame(deltaSeconds) {
|
|
211
|
+
if (this.disposed) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Gate the simulation on playback, exactly like the editor's transport: stopped => 0, so live
|
|
215
|
+
// particles and mesh clocks hold instead of drifting while the effect is not playing.
|
|
216
|
+
const simulationDelta = this.playing ? deltaSeconds : 0;
|
|
217
|
+
if (this.playing) {
|
|
218
|
+
this.advance(deltaSeconds);
|
|
219
|
+
}
|
|
220
|
+
this.meshClock += simulationDelta;
|
|
221
|
+
for (let i = 0; i < this.meshes.length; i += 1) {
|
|
222
|
+
const mesh = this.meshes[i];
|
|
223
|
+
// World-space velocity/angular velocity since the previous frame, from the mesh's own
|
|
224
|
+
// matrixWorld (posed by `advance()` -> `driveFrame()` above, or by a host live-driving it
|
|
225
|
+
// directly through `getMesh()`) - never from PARTICLE_POSITION-style local state, since a
|
|
226
|
+
// mesh is a single non-instanced object with no such thing.
|
|
227
|
+
mesh.updateWorldMatrix(true, false);
|
|
228
|
+
mesh.matrixWorld.decompose(this.meshPositionScratch, this.meshQuaternionScratch, this.meshScaleScratch);
|
|
229
|
+
const motion = this.meshMotionTrackers[i].sample({
|
|
230
|
+
position: [
|
|
231
|
+
this.meshPositionScratch.x,
|
|
232
|
+
this.meshPositionScratch.y,
|
|
233
|
+
this.meshPositionScratch.z,
|
|
234
|
+
],
|
|
235
|
+
quaternion: [
|
|
236
|
+
this.meshQuaternionScratch.x,
|
|
237
|
+
this.meshQuaternionScratch.y,
|
|
238
|
+
this.meshQuaternionScratch.z,
|
|
239
|
+
this.meshQuaternionScratch.w,
|
|
240
|
+
],
|
|
241
|
+
}, simulationDelta);
|
|
242
|
+
const material = this.meshMaterials[i];
|
|
243
|
+
material.setElapsedTime(this.meshClock);
|
|
244
|
+
material.setDeltaTime(simulationDelta);
|
|
245
|
+
material.setObjectVelocity(motion.velocity);
|
|
246
|
+
material.setObjectAngularVelocity(motion.angularVelocity);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The runtime emitter named `name` (its `FXEmitter`, an `Object3D`) - for a live channel the
|
|
251
|
+
* editor marked "fake" (see {@link FXEffectSpec}), pose it directly through its own
|
|
252
|
+
* `position`/`quaternion`/`scale`; `driveFrame` never touches that channel again. `undefined`
|
|
253
|
+
* for an unknown name or once disposed (matching {@link dispose}'s emptied index).
|
|
254
|
+
*/
|
|
255
|
+
getEmitter(name) {
|
|
256
|
+
if (this.disposed) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
const index = this.emitterIndexByName.get(name);
|
|
260
|
+
return index === undefined ? undefined : this.emitters[index];
|
|
261
|
+
}
|
|
262
|
+
/** The runtime mesh named `name` (a `three.Mesh`), for the same live-channel posing as {@link getEmitter}. */
|
|
263
|
+
getMesh(name) {
|
|
264
|
+
if (this.disposed) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const index = this.meshIndexByName.get(name);
|
|
268
|
+
return index === undefined ? undefined : this.meshes[index];
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Pushes `value` into emitter `name`'s Timeline Value `parameter` (a uniform/binding slot the
|
|
272
|
+
* editor marked "fake", so it is never sampled from baked keyframes). No-op for an unknown
|
|
273
|
+
* emitter, or once disposed - the underlying slot writer already no-ops for an unknown parameter.
|
|
274
|
+
*/
|
|
275
|
+
setEmitterParam(name, parameter, value) {
|
|
276
|
+
if (this.disposed) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const index = this.emitterIndexByName.get(name);
|
|
280
|
+
if (index === undefined) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const specification = this.specification.emitters[index];
|
|
284
|
+
const uniforms = {};
|
|
285
|
+
const bindings = {};
|
|
286
|
+
writeParameterSlots(specification.render, specification.behavior, parameter, materializeValue(value), uniforms, bindings);
|
|
287
|
+
if (Object.keys(uniforms).length > 0 || Object.keys(bindings).length > 0) {
|
|
288
|
+
this.emitters[index].applyValues({ uniforms, bindings });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Pushes `value` into mesh `name`'s Timeline Value `parameter`. See {@link setEmitterParam}. */
|
|
292
|
+
setMeshParam(name, parameter, value) {
|
|
293
|
+
if (this.disposed) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const index = this.meshIndexByName.get(name);
|
|
297
|
+
if (index === undefined) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const specification = this.specification.meshes[index];
|
|
301
|
+
const uniforms = {};
|
|
302
|
+
const bindings = {};
|
|
303
|
+
writeParameterSlots(specification.render, NO_BEHAVIOR_BINDINGS, parameter, materializeValue(value), uniforms, bindings);
|
|
304
|
+
if (Object.keys(uniforms).length > 0) {
|
|
305
|
+
this.meshMaterials[index].applyUniformValues(uniforms);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Unsubscribes from its world, destroys every emitter, and disposes mesh resources; the group
|
|
310
|
+
* empties itself from its parent. Idempotent.
|
|
311
|
+
*/
|
|
312
|
+
dispose() {
|
|
313
|
+
if (this.disposed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.disposed = true;
|
|
317
|
+
this.playing = false;
|
|
318
|
+
this.world.unregisterEffect(this);
|
|
319
|
+
for (const emitter of this.emitters) {
|
|
320
|
+
emitter.destroy();
|
|
321
|
+
}
|
|
322
|
+
this.emitters.length = 0;
|
|
323
|
+
for (let i = 0; i < this.meshes.length; i += 1) {
|
|
324
|
+
const mesh = this.meshes[i];
|
|
325
|
+
// A "custom" geometry is the app's own `assets` object - only a primitive built
|
|
326
|
+
// by `resolveGeometrySource` is ours to dispose.
|
|
327
|
+
if (this.specification.meshes[i].geometry.type === "primitive") {
|
|
328
|
+
mesh.geometry.dispose();
|
|
329
|
+
}
|
|
330
|
+
mesh.material.dispose();
|
|
331
|
+
}
|
|
332
|
+
this.meshes.length = 0;
|
|
333
|
+
this.meshMaterials.length = 0;
|
|
334
|
+
this.removeFromParent();
|
|
335
|
+
}
|
|
336
|
+
resetTransport() {
|
|
337
|
+
this.time = 0;
|
|
338
|
+
this.lastTime = 0;
|
|
339
|
+
this.lastFrame = -1;
|
|
340
|
+
// meshClock is deliberately NOT reset: a mesh's time-driven shader runs on monotonic sim time,
|
|
341
|
+
// decoupled from the timeline playhead, so a replay does not snap its phase back. The motion
|
|
342
|
+
// trackers ARE reset - unlike meshClock's phase, a restarted effect should not report a
|
|
343
|
+
// teleport-spike velocity from wherever a mesh was last posed.
|
|
344
|
+
for (const emitter of this.emitters) {
|
|
345
|
+
emitter.stop();
|
|
346
|
+
emitter.reset();
|
|
347
|
+
}
|
|
348
|
+
for (const tracker of this.meshMotionTrackers) {
|
|
349
|
+
tracker.reset();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
advance(deltaSeconds) {
|
|
353
|
+
const duration = this.specification.duration;
|
|
354
|
+
let now = this.time + deltaSeconds;
|
|
355
|
+
if (duration > 0 && now >= duration) {
|
|
356
|
+
// Loop wrap: finish this lap's tail, reset, then replay from the remainder.
|
|
357
|
+
this.fire(this.lastTime, duration);
|
|
358
|
+
for (const emitter of this.emitters) {
|
|
359
|
+
emitter.stop();
|
|
360
|
+
emitter.reset();
|
|
361
|
+
}
|
|
362
|
+
now = now % duration;
|
|
363
|
+
this.lastTime = 0;
|
|
364
|
+
this.lastFrame = -1;
|
|
365
|
+
}
|
|
366
|
+
this.fire(this.lastTime, now);
|
|
367
|
+
this.lastTime = now;
|
|
368
|
+
this.time = now;
|
|
369
|
+
const frame = frameOfTime(now, this.specification.fps);
|
|
370
|
+
if (frame !== this.lastFrame) {
|
|
371
|
+
this.lastFrame = frame;
|
|
372
|
+
this.driveFrame(timeOfFrame(frame, this.specification.fps));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Fires burst/play events whose time lies in `[from, to)` on their owning emitter. */
|
|
376
|
+
fire(from, to) {
|
|
377
|
+
if (to <= from) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
for (let i = 0; i < this.specification.emitters.length; i += 1) {
|
|
381
|
+
const emitter = this.emitters[i];
|
|
382
|
+
for (const event of this.specification.emitters[i].events) {
|
|
383
|
+
if (event.time < from || event.time >= to) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// Guard degenerate authored counts/rates: FXEmitter asserts a positive count/rate and would
|
|
387
|
+
// throw straight out of the host's frame loop, so a zero-count burst is a no-op here.
|
|
388
|
+
if (event.kind === "burst") {
|
|
389
|
+
if (event.count > 0) {
|
|
390
|
+
emitter.burst(event.count);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else if (event.rate > 0) {
|
|
394
|
+
emitter.play(event.rate, event.duration > 0 ? { duration: event.duration } : {});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Poses every entity and scrubs its sampled parameter values into the runtime at time `time`. The
|
|
401
|
+
* root itself is never re-posed here (see {@link FXEffectSpec.transformTracks}) - only
|
|
402
|
+
* emitters/meshes, and only their non-live channels/parameters.
|
|
403
|
+
*/
|
|
404
|
+
driveFrame(time) {
|
|
405
|
+
for (let i = 0; i < this.specification.emitters.length; i += 1) {
|
|
406
|
+
const emitterSpec = this.specification.emitters[i];
|
|
407
|
+
applyTransform(this.emitters[i], sampleTransform(emitterSpec.transform, emitterSpec.transformTracks, time), emitterSpec.liveChannels);
|
|
408
|
+
this.driveEmitterValues(i, sampleTracks(emitterSpec.tracks, time));
|
|
409
|
+
}
|
|
410
|
+
for (let i = 0; i < this.specification.meshes.length; i += 1) {
|
|
411
|
+
const meshSpec = this.specification.meshes[i];
|
|
412
|
+
applyTransform(this.meshes[i], sampleTransform(meshSpec.transform, meshSpec.transformTracks, time), meshSpec.liveChannels);
|
|
413
|
+
this.driveMeshValues(i, sampleTracks(meshSpec.tracks, time));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
driveEmitterValues(index, values) {
|
|
417
|
+
const specification = this.specification.emitters[index];
|
|
418
|
+
const driven = this.emitterDriven[index];
|
|
419
|
+
const uniforms = {};
|
|
420
|
+
const bindings = {};
|
|
421
|
+
for (const [name, value] of values) {
|
|
422
|
+
writeParameterSlots(specification.render, specification.behavior, name, value, uniforms, bindings);
|
|
423
|
+
driven.add(name);
|
|
424
|
+
}
|
|
425
|
+
for (const name of [...driven]) {
|
|
426
|
+
if (!values.has(name)) {
|
|
427
|
+
writeParameterBaseline(specification.render, specification.behavior, name, uniforms, bindings);
|
|
428
|
+
driven.delete(name);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (Object.keys(uniforms).length > 0 || Object.keys(bindings).length > 0) {
|
|
432
|
+
this.emitters[index].applyValues({ uniforms, bindings });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
driveMeshValues(index, values) {
|
|
436
|
+
const specification = this.specification.meshes[index];
|
|
437
|
+
const driven = this.meshDriven[index];
|
|
438
|
+
const uniforms = {};
|
|
439
|
+
const bindings = {};
|
|
440
|
+
for (const [name, value] of values) {
|
|
441
|
+
writeParameterSlots(specification.render, NO_BEHAVIOR_BINDINGS, name, value, uniforms, bindings);
|
|
442
|
+
driven.add(name);
|
|
443
|
+
}
|
|
444
|
+
for (const name of [...driven]) {
|
|
445
|
+
if (!values.has(name)) {
|
|
446
|
+
writeParameterBaseline(specification.render, NO_BEHAVIOR_BINDINGS, name, uniforms, bindings);
|
|
447
|
+
driven.delete(name);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (Object.keys(uniforms).length > 0) {
|
|
451
|
+
this.meshMaterials[index].applyUniformValues(uniforms);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FXKeyframe, FXQuat, FXTrack, FXTransform, FXTransformTrack, FXVec3 } from "./FXEffectSpec.js";
|
|
2
|
+
export declare function frameOfTime(time: number, fps: number): number;
|
|
3
|
+
export declare function timeOfFrame(frame: number, fps: number): number;
|
|
4
|
+
/** The bounding keys straddling `time` and the fraction between (held at the ends). */
|
|
5
|
+
export declare function keyframeSegment(keys: readonly FXKeyframe[], time: number): {
|
|
6
|
+
readonly a: FXKeyframe;
|
|
7
|
+
readonly b: FXKeyframe;
|
|
8
|
+
readonly u: number;
|
|
9
|
+
} | undefined;
|
|
10
|
+
export declare function sampleTrack(track: FXTrack, time: number): number | number[] | undefined;
|
|
11
|
+
export declare function sampleTracks(tracks: readonly FXTrack[], time: number): Map<string, number | number[]>;
|
|
12
|
+
/** A keyframe value read as a vec3 (missing components default to 0). */
|
|
13
|
+
export declare function vectorFromValue(value: number | readonly number[]): FXVec3;
|
|
14
|
+
/** A keyframe value read as a quaternion, falling back to identity for a malformed value. */
|
|
15
|
+
export declare function quaternionFromValue(value: number | readonly number[]): FXQuat;
|
|
16
|
+
/** Renormalizes a quaternion (a componentwise-lerped or hand-edited one drifts off the unit sphere). */
|
|
17
|
+
export declare function normalizeQuaternion(quaternion: FXQuat): FXQuat;
|
|
18
|
+
/** Spherical-linear blend of two quaternions at `u in [0, 1]` (shortest arc). */
|
|
19
|
+
export declare function slerpQuaternion(a: FXQuat, b: FXQuat, u: number): FXQuat;
|
|
20
|
+
/**
|
|
21
|
+
* The effective transform at time `time`: each channel with keyframes is sampled from its track, and
|
|
22
|
+
* any channel without keys holds the entity's authored `base` value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sampleTransform(base: FXTransform, tracks: readonly FXTransformTrack[], time: number): FXTransform;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure keyframe/transform sampling math for {@link FXEffect}'s timeline drive: hold-before-first,
|
|
3
|
+
* hold-after-last, linear interpolation between (slerp for rotation). No side effects, no `three`
|
|
4
|
+
* dependency. Every export here also backs the editor's own track/transform sampling: the editor
|
|
5
|
+
* used to hand-duplicate this exact math under different type names, and now imports it from here
|
|
6
|
+
* (this stays an `.Internal.ts` module, outside the public `sparcoon` package surface) so the two
|
|
7
|
+
* can never drift apart.
|
|
8
|
+
*/
|
|
9
|
+
const IDENTITY_QUAT = [0, 0, 0, 1];
|
|
10
|
+
export function frameOfTime(time, fps) {
|
|
11
|
+
return fps > 0 ? Math.round(time * fps) : 0;
|
|
12
|
+
}
|
|
13
|
+
export function timeOfFrame(frame, fps) {
|
|
14
|
+
return fps > 0 ? frame / fps : 0;
|
|
15
|
+
}
|
|
16
|
+
/** The bounding keys straddling `time` and the fraction between (held at the ends). */
|
|
17
|
+
export function keyframeSegment(keys, time) {
|
|
18
|
+
if (keys.length === 0) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const first = keys[0];
|
|
22
|
+
if (time <= first.time || keys.length === 1) {
|
|
23
|
+
return { a: first, b: first, u: 0 };
|
|
24
|
+
}
|
|
25
|
+
const last = keys[keys.length - 1];
|
|
26
|
+
if (time >= last.time) {
|
|
27
|
+
return { a: last, b: last, u: 0 };
|
|
28
|
+
}
|
|
29
|
+
let a = first;
|
|
30
|
+
let b = last;
|
|
31
|
+
for (let i = 1; i < keys.length; i += 1) {
|
|
32
|
+
const key = keys[i];
|
|
33
|
+
if (key.time >= time) {
|
|
34
|
+
a = keys[i - 1];
|
|
35
|
+
b = key;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const span = b.time - a.time;
|
|
40
|
+
return { a, b, u: span > 0 ? (time - a.time) / span : 0 };
|
|
41
|
+
}
|
|
42
|
+
function lerpValue(a, b, u) {
|
|
43
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
44
|
+
return a + (b - a) * u;
|
|
45
|
+
}
|
|
46
|
+
const aArray = typeof a === "number" ? [a] : a;
|
|
47
|
+
const bArray = typeof b === "number" ? [b] : b;
|
|
48
|
+
const width = Math.min(aArray.length, bArray.length);
|
|
49
|
+
const output = [];
|
|
50
|
+
for (let i = 0; i < width; i += 1) {
|
|
51
|
+
output.push(aArray[i] + (bArray[i] - aArray[i]) * u);
|
|
52
|
+
}
|
|
53
|
+
return output;
|
|
54
|
+
}
|
|
55
|
+
export function sampleTrack(track, time) {
|
|
56
|
+
const segment = keyframeSegment(track.keys, time);
|
|
57
|
+
if (segment === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return lerpValue(segment.a.value, segment.b.value, segment.u);
|
|
61
|
+
}
|
|
62
|
+
export function sampleTracks(tracks, time) {
|
|
63
|
+
const values = new Map();
|
|
64
|
+
for (const track of tracks) {
|
|
65
|
+
const value = sampleTrack(track, time);
|
|
66
|
+
if (value !== undefined) {
|
|
67
|
+
values.set(track.name, value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return values;
|
|
71
|
+
}
|
|
72
|
+
/** A keyframe value read as a vec3 (missing components default to 0). */
|
|
73
|
+
export function vectorFromValue(value) {
|
|
74
|
+
if (typeof value === "number") {
|
|
75
|
+
return [value, value, value];
|
|
76
|
+
}
|
|
77
|
+
return [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0];
|
|
78
|
+
}
|
|
79
|
+
/** A keyframe value read as a quaternion, falling back to identity for a malformed value. */
|
|
80
|
+
export function quaternionFromValue(value) {
|
|
81
|
+
if (typeof value === "number" || value.length < 4) {
|
|
82
|
+
return IDENTITY_QUAT;
|
|
83
|
+
}
|
|
84
|
+
return [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0, value[3] ?? 1];
|
|
85
|
+
}
|
|
86
|
+
/** Renormalizes a quaternion (a componentwise-lerped or hand-edited one drifts off the unit sphere). */
|
|
87
|
+
export function normalizeQuaternion(quaternion) {
|
|
88
|
+
const length = Math.hypot(quaternion[0], quaternion[1], quaternion[2], quaternion[3]);
|
|
89
|
+
if (length === 0) {
|
|
90
|
+
return IDENTITY_QUAT;
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
quaternion[0] / length,
|
|
94
|
+
quaternion[1] / length,
|
|
95
|
+
quaternion[2] / length,
|
|
96
|
+
quaternion[3] / length,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
/** Spherical-linear blend of two quaternions at `u in [0, 1]` (shortest arc). */
|
|
100
|
+
export function slerpQuaternion(a, b, u) {
|
|
101
|
+
let ax = a[0];
|
|
102
|
+
let ay = a[1];
|
|
103
|
+
let az = a[2];
|
|
104
|
+
let aw = a[3];
|
|
105
|
+
const bx = b[0];
|
|
106
|
+
const by = b[1];
|
|
107
|
+
const bz = b[2];
|
|
108
|
+
const bw = b[3];
|
|
109
|
+
let dot = ax * bx + ay * by + az * bz + aw * bw;
|
|
110
|
+
if (dot < 0) {
|
|
111
|
+
ax = -ax;
|
|
112
|
+
ay = -ay;
|
|
113
|
+
az = -az;
|
|
114
|
+
aw = -aw;
|
|
115
|
+
dot = -dot;
|
|
116
|
+
}
|
|
117
|
+
if (dot > 0.9995) {
|
|
118
|
+
return normalizeQuaternion([
|
|
119
|
+
ax + (bx - ax) * u,
|
|
120
|
+
ay + (by - ay) * u,
|
|
121
|
+
az + (bz - az) * u,
|
|
122
|
+
aw + (bw - aw) * u,
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
const theta0 = Math.acos(dot);
|
|
126
|
+
const theta = theta0 * u;
|
|
127
|
+
const sinTheta0 = Math.sin(theta0);
|
|
128
|
+
const s0 = Math.cos(theta) - (dot * Math.sin(theta)) / sinTheta0;
|
|
129
|
+
const s1 = Math.sin(theta) / sinTheta0;
|
|
130
|
+
return [ax * s0 + bx * s1, ay * s0 + by * s1, az * s0 + bz * s1, aw * s0 + bw * s1];
|
|
131
|
+
}
|
|
132
|
+
function sampleVectorChannel(track, time) {
|
|
133
|
+
const segment = track === undefined ? undefined : keyframeSegment(track.keys, time);
|
|
134
|
+
if (segment === undefined) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const a = vectorFromValue(segment.a.value);
|
|
138
|
+
const b = vectorFromValue(segment.b.value);
|
|
139
|
+
return [
|
|
140
|
+
a[0] + (b[0] - a[0]) * segment.u,
|
|
141
|
+
a[1] + (b[1] - a[1]) * segment.u,
|
|
142
|
+
a[2] + (b[2] - a[2]) * segment.u,
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
function sampleQuaternionChannel(track, time) {
|
|
146
|
+
const segment = track === undefined ? undefined : keyframeSegment(track.keys, time);
|
|
147
|
+
if (segment === undefined) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return slerpQuaternion(quaternionFromValue(segment.a.value), quaternionFromValue(segment.b.value), segment.u);
|
|
151
|
+
}
|
|
152
|
+
function transformChannel(tracks, name) {
|
|
153
|
+
return tracks.find((track) => track.channel === name);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The effective transform at time `time`: each channel with keyframes is sampled from its track, and
|
|
157
|
+
* any channel without keys holds the entity's authored `base` value.
|
|
158
|
+
*/
|
|
159
|
+
export function sampleTransform(base, tracks, time) {
|
|
160
|
+
return {
|
|
161
|
+
position: sampleVectorChannel(transformChannel(tracks, "position"), time) ?? base.position,
|
|
162
|
+
rotation: sampleQuaternionChannel(transformChannel(tracks, "rotation"), time) ?? base.rotation,
|
|
163
|
+
scale: sampleVectorChannel(transformChannel(tracks, "scale"), time) ?? base.scale,
|
|
164
|
+
};
|
|
165
|
+
}
|