sparcoon 0.9.0 → 0.9.2
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 +11 -6
- package/dist/artifact/FXArtifact.d.ts +51 -3
- package/dist/behaviorTransformFeedbackLayout.d.ts +24 -0
- package/dist/behaviorTransformFeedbackLayout.js +24 -0
- package/dist/coreLayout.d.ts +9 -5
- package/dist/coreLayout.js +17 -4
- package/dist/editor/index.d.ts +9 -0
- package/dist/editor/index.js +17 -0
- package/dist/effect/FXEffect.d.ts +2 -0
- package/dist/effect/FXEffect.js +30 -10
- package/dist/effect/FXEffectSpec.d.ts +21 -4
- package/dist/emitter/FXApplyRenderArtifactOptions.d.ts +15 -0
- package/dist/emitter/FXApplyRenderArtifactOptions.js +1 -0
- package/dist/emitter/FXEmitter.d.ts +42 -13
- package/dist/emitter/FXEmitter.js +113 -152
- package/dist/emitter/FXEmitterDriver.Internal.d.ts +26 -0
- package/dist/emitter/FXEmitterDriver.Internal.js +70 -0
- package/dist/emitter/FXFromArtifactsOptions.d.ts +12 -1
- package/dist/emitter/FXGPUEmitterDriver.Internal.d.ts +1 -0
- package/dist/emitter/FXGPUEmitterDriver.Internal.js +137 -0
- package/dist/emitter/FXJSEmitterDriver.Internal.d.ts +1 -0
- package/dist/emitter/FXJSEmitterDriver.Internal.js +133 -0
- package/dist/emitter/FXSimulationHolder.js +7 -5
- package/dist/emitter/FXTransformFeedbackSimulationHolder.d.ts +1 -0
- package/dist/emitter/FXTransformFeedbackSimulationHolder.js +181 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/instancedParticle/FXInstancedParticle.js +28 -1
- package/dist/instancedParticle/FXTransformFeedbackParticle.d.ts +1 -0
- package/dist/instancedParticle/FXTransformFeedbackParticle.js +169 -0
- package/dist/instancedParticle/primitiveGeometry.d.ts +10 -1
- package/dist/instancedParticle/primitiveGeometry.js +1 -1
- package/dist/miscellaneous/fxMath.d.ts +18 -11
- package/dist/miscellaneous/fxMath.js +89 -209
- package/dist/render/FXArtifactMaterial.Internal.js +66 -40
- package/dist/render/FXArtifactMaterial.js +1 -1
- package/dist/render/FXMesh.d.ts +47 -0
- package/dist/render/FXMesh.js +97 -0
- package/dist/world/FXWorld.d.ts +27 -17
- package/dist/world/FXWorld.js +32 -31
- package/package.json +5 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Mesh, type BufferGeometry, type Texture } from "three";
|
|
2
|
+
import type { FXRenderArtifact } from "../artifact/FXArtifact.js";
|
|
3
|
+
import { FXWorld } from "../world/FXWorld.js";
|
|
4
|
+
/** Options for {@link FXMesh.fromArtifact}. */
|
|
5
|
+
export interface FXFromMeshArtifactOptions {
|
|
6
|
+
/** External textures by slot name (a `{ type: "sampler2D", external }` uniform binds from here). */
|
|
7
|
+
readonly textures?: Readonly<Record<string, Texture>>;
|
|
8
|
+
/** Custom geometries by name, resolving a `{ type: "custom", external }` geometry source. */
|
|
9
|
+
readonly geometries?: Readonly<Record<string, BufferGeometry>>;
|
|
10
|
+
/** Cast a shape-aware shadow (builds a depth material from the render artifact). default false */
|
|
11
|
+
readonly castShadow?: boolean;
|
|
12
|
+
/** Receive shadows. default false */
|
|
13
|
+
readonly receiveShadow?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A single, non-instanced VFX mesh built from one precompiled render artifact - the mesh twin of
|
|
17
|
+
* {@link FXEmitter}. Extends `THREE.Mesh`, so the host adds it to its own scene; it self-subscribes
|
|
18
|
+
* to an {@link FXWorld} on construction and unsubscribes on {@link destroy}, so one
|
|
19
|
+
* {@link FXWorld.update} per frame drives it alongside every emitter. Each tick pushes the shared
|
|
20
|
+
* clock and the mesh's world-space velocity/angular velocity, so a render graph reading the
|
|
21
|
+
* object-velocity/object-angular-velocity builtins works without the host tracking motion by hand.
|
|
22
|
+
*
|
|
23
|
+
* Not exported from the main package (the runtime path is running whole projects through
|
|
24
|
+
* {@link FXEffect}); reached through the `sparcoon/editor` entry, for editor live preview and
|
|
25
|
+
* similar tooling that renders one artifact-driven mesh at a time.
|
|
26
|
+
*/
|
|
27
|
+
export declare class FXMesh extends Mesh {
|
|
28
|
+
private readonly world;
|
|
29
|
+
private readonly driver;
|
|
30
|
+
private readonly motionTracker;
|
|
31
|
+
private readonly ownsGeometry;
|
|
32
|
+
private readonly depthMaterial?;
|
|
33
|
+
private readonly motionPositionScratch;
|
|
34
|
+
private readonly motionQuaternionScratch;
|
|
35
|
+
private readonly motionScaleScratch;
|
|
36
|
+
private destroyed;
|
|
37
|
+
private constructor();
|
|
38
|
+
/** Builds a mesh into `world` from a precompiled render artifact. */
|
|
39
|
+
static fromArtifact(render: FXRenderArtifact, options?: FXFromMeshArtifactOptions, world?: FXWorld): FXMesh;
|
|
40
|
+
/** Scrubs live uniform values by name (picked up next frame). Unknown name = safe no-op. */
|
|
41
|
+
applyValues(values: Readonly<Record<string, number | readonly number[] | Texture>>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Unsubscribes from its world and disposes GPU resources (the mounted material, its depth
|
|
44
|
+
* material, and a built-in primitive geometry - never an app-supplied custom one). Idempotent.
|
|
45
|
+
*/
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Mesh, Quaternion, Vector3 } from "three";
|
|
2
|
+
import { FXObjectMotionTracker } from "../emitter/FXObjectMotion.Internal.js";
|
|
3
|
+
import { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
|
|
4
|
+
import { FXWorld } from "../world/FXWorld.js";
|
|
5
|
+
import { FXMeshMaterial } from "./FXMeshMaterial.js";
|
|
6
|
+
/**
|
|
7
|
+
* A single, non-instanced VFX mesh built from one precompiled render artifact - the mesh twin of
|
|
8
|
+
* {@link FXEmitter}. Extends `THREE.Mesh`, so the host adds it to its own scene; it self-subscribes
|
|
9
|
+
* to an {@link FXWorld} on construction and unsubscribes on {@link destroy}, so one
|
|
10
|
+
* {@link FXWorld.update} per frame drives it alongside every emitter. Each tick pushes the shared
|
|
11
|
+
* clock and the mesh's world-space velocity/angular velocity, so a render graph reading the
|
|
12
|
+
* object-velocity/object-angular-velocity builtins works without the host tracking motion by hand.
|
|
13
|
+
*
|
|
14
|
+
* Not exported from the main package (the runtime path is running whole projects through
|
|
15
|
+
* {@link FXEffect}); reached through the `sparcoon/editor` entry, for editor live preview and
|
|
16
|
+
* similar tooling that renders one artifact-driven mesh at a time.
|
|
17
|
+
*/
|
|
18
|
+
export class FXMesh extends Mesh {
|
|
19
|
+
constructor(geometry, driver, threeMaterial, depthMaterial, ownsGeometry, world) {
|
|
20
|
+
super(geometry, threeMaterial);
|
|
21
|
+
this.motionTracker = new FXObjectMotionTracker();
|
|
22
|
+
// Reused decompose scratch (avoids a per-tick allocation); scale is discarded.
|
|
23
|
+
this.motionPositionScratch = new Vector3();
|
|
24
|
+
this.motionQuaternionScratch = new Quaternion();
|
|
25
|
+
this.motionScaleScratch = new Vector3();
|
|
26
|
+
this.destroyed = false;
|
|
27
|
+
this.driver = driver;
|
|
28
|
+
this.ownsGeometry = ownsGeometry;
|
|
29
|
+
this.world = world;
|
|
30
|
+
if (depthMaterial !== undefined) {
|
|
31
|
+
this.depthMaterial = depthMaterial;
|
|
32
|
+
this.castShadow = true;
|
|
33
|
+
this.customDepthMaterial = depthMaterial;
|
|
34
|
+
}
|
|
35
|
+
this.world.registerObject(this);
|
|
36
|
+
}
|
|
37
|
+
/** Builds a mesh into `world` from a precompiled render artifact. */
|
|
38
|
+
static fromArtifact(render, options = {}, world = FXWorld.getDefault()) {
|
|
39
|
+
const driver = new FXMeshMaterial(render, options.textures);
|
|
40
|
+
const geometry = resolveGeometrySource(render.geometry, options.geometries ?? {});
|
|
41
|
+
const ownsGeometry = render.geometry === undefined || render.geometry.type === "primitive";
|
|
42
|
+
const depthMaterial = (options.castShadow ?? false) ? driver.buildThreeDepthMaterial() : undefined;
|
|
43
|
+
const mesh = new FXMesh(geometry, driver, driver.buildThreeMaterial(), depthMaterial, ownsGeometry, world);
|
|
44
|
+
if (options.receiveShadow ?? false) {
|
|
45
|
+
mesh.receiveShadow = true;
|
|
46
|
+
}
|
|
47
|
+
return mesh;
|
|
48
|
+
}
|
|
49
|
+
/** @internal Driven by {@link FXWorld.update}. */
|
|
50
|
+
onWorldTick(deltaTime, elapsedTime) {
|
|
51
|
+
this.updateWorldMatrix(true, false);
|
|
52
|
+
this.matrixWorld.decompose(this.motionPositionScratch, this.motionQuaternionScratch, this.motionScaleScratch);
|
|
53
|
+
const motion = this.motionTracker.sample({
|
|
54
|
+
position: [
|
|
55
|
+
this.motionPositionScratch.x,
|
|
56
|
+
this.motionPositionScratch.y,
|
|
57
|
+
this.motionPositionScratch.z,
|
|
58
|
+
],
|
|
59
|
+
quaternion: [
|
|
60
|
+
this.motionQuaternionScratch.x,
|
|
61
|
+
this.motionQuaternionScratch.y,
|
|
62
|
+
this.motionQuaternionScratch.z,
|
|
63
|
+
this.motionQuaternionScratch.w,
|
|
64
|
+
],
|
|
65
|
+
}, deltaTime);
|
|
66
|
+
this.driver.setElapsedTime(elapsedTime);
|
|
67
|
+
this.driver.setDeltaTime(deltaTime);
|
|
68
|
+
this.driver.setObjectVelocity(motion.velocity);
|
|
69
|
+
this.driver.setObjectAngularVelocity(motion.angularVelocity);
|
|
70
|
+
}
|
|
71
|
+
/** Scrubs live uniform values by name (picked up next frame). Unknown name = safe no-op. */
|
|
72
|
+
applyValues(values) {
|
|
73
|
+
if (this.destroyed) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.driver.applyUniformValues(values);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Unsubscribes from its world and disposes GPU resources (the mounted material, its depth
|
|
80
|
+
* material, and a built-in primitive geometry - never an app-supplied custom one). Idempotent.
|
|
81
|
+
*/
|
|
82
|
+
destroy() {
|
|
83
|
+
if (this.destroyed) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.destroyed = true;
|
|
87
|
+
this.world.unregisterObject(this);
|
|
88
|
+
if (this.ownsGeometry) {
|
|
89
|
+
this.geometry.dispose();
|
|
90
|
+
}
|
|
91
|
+
this.material.dispose();
|
|
92
|
+
if (this.depthMaterial !== undefined) {
|
|
93
|
+
this.depthMaterial.dispose();
|
|
94
|
+
}
|
|
95
|
+
this.removeFromParent();
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/world/FXWorld.d.ts
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* A world-registered renderable driven once per frame: an emitter or a VFX mesh. `onWorldTick` reads
|
|
3
|
+
* the object's own `matrixWorld` and pushes the shared clock plus its world-space motion; `destroy`
|
|
4
|
+
* frees its GPU resources and unregisters it.
|
|
5
|
+
*/
|
|
6
|
+
export interface FXWorldObject {
|
|
7
|
+
onWorldTick(deltaTime: number, elapsedTime: number): void;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The tick domain that drives effects and their renderables. A world owns a registry of effects, a
|
|
12
|
+
* pool of world objects (emitters and meshes), and a single monotonic clock; one
|
|
13
|
+
* {@link FXWorld.update} call per frame advances every playing effect's timeline, then ticks every
|
|
14
|
+
* registered emitter and mesh exactly once with the shared clock.
|
|
5
15
|
*
|
|
6
|
-
* An {@link FXEffect}
|
|
7
|
-
*
|
|
8
|
-
* `FXEffectOptions.world` to place an effect in a specific world; omit it and
|
|
9
|
-
* lazily created default world, which the static {@link FXWorld.update} ticks.
|
|
16
|
+
* An {@link FXEffect} (and each standalone emitter/mesh it or the editor builds) subscribes to a
|
|
17
|
+
* world on construction and unsubscribes on disposal - the host never wires that up by hand. Pass a
|
|
18
|
+
* world through `FXEffectOptions.world` to place an effect in a specific world; omit it and it joins
|
|
19
|
+
* the lazily created default world, which the static {@link FXWorld.update} ticks.
|
|
10
20
|
*
|
|
11
|
-
* A world is only a tick/clock domain, NOT a scene graph:
|
|
12
|
-
* add to your own scene. Two worlds give two independent clocks (for an independent time scale or
|
|
13
|
-
* isolated group); within one world every effect shares the clock and one delta per frame, so
|
|
14
|
-
* {@link FXEffect.stop} clears rather than freezes - freeze one effect independently by giving it
|
|
15
|
-
*
|
|
21
|
+
* A world is only a tick/clock domain, NOT a scene graph: a renderable is still a `THREE.Object3D`
|
|
22
|
+
* you add to your own scene. Two worlds give two independent clocks (for an independent time scale or
|
|
23
|
+
* an isolated group); within one world every effect shares the clock and one delta per frame, so
|
|
24
|
+
* {@link FXEffect.stop} clears rather than freezes - freeze one effect independently by giving it its
|
|
25
|
+
* own world.
|
|
16
26
|
*/
|
|
17
27
|
export declare class FXWorld {
|
|
18
28
|
private static defaultInstance;
|
|
19
|
-
private readonly
|
|
29
|
+
private readonly objects;
|
|
20
30
|
private readonly effects;
|
|
21
31
|
private elapsedTime;
|
|
22
32
|
private disposed;
|
|
@@ -29,14 +39,14 @@ export declare class FXWorld {
|
|
|
29
39
|
static update(deltaTime: number): void;
|
|
30
40
|
/**
|
|
31
41
|
* Advances this world by `deltaTime` seconds: every playing effect drives its own timeline and
|
|
32
|
-
* meshes, then
|
|
33
|
-
* A disposed world is a no-op.
|
|
42
|
+
* meshes, then every registered emitter and mesh ticks once. Call exactly once per frame, before
|
|
43
|
+
* rendering. A disposed world is a no-op.
|
|
34
44
|
*/
|
|
35
45
|
update(deltaTime: number): void;
|
|
36
46
|
/**
|
|
37
47
|
* Disposes every effect registered in this world (each frees its emitters and mesh resources),
|
|
38
|
-
* then drops any remaining
|
|
39
|
-
*
|
|
48
|
+
* then drops any remaining objects. Idempotent. After this the world's {@link update} is a no-op;
|
|
49
|
+
* disposing the default world simply makes the next {@link getDefault} create a fresh one.
|
|
40
50
|
*/
|
|
41
51
|
dispose(): void;
|
|
42
52
|
}
|
package/dist/world/FXWorld.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { EMITTER_TIME_WRAP_PERIOD } from "../emitter/FXEmitter.Internal.js";
|
|
2
2
|
/**
|
|
3
|
-
* The tick domain that drives effects. A world owns a
|
|
4
|
-
*
|
|
5
|
-
* effect's timeline
|
|
3
|
+
* The tick domain that drives effects and their renderables. A world owns a registry of effects, a
|
|
4
|
+
* pool of world objects (emitters and meshes), and a single monotonic clock; one
|
|
5
|
+
* {@link FXWorld.update} call per frame advances every playing effect's timeline, then ticks every
|
|
6
|
+
* registered emitter and mesh exactly once with the shared clock.
|
|
6
7
|
*
|
|
7
|
-
* An {@link FXEffect}
|
|
8
|
-
*
|
|
9
|
-
* `FXEffectOptions.world` to place an effect in a specific world; omit it and
|
|
10
|
-
* lazily created default world, which the static {@link FXWorld.update} ticks.
|
|
8
|
+
* An {@link FXEffect} (and each standalone emitter/mesh it or the editor builds) subscribes to a
|
|
9
|
+
* world on construction and unsubscribes on disposal - the host never wires that up by hand. Pass a
|
|
10
|
+
* world through `FXEffectOptions.world` to place an effect in a specific world; omit it and it joins
|
|
11
|
+
* the lazily created default world, which the static {@link FXWorld.update} ticks.
|
|
11
12
|
*
|
|
12
|
-
* A world is only a tick/clock domain, NOT a scene graph:
|
|
13
|
-
* add to your own scene. Two worlds give two independent clocks (for an independent time scale or
|
|
14
|
-
* isolated group); within one world every effect shares the clock and one delta per frame, so
|
|
15
|
-
* {@link FXEffect.stop} clears rather than freezes - freeze one effect independently by giving it
|
|
16
|
-
*
|
|
13
|
+
* A world is only a tick/clock domain, NOT a scene graph: a renderable is still a `THREE.Object3D`
|
|
14
|
+
* you add to your own scene. Two worlds give two independent clocks (for an independent time scale or
|
|
15
|
+
* an isolated group); within one world every effect shares the clock and one delta per frame, so
|
|
16
|
+
* {@link FXEffect.stop} clears rather than freezes - freeze one effect independently by giving it its
|
|
17
|
+
* own world.
|
|
17
18
|
*/
|
|
18
19
|
export class FXWorld {
|
|
19
20
|
constructor() {
|
|
20
|
-
this.
|
|
21
|
+
this.objects = [];
|
|
21
22
|
this.effects = [];
|
|
22
23
|
this.elapsedTime = 0;
|
|
23
24
|
this.disposed = false;
|
|
@@ -38,8 +39,8 @@ export class FXWorld {
|
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* Advances this world by `deltaTime` seconds: every playing effect drives its own timeline and
|
|
41
|
-
* meshes, then
|
|
42
|
-
* A disposed world is a no-op.
|
|
42
|
+
* meshes, then every registered emitter and mesh ticks once. Call exactly once per frame, before
|
|
43
|
+
* rendering. A disposed world is a no-op.
|
|
43
44
|
*/
|
|
44
45
|
update(deltaTime) {
|
|
45
46
|
if (this.disposed) {
|
|
@@ -49,31 +50,31 @@ export class FXWorld {
|
|
|
49
50
|
effect.advanceFrame(deltaTime);
|
|
50
51
|
}
|
|
51
52
|
// Wrap the clock so a float32 `u_time` keeps sub-millisecond resolution in long sessions, then
|
|
52
|
-
// tick
|
|
53
|
+
// tick every registered object once with the shared clock.
|
|
53
54
|
this.elapsedTime = (this.elapsedTime + deltaTime) % EMITTER_TIME_WRAP_PERIOD;
|
|
54
|
-
for (const
|
|
55
|
-
|
|
55
|
+
for (const object of this.objects) {
|
|
56
|
+
object.onWorldTick(deltaTime, this.elapsedTime);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* Disposes every effect registered in this world (each frees its emitters and mesh resources),
|
|
60
|
-
* then drops any remaining
|
|
61
|
-
*
|
|
61
|
+
* then drops any remaining objects. Idempotent. After this the world's {@link update} is a no-op;
|
|
62
|
+
* disposing the default world simply makes the next {@link getDefault} create a fresh one.
|
|
62
63
|
*/
|
|
63
64
|
dispose() {
|
|
64
65
|
if (this.disposed) {
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
this.disposed = true;
|
|
68
|
-
//
|
|
69
|
+
// Each disposal unregisters from this world, so iterate snapshots.
|
|
69
70
|
for (const effect of [...this.effects]) {
|
|
70
71
|
effect.dispose();
|
|
71
72
|
}
|
|
72
|
-
for (const
|
|
73
|
-
|
|
73
|
+
for (const object of [...this.objects]) {
|
|
74
|
+
object.destroy();
|
|
74
75
|
}
|
|
75
76
|
this.effects.length = 0;
|
|
76
|
-
this.
|
|
77
|
+
this.objects.length = 0;
|
|
77
78
|
if (FXWorld.defaultInstance === this) {
|
|
78
79
|
FXWorld.defaultInstance = undefined;
|
|
79
80
|
}
|
|
@@ -89,15 +90,15 @@ export class FXWorld {
|
|
|
89
90
|
this.effects.splice(index, 1);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
/** @internal Called by
|
|
93
|
-
|
|
94
|
-
this.
|
|
93
|
+
/** @internal Called by an emitter's or mesh's constructor. */
|
|
94
|
+
registerObject(object) {
|
|
95
|
+
this.objects.push(object);
|
|
95
96
|
}
|
|
96
|
-
/** @internal Called
|
|
97
|
-
|
|
98
|
-
const index = this.
|
|
97
|
+
/** @internal Called on an emitter's or mesh's destroy. */
|
|
98
|
+
unregisterObject(object) {
|
|
99
|
+
const index = this.objects.indexOf(object);
|
|
99
100
|
if (index !== -1) {
|
|
100
|
-
this.
|
|
101
|
+
this.objects.splice(index, 1);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparcoon",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Runtime for VFX artifacts authored in the Sparcoon Editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"three",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/index.d.ts",
|
|
37
37
|
"import": "./dist/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./editor": {
|
|
40
|
+
"types": "./dist/editor/index.d.ts",
|
|
41
|
+
"import": "./dist/editor/index.js"
|
|
42
|
+
},
|
|
39
43
|
"./package.json": "./package.json"
|
|
40
44
|
},
|
|
41
45
|
"main": "./dist/index.js",
|