reze-engine 0.56.5 → 0.56.7
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/dist/effect-schedule.d.ts +93 -0
- package/dist/effect-schedule.d.ts.map +1 -1
- package/dist/effect-schedule.js +118 -9
- package/dist/engine.d.ts +33 -34
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +135 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/effect-schedule.ts +150 -6
- package/src/engine.ts +152 -55
- package/src/index.ts +9 -1
package/dist/engine.js
CHANGED
|
@@ -20,7 +20,7 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
|
|
|
20
20
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow";
|
|
21
21
|
import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug";
|
|
22
22
|
import { paramChanged, sampleParamTrack } from "./param-track";
|
|
23
|
-
import { effectState } from "./effect-schedule";
|
|
23
|
+
import { advanceSim, DISSOLVE_PARAMS, dissolveConstants, dissolveCycleOf, effectState, sampleDissolveCycle, scheduledDissolve, } from "./effect-schedule";
|
|
24
24
|
import { parentKeySpan } from "./parent-keys";
|
|
25
25
|
import { SHADOW_CASCADES, buildShadowVP } from "./shadow-cascades";
|
|
26
26
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
@@ -1428,6 +1428,9 @@ export class Engine {
|
|
|
1428
1428
|
this.gizmoDrag = null;
|
|
1429
1429
|
this.camera?.setInputLocked(false);
|
|
1430
1430
|
};
|
|
1431
|
+
/** Models this wrote a dissolve onto last frame, so one nothing asks for any
|
|
1432
|
+
* more comes back whole instead of holding wherever it was left. */
|
|
1433
|
+
this.dissolveTouched = new Set();
|
|
1431
1434
|
this.canvas = canvas;
|
|
1432
1435
|
const d = DEFAULT_ENGINE_OPTIONS;
|
|
1433
1436
|
this.world = {
|
|
@@ -3362,6 +3365,10 @@ export class Engine {
|
|
|
3362
3365
|
influence: 1,
|
|
3363
3366
|
window: null,
|
|
3364
3367
|
weight: 1,
|
|
3368
|
+
sim: { start: null, time: null },
|
|
3369
|
+
simStep: 0,
|
|
3370
|
+
simReset: false,
|
|
3371
|
+
dissolve: d.dissolve ? dissolveConstants(authored) : null,
|
|
3365
3372
|
// Its OWN resolution, no longer the scene's: an effect that never asked
|
|
3366
3373
|
// for full res is not promoted because a neighbour did.
|
|
3367
3374
|
// FULL RESOLUTION UNLESS TOLD OTHERWISE.
|
|
@@ -3617,10 +3624,11 @@ export class Engine {
|
|
|
3617
3624
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render");
|
|
3618
3625
|
if (Array.isArray(renderModule))
|
|
3619
3626
|
return { ok: false, diagnostics: renderModule };
|
|
3627
|
+
// COPY_DST so a scheduled effect can empty its pool when its window starts.
|
|
3620
3628
|
const buffer = this.device.createBuffer({
|
|
3621
3629
|
label: "particle pool",
|
|
3622
3630
|
size: count * PARTICLE_STRIDE,
|
|
3623
|
-
usage: GPUBufferUsage.STORAGE,
|
|
3631
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
3624
3632
|
});
|
|
3625
3633
|
const uniform = this.device.createBuffer({
|
|
3626
3634
|
label: "particle uniforms",
|
|
@@ -3870,6 +3878,16 @@ export class Engine {
|
|
|
3870
3878
|
const p = e.particles;
|
|
3871
3879
|
if (!p)
|
|
3872
3880
|
continue;
|
|
3881
|
+
const scheduled = e.window !== null;
|
|
3882
|
+
// A scheduled pool that has not moved holds still: a paused transport, or
|
|
3883
|
+
// a time outside every window. A dispatch at dt 0 would still run the
|
|
3884
|
+
// author's step, and not every step is a function of dt.
|
|
3885
|
+
if (scheduled && !e.simReset && e.simStep === 0)
|
|
3886
|
+
continue;
|
|
3887
|
+
// Entering its window, or going back in it: the pool starts empty, and
|
|
3888
|
+
// every slot spawns on this dispatch from the effect's own local time.
|
|
3889
|
+
if (e.simReset)
|
|
3890
|
+
encoder.clearBuffer(p.buffer);
|
|
3873
3891
|
p.data[0] = this.sceneClock - e.epochScene;
|
|
3874
3892
|
// The SIMULATION runs at every weight, 0 included — only the draw stops.
|
|
3875
3893
|
// A scheduled effect that froze while faded out would resume from the
|
|
@@ -3878,7 +3896,9 @@ export class Engine {
|
|
|
3878
3896
|
p.data[4] = e.weight;
|
|
3879
3897
|
// Clamped: a backgrounded tab returns with a delta of whole seconds, and an
|
|
3880
3898
|
// unclamped step flings every particle out of the scene in one frame.
|
|
3881
|
-
|
|
3899
|
+
// A scheduled effect steps by the transport instead, clamped the same way
|
|
3900
|
+
// in advanceSim, so a pause freezes it and an export steps it exactly.
|
|
3901
|
+
p.data[1] = scheduled ? e.simStep : Math.min(0.1, Math.max(0, deltaTime));
|
|
3882
3902
|
p.counts[2] = p.count;
|
|
3883
3903
|
p.counts[3] = this.particleFrame++;
|
|
3884
3904
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer);
|
|
@@ -4325,7 +4345,8 @@ export class Engine {
|
|
|
4325
4345
|
label: `grid grid ${n}`,
|
|
4326
4346
|
size: [size, size],
|
|
4327
4347
|
format: SIM_FORMAT,
|
|
4328
|
-
|
|
4348
|
+
// RENDER_ATTACHMENT so a scheduled effect can clear it when its window starts.
|
|
4349
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
4329
4350
|
});
|
|
4330
4351
|
const textures = [make(0), make(1)];
|
|
4331
4352
|
const read = [textures[0].createView(), textures[1].createView()];
|
|
@@ -4394,11 +4415,29 @@ export class Engine {
|
|
|
4394
4415
|
const grid = e.grid;
|
|
4395
4416
|
if (!grid)
|
|
4396
4417
|
continue;
|
|
4418
|
+
const scheduled = e.window !== null;
|
|
4419
|
+
// Held still like the particles, for the same reason.
|
|
4420
|
+
if (scheduled && !e.simReset && e.simStep === 0)
|
|
4421
|
+
continue;
|
|
4422
|
+
if (e.simReset) {
|
|
4423
|
+
// Back to how install left it: both textures empty and frame 0 again,
|
|
4424
|
+
// the frame an effect seeds its grid on.
|
|
4425
|
+
for (const view of grid.read) {
|
|
4426
|
+
encoder
|
|
4427
|
+
.beginRenderPass({
|
|
4428
|
+
label: "grid reset",
|
|
4429
|
+
colorAttachments: [{ view, loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 } }],
|
|
4430
|
+
})
|
|
4431
|
+
.end();
|
|
4432
|
+
}
|
|
4433
|
+
grid.frame = 0;
|
|
4434
|
+
grid.parity = 0;
|
|
4435
|
+
}
|
|
4397
4436
|
grid.data[0] = this.sceneClock - e.epochScene;
|
|
4398
4437
|
// Clamped like the particle step: a backgrounded tab returns with a delta of
|
|
4399
4438
|
// whole seconds, and one unclamped step of an advection kernel throws the
|
|
4400
4439
|
// whole grid off its own edge.
|
|
4401
|
-
grid.data[1] = Math.min(0.1, Math.max(0, deltaTime));
|
|
4440
|
+
grid.data[1] = scheduled ? e.simStep : Math.min(0.1, Math.max(0, deltaTime));
|
|
4402
4441
|
grid.data[2] = grid.size;
|
|
4403
4442
|
grid.data[3] = grid.frame++;
|
|
4404
4443
|
this.device.queue.writeBuffer(grid.uniform, 0, grid.data.buffer);
|
|
@@ -4497,6 +4536,12 @@ export class Engine {
|
|
|
4497
4536
|
if (!fx)
|
|
4498
4537
|
return;
|
|
4499
4538
|
fx.window = windows && windows.length ? windows : null;
|
|
4539
|
+
// Unscheduled forgets where its simulation stood. A changed lane keeps it:
|
|
4540
|
+
// advanceSim restarts the pool only if the window the transport is in now
|
|
4541
|
+
// starts somewhere else, so trimming a block, or turning the influence
|
|
4542
|
+
// that is sent with every lane, does not restart a burst mid-flight.
|
|
4543
|
+
if (!fx.window)
|
|
4544
|
+
fx.sim = { start: null, time: null };
|
|
4500
4545
|
}
|
|
4501
4546
|
getEffectSchedule(index) {
|
|
4502
4547
|
return this.effects[index]?.window ?? null;
|
|
@@ -4513,12 +4558,20 @@ export class Engine {
|
|
|
4513
4558
|
// Read ONCE: it walks the cast, and every effect wants the same answer.
|
|
4514
4559
|
const transport = this.transportTime();
|
|
4515
4560
|
for (const fx of this.effects) {
|
|
4561
|
+
fx.simReset = false;
|
|
4516
4562
|
if (!fx.window || fx.window.length === 0) {
|
|
4517
4563
|
fx.weight = fx.influence;
|
|
4518
4564
|
continue;
|
|
4519
4565
|
}
|
|
4520
4566
|
const at = effectState(fx.window, fx.influence, transport);
|
|
4521
4567
|
fx.weight = at.weight;
|
|
4568
|
+
// The particles and the grid carry state from frame to frame, so they get
|
|
4569
|
+
// more than a clock: a restart on entering a window or going back in one,
|
|
4570
|
+
// and a step measured on the transport. See advanceSim.
|
|
4571
|
+
const sim = advanceSim(fx.sim, fx.window, transport);
|
|
4572
|
+
fx.sim = sim.clock;
|
|
4573
|
+
fx.simStep = sim.step;
|
|
4574
|
+
fx.simReset = sim.reset;
|
|
4522
4575
|
// Its own clock, expressed the way the mounts read it. Every mount
|
|
4523
4576
|
// derives time from the epoch against sceneClock, so this one write moves
|
|
4524
4577
|
// the field, the particles, the ribbons, lightEmit and the grid together
|
|
@@ -12255,6 +12308,9 @@ export class Engine {
|
|
|
12255
12308
|
if (pose)
|
|
12256
12309
|
this.camera.setVmdPose(pose);
|
|
12257
12310
|
}
|
|
12311
|
+
// Before the cast is written, which carries every subject's dissolve to the
|
|
12312
|
+
// effects reading it.
|
|
12313
|
+
this.evaluateDissolves();
|
|
12258
12314
|
this.updateCameraUniforms();
|
|
12259
12315
|
this.updateShadowLightVP();
|
|
12260
12316
|
// Depth of field's entire disabled cost is this branch: depth stays in
|
|
@@ -12395,7 +12451,6 @@ export class Engine {
|
|
|
12395
12451
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
12396
12452
|
// Material parameters on the scene clock, before anything reads their
|
|
12397
12453
|
// uniforms this frame.
|
|
12398
|
-
this.evaluateDissolveCycles();
|
|
12399
12454
|
this.evaluateParamTracks();
|
|
12400
12455
|
// FIRST among the things that read an effect, because every one of them
|
|
12401
12456
|
// reads what this writes: the sim's clock, the particle uniform's weight,
|
|
@@ -12864,22 +12919,6 @@ export class Engine {
|
|
|
12864
12919
|
}
|
|
12865
12920
|
return true;
|
|
12866
12921
|
}
|
|
12867
|
-
/**
|
|
12868
|
-
* A repeating dissolve, on the scene clock.
|
|
12869
|
-
*
|
|
12870
|
-
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
12871
|
-
* the wrong shape twice: an exported take stepped at another rate would land
|
|
12872
|
-
* on different values than the preview did, and the effect drawing the sparks
|
|
12873
|
-
* would be reading a number some other clock wrote. Here the engine samples it
|
|
12874
|
-
* where it samples everything else time-driven, so a take reproduces exactly
|
|
12875
|
-
* and rzSubject().dissolve is the same value the material shell used on that
|
|
12876
|
-
* very frame.
|
|
12877
|
-
*
|
|
12878
|
-
* The five numbers are seconds within one cycle: when she starts to go, when
|
|
12879
|
-
* she is fully gone, when she starts to come back, and when she is whole. The
|
|
12880
|
-
* gaps between them are the timing, and the hold between the middle two is how
|
|
12881
|
-
* long she is away.
|
|
12882
|
-
*/
|
|
12883
12922
|
/**
|
|
12884
12923
|
* Eyes on the camera, per model — see Model.setEyeTracking. Live: solved
|
|
12885
12924
|
* every frame against wherever the camera is, orbit or motion alike. Null
|
|
@@ -12892,6 +12931,20 @@ export class Engine {
|
|
|
12892
12931
|
inst.model.setEyeTracking(options);
|
|
12893
12932
|
return true;
|
|
12894
12933
|
}
|
|
12934
|
+
/**
|
|
12935
|
+
* A repeating dissolve on one model, on the scene clock.
|
|
12936
|
+
*
|
|
12937
|
+
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
12938
|
+
* the wrong shape twice: an exported take stepped at another rate would land
|
|
12939
|
+
* on different values than the preview did, and the effect drawing the sparks
|
|
12940
|
+
* would be reading a number some other clock wrote. Here the engine samples it
|
|
12941
|
+
* where it samples everything else time-driven, so a take reproduces exactly
|
|
12942
|
+
* and rzSubject().dissolve is the same value the material shell used on that
|
|
12943
|
+
* very frame.
|
|
12944
|
+
*
|
|
12945
|
+
* An effect that declares `#dissolve` needs none of this: it carries its own
|
|
12946
|
+
* cycle and follows its own clips — see evaluateDissolves.
|
|
12947
|
+
*/
|
|
12895
12948
|
setModelDissolveCycle(modelName, cycle) {
|
|
12896
12949
|
if (!this.modelInstances.has(modelName))
|
|
12897
12950
|
return false;
|
|
@@ -12903,26 +12956,69 @@ export class Engine {
|
|
|
12903
12956
|
this.dissolveCycles.set(modelName, cycle);
|
|
12904
12957
|
return true;
|
|
12905
12958
|
}
|
|
12906
|
-
/**
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12959
|
+
/** The model at one cast slot — the subject rzSubject(index) reads. */
|
|
12960
|
+
castSubjectName(index) {
|
|
12961
|
+
let n = 0;
|
|
12962
|
+
let found = null;
|
|
12963
|
+
this.forEachInstance((inst) => {
|
|
12964
|
+
if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp)
|
|
12965
|
+
return;
|
|
12966
|
+
if (n === index)
|
|
12967
|
+
found = inst.name;
|
|
12968
|
+
n++;
|
|
12969
|
+
});
|
|
12970
|
+
return found;
|
|
12971
|
+
}
|
|
12972
|
+
/** One effect's four durations: its dials where it declares them, the
|
|
12973
|
+
* constants it wrote where it does not. */
|
|
12974
|
+
dissolveTimingsOf(fx, wrote) {
|
|
12975
|
+
const t = { ...wrote };
|
|
12976
|
+
for (const [key, name] of DISSOLVE_PARAMS) {
|
|
12977
|
+
const slot = fx.paramLayout.get(name);
|
|
12978
|
+
if (slot)
|
|
12979
|
+
t[key] = Math.max(0, fx.paramsData[slot.offset]);
|
|
12980
|
+
}
|
|
12981
|
+
return t;
|
|
12982
|
+
}
|
|
12983
|
+
/**
|
|
12984
|
+
* Every dissolve, once a frame, before the cast is written — so the value a
|
|
12985
|
+
* mote reads as rzSubject(i).dissolve is the one her materials wore on that
|
|
12986
|
+
* very frame, rather than the one they wore on the last.
|
|
12987
|
+
*
|
|
12988
|
+
* Two things ask for one. A cycle set on a model (setModelDissolveCycle) runs
|
|
12989
|
+
* on the scene clock and repeats, as it always has. An effect declaring
|
|
12990
|
+
* `#dissolve` takes subject 0 apart on ITS OWN terms: scheduled, the cycle on
|
|
12991
|
+
* each clip's clock with her whole between clips; unscheduled, the same
|
|
12992
|
+
* repeating cycle as before. Where both ask, the more dissolved wins — two
|
|
12993
|
+
* answers about one body, and half a body is not one of them.
|
|
12994
|
+
*/
|
|
12995
|
+
evaluateDissolves() {
|
|
12996
|
+
const want = new Map();
|
|
12997
|
+
const put = (name, v) => want.set(name, Math.min(want.get(name) ?? 1, v));
|
|
12998
|
+
for (const [name, c] of this.dissolveCycles)
|
|
12999
|
+
put(name, sampleDissolveCycle(c, this.sceneClock));
|
|
13000
|
+
let transport = null;
|
|
13001
|
+
for (const fx of this.effects) {
|
|
13002
|
+
if (!fx.dissolve)
|
|
13003
|
+
continue;
|
|
13004
|
+
const cycle = dissolveCycleOf(this.dissolveTimingsOf(fx, fx.dissolve));
|
|
13005
|
+
const subject = this.castSubjectName(0);
|
|
13006
|
+
if (!cycle || !subject)
|
|
13007
|
+
continue;
|
|
13008
|
+
if (fx.window && fx.window.length > 0) {
|
|
13009
|
+
transport ?? (transport = this.transportTime());
|
|
13010
|
+
put(subject, scheduledDissolve(fx.window, cycle, transport));
|
|
12920
13011
|
}
|
|
12921
|
-
else
|
|
12922
|
-
|
|
13012
|
+
else {
|
|
13013
|
+
put(subject, sampleDissolveCycle(cycle, this.sceneClock));
|
|
12923
13014
|
}
|
|
12924
|
-
this.setModelDissolve(name, v);
|
|
12925
13015
|
}
|
|
13016
|
+
for (const name of this.dissolveTouched)
|
|
13017
|
+
if (!want.has(name))
|
|
13018
|
+
this.setModelDissolve(name, 1);
|
|
13019
|
+
this.dissolveTouched = new Set(want.keys());
|
|
13020
|
+
for (const [name, v] of want)
|
|
13021
|
+
this.setModelDissolve(name, v);
|
|
12926
13022
|
}
|
|
12927
13023
|
/** What setModelDissolve last set, or 1 for a model that has never dissolved. */
|
|
12928
13024
|
getModelDissolve(modelName) {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { parseMidi } from "./midi-loader";
|
|
|
4
4
|
export { parseHDR, type HdrImage } from "./hdr";
|
|
5
5
|
export { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, parseLRC, type LyricLine, type LyricRect } from "./shaders/lyrics-api";
|
|
6
6
|
export { sampleParamTrack, type ParamKey, type ParamValue } from "./param-track";
|
|
7
|
-
export { effectState, type EffectWindow, type EffectState } from "./effect-schedule";
|
|
7
|
+
export { activeWindow, advanceSim, effectState, SIM_MAX_STEP, type EffectWindow, type EffectState, type SimClock, } from "./effect-schedule";
|
|
8
8
|
export { parentKeyIndex, type ModelParentKey } from "./parent-keys";
|
|
9
9
|
export { compileGraph, validateGraph, assignStyleSlots, type CompileOptions, type CompileResult, type StyleSlot, } from "./graph/compile";
|
|
10
10
|
export type { ShaderGraph, GraphNode, GraphLink, ExposedParam, SocketValue, Diagnostic, } from "./graph/schema";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,8BAA8B,EAC9B,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,QAAQ,GACd,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAIzC,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE/C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAIrH,OAAO,EAAE,gBAAgB,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAIhF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { sampleParamTrack } from "./param-track";
|
|
|
14
14
|
// The strip an effect is scheduled by, and the pure evaluator behind it —
|
|
15
15
|
// exported so a caller can draw a lane against the same numbers the engine
|
|
16
16
|
// renders from, rather than a second copy of the ramp maths.
|
|
17
|
-
export { effectState } from "./effect-schedule";
|
|
17
|
+
export { activeWindow, advanceSim, effectState, SIM_MAX_STEP, } from "./effect-schedule";
|
|
18
18
|
// Who a model hangs from over time, and the lookup behind it — exported so a
|
|
19
19
|
// timeline draws its holds from the same rule the engine switches by.
|
|
20
20
|
export { parentKeyIndex } from "./parent-keys";
|
package/package.json
CHANGED
package/src/effect-schedule.ts
CHANGED
|
@@ -88,12 +88,7 @@ export function effectState(
|
|
|
88
88
|
// it keeps "which strip am I in" a question with one answer — but a document
|
|
89
89
|
// can be hand-edited, and "the one most recently entered" is the reading that
|
|
90
90
|
// matches what you would see if they were laid down in order.
|
|
91
|
-
|
|
92
|
-
for (const w of windows) {
|
|
93
|
-
if (sceneTime < w.start) continue
|
|
94
|
-
if (w.end !== undefined && (w.end <= w.start || sceneTime > w.end)) continue
|
|
95
|
-
if (!window || w.start > window.start) window = w
|
|
96
|
-
}
|
|
91
|
+
const window = activeWindow(windows, sceneTime)
|
|
97
92
|
if (!window) return SILENT
|
|
98
93
|
|
|
99
94
|
const { start, end } = window
|
|
@@ -118,3 +113,152 @@ export function effectState(
|
|
|
118
113
|
// dip toward zero in the middle of a short strip, which is not a fade.
|
|
119
114
|
return { weight: level * clamp01(ramp), time }
|
|
120
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The window that holds `sceneTime`: the latest start that contains it, edges
|
|
119
|
+
* inclusive, an end at or before its start holding nothing. Null outside every
|
|
120
|
+
* window and for a missing lane. effectState plays this window.
|
|
121
|
+
*/
|
|
122
|
+
export function activeWindow(windows: readonly EffectWindow[] | null, sceneTime: number): EffectWindow | null {
|
|
123
|
+
if (!windows) return null
|
|
124
|
+
let window: EffectWindow | null = null
|
|
125
|
+
for (const w of windows) {
|
|
126
|
+
if (sceneTime < w.start) continue
|
|
127
|
+
if (w.end !== undefined && (w.end <= w.start || sceneTime > w.end)) continue
|
|
128
|
+
if (!window || w.start > window.start) window = w
|
|
129
|
+
}
|
|
130
|
+
return window
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The largest single step a scheduled simulation takes, in seconds. A hitch or
|
|
134
|
+
* a scrub forward advances it this far and no further, and restarts nothing. */
|
|
135
|
+
export const SIM_MAX_STEP = 0.1
|
|
136
|
+
|
|
137
|
+
/** Where a scheduled effect's simulation stood last frame: the start of the
|
|
138
|
+
* window it was in, and its own time there. Both null outside every window. */
|
|
139
|
+
export type SimClock = { start: number | null; time: number | null }
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One frame of a scheduled effect's simulation: its particle pool and its grid.
|
|
143
|
+
*
|
|
144
|
+
* Those carry state from one frame to the next, so the window's clock alone
|
|
145
|
+
* cannot place them. A burst scheduled at bar 33 has to start there from an
|
|
146
|
+
* empty pool, and play the same way every time the scene reaches bar 33 —
|
|
147
|
+
* in the editor, on a loop, and in an export.
|
|
148
|
+
*
|
|
149
|
+
* - Entering a window RESTARTS it, from an empty pool and an unseeded grid.
|
|
150
|
+
* - Time going back inside a window restarts it too: a scrub back or a loop
|
|
151
|
+
* wrapping, where what the pool holds is from later.
|
|
152
|
+
* - Forward, it steps by exactly how far the transport moved, at most
|
|
153
|
+
* SIM_MAX_STEP. A paused transport steps it by zero, and an export steps it
|
|
154
|
+
* the same way on every render.
|
|
155
|
+
* - Outside every window it does not step.
|
|
156
|
+
*/
|
|
157
|
+
export function advanceSim(
|
|
158
|
+
prev: SimClock,
|
|
159
|
+
windows: readonly EffectWindow[] | null,
|
|
160
|
+
sceneTime: number,
|
|
161
|
+
): { reset: boolean; step: number; clock: SimClock } {
|
|
162
|
+
const window = activeWindow(windows, sceneTime)
|
|
163
|
+
if (!window) return { reset: false, step: 0, clock: { start: null, time: null } }
|
|
164
|
+
const time = sceneTime - window.start
|
|
165
|
+
const clock = { start: window.start, time }
|
|
166
|
+
if (prev.start !== window.start || prev.time === null || time < prev.time) return { reset: true, step: 0, clock }
|
|
167
|
+
return { reset: false, step: Math.min(SIM_MAX_STEP, time - prev.time), clock }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A repeating dissolve, in seconds within one cycle.
|
|
172
|
+
*
|
|
173
|
+
* Four moments rather than a duration and a delay: every one of them is a thing
|
|
174
|
+
* you can see happen, and an author tuning this is watching for exactly those
|
|
175
|
+
* four frames.
|
|
176
|
+
*/
|
|
177
|
+
export interface DissolveCycle {
|
|
178
|
+
period: number
|
|
179
|
+
/** She starts to come apart. */
|
|
180
|
+
breakAt: number
|
|
181
|
+
/** Fully gone. */
|
|
182
|
+
hiddenAt: number
|
|
183
|
+
/** She starts to come back. */
|
|
184
|
+
backAt: number
|
|
185
|
+
/** Whole again. */
|
|
186
|
+
doneAt: number
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The four durations an effect declares, in seconds. */
|
|
190
|
+
export type DissolveTimings = { apart: number; gone: number; back: number; whole: number }
|
|
191
|
+
|
|
192
|
+
/** Each duration and the name an effect gives it, as a dial or as a constant. */
|
|
193
|
+
export const DISSOLVE_PARAMS = [
|
|
194
|
+
["apart", "DISSOLVE_APART"],
|
|
195
|
+
["gone", "DISSOLVE_GONE"],
|
|
196
|
+
["back", "DISSOLVE_BACK"],
|
|
197
|
+
["whole", "DISSOLVE_WHOLE"],
|
|
198
|
+
] as const satisfies readonly (readonly [keyof DissolveTimings, string])[]
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The four durations an effect wrote as constants.
|
|
202
|
+
*
|
|
203
|
+
* Where an effect declares them as `#param` dials instead, these are absent and
|
|
204
|
+
* the live dial stands in — which is what lets an author retime a teleport while
|
|
205
|
+
* watching it.
|
|
206
|
+
*/
|
|
207
|
+
export function dissolveConstants(wgsl: string): DissolveTimings {
|
|
208
|
+
const one = (name: string): number => {
|
|
209
|
+
const m = new RegExp(`^\\s*const\\s+${name}\\s*(?::\\s*f32\\s*)?=\\s*(-?[\\d.]+)`, "m").exec(wgsl)
|
|
210
|
+
const v = m ? Number(m[1]) : 0
|
|
211
|
+
return Number.isFinite(v) && v > 0 ? v : 0
|
|
212
|
+
}
|
|
213
|
+
return { apart: one("DISSOLVE_APART"), gone: one("DISSOLVE_GONE"), back: one("DISSOLVE_BACK"), whole: one("DISSOLVE_WHOLE") }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Four durations as the four moments the engine performs, or null when they add
|
|
218
|
+
* up to nothing — a period of zero is not a dissolve that never fires, it is a
|
|
219
|
+
* number everything downstream divides by.
|
|
220
|
+
*
|
|
221
|
+
* The cycle STARTS WHOLE: the wait comes first, so it is the gap you see between
|
|
222
|
+
* teleports rather than a tail nobody can find the start of.
|
|
223
|
+
*/
|
|
224
|
+
export function dissolveCycleOf(t: DissolveTimings): DissolveCycle | null {
|
|
225
|
+
const apart = Math.max(0, t.apart)
|
|
226
|
+
const gone = Math.max(0, t.gone)
|
|
227
|
+
const back = Math.max(0, t.back)
|
|
228
|
+
const whole = Math.max(0, t.whole)
|
|
229
|
+
if (apart + gone + back + whole <= 0.01) return null
|
|
230
|
+
const breakAt = whole
|
|
231
|
+
const hiddenAt = breakAt + apart
|
|
232
|
+
const backAt = hiddenAt + gone
|
|
233
|
+
const doneAt = backAt + back
|
|
234
|
+
return { period: doneAt, breakAt, hiddenAt, backAt, doneAt }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** How much of her is there at one moment of a cycle, 1 whole and 0 gone. */
|
|
238
|
+
export function sampleDissolveCycle(c: DissolveCycle, time: number): number {
|
|
239
|
+
const period = Math.max(c.period, 1e-3)
|
|
240
|
+
const t = time - Math.floor(time / period) * period
|
|
241
|
+
if (t >= c.breakAt && t < c.hiddenAt) return 1 - (t - c.breakAt) / Math.max(c.hiddenAt - c.breakAt, 1e-4)
|
|
242
|
+
if (t >= c.hiddenAt && t < c.backAt) return 0
|
|
243
|
+
if (t >= c.backAt && t < c.doneAt) return (t - c.backAt) / Math.max(c.doneAt - c.backAt, 1e-4)
|
|
244
|
+
return 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A scheduled effect's dissolve: whole outside every window, and inside one the
|
|
249
|
+
* cycle on that window's own clock.
|
|
250
|
+
*
|
|
251
|
+
* THE WINDOW OPENS ON THE DEPARTURE. The cycle itself starts whole, which is
|
|
252
|
+
* right for an effect that simply repeats, and wrong for a clip someone placed
|
|
253
|
+
* on a beat: they put it where the teleport happens, not three seconds before
|
|
254
|
+
* it. So the clip enters at `breakAt`, and the wait becomes the tail the motes
|
|
255
|
+
* finish falling in.
|
|
256
|
+
*/
|
|
257
|
+
export function scheduledDissolve(
|
|
258
|
+
windows: readonly EffectWindow[],
|
|
259
|
+
c: DissolveCycle,
|
|
260
|
+
transport: number,
|
|
261
|
+
): number {
|
|
262
|
+
const w = activeWindow(windows, transport)
|
|
263
|
+
return w ? sampleDissolveCycle(c, transport - w.start + c.breakAt) : 1
|
|
264
|
+
}
|