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/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
- p.data[1] = Math.min(0.1, Math.max(0, deltaTime));
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
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
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
- /** Every dissolve cycle, at the current scene clock. Once per frame, before
12907
- * the cast is written and long before any effect reads it. */
12908
- evaluateDissolveCycles() {
12909
- if (this.dissolveCycles.size === 0)
12910
- return;
12911
- for (const [name, c] of this.dissolveCycles) {
12912
- const period = Math.max(c.period, 1e-3);
12913
- const t = this.sceneClock - Math.floor(this.sceneClock / period) * period;
12914
- let v = 1;
12915
- if (t >= c.breakAt && t < c.hiddenAt) {
12916
- v = 1 - (t - c.breakAt) / Math.max(c.hiddenAt - c.breakAt, 1e-4);
12917
- }
12918
- else if (t >= c.hiddenAt && t < c.backAt) {
12919
- v = 0;
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 if (t >= c.backAt && t < c.doneAt) {
12922
- v = (t - c.backAt) / Math.max(c.doneAt - c.backAt, 1e-4);
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";
@@ -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,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpF,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAInD,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,EACL,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,aAAa,GACnB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAA;AACrE,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC5H,OAAO,EACL,GAAG,EACH,iBAAiB,EACjB,wBAAwB,EACxB,mCAAmC,GACpC,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,SAAS,EAAE,4BAA4B,EAAE,KAAK,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AACzG,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,QAAQ,GACd,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAI3F,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,yBAAyB,EACzB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,IAAI,EACT,KAAK,SAAS,GACf,MAAM,WAAW,CAAA;AAElB;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AACnF,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.56.5",
3
+ "version": "0.56.7",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
- let window: EffectWindow | null = null
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
+ }