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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_POSITION, FX_ID, meshPropertyKeyFor, } from "../coreLayout.js";
|
|
2
|
+
import { FXInstancedParticle } from "../instancedParticle/FXInstancedParticle.js";
|
|
3
|
+
import { mergeAttributeWidths, sameVaryings, varyingsFromComponents, } from "./FXEmitterDriver.Internal.js";
|
|
4
|
+
import { FXSimulationHolder } from "./FXSimulationHolder.js";
|
|
5
|
+
/**
|
|
6
|
+
* @internal The CPU (JS) particle-simulation backend: a dense, compacting `FXInstancedParticle`
|
|
7
|
+
* mesh driven by a precompiled `FXBehaviorArtifact` through `FXSimulationHolder`. Self-sufficient
|
|
8
|
+
* from its own artifact - its varyings come from `behavior.attributeWrites` (+ the render
|
|
9
|
+
* artifact's `attributeReads`), never from a GPU kernel artifact.
|
|
10
|
+
*/
|
|
11
|
+
export class FXJSEmitterDriver {
|
|
12
|
+
constructor(behavior, material, capacity, capacityStep, baseGeometry) {
|
|
13
|
+
this.kernelBuffersCacheVersion = -1;
|
|
14
|
+
// Host-owned, monotonic per-emitter counter (like age, never touched by the compiled kernel/
|
|
15
|
+
// graph) - persists across reset() the same way FXTransformFeedbackSimulationHolder's
|
|
16
|
+
// randSeedCounter does, only restarting when this driver itself is rebuilt from artifacts.
|
|
17
|
+
this.nextParticleId = 0;
|
|
18
|
+
this.simulation = new FXSimulationHolder(behavior);
|
|
19
|
+
const components = mergeAttributeWidths(this.simulation.attributeWrites, material.attributeReads);
|
|
20
|
+
this.varyings = varyingsFromComponents(components);
|
|
21
|
+
const threeMaterial = material.buildThreeMaterial(this.varyings);
|
|
22
|
+
this.mesh = new FXInstancedParticle(this.varyings, capacity, capacityStep, threeMaterial, baseGeometry);
|
|
23
|
+
}
|
|
24
|
+
get particleCount() {
|
|
25
|
+
return this.mesh.instanceCount;
|
|
26
|
+
}
|
|
27
|
+
get particleCapacity() {
|
|
28
|
+
return this.mesh.instanceCapacity;
|
|
29
|
+
}
|
|
30
|
+
get canSpawn() {
|
|
31
|
+
return this.simulation.canSpawn;
|
|
32
|
+
}
|
|
33
|
+
spawn(count, transform) {
|
|
34
|
+
const instanceBegin = this.mesh.instanceCount;
|
|
35
|
+
this.mesh.createInstances(count);
|
|
36
|
+
const instanceEnd = this.mesh.instanceCount;
|
|
37
|
+
// Host-owned, like age: no write slot exists for id (the editor's FXParticleBehaviorTarget.ts
|
|
38
|
+
// scalarWriteSlots never lists it), so no graph can ever collide with this assignment.
|
|
39
|
+
const lifecycle = this.mesh.propertyBuffers[FX_CORE_LIFECYCLE];
|
|
40
|
+
const { array: lifecycleArray, itemSize: lifecycleItemSize } = lifecycle;
|
|
41
|
+
for (let i = instanceBegin; i < instanceEnd; i++) {
|
|
42
|
+
lifecycleArray[i * lifecycleItemSize + FX_ID] = this.nextParticleId++;
|
|
43
|
+
}
|
|
44
|
+
// Re-checked (defensive): no spawn function would leave the freshly zeroed rows born dead.
|
|
45
|
+
if (this.simulation.canSpawn) {
|
|
46
|
+
this.simulation.spawn(this.kernelBuffers(), instanceBegin, instanceEnd - instanceBegin, transform);
|
|
47
|
+
this.markBuffersNeedUpdate(this.simulation.spawnWrittenBuffers);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
beginTick(deltaTime) {
|
|
51
|
+
// Age increment + cull. Age is owned by this fixed loop, not the graph - same for every effect.
|
|
52
|
+
const lifecycle = this.mesh.propertyBuffers[FX_CORE_LIFECYCLE];
|
|
53
|
+
const { array, itemSize } = lifecycle;
|
|
54
|
+
const instanceCount = this.mesh.instanceCount;
|
|
55
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
56
|
+
array[i * itemSize + FX_AGE] += deltaTime;
|
|
57
|
+
}
|
|
58
|
+
this.mesh.removeDeadParticles();
|
|
59
|
+
}
|
|
60
|
+
endTick(deltaTime, transform) {
|
|
61
|
+
if (this.mesh.instanceCount === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Motion integration lives in the graph (an `integrate-motion` node writes
|
|
65
|
+
// `position += velocity * dt`), not in this host loop.
|
|
66
|
+
this.simulation.update(this.kernelBuffers(), this.mesh.instanceCount, deltaTime, transform);
|
|
67
|
+
this.markBuffersNeedUpdate(this.simulation.updateWrittenBuffers);
|
|
68
|
+
// Age (written in beginTick) must reach the GPU this frame; the update phase's buffers were
|
|
69
|
+
// flagged already.
|
|
70
|
+
this.mesh.propertyBuffers[FX_CORE_LIFECYCLE].needsUpdate = true;
|
|
71
|
+
}
|
|
72
|
+
sortByDistance(cameraWorldPosition) {
|
|
73
|
+
if (this.mesh.instanceCount === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Sort in world space to match the shader's `p_cameraDistance`, correct under any transform.
|
|
77
|
+
this.mesh.updateWorldMatrix(true, false);
|
|
78
|
+
this.mesh.sortByDistance(cameraWorldPosition, this.mesh.matrixWorld);
|
|
79
|
+
}
|
|
80
|
+
applyBindingValues(values) {
|
|
81
|
+
this.simulation.applyBindingValues(values);
|
|
82
|
+
}
|
|
83
|
+
reset() {
|
|
84
|
+
this.mesh.drop();
|
|
85
|
+
}
|
|
86
|
+
applyRenderArtifact(material, baseGeometry) {
|
|
87
|
+
// Recomputed exactly as the constructor does, against this driver's OWN behavior writes
|
|
88
|
+
// (never the other backend's) - a stale merge here would wrongly pass a real layout change.
|
|
89
|
+
const components = mergeAttributeWidths(this.simulation.attributeWrites, material.attributeReads);
|
|
90
|
+
const nextVaryings = varyingsFromComponents(components);
|
|
91
|
+
if (!sameVaryings(this.varyings, nextVaryings)) {
|
|
92
|
+
throw new Error("FXJSEmitterDriver.applyRenderArtifact: the new render artifact's attribute reads no " +
|
|
93
|
+
"longer match this emitter's allocated buffers - a full rebuild (fromArtifacts) is required");
|
|
94
|
+
}
|
|
95
|
+
// this.varyings is value-equal to nextVaryings (just proved by sameVaryings above) - built
|
|
96
|
+
// from nextVaryings so a future reader never has to re-derive that equivalence to trust this.
|
|
97
|
+
const threeMaterial = material.buildThreeMaterial(nextVaryings);
|
|
98
|
+
this.mesh.replaceMaterialAndBaseGeometry(threeMaterial, baseGeometry);
|
|
99
|
+
}
|
|
100
|
+
destroy() {
|
|
101
|
+
this.kernelBuffersCache = undefined;
|
|
102
|
+
this.mesh.destroy();
|
|
103
|
+
}
|
|
104
|
+
// Maps each kernel state-buffer name to its backing Float32Array. Cached across ticks/bursts and
|
|
105
|
+
// rebuilt only when the mesh's buffer set changes (`bufferVersion` - capacity growth).
|
|
106
|
+
kernelBuffers() {
|
|
107
|
+
const version = this.mesh.bufferVersion;
|
|
108
|
+
if (this.kernelBuffersCache !== undefined && this.kernelBuffersCacheVersion === version) {
|
|
109
|
+
return this.kernelBuffersCache;
|
|
110
|
+
}
|
|
111
|
+
const buffers = {
|
|
112
|
+
[FX_CORE_POSITION]: this.mesh.propertyBuffers[FX_CORE_POSITION].array,
|
|
113
|
+
[FX_CORE_LIFECYCLE]: this.mesh.propertyBuffers[FX_CORE_LIFECYCLE].array,
|
|
114
|
+
};
|
|
115
|
+
for (const buffer of this.simulation.attributeBuffers) {
|
|
116
|
+
const attribute = this.mesh.propertyBuffers[meshPropertyKeyFor(buffer.name)];
|
|
117
|
+
if (attribute !== undefined) {
|
|
118
|
+
buffers[buffer.name] = attribute.array;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this.kernelBuffersCache = buffers;
|
|
122
|
+
this.kernelBuffersCacheVersion = version;
|
|
123
|
+
return buffers;
|
|
124
|
+
}
|
|
125
|
+
markBuffersNeedUpdate(names) {
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
const attribute = this.mesh.propertyBuffers[meshPropertyKeyFor(name)];
|
|
128
|
+
if (attribute !== undefined) {
|
|
129
|
+
attribute.needsUpdate = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const CORE_BUFFER_NAMES = new Set([FX_CORE_POSITION, FX_CORE_LIFECYCLE]);
|
|
1
|
+
import { isCoreBufferName } from "../coreLayout.js";
|
|
3
2
|
/**
|
|
4
|
-
* @internal The runtime's behavior driver
|
|
3
|
+
* @internal The runtime's JS (CPU) behavior driver - the GPU sibling is
|
|
4
|
+
* `FXTransformFeedbackSimulationHolder`. Holds a precompiled {@link FXBehaviorArtifact} and just
|
|
5
5
|
* calls its `spawn`/`update` each tick (no graph, compiler, or `new Function`). The artifact's
|
|
6
6
|
* `bindings` record is passed through on every call, so a value scrub is read next tick.
|
|
7
7
|
*/
|
|
@@ -11,7 +11,7 @@ export class FXSimulationHolder {
|
|
|
11
11
|
}
|
|
12
12
|
/** Declared buffers minus the fixed core `position`/`lifecycle` - one per user attribute. */
|
|
13
13
|
get attributeBuffers() {
|
|
14
|
-
return this.artifact.buffers.filter((buffer) => !
|
|
14
|
+
return this.artifact.buffers.filter((buffer) => !isCoreBufferName(buffer.name));
|
|
15
15
|
}
|
|
16
16
|
get attributeWrites() {
|
|
17
17
|
return this.artifact.attributeWrites;
|
|
@@ -32,7 +32,9 @@ export class FXSimulationHolder {
|
|
|
32
32
|
update(buffers, count, deltaTime, emitter) {
|
|
33
33
|
this.artifact.update(buffers, count, deltaTime, this.artifact.bindings, emitter);
|
|
34
34
|
}
|
|
35
|
-
/** Behavior half of {@link FXEmitter.applyValues}: mutate each named slot in place; unknown name
|
|
35
|
+
/** Behavior half of {@link FXEmitter.applyValues}: mutate each named slot in place; unknown name
|
|
36
|
+
* = no-op. Mirrored by `FXTransformFeedbackSimulationHolder.applyBindingValues` for the GPU
|
|
37
|
+
* tier - `FXEmitter.applyValues` scrubs both, since either backend may be the active one. */
|
|
36
38
|
applyBindingValues(values) {
|
|
37
39
|
for (const name in values) {
|
|
38
40
|
if (name in this.artifact.bindings) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { meshPropertyKeyFor } from "../coreLayout.js";
|
|
2
|
+
import { FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM, FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM, FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM, FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM, FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM, FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM, } from "../behaviorTransformFeedbackLayout.js";
|
|
3
|
+
/**
|
|
4
|
+
* @internal The GPU sibling of `FXSimulationHolder`: holds a precompiled {@link FXParticleKernelArtifact}
|
|
5
|
+
* (one fused GLSL program covering both spawn and update) and drives it via raw WebGL2 transform
|
|
6
|
+
* feedback each tick. Bypasses three.js's `Material`/
|
|
7
|
+
* `WebGLProgram` machinery entirely - three has no transform-feedback support at all - the same
|
|
8
|
+
* way the render side never touches it either.
|
|
9
|
+
*/
|
|
10
|
+
export class FXTransformFeedbackSimulationHolder {
|
|
11
|
+
constructor(renderer, artifact) {
|
|
12
|
+
this.renderer = renderer;
|
|
13
|
+
this.artifact = artifact;
|
|
14
|
+
this.uniformLocations = {};
|
|
15
|
+
// A fresh, monotonic seed every tick - decorrelates `rand()` draws across ticks (and, crucially,
|
|
16
|
+
// across a buffer slot's successive rebirths under the cursor/overwrite scheme - see the `rand`
|
|
17
|
+
// registry entry's own doc comment in the editor's core/ir/FXFunctions.Internal.ts for why this
|
|
18
|
+
// matters). Wraparound is harmless: it only ever feeds a hash's bit-mixing, never arithmetic
|
|
19
|
+
// that needs exact precision.
|
|
20
|
+
this.randSeedCounter = 0;
|
|
21
|
+
// Running total of particles ever spawned by this emitter - the base a newborn's id offsets
|
|
22
|
+
// from this tick's relative spawn index. Never reset (no reset() exists on this class, only
|
|
23
|
+
// destroy()): a stop/replay must not reissue ids already handed out, same reasoning as
|
|
24
|
+
// randSeedCounter above.
|
|
25
|
+
this.spawnIdBase = 0;
|
|
26
|
+
// Every emitter driven by this class already required the live render tier to be "standard"
|
|
27
|
+
// (a real WebGL2 context) before this class is ever constructed (FXEmitter.fromArtifacts's
|
|
28
|
+
// driver-selection rule) - the cast reflects an already-established precondition, not a new
|
|
29
|
+
// capability check.
|
|
30
|
+
const gl = this.renderer.getContext();
|
|
31
|
+
this.gl = gl;
|
|
32
|
+
// `artifact.buffers[].name` is the raw, unprefixed kernel-compiled name (e.g. "velocity") -
|
|
33
|
+
// `mesh.readBuffer`/`writeBuffer`/`swapBuffers` key by the mesh's own property-buffer key
|
|
34
|
+
// instead (`fx_velocity`, matching `FXGPUEmitterDriver`'s own `varyings`), so every name must go
|
|
35
|
+
// through the same mapping the JS-driven path also uses (`meshPropertyKeyFor`) before reaching
|
|
36
|
+
// the mesh - the two core buffers pass through unchanged either way, which is why this only
|
|
37
|
+
// actually mattered for attribute-buffer emitters, not the position/lifetime-only fixtures this
|
|
38
|
+
// class's own tests use.
|
|
39
|
+
this.bufferNames = artifact.buffers.map((buffer) => meshPropertyKeyFor(buffer.name));
|
|
40
|
+
this.strides = artifact.buffers.map((buffer) => buffer.stride);
|
|
41
|
+
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, artifact.vertexSource);
|
|
42
|
+
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, artifact.fragmentSource);
|
|
43
|
+
const program = gl.createProgram();
|
|
44
|
+
gl.attachShader(program, vertexShader);
|
|
45
|
+
gl.attachShader(program, fragmentShader);
|
|
46
|
+
// Must be called before linking (WebGL2 spec) - SEPARATE_ATTRIBS matches this class's one
|
|
47
|
+
// dedicated WebGLBuffer per state buffer (never one interleaved buffer for all of them).
|
|
48
|
+
gl.transformFeedbackVaryings(program, [...artifact.transformFeedbackVaryings], gl.SEPARATE_ATTRIBS);
|
|
49
|
+
gl.linkProgram(program);
|
|
50
|
+
// Shaders are safe to delete once linked - GL keeps them alive by refcount until the program
|
|
51
|
+
// itself is deleted, regardless of link success.
|
|
52
|
+
gl.deleteShader(vertexShader);
|
|
53
|
+
gl.deleteShader(fragmentShader);
|
|
54
|
+
if (gl.getProgramParameter(program, gl.LINK_STATUS) !== true) {
|
|
55
|
+
const info = gl.getProgramInfoLog(program);
|
|
56
|
+
gl.deleteProgram(program);
|
|
57
|
+
throw new Error(`FXTransformFeedbackSimulationHolder: program failed to link: ${info ?? ""}`);
|
|
58
|
+
}
|
|
59
|
+
this.program = program;
|
|
60
|
+
for (const name of FIXED_UNIFORM_NAMES) {
|
|
61
|
+
this.uniformLocations[name] = gl.getUniformLocation(program, name);
|
|
62
|
+
}
|
|
63
|
+
for (const name of Object.keys(artifact.bindings)) {
|
|
64
|
+
this.uniformLocations[name] = gl.getUniformLocation(program, name);
|
|
65
|
+
}
|
|
66
|
+
// A dedicated VAO for this simulation pass only - never one three.js's own WebGLBindingStates
|
|
67
|
+
// manages, so raw attribute setup here can never corrupt a mesh's cached vertex state. This
|
|
68
|
+
// is exactly the raw gl.bindVertexArray() call whose effect on three's VAO cache `tick()`'s
|
|
69
|
+
// closing `resetState()` call exists to correct (see that call site's own doc comment).
|
|
70
|
+
this.vao = gl.createVertexArray();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Runs one transform-feedback pass: reads `mesh`'s current buffers, writes the fused spawn+
|
|
74
|
+
* update program's result into the other half of each ping-pong pair, then swaps. `spawnRange`
|
|
75
|
+
* is this tick's birth range under the buffer's cursor/overwrite scheme - `count === 0` is a
|
|
76
|
+
* valid, ordinary "no births this tick" case, not skipped specially.
|
|
77
|
+
*/
|
|
78
|
+
tick(mesh, deltaTime, spawnRange, emitter) {
|
|
79
|
+
const gl = this.gl;
|
|
80
|
+
this.randSeedCounter += 1;
|
|
81
|
+
// Routed through the renderer's own cache-aware setter (not raw gl.useProgram()) so three's
|
|
82
|
+
// `WebGLState.currentProgram` cache stays truthful - its own next draw then correctly detects
|
|
83
|
+
// the mismatch and re-binds, with no reset needed for this half of the state-hygiene problem.
|
|
84
|
+
// The other half (the VAO-identity cache) has no such setter - see the raw
|
|
85
|
+
// gl.bindVertexArray() call below and resetState() at the end of this method.
|
|
86
|
+
this.renderer.state.useProgram(this.program);
|
|
87
|
+
gl.bindVertexArray(this.vao);
|
|
88
|
+
this.bufferNames.forEach((name, index) => {
|
|
89
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.readBuffer(name));
|
|
90
|
+
gl.vertexAttribPointer(index, this.strides[index], gl.FLOAT, false, 0, 0);
|
|
91
|
+
gl.enableVertexAttribArray(index);
|
|
92
|
+
});
|
|
93
|
+
this.bufferNames.forEach((name, index) => {
|
|
94
|
+
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, index, mesh.writeBuffer(name));
|
|
95
|
+
});
|
|
96
|
+
this.setUniforms(deltaTime, spawnRange, emitter, mesh.capacity);
|
|
97
|
+
gl.enable(gl.RASTERIZER_DISCARD);
|
|
98
|
+
gl.beginTransformFeedback(gl.POINTS);
|
|
99
|
+
gl.drawArrays(gl.POINTS, 0, mesh.capacity);
|
|
100
|
+
gl.endTransformFeedback();
|
|
101
|
+
gl.disable(gl.RASTERIZER_DISCARD);
|
|
102
|
+
// A bound TRANSFORM_FEEDBACK_BUFFER base is otherwise still "active" for this binding point;
|
|
103
|
+
// release it rather than leave a stale reference around between ticks.
|
|
104
|
+
for (let index = 0; index < this.bufferNames.length; index += 1) {
|
|
105
|
+
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, index, null);
|
|
106
|
+
}
|
|
107
|
+
for (const name of this.bufferNames) {
|
|
108
|
+
mesh.swapBuffers(name);
|
|
109
|
+
}
|
|
110
|
+
// Consumed - advance past every id this tick's spawn range just claimed.
|
|
111
|
+
this.spawnIdBase += spawnRange.count;
|
|
112
|
+
// Our raw gl.bindVertexArray() call above is invisible to three's own VAO-identity cache
|
|
113
|
+
// (WebGLBindingStates has no scoped, public way to invalidate just this mesh's entry) -
|
|
114
|
+
// resetState() is the only public lever, so it runs every tick. Per-emitter for now, not
|
|
115
|
+
// batched across every GPU-driven emitter into one call per frame - a real, deferred
|
|
116
|
+
// optimization, not required for correctness.
|
|
117
|
+
this.renderer.resetState();
|
|
118
|
+
}
|
|
119
|
+
destroy() {
|
|
120
|
+
this.gl.deleteProgram(this.program);
|
|
121
|
+
this.gl.deleteVertexArray(this.vao);
|
|
122
|
+
}
|
|
123
|
+
/** GPU-tier mirror of `FXSimulationHolder.applyBindingValues`: mutate each named uniform's live
|
|
124
|
+
* value in place; unknown name = no-op. `FXEmitter.applyValues` calls both mirrors, since
|
|
125
|
+
* either backend may be the active one. */
|
|
126
|
+
applyBindingValues(values) {
|
|
127
|
+
for (const name in values) {
|
|
128
|
+
if (name in this.artifact.bindings) {
|
|
129
|
+
this.artifact.bindings[name].value = values[name];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
setUniforms(deltaTime, spawnRange, emitter, capacity) {
|
|
134
|
+
const gl = this.gl;
|
|
135
|
+
gl.uniform1i(this.uniformLocations[FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM], this.randSeedCounter);
|
|
136
|
+
gl.uniform1i(this.uniformLocations[FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM], spawnRange.start);
|
|
137
|
+
gl.uniform1i(this.uniformLocations[FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM], spawnRange.count);
|
|
138
|
+
gl.uniform1i(this.uniformLocations[FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM], this.spawnIdBase);
|
|
139
|
+
gl.uniform1i(this.uniformLocations[FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM], capacity);
|
|
140
|
+
gl.uniform1f(this.uniformLocations[FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM], deltaTime);
|
|
141
|
+
gl.uniformMatrix4fv(this.uniformLocations[FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM], false, [
|
|
142
|
+
...emitter.worldMatrix,
|
|
143
|
+
]);
|
|
144
|
+
gl.uniform3fv(this.uniformLocations[FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM], [
|
|
145
|
+
...emitter.velocity,
|
|
146
|
+
]);
|
|
147
|
+
gl.uniform3fv(this.uniformLocations[FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM], [
|
|
148
|
+
...emitter.angularVelocity,
|
|
149
|
+
]);
|
|
150
|
+
for (const [name, slot] of Object.entries(this.artifact.bindings)) {
|
|
151
|
+
// Float32Array-valued (sampler2D/LUT) bindings are reserved for a future node - see
|
|
152
|
+
// FXParticleKernelArtifact.bindings's own doc comment - never produced by any node today.
|
|
153
|
+
gl.uniform1f(this.uniformLocations[name], slot.value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const FIXED_UNIFORM_NAMES = [
|
|
158
|
+
FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM,
|
|
159
|
+
FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM,
|
|
160
|
+
FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM,
|
|
161
|
+
FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM,
|
|
162
|
+
FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM,
|
|
163
|
+
FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM,
|
|
164
|
+
FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM,
|
|
165
|
+
FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM,
|
|
166
|
+
FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM,
|
|
167
|
+
];
|
|
168
|
+
function compileShader(gl, type, source) {
|
|
169
|
+
const shader = gl.createShader(type);
|
|
170
|
+
if (shader === null) {
|
|
171
|
+
throw new Error("FXTransformFeedbackSimulationHolder: gl.createShader() failed");
|
|
172
|
+
}
|
|
173
|
+
gl.shaderSource(shader, source);
|
|
174
|
+
gl.compileShader(shader);
|
|
175
|
+
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) !== true) {
|
|
176
|
+
const info = gl.getShaderInfoLog(shader);
|
|
177
|
+
gl.deleteShader(shader);
|
|
178
|
+
throw new Error(`FXTransformFeedbackSimulationHolder: shader failed to compile: ${info ?? ""}`);
|
|
179
|
+
}
|
|
180
|
+
return shader;
|
|
181
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export type { FXAttributeDecl, FXBehaviorArtifact, FXBufferLayout, FXGeometrySource, FXKernelBuffers, FXRenderArtifact, FXShaderStageSource, FXUniformInit, FXValueSlot, } from "./artifact/FXArtifact.js";
|
|
2
|
-
export { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_LIFECYCLE_STRIDE, FX_CORE_LIFECYCLE_VARYING, FX_CORE_PARTICLE_DEFINES, FX_CORE_POSITION, FX_CORE_POSITION_STRIDE, FX_CORE_POSITION_VARYING, FX_LIFETIME, FX_POSITION_X, FX_POSITION_Y, FX_POSITION_Z, } from "./coreLayout.js";
|
|
1
|
+
export type { FXAttributeDecl, FXBehaviorArtifact, FXBufferLayout, FXGeometrySource, FXKernelBuffers, FXParticleKernelArtifact, FXRenderArtifact, FXShaderStageSource, FXUniformInit, FXValueSlot, } from "./artifact/FXArtifact.js";
|
|
2
|
+
export { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_LIFECYCLE_STRIDE, FX_CORE_LIFECYCLE_VARYING, FX_CORE_PARTICLE_DEFINES, FX_CORE_POSITION, FX_CORE_POSITION_STRIDE, FX_CORE_POSITION_VARYING, FX_ID, FX_LIFETIME, FX_POSITION_X, FX_POSITION_Y, FX_POSITION_Z, } from "./coreLayout.js";
|
|
3
|
+
export { FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM, FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM, FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM, FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM, FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM, FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM, } from "./behaviorTransformFeedbackLayout.js";
|
|
3
4
|
export type { FXGraphLambertMaterialOptions, FXGraphUnlitMaterialOptions, FXRenderMode, } from "./render/FXMaterialOptions.js";
|
|
4
5
|
export type { FXGeometryPrimitive } from "./artifact/FXArtifact.js";
|
|
5
6
|
export { FXEffect } from "./effect/FXEffect.js";
|
|
6
7
|
export type { FXEffectOptions, FXEffectSpec } from "./effect/FXEffectSpec.js";
|
|
8
|
+
export type { FXEmitter } from "./emitter/FXEmitter.js";
|
|
7
9
|
export { FXWorld } from "./world/FXWorld.js";
|
|
8
|
-
export { fxFbm,
|
|
10
|
+
export { fxFbm, fxFbm3, fxFract, fxMix, fxMod, fxNoise1, fxNoise2, fxNoise3, fxSampleLut, fxSmoothstep, } from "./miscellaneous/fxMath.js";
|
|
9
11
|
export { fxDataTexture } from "./miscellaneous/texture/fxDataTexture.js";
|
|
10
12
|
export type { FXDataTextureOptions } from "./miscellaneous/texture/fxDataTexture.js";
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// FXWorld, and is driven by a single FXWorld.update per frame - plus the runtime-owned types the
|
|
3
3
|
// editor imports (artifact contracts and the core layout). No graph, compiler, validator, or live
|
|
4
4
|
// protocol lives here.
|
|
5
|
-
export { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_LIFECYCLE_STRIDE, FX_CORE_LIFECYCLE_VARYING, FX_CORE_PARTICLE_DEFINES, FX_CORE_POSITION, FX_CORE_POSITION_STRIDE, FX_CORE_POSITION_VARYING, FX_LIFETIME, FX_POSITION_X, FX_POSITION_Y, FX_POSITION_Z, } from "./coreLayout.js";
|
|
5
|
+
export { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_LIFECYCLE_STRIDE, FX_CORE_LIFECYCLE_VARYING, FX_CORE_PARTICLE_DEFINES, FX_CORE_POSITION, FX_CORE_POSITION_STRIDE, FX_CORE_POSITION_VARYING, FX_ID, FX_LIFETIME, FX_POSITION_X, FX_POSITION_Y, FX_POSITION_Z, } from "./coreLayout.js";
|
|
6
|
+
export { FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM, FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM, FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM, FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM, FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM, FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM, FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM, } from "./behaviorTransformFeedbackLayout.js";
|
|
6
7
|
// Whole-project playback: the generic runtime behind every editor-emitted project module (base
|
|
7
8
|
// class + scene specification). An emitted module supplies only its `FXEffectSpec` data and a thin subclass.
|
|
8
9
|
export { FXEffect } from "./effect/FXEffect.js";
|
|
9
10
|
// The tick domain: an effect joins one on construction; a single FXWorld.update per frame drives
|
|
10
11
|
// every effect's timeline and the shared particle pool.
|
|
11
12
|
export { FXWorld } from "./world/FXWorld.js";
|
|
12
|
-
export { fxFbm,
|
|
13
|
+
export { fxFbm, fxFbm3, fxFract, fxMix, fxMod, fxNoise1, fxNoise2, fxNoise3, fxSampleLut, fxSmoothstep, } from "./miscellaneous/fxMath.js";
|
|
13
14
|
export { fxDataTexture } from "./miscellaneous/texture/fxDataTexture.js";
|
|
@@ -68,7 +68,7 @@ export class FXInstancedParticle extends Mesh {
|
|
|
68
68
|
this.instancedGeometry.instanceCount += count;
|
|
69
69
|
}
|
|
70
70
|
removeDeadParticles() {
|
|
71
|
-
// Cull against core `lifecycle` (
|
|
71
|
+
// Cull against core `lifecycle` (vec3 [age, lifetime, id]); alive while `age < lifetime`. Rides the
|
|
72
72
|
// same copy-down as every other property buffer.
|
|
73
73
|
const lifecycleBuffer = this.propertyBuffers[FX_CORE_LIFECYCLE];
|
|
74
74
|
if (lifecycleBuffer === undefined) {
|
|
@@ -183,6 +183,33 @@ export class FXInstancedParticle extends Mesh {
|
|
|
183
183
|
drop() {
|
|
184
184
|
this.instancedGeometry.instanceCount = 0;
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Swaps the mounted material and base geometry in place for a render-only structural edit -
|
|
188
|
+
* every per-particle buffer in {@link propertyBuffers} and the current instance count are left
|
|
189
|
+
* completely untouched, so live particles never reset. Never called when the varyings shape
|
|
190
|
+
* itself changed (the driver's own `applyRenderArtifact` guards that) - only the constructor
|
|
191
|
+
* path handles a real buffer-layout change.
|
|
192
|
+
*/
|
|
193
|
+
replaceMaterialAndBaseGeometry(material, baseGeometry) {
|
|
194
|
+
// Detach the previous base attributes first, same as destroy()'s dance: they were borrowed by
|
|
195
|
+
// reference from a caller-owned geometry (possibly shared with other live consumers), so they
|
|
196
|
+
// must never be disposed here.
|
|
197
|
+
this.instancedGeometry.deleteAttribute("position");
|
|
198
|
+
this.instancedGeometry.deleteAttribute("uv");
|
|
199
|
+
this.instancedGeometry.deleteAttribute("normal");
|
|
200
|
+
this.instancedGeometry.setIndex(null);
|
|
201
|
+
const baseIndex = baseGeometry.getIndex();
|
|
202
|
+
if (baseIndex !== null) {
|
|
203
|
+
this.instancedGeometry.setIndex(baseIndex);
|
|
204
|
+
}
|
|
205
|
+
this.instancedGeometry.setAttribute("position", baseGeometry.getAttribute("position"));
|
|
206
|
+
this.instancedGeometry.setAttribute("uv", baseGeometry.getAttribute("uv"));
|
|
207
|
+
this.instancedGeometry.setAttribute("normal", baseGeometry.getAttribute("normal"));
|
|
208
|
+
// The mounted material IS disposable here (unlike the base geometry attributes above) - it was
|
|
209
|
+
// built exclusively for this mesh, never shared.
|
|
210
|
+
this.material?.dispose();
|
|
211
|
+
this.material = material;
|
|
212
|
+
}
|
|
186
213
|
// Disposes the instanced geometry and the currently mounted material - not the constructor one,
|
|
187
214
|
// which a live material hot-swap may have replaced and disposed.
|
|
188
215
|
destroy() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { GLBufferAttribute, InstancedBufferGeometry, Mesh } from "three";
|
|
2
|
+
/**
|
|
3
|
+
* @internal A `GLBufferAttribute` additionally declared instanced (divisor 1, one value per drawn
|
|
4
|
+
* particle instead of per vertex). Confirmed by reading `WebGLBindingStates.js:432-444`: the
|
|
5
|
+
* per-instance vertex-attribute divisor is driven by `isInstancedBufferAttribute`/
|
|
6
|
+
* `meshPerAttribute` on the attribute object itself - a plain `GLBufferAttribute` never sets
|
|
7
|
+
* either, so without this every particle's state would be read as per-VERTEX (one value shared
|
|
8
|
+
* by the whole base primitive) instead of per-INSTANCE.
|
|
9
|
+
*/
|
|
10
|
+
class FXGLInstancedBufferAttribute extends GLBufferAttribute {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.isInstancedBufferAttribute = true;
|
|
14
|
+
this.meshPerAttribute = 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `InstancedBufferGeometry.setAttribute` is typed only for `BufferAttribute`/
|
|
19
|
+
* `InterleavedBufferAttribute` - its `Attributes` generic defaults to `NormalBufferAttributes`,
|
|
20
|
+
* and `InstancedBufferGeometry` itself is declared non-generic, so the wider
|
|
21
|
+
* `NormalOrGLBufferAttributes` union (which DOES include `GLBufferAttribute`) is unreachable
|
|
22
|
+
* through it at the type level, even though `GLBufferAttribute` is a real, three.js-documented
|
|
23
|
+
* attribute type for exactly this GPGPU use case. Narrow, isolated escape hatch - matches the
|
|
24
|
+
* existing three.js-typing-gap precedent in `FXInstancedParticle.invalidateInstanceCap`.
|
|
25
|
+
*/
|
|
26
|
+
function setInstancedGLAttribute(geometry, name, attribute) {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
geometry.setAttribute(name, attribute);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @internal GPU-resident sibling of `FXInstancedParticle`: same instanced-mesh shape (one base
|
|
32
|
+
* primitive drawn `capacity` times, one property buffer per declared varying), but every property
|
|
33
|
+
* buffer is a **ping-pong pair** of raw `WebGLBuffer`s the transform-feedback simulation holder
|
|
34
|
+
* writes directly - never a `Float32Array`-backed `InstancedBufferAttribute` three.js uploads
|
|
35
|
+
* itself (`FXInstancedParticle`'s own mechanism, untouched). Capacity is fixed at construction, no
|
|
36
|
+
* growth: this class is never resized, unlike `FXInstancedParticle.ensureCapacity` (a deferred
|
|
37
|
+
* future extension, not required for correctness today). Every
|
|
38
|
+
* particle slot is always drawn (`instanceCount` is always `capacity`) - the CPU never knows the
|
|
39
|
+
* true live count for a GPU-driven emitter, so dead slots are hidden by the render shader's own
|
|
40
|
+
* age/lifetime test, not by shrinking the draw range.
|
|
41
|
+
*/
|
|
42
|
+
export class FXTransformFeedbackParticle extends Mesh {
|
|
43
|
+
constructor(gl, varyings, capacity, material, baseGeometry) {
|
|
44
|
+
const instancedGeometry = new InstancedBufferGeometry();
|
|
45
|
+
// Copy the base geometry's position/uv/normal + index onto the instanced geometry, same as
|
|
46
|
+
// FXInstancedParticle - these stay real, CPU-readable BufferAttributes (the base primitive's
|
|
47
|
+
// own shape, not per-particle state), borrowed by reference, never disposed here.
|
|
48
|
+
const baseIndex = baseGeometry.getIndex();
|
|
49
|
+
if (baseIndex !== null) {
|
|
50
|
+
instancedGeometry.setIndex(baseIndex);
|
|
51
|
+
}
|
|
52
|
+
instancedGeometry.setAttribute("position", baseGeometry.getAttribute("position"));
|
|
53
|
+
instancedGeometry.setAttribute("uv", baseGeometry.getAttribute("uv"));
|
|
54
|
+
instancedGeometry.setAttribute("normal", baseGeometry.getAttribute("normal"));
|
|
55
|
+
super(instancedGeometry, material);
|
|
56
|
+
this.propertyBuffers = {};
|
|
57
|
+
// Per-buffer-name ping-pong pair; `currentIndex[name]` (0 or 1) is which half is the currently
|
|
58
|
+
// valid, renderable side - the other half is what the transform-feedback pass writes into this
|
|
59
|
+
// tick. Never read and written by name in the same pass (see `readBuffer`/`writeBuffer`).
|
|
60
|
+
this.rawBufferPairs = {};
|
|
61
|
+
this.currentIndex = {};
|
|
62
|
+
this.strides = {};
|
|
63
|
+
this.frustumCulled = false;
|
|
64
|
+
this.gl = gl;
|
|
65
|
+
this.instancedGeometry = instancedGeometry;
|
|
66
|
+
this.capacity = capacity;
|
|
67
|
+
this.instancedGeometry.instanceCount = capacity;
|
|
68
|
+
for (const name in varyings) {
|
|
69
|
+
const { bufferSize } = varyings[name];
|
|
70
|
+
this.strides[name] = bufferSize;
|
|
71
|
+
const bufferA = this.createSizedBuffer(bufferSize, capacity);
|
|
72
|
+
const bufferB = this.createSizedBuffer(bufferSize, capacity);
|
|
73
|
+
this.rawBufferPairs[name] = [bufferA, bufferB];
|
|
74
|
+
this.currentIndex[name] = 0;
|
|
75
|
+
const attribute = new FXGLInstancedBufferAttribute(bufferA, gl.FLOAT, bufferSize, 4, capacity);
|
|
76
|
+
setInstancedGLAttribute(this.instancedGeometry, `a_${name}`, attribute);
|
|
77
|
+
this.propertyBuffers[name] = attribute;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The buffer transform feedback should bind as its `in` (read) source this tick - last tick's
|
|
81
|
+
* result, or uninitialized garbage on the very first tick (safe: the assembled program's spawn
|
|
82
|
+
* branch always zero-defaults every buffer before the graph's own writes, so a freshly claimed
|
|
83
|
+
* slot never actually reads it before being overwritten). */
|
|
84
|
+
readBuffer(name) {
|
|
85
|
+
return this.rawBufferPairs[name][this.currentIndex[name]];
|
|
86
|
+
}
|
|
87
|
+
/** The buffer transform feedback should bind as its transform-feedback (write) target this tick
|
|
88
|
+
* - the stale half of the pair, about to become current once `swapBuffers` runs. */
|
|
89
|
+
writeBuffer(name) {
|
|
90
|
+
return this.rawBufferPairs[name][this.currentIndex[name] === 0 ? 1 : 0];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Call once per state buffer after a tick's transform-feedback pass has finished writing
|
|
94
|
+
* `writeBuffer(name)`: flips which half is current, and replaces the whole attribute object on
|
|
95
|
+
* the geometry - never just `.setBuffer()` + `.needsUpdate` on the existing one. Confirmed by
|
|
96
|
+
* reading `WebGLBindingStates.needsUpdate()` (`WebGLBindingStates.js:166-209`): it decides
|
|
97
|
+
* whether to rebind a cached VAO's vertex-attribute pointers purely by comparing attribute
|
|
98
|
+
* OBJECT identity, never `.version`/`.buffer` - reusing the same attribute instance across a
|
|
99
|
+
* swap would leave the render draw silently bound to the FIRST buffer forever (independently
|
|
100
|
+
* confirmed against three.js issue #22843, which describes the identical root cause). Mirrors
|
|
101
|
+
* `FXInstancedParticle.ensureCapacity`, which replaces the whole `InstancedBufferAttribute` for
|
|
102
|
+
* the same reason - there discovered as an array-reallocation side effect, here load-bearing on
|
|
103
|
+
* its own since nothing about capacity itself ever changes.
|
|
104
|
+
*/
|
|
105
|
+
swapBuffers(name) {
|
|
106
|
+
const nextIndex = this.currentIndex[name] === 0 ? 1 : 0;
|
|
107
|
+
this.currentIndex[name] = nextIndex;
|
|
108
|
+
const stride = this.strides[name];
|
|
109
|
+
const attribute = new FXGLInstancedBufferAttribute(this.rawBufferPairs[name][nextIndex], this.gl.FLOAT, stride, 4, this.capacity);
|
|
110
|
+
setInstancedGLAttribute(this.instancedGeometry, `a_${name}`, attribute);
|
|
111
|
+
this.propertyBuffers[name] = attribute;
|
|
112
|
+
}
|
|
113
|
+
drop() {
|
|
114
|
+
// No live-particle count to reset for a GPU-driven emitter (instanceCount is always
|
|
115
|
+
// `capacity`) - a reset instead means "everything currently alive should read as dead", which
|
|
116
|
+
// is the simulation holder's job (it owns dt/age, not this buffer-holding class).
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Swaps the mounted material and base geometry in place for a render-only structural edit -
|
|
120
|
+
* every ping-pong state buffer and `capacity` are left completely untouched, so playback never
|
|
121
|
+
* resets. Never called when the varyings shape itself changed (the driver's own
|
|
122
|
+
* `applyRenderArtifact` guards that) - only the constructor path handles a real buffer-layout
|
|
123
|
+
* change.
|
|
124
|
+
*/
|
|
125
|
+
replaceMaterialAndBaseGeometry(material, baseGeometry) {
|
|
126
|
+
// Detach the previous base attributes first, same as destroy()'s dance: they were borrowed by
|
|
127
|
+
// reference from a caller-owned geometry (possibly shared with other live consumers), so they
|
|
128
|
+
// must never be disposed here.
|
|
129
|
+
this.instancedGeometry.deleteAttribute("position");
|
|
130
|
+
this.instancedGeometry.deleteAttribute("uv");
|
|
131
|
+
this.instancedGeometry.deleteAttribute("normal");
|
|
132
|
+
this.instancedGeometry.setIndex(null);
|
|
133
|
+
const baseIndex = baseGeometry.getIndex();
|
|
134
|
+
if (baseIndex !== null) {
|
|
135
|
+
this.instancedGeometry.setIndex(baseIndex);
|
|
136
|
+
}
|
|
137
|
+
this.instancedGeometry.setAttribute("position", baseGeometry.getAttribute("position"));
|
|
138
|
+
this.instancedGeometry.setAttribute("uv", baseGeometry.getAttribute("uv"));
|
|
139
|
+
this.instancedGeometry.setAttribute("normal", baseGeometry.getAttribute("normal"));
|
|
140
|
+
// The mounted material IS disposable here (unlike the base geometry attributes above) - it was
|
|
141
|
+
// built exclusively for this mesh, never shared.
|
|
142
|
+
this.material?.dispose();
|
|
143
|
+
this.material = material;
|
|
144
|
+
}
|
|
145
|
+
// Disposes the instanced geometry, the currently mounted material, and every raw ping-pong
|
|
146
|
+
// buffer this instance owns - never the base geometry's borrowed position/uv/normal/index.
|
|
147
|
+
destroy() {
|
|
148
|
+
this.instancedGeometry.deleteAttribute("position");
|
|
149
|
+
this.instancedGeometry.deleteAttribute("uv");
|
|
150
|
+
this.instancedGeometry.deleteAttribute("normal");
|
|
151
|
+
this.instancedGeometry.setIndex(null);
|
|
152
|
+
this.instancedGeometry.dispose();
|
|
153
|
+
this.material?.dispose();
|
|
154
|
+
for (const name in this.rawBufferPairs) {
|
|
155
|
+
const [bufferA, bufferB] = this.rawBufferPairs[name];
|
|
156
|
+
this.gl.deleteBuffer(bufferA);
|
|
157
|
+
this.gl.deleteBuffer(bufferB);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
createSizedBuffer(itemSize, count) {
|
|
161
|
+
const buffer = this.gl.createBuffer();
|
|
162
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
|
|
163
|
+
// Sized but uninitialized (garbage) - the spawn branch's zero-default (see the constructor
|
|
164
|
+
// doc) makes this safe for every buffer, core and attribute alike.
|
|
165
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, itemSize * count * Float32Array.BYTES_PER_ELEMENT, this.gl.DYNAMIC_DRAW);
|
|
166
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
167
|
+
return buffer;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import type { BufferGeometry } from "three";
|
|
2
|
+
import type { FXGeometrySource } from "../artifact/FXArtifact.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves an artifact's {@link FXGeometrySource} to a concrete `BufferGeometry`: a
|
|
5
|
+
* built-in primitive is built fresh; a `"custom"` slot is looked up by name in `customGeometries`
|
|
6
|
+
* (app-supplied, e.g. from the exported module's `assets` or the editor's own live content
|
|
7
|
+
* library) - falling back to the `"plane"` primitive when the name is unbound, the same graceful
|
|
8
|
+
* degradation an unbound external texture slot gets.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveGeometrySource(source: FXGeometrySource | undefined, customGeometries: Readonly<Record<string, BufferGeometry>>): BufferGeometry;
|
|
@@ -16,7 +16,7 @@ export function buildPrimitiveGeometry(primitive) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Resolves an artifact's {@link FXGeometrySource} to a concrete `BufferGeometry`: a
|
|
20
20
|
* built-in primitive is built fresh; a `"custom"` slot is looked up by name in `customGeometries`
|
|
21
21
|
* (app-supplied, e.g. from the exported module's `assets` or the editor's own live content
|
|
22
22
|
* library) - falling back to the `"plane"` primitive when the name is unbound, the same graceful
|