reze-engine 0.56.6 → 0.56.8
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 +57 -0
- package/dist/effect-schedule.d.ts.map +1 -1
- package/dist/effect-schedule.js +69 -0
- package/dist/engine.d.ts +45 -34
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +107 -43
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +7 -6
- package/package.json +1 -1
- package/src/effect-schedule.ts +96 -0
- package/src/engine.ts +131 -58
- package/src/shaders/passes/ground.ts +7 -6
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,
|
|
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";
|
|
@@ -896,6 +896,9 @@ export class Engine {
|
|
|
896
896
|
this.cullCameraArgs = null;
|
|
897
897
|
this.cullShadowArgs = null;
|
|
898
898
|
this.cullMirrorArgs = null;
|
|
899
|
+
/** Where addGround put the floor. The mirror reflects across it, so a raised
|
|
900
|
+
* floor that kept y = 0 would reflect the scene into the wrong plane. */
|
|
901
|
+
this.groundY = 0;
|
|
899
902
|
this.mirrorPlane = new Float32Array([0, 1, 0, 0]);
|
|
900
903
|
this.mirrorCameraData = new Float32Array(40);
|
|
901
904
|
// proj x mirrorView for the ground's projective sample and the cull planes,
|
|
@@ -1428,6 +1431,9 @@ export class Engine {
|
|
|
1428
1431
|
this.gizmoDrag = null;
|
|
1429
1432
|
this.camera?.setInputLocked(false);
|
|
1430
1433
|
};
|
|
1434
|
+
/** Models this wrote a dissolve onto last frame, so one nothing asks for any
|
|
1435
|
+
* more comes back whole instead of holding wherever it was left. */
|
|
1436
|
+
this.dissolveTouched = new Set();
|
|
1431
1437
|
this.canvas = canvas;
|
|
1432
1438
|
const d = DEFAULT_ENGINE_OPTIONS;
|
|
1433
1439
|
this.world = {
|
|
@@ -2147,7 +2153,9 @@ export class Engine {
|
|
|
2147
2153
|
planeFromPointNormal(m[12], m[13], m[14], m[8], m[9], m[10], this.mirrorPlane);
|
|
2148
2154
|
}
|
|
2149
2155
|
else {
|
|
2156
|
+
// The floor's own plane: up, offset to wherever addGround put it.
|
|
2150
2157
|
this.mirrorPlane.set(Engine.GROUND_PLANE);
|
|
2158
|
+
this.mirrorPlane[3] = -this.groundY;
|
|
2151
2159
|
}
|
|
2152
2160
|
// The ground inside the mirror pass clips against this same plane — a
|
|
2153
2161
|
// mirror shows nothing behind itself, and the floor is what is behind one.
|
|
@@ -3365,6 +3373,7 @@ export class Engine {
|
|
|
3365
3373
|
sim: { start: null, time: null },
|
|
3366
3374
|
simStep: 0,
|
|
3367
3375
|
simReset: false,
|
|
3376
|
+
dissolve: d.dissolve ? dissolveConstants(authored) : null,
|
|
3368
3377
|
// Its OWN resolution, no longer the scene's: an effect that never asked
|
|
3369
3378
|
// for full res is not promoted because a neighbour did.
|
|
3370
3379
|
// FULL RESOLUTION UNLESS TOLD OTHERWISE.
|
|
@@ -7782,6 +7791,7 @@ export class Engine {
|
|
|
7782
7791
|
const opts = {
|
|
7783
7792
|
width: 160,
|
|
7784
7793
|
height: 160,
|
|
7794
|
+
y: 0,
|
|
7785
7795
|
diffuseColor: new Vec3(0.9, 0.1, 1.0),
|
|
7786
7796
|
fadeStart: 10.0,
|
|
7787
7797
|
fadeEnd: 80.0,
|
|
@@ -7797,7 +7807,8 @@ export class Engine {
|
|
|
7797
7807
|
shadowSoftness: 0,
|
|
7798
7808
|
...options,
|
|
7799
7809
|
};
|
|
7800
|
-
this.
|
|
7810
|
+
this.groundY = opts.y;
|
|
7811
|
+
this.createGroundGeometry(opts.width, opts.height, opts.y);
|
|
7801
7812
|
this.createShadowGroundResources(opts);
|
|
7802
7813
|
this.hasGround = true;
|
|
7803
7814
|
this.groundDrawCall = {
|
|
@@ -7810,7 +7821,14 @@ export class Engine {
|
|
|
7810
7821
|
// The ground belongs to no model instance, so it is not in the cull list —
|
|
7811
7822
|
// cullIndex -1 leaves renderGround unconditional. Its box is filled in
|
|
7812
7823
|
// anyway rather than left a lie for whoever reads this next.
|
|
7813
|
-
bounds: new Float32Array([
|
|
7824
|
+
bounds: new Float32Array([
|
|
7825
|
+
-opts.width / 2,
|
|
7826
|
+
opts.y,
|
|
7827
|
+
-opts.height / 2,
|
|
7828
|
+
opts.width / 2,
|
|
7829
|
+
opts.y,
|
|
7830
|
+
opts.height / 2,
|
|
7831
|
+
]),
|
|
7814
7832
|
cullIndex: -1,
|
|
7815
7833
|
};
|
|
7816
7834
|
}
|
|
@@ -10592,13 +10610,13 @@ export class Engine {
|
|
|
10592
10610
|
baseBuf,
|
|
10593
10611
|
};
|
|
10594
10612
|
}
|
|
10595
|
-
createGroundGeometry(width = 100, height = 100) {
|
|
10613
|
+
createGroundGeometry(width = 100, height = 100, y = 0) {
|
|
10596
10614
|
const halfWidth = width / 2;
|
|
10597
10615
|
const halfHeight = height / 2;
|
|
10598
10616
|
const vertices = new Float32Array([
|
|
10599
10617
|
// Bottom-left
|
|
10600
10618
|
-halfWidth,
|
|
10601
|
-
|
|
10619
|
+
y,
|
|
10602
10620
|
-halfHeight, // position
|
|
10603
10621
|
0,
|
|
10604
10622
|
1,
|
|
@@ -10607,7 +10625,7 @@ export class Engine {
|
|
|
10607
10625
|
0, // uv
|
|
10608
10626
|
// Bottom-right
|
|
10609
10627
|
halfWidth,
|
|
10610
|
-
|
|
10628
|
+
y,
|
|
10611
10629
|
-halfHeight, // position
|
|
10612
10630
|
0,
|
|
10613
10631
|
1,
|
|
@@ -10616,7 +10634,7 @@ export class Engine {
|
|
|
10616
10634
|
0, // uv
|
|
10617
10635
|
// Top-right
|
|
10618
10636
|
halfWidth,
|
|
10619
|
-
|
|
10637
|
+
y,
|
|
10620
10638
|
halfHeight, // position
|
|
10621
10639
|
0,
|
|
10622
10640
|
1,
|
|
@@ -10625,7 +10643,7 @@ export class Engine {
|
|
|
10625
10643
|
1, // uv
|
|
10626
10644
|
// Top-left
|
|
10627
10645
|
-halfWidth,
|
|
10628
|
-
|
|
10646
|
+
y,
|
|
10629
10647
|
halfHeight, // position
|
|
10630
10648
|
0,
|
|
10631
10649
|
1,
|
|
@@ -10693,7 +10711,7 @@ export class Engine {
|
|
|
10693
10711
|
return this.groundShadowSoftPipeline;
|
|
10694
10712
|
}
|
|
10695
10713
|
createShadowGroundResources(opts) {
|
|
10696
|
-
const { diffuseColor, fadeStart, fadeEnd, shadowStrength, gridSpacing, gridLineWidth, gridLineOpacity, gridLineColor, noiseStrength, opacity, mirror, mirrorBlur, shadowSoftness, } = opts;
|
|
10714
|
+
const { diffuseColor, y, fadeStart, fadeEnd, shadowStrength, gridSpacing, gridLineWidth, gridLineOpacity, gridLineColor, noiseStrength, opacity, mirror, mirrorBlur, shadowSoftness, } = opts;
|
|
10697
10715
|
// Shadow map is already created in setupPipelines()
|
|
10698
10716
|
// 20 floats: 16 for the original block, then (mirrorBlur, pad, pad, pad)
|
|
10699
10717
|
// keeping the uniform vec4-aligned.
|
|
@@ -10721,6 +10739,9 @@ export class Engine {
|
|
|
10721
10739
|
// gb[18] — shadow edge softness. Was padding; the shader reads it as the
|
|
10722
10740
|
// Vogel disk's radius, and 0 takes the sharp nine-tap path unchanged.
|
|
10723
10741
|
gb[18] = Math.min(Math.max(shadowSoftness, 0), 1);
|
|
10742
|
+
// gb[19] — the floor's height. Was padding; the mirror branch reflects the
|
|
10743
|
+
// eye across it, and a hardcoded 0 slid the reflection off a raised floor.
|
|
10744
|
+
gb[19] = y;
|
|
10724
10745
|
// Which variant the draw picks. Zero is the sharp shader, which is the one
|
|
10725
10746
|
// that existed before softness did.
|
|
10726
10747
|
this.groundSoft = gb[18] > 0;
|
|
@@ -12304,6 +12325,9 @@ export class Engine {
|
|
|
12304
12325
|
if (pose)
|
|
12305
12326
|
this.camera.setVmdPose(pose);
|
|
12306
12327
|
}
|
|
12328
|
+
// Before the cast is written, which carries every subject's dissolve to the
|
|
12329
|
+
// effects reading it.
|
|
12330
|
+
this.evaluateDissolves();
|
|
12307
12331
|
this.updateCameraUniforms();
|
|
12308
12332
|
this.updateShadowLightVP();
|
|
12309
12333
|
// Depth of field's entire disabled cost is this branch: depth stays in
|
|
@@ -12444,7 +12468,6 @@ export class Engine {
|
|
|
12444
12468
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
12445
12469
|
// Material parameters on the scene clock, before anything reads their
|
|
12446
12470
|
// uniforms this frame.
|
|
12447
|
-
this.evaluateDissolveCycles();
|
|
12448
12471
|
this.evaluateParamTracks();
|
|
12449
12472
|
// FIRST among the things that read an effect, because every one of them
|
|
12450
12473
|
// reads what this writes: the sim's clock, the particle uniform's weight,
|
|
@@ -12913,22 +12936,6 @@ export class Engine {
|
|
|
12913
12936
|
}
|
|
12914
12937
|
return true;
|
|
12915
12938
|
}
|
|
12916
|
-
/**
|
|
12917
|
-
* A repeating dissolve, on the scene clock.
|
|
12918
|
-
*
|
|
12919
|
-
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
12920
|
-
* the wrong shape twice: an exported take stepped at another rate would land
|
|
12921
|
-
* on different values than the preview did, and the effect drawing the sparks
|
|
12922
|
-
* would be reading a number some other clock wrote. Here the engine samples it
|
|
12923
|
-
* where it samples everything else time-driven, so a take reproduces exactly
|
|
12924
|
-
* and rzSubject().dissolve is the same value the material shell used on that
|
|
12925
|
-
* very frame.
|
|
12926
|
-
*
|
|
12927
|
-
* The five numbers are seconds within one cycle: when she starts to go, when
|
|
12928
|
-
* she is fully gone, when she starts to come back, and when she is whole. The
|
|
12929
|
-
* gaps between them are the timing, and the hold between the middle two is how
|
|
12930
|
-
* long she is away.
|
|
12931
|
-
*/
|
|
12932
12939
|
/**
|
|
12933
12940
|
* Eyes on the camera, per model — see Model.setEyeTracking. Live: solved
|
|
12934
12941
|
* every frame against wherever the camera is, orbit or motion alike. Null
|
|
@@ -12941,6 +12948,20 @@ export class Engine {
|
|
|
12941
12948
|
inst.model.setEyeTracking(options);
|
|
12942
12949
|
return true;
|
|
12943
12950
|
}
|
|
12951
|
+
/**
|
|
12952
|
+
* A repeating dissolve on one model, on the scene clock.
|
|
12953
|
+
*
|
|
12954
|
+
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
12955
|
+
* the wrong shape twice: an exported take stepped at another rate would land
|
|
12956
|
+
* on different values than the preview did, and the effect drawing the sparks
|
|
12957
|
+
* would be reading a number some other clock wrote. Here the engine samples it
|
|
12958
|
+
* where it samples everything else time-driven, so a take reproduces exactly
|
|
12959
|
+
* and rzSubject().dissolve is the same value the material shell used on that
|
|
12960
|
+
* very frame.
|
|
12961
|
+
*
|
|
12962
|
+
* An effect that declares `#dissolve` needs none of this: it carries its own
|
|
12963
|
+
* cycle and follows its own clips — see evaluateDissolves.
|
|
12964
|
+
*/
|
|
12944
12965
|
setModelDissolveCycle(modelName, cycle) {
|
|
12945
12966
|
if (!this.modelInstances.has(modelName))
|
|
12946
12967
|
return false;
|
|
@@ -12952,26 +12973,69 @@ export class Engine {
|
|
|
12952
12973
|
this.dissolveCycles.set(modelName, cycle);
|
|
12953
12974
|
return true;
|
|
12954
12975
|
}
|
|
12955
|
-
/**
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12976
|
+
/** The model at one cast slot — the subject rzSubject(index) reads. */
|
|
12977
|
+
castSubjectName(index) {
|
|
12978
|
+
let n = 0;
|
|
12979
|
+
let found = null;
|
|
12980
|
+
this.forEachInstance((inst) => {
|
|
12981
|
+
if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp)
|
|
12982
|
+
return;
|
|
12983
|
+
if (n === index)
|
|
12984
|
+
found = inst.name;
|
|
12985
|
+
n++;
|
|
12986
|
+
});
|
|
12987
|
+
return found;
|
|
12988
|
+
}
|
|
12989
|
+
/** One effect's four durations: its dials where it declares them, the
|
|
12990
|
+
* constants it wrote where it does not. */
|
|
12991
|
+
dissolveTimingsOf(fx, wrote) {
|
|
12992
|
+
const t = { ...wrote };
|
|
12993
|
+
for (const [key, name] of DISSOLVE_PARAMS) {
|
|
12994
|
+
const slot = fx.paramLayout.get(name);
|
|
12995
|
+
if (slot)
|
|
12996
|
+
t[key] = Math.max(0, fx.paramsData[slot.offset]);
|
|
12997
|
+
}
|
|
12998
|
+
return t;
|
|
12999
|
+
}
|
|
13000
|
+
/**
|
|
13001
|
+
* Every dissolve, once a frame, before the cast is written — so the value a
|
|
13002
|
+
* mote reads as rzSubject(i).dissolve is the one her materials wore on that
|
|
13003
|
+
* very frame, rather than the one they wore on the last.
|
|
13004
|
+
*
|
|
13005
|
+
* Two things ask for one. A cycle set on a model (setModelDissolveCycle) runs
|
|
13006
|
+
* on the scene clock and repeats, as it always has. An effect declaring
|
|
13007
|
+
* `#dissolve` takes subject 0 apart on ITS OWN terms: scheduled, the cycle on
|
|
13008
|
+
* each clip's clock with her whole between clips; unscheduled, the same
|
|
13009
|
+
* repeating cycle as before. Where both ask, the more dissolved wins — two
|
|
13010
|
+
* answers about one body, and half a body is not one of them.
|
|
13011
|
+
*/
|
|
13012
|
+
evaluateDissolves() {
|
|
13013
|
+
const want = new Map();
|
|
13014
|
+
const put = (name, v) => want.set(name, Math.min(want.get(name) ?? 1, v));
|
|
13015
|
+
for (const [name, c] of this.dissolveCycles)
|
|
13016
|
+
put(name, sampleDissolveCycle(c, this.sceneClock));
|
|
13017
|
+
let transport = null;
|
|
13018
|
+
for (const fx of this.effects) {
|
|
13019
|
+
if (!fx.dissolve)
|
|
13020
|
+
continue;
|
|
13021
|
+
const cycle = dissolveCycleOf(this.dissolveTimingsOf(fx, fx.dissolve));
|
|
13022
|
+
const subject = this.castSubjectName(0);
|
|
13023
|
+
if (!cycle || !subject)
|
|
13024
|
+
continue;
|
|
13025
|
+
if (fx.window && fx.window.length > 0) {
|
|
13026
|
+
transport ?? (transport = this.transportTime());
|
|
13027
|
+
put(subject, scheduledDissolve(fx.window, cycle, transport));
|
|
12969
13028
|
}
|
|
12970
|
-
else
|
|
12971
|
-
|
|
13029
|
+
else {
|
|
13030
|
+
put(subject, sampleDissolveCycle(cycle, this.sceneClock));
|
|
12972
13031
|
}
|
|
12973
|
-
this.setModelDissolve(name, v);
|
|
12974
13032
|
}
|
|
13033
|
+
for (const name of this.dissolveTouched)
|
|
13034
|
+
if (!want.has(name))
|
|
13035
|
+
this.setModelDissolve(name, 1);
|
|
13036
|
+
this.dissolveTouched = new Set(want.keys());
|
|
13037
|
+
for (const [name, v] of want)
|
|
13038
|
+
this.setModelDissolve(name, v);
|
|
12975
13039
|
}
|
|
12976
13040
|
/** What setModelDissolve last set, or 1 for a model that has never dissolved. */
|
|
12977
13041
|
getModelDissolve(modelName) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAgFA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,UAAQ,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAgFA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,UAAQ,GAAG,MAAM,CAoWrD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC;;;oEAGoE;AACpE,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,+oCAiClC,CAAA"}
|
|
@@ -90,7 +90,7 @@ struct GroundShadowMat {
|
|
|
90
90
|
gridLineColor: vec3f, mirror: f32,
|
|
91
91
|
// farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
|
|
92
92
|
// with no stage the far map is never drawn into, so its taps are known.
|
|
93
|
-
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32,
|
|
93
|
+
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
|
|
94
94
|
// Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
|
|
95
95
|
// nothing casts, negative means "do not use this" (a rigid caster has no
|
|
96
96
|
// sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
|
|
@@ -356,20 +356,21 @@ ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ",
|
|
|
356
356
|
// shared projection's pair (viewZ = projB / (z - projA); the formula
|
|
357
357
|
// inverts both depth conventions, see the composite's linearDepth), and
|
|
358
358
|
// reconstruct the reflected image point along the ray from the MIRRORED
|
|
359
|
-
// eye through this fragment.
|
|
360
|
-
//
|
|
361
|
-
//
|
|
359
|
+
// eye through this fragment. Mirroring across the floor's own plane is a
|
|
360
|
+
// sign flip on y about that height (the forward vector is a direction, so
|
|
361
|
+
// it flips alone), and the image point's depth below the plane IS the
|
|
362
|
+
// reflected object's height above it.
|
|
362
363
|
let dims = vec2f(textureDimensions(mirrorDepth));
|
|
363
364
|
let texel = clamp(vec2i(muv * dims), vec2i(0), vec2i(dims) - vec2i(1));
|
|
364
365
|
let z = textureLoad(mirrorDepth, texel, 0);
|
|
365
366
|
let viewZ = clamp(mirrorVP.params.y / (z - mirrorVP.params.x), 0.05, 100000.0);
|
|
366
|
-
let eyeM = vec3f(camera.viewPos.x, -camera.viewPos.y, camera.viewPos.z);
|
|
367
|
+
let eyeM = vec3f(camera.viewPos.x, 2.0 * material.groundY - camera.viewPos.y, camera.viewPos.z);
|
|
367
368
|
let fwd = vec3f(camera.view[0][2], camera.view[1][2], camera.view[2][2]);
|
|
368
369
|
let fwdM = vec3f(fwd.x, -fwd.y, fwd.z);
|
|
369
370
|
let toFrag = i.worldPos - eyeM;
|
|
370
371
|
let dir = toFrag * inverseSqrt(max(dot(toFrag, toFrag), 1e-8));
|
|
371
372
|
let t = viewZ / max(dot(dir, fwdM), 1e-4);
|
|
372
|
-
let height = max(-(eyeM.y + dir.y * t), 0.0);
|
|
373
|
+
let height = max(material.groundY - (eyeM.y + dir.y * t), 0.0);
|
|
373
374
|
// Full softness by BLUR_SPAN units above the floor — about a character's
|
|
374
375
|
// height. The hardware clamps the level, so the nominal 5.0 needs no
|
|
375
376
|
// knowledge of the real chain length, and blur 0 reads exactly level 0.
|
package/package.json
CHANGED
package/src/effect-schedule.ts
CHANGED
|
@@ -166,3 +166,99 @@ export function advanceSim(
|
|
|
166
166
|
if (prev.start !== window.start || prev.time === null || time < prev.time) return { reset: true, step: 0, clock }
|
|
167
167
|
return { reset: false, step: Math.min(SIM_MAX_STEP, time - prev.time), clock }
|
|
168
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
|
+
}
|