reze-engine 0.51.0 → 0.53.0

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.
Files changed (44) hide show
  1. package/dist/effect-schedule.d.ts +67 -0
  2. package/dist/effect-schedule.d.ts.map +1 -0
  3. package/dist/effect-schedule.js +96 -0
  4. package/dist/engine.d.ts +152 -21
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +399 -204
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +10 -1
  10. package/dist/shaders/anchor-table.d.ts +1 -1
  11. package/dist/shaders/anchor-table.js +3 -3
  12. package/dist/shaders/cast-api.d.ts +1 -1
  13. package/dist/shaders/cast-api.js +2 -2
  14. package/dist/shaders/directives.d.ts +73 -0
  15. package/dist/shaders/directives.d.ts.map +1 -0
  16. package/dist/shaders/directives.js +238 -0
  17. package/dist/shaders/lights.d.ts +0 -10
  18. package/dist/shaders/lights.d.ts.map +1 -1
  19. package/dist/shaders/lights.js +11 -19
  20. package/dist/shaders/passes/composite.d.ts +5 -5
  21. package/dist/shaders/passes/composite.d.ts.map +1 -1
  22. package/dist/shaders/passes/composite.js +12 -5
  23. package/dist/shaders/passes/grid.d.ts +0 -2
  24. package/dist/shaders/passes/grid.d.ts.map +1 -1
  25. package/dist/shaders/passes/grid.js +0 -9
  26. package/dist/shaders/passes/particles.d.ts +0 -6
  27. package/dist/shaders/passes/particles.d.ts.map +1 -1
  28. package/dist/shaders/passes/particles.js +15 -19
  29. package/dist/shaders/passes/scene-contract.d.ts +1 -1
  30. package/dist/shaders/passes/trails.d.ts.map +1 -1
  31. package/dist/shaders/passes/trails.js +9 -4
  32. package/package.json +2 -2
  33. package/src/effect-schedule.ts +120 -0
  34. package/src/engine.ts +398 -173
  35. package/src/index.ts +17 -1
  36. package/src/shaders/anchor-table.ts +3 -3
  37. package/src/shaders/cast-api.ts +2 -2
  38. package/src/shaders/directives.ts +289 -0
  39. package/src/shaders/lights.ts +11 -19
  40. package/src/shaders/passes/composite.ts +13 -6
  41. package/src/shaders/passes/grid.ts +0 -9
  42. package/src/shaders/passes/particles.ts +15 -21
  43. package/src/shaders/passes/scene-contract.ts +1 -1
  44. package/src/shaders/passes/trails.ts +9 -4
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA6LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,yqaA+H5B,CAAA;AA4LD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAuDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAqEtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA6LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,yqaA+H5B,CAAA;AA4LD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAuDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA4EtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -56,13 +56,13 @@ import { gridReadApi } from "./grid";
56
56
  /**
57
57
  * The bones an effect asked for, in declaration order — the slots rzAnchor reads.
58
58
  *
59
- * // @anchor 左手首 trail
60
- * // @anchor 頭
59
+ * #anchor 左手首 trail
60
+ * #anchor 頭
61
61
  *
62
62
  * A declaration in the source, like the mounts: what a file names is what gets
63
63
  * resolved and uploaded, so naming none costs nothing and nobody pays for a
64
64
  * rig's other five hundred bones. Anchored to the start of a line so that
65
- * writing the word @anchor in ordinary prose does not silently add a slot —
65
+ * writing the word #anchor in ordinary prose does not silently add a slot —
66
66
  * which would shift every slot after it.
67
67
  *
68
68
  * `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
@@ -73,7 +73,7 @@ import { gridReadApi } from "./grid";
73
73
  * not have simply reports invalid.
74
74
  */
75
75
  export function parseEffectAnchors(wgsl, max) {
76
- return [...wgsl.matchAll(/^[ \t]*\/\/[ \t]*@anchor[ \t]+(\S+)([ \t]+trail)?[ \t]*$/gm)]
76
+ return [...wgsl.matchAll(/^[ \t]*\/\/[ \t]*#anchor[ \t]+(\S+)([ \t]+trail)?[ \t]*$/gm)]
77
77
  .map((m) => ({ bone: m[1], trail: m[2] !== undefined }))
78
78
  .slice(0, max);
79
79
  }
@@ -691,7 +691,7 @@ export function buildFieldShader(effect) {
691
691
  * whose lightEmit read its own epoch disagreed with its own background()
692
692
  * about what time it was. One buffer per effect, one answer.
693
693
  */
694
- @group(0) @binding(22) var<uniform> _rzFieldClock: vec4f;
694
+ @group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight, _, _)
695
695
 
696
696
  @vertex fn fieldVs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
697
697
  let x = f32((vi & 1u) << 2u) - 1.0;
@@ -715,6 +715,13 @@ struct FieldOut {
715
715
  out.fg = vec4f(0.0);
716
716
  ${bgLine}
717
717
  ${fgLine}
718
+ // WEIGHT, applied where the author cannot decline it.
719
+ //
720
+ // Alpha only: both field blends multiply the fragment's colour by src-alpha,
721
+ // so this is the fade for the alpha-over layer and the additive one alike.
722
+ // Scaling colour as well would fade as the square.
723
+ out.bg.a *= _rzFieldClock.y;
724
+ out.fg.a *= _rzFieldClock.y;
718
725
  return out;
719
726
  }
720
727
  `);
@@ -2,8 +2,6 @@ import { type CastLayout } from "./particles";
2
2
  export declare const GRID_MAX = 1024;
3
3
  /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
4
4
  export declare const SIM_FORMAT: GPUTextureFormat;
5
- /** `// @grid 256` — the grid's resolution, in texels per side. */
6
- export declare function parseGridSize(wgsl: string, max: number): number;
7
5
  /** Whether this effect drives a grid at all. */
8
6
  export declare function gridEntryPoint(wgsl: string): boolean;
9
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6DnF"}
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6DnF"}
@@ -36,15 +36,6 @@ import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
36
36
  export const GRID_MAX = 1024;
37
37
  /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
38
38
  export const SIM_FORMAT = "rgba16float";
39
- /** `// @grid 256` — the grid's resolution, in texels per side. */
40
- export function parseGridSize(wgsl, max) {
41
- const m = /^\s*\/\/\s*@grid\s+(\d+)\s*$/m.exec(wgsl);
42
- if (!m)
43
- return 0;
44
- // Clamped rather than rejected, as the particle count is: an author asking for
45
- // 4096 gets the most the engine will give and a scene that still runs.
46
- return Math.max(8, Math.min(max, parseInt(m[1], 10)));
47
- }
48
39
  /** Whether this effect drives a grid at all. */
49
40
  export function gridEntryPoint(wgsl) {
50
41
  return /\bfn\s+gridStep\s*\(/.test(wgsl);
@@ -26,12 +26,6 @@ type ParticleSource = {
26
26
  };
27
27
  /** Bytes per particle. Explicitly padded — see the struct below. */
28
28
  export declare const PARTICLE_STRIDE = 48;
29
- /** `// @particles 4096` — how many live at once. */
30
- export declare function parseParticleCount(wgsl: string, max: number): number;
31
- /** `// @bloom` — opt in to the bloom pyramid. Sparks want it; rain does not. */
32
- export declare function parseParticleBloom(wgsl: string): boolean;
33
- /** `// @blend additive` — default is straight alpha. */
34
- export declare function parseParticleBlend(wgsl: string): ParticleBlend;
35
29
  /** Does the source define the particle contract? All three are required together. */
36
30
  export declare function particleEntryPoints(wgsl: string): {
37
31
  init: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAyBA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAkDjC,oDAAoD;AACpD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAE9D;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAgHvF"}
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAyBA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAuDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAyHvF"}
@@ -51,6 +51,11 @@ struct ParticleU {
51
51
  dt: f32,
52
52
  count: u32,
53
53
  frame: u32,
54
+ /** The effect's evaluated influence, applied at the one output site below. */
55
+ weight: f32,
56
+ _pad0: f32,
57
+ _pad1: f32,
58
+ _pad2: f32,
54
59
  }
55
60
  `;
56
61
  /**
@@ -81,24 +86,6 @@ fn rzProject(p: vec3f) -> vec3f {
81
86
  }
82
87
  fn rzCamPos() -> vec3f { return cam.camPos; }
83
88
  `;
84
- /** `// @particles 4096` — how many live at once. */
85
- export function parseParticleCount(wgsl, max) {
86
- const m = /^\s*\/\/\s*@particles\s+(\d+)\s*$/m.exec(wgsl);
87
- if (!m)
88
- return 0;
89
- // Clamped rather than rejected: an author asking for a million gets the most
90
- // the engine will give and a scene that still runs, which is a better failure
91
- // than a compile error naming a number they had no way to know.
92
- return Math.max(1, Math.min(max, parseInt(m[1], 10)));
93
- }
94
- /** `// @bloom` — opt in to the bloom pyramid. Sparks want it; rain does not. */
95
- export function parseParticleBloom(wgsl) {
96
- return /^\s*\/\/\s*@bloom\s*$/m.test(wgsl);
97
- }
98
- /** `// @blend additive` — default is straight alpha. */
99
- export function parseParticleBlend(wgsl) {
100
- return /^\s*\/\/\s*@blend\s+additive\s*$/m.test(wgsl) ? "additive" : "alpha";
101
- }
102
89
  /** Does the source define the particle contract? All three are required together. */
103
90
  export function particleEntryPoints(wgsl) {
104
91
  return {
@@ -256,7 +243,16 @@ ${sceneIdFieldWgsl()}}
256
243
  @fragment
257
244
  fn fs(in: VSOut) -> FSOut {
258
245
  let p = particles[in.id];
259
- let c = particleShade(p, in.uv);
246
+ // WEIGHT IS APPLIED HERE, on the author's result, before anything reads it.
247
+ //
248
+ // Alpha alone, and that is not a shortcut: this fragment is premultiplied
249
+ // two lines down, so scaling alpha scales the colour with it — and where the
250
+ // target blends additively the premultiply is what carries the fade. One
251
+ // multiply is the correct fade for both particle targets.
252
+ var c = particleShade(p, in.uv);
253
+ c.a *= pu.weight;
254
+ // Weight 0 therefore discards every fragment, so an effect faded out costs
255
+ // nothing past the vertex stage even on the frame the draw is still issued.
260
256
  if (c.a <= 0.0) { discard; }
261
257
  var out: FSOut;
262
258
  // PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
@@ -61,7 +61,7 @@ type SceneRenderClass =
61
61
  | "outline"
62
62
  /** Particles and ribbons in their default, non-additive mode. */
63
63
  | "particle"
64
- /** Particles declaring `// @blend additive` — LIGHT rather than matter, so
64
+ /** Particles declaring `#blend additive` — LIGHT rather than matter, so
65
65
  * colour sums and alpha is left alone: a glow must not claim coverage it
66
66
  * never occluded. The aux target sums with it, which is what lets an
67
67
  * additive effect reach the bloom gate at all. */
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAwUR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CA6UR"}
@@ -83,7 +83,8 @@ struct TrailU {
83
83
  // model is added or removed, and recompiling every trail shader for that
84
84
  // would be absurd.
85
85
  subjects: f32,
86
- _pad1: f32,
86
+ /** The effect's evaluated influence, applied at the one output site below. */
87
+ weight: f32,
87
88
  _pad2: f32,
88
89
  }
89
90
  @group(0) @binding(0) var<storage, read> _rzCast: array<vec4f>;
@@ -353,15 +354,19 @@ fn fs(in: VSOut) -> TrailFSOut {
353
354
  // hardware does it, correctly and for free, and the reversed-Z trap that
354
355
  // needed its own regression test goes with it.
355
356
  let c = trailShade(in.uv.x, in.uv.y, in.age, in.weight, i32(in.slot));
356
- if (c.a <= 0.0) { discard; }
357
+ // THE EFFECT'S weight, not the ribbon's — in.weight above is the strand's
358
+ // own taper and belongs to the author. This one is the scheduler's, and it
359
+ // scales alpha because the target below multiplies colour by src-alpha.
360
+ let a = c.a * tu.weight;
361
+ if (a <= 0.0) { discard; }
357
362
  var o: TrailFSOut;
358
363
  // STRAIGHT colour into an ADDITIVE target, which reverses the old MAX rule
359
364
  // deliberately: max existed so parallel strands could not double into bright
360
365
  // dashes on a layer composited after tone mapping. In HDR before bloom,
361
366
  // overlapping light SHOULD sum — that is what neon does — and the tone
362
367
  // mapper is what keeps the sum from clipping.
363
- o.color = vec4f(c.rgb, c.a);
364
- o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, c.a);
368
+ o.color = vec4f(c.rgb, a);
369
+ o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, a);
365
370
  ${sceneIdPadWgsl("o")} return o;
366
371
  }
367
372
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
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",
@@ -41,4 +41,4 @@
41
41
  "@types/node": "^20",
42
42
  "typescript": "^5"
43
43
  }
44
- }
44
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * WHEN an effect is on, and how much of it.
3
+ *
4
+ * The engine evaluates this, not the app, and that is not an arbitrary split.
5
+ * Everything time-driven that lives above the engine has to be ticked by every
6
+ * loop that renders — playback, offline export, a warm-up pass — and the export
7
+ * loop already carries a scar about it: a score-driven effect once exported a
8
+ * still keyboard while the rest of the scene animated, because that loop
9
+ * advanced the audio clock and forgot the score's. A schedule evaluated where
10
+ * the scene clock advances cannot be forgotten by anyone.
11
+ *
12
+ * It is also where the three references put it. Unity's Timeline evaluates clip
13
+ * ease in its playable graph, Unreal's Sequencer evaluates section easing, and
14
+ * Blender's NLA evaluates strip influence — in all three the editor only edits
15
+ * the data. This is that.
16
+ *
17
+ * SECONDS, because that is the engine's clock. The document above works in
18
+ * MMD's 30fps frames and converts on the way in; nothing here knows the render
19
+ * rate, which is what lets a 60Hz preview and a 30 or 60fps export agree about
20
+ * which beat an effect lands on.
21
+ *
22
+ * The model is Blender's NLA strip, minus what needs a strip STACK: no
23
+ * extrapolation modes (outside a window is off, always), no auto-blend (it
24
+ * derives from neighbouring strips, and effects have a lane each), no repeat or
25
+ * scale (an author who wants a cycle can wrap their own clock).
26
+ *
27
+ * PURE, in its own file, for the reason param-track.ts is: everything else in
28
+ * this feature needs a GPU and this needs one number, so the edge cases can be
29
+ * tested exhaustively. They are where it goes wrong, and a wrong one is a beat
30
+ * missed in a video someone posts.
31
+ */
32
+
33
+ /** One strip on an effect's LANE — and a lane holds as many as you place.
34
+ * Seconds throughout. */
35
+ export type EffectWindow = {
36
+ /** When it comes alive, and where its own clock reads zero. */
37
+ start: number
38
+ /** When it stops. Omitted = it runs to the end of the scene. */
39
+ end?: number
40
+ /** Ramp up over this long from `start`. Omitted or 0 is a HARD CUT, which is
41
+ * the right answer for a flash and the wrong one for a glow. */
42
+ blendIn?: number
43
+ /** Ramp down over this long back from `end`. Needs an `end` to measure from:
44
+ * an effect with no end has nothing to ramp toward. */
45
+ blendOut?: number
46
+ }
47
+
48
+ /** What an effect is doing at one instant: a weight for its mounts, and where
49
+ * its own clock has reached. */
50
+ export type EffectState = { weight: number; time: number }
51
+
52
+ /** Off, and stopped at its own beginning. */
53
+ const SILENT: EffectState = { weight: 0, time: 0 }
54
+
55
+ const clamp01 = (n: number) => (n < 0 ? 0 : n > 1 ? 1 : Number.isFinite(n) ? n : 0)
56
+
57
+ /**
58
+ * One effect's state at one moment of the scene.
59
+ *
60
+ * `influence` is the level it reaches INSIDE the window — Blender's word and
61
+ * Blender's meaning. The blends ramp toward it rather than toward 1, so a
62
+ * permanently half-strength effect and a scheduled one are the same dial
63
+ * instead of two that can disagree.
64
+ *
65
+ * LINEAR ramps, matching the NLA's. A curve is a better default for light and a
66
+ * worse one for everything else, and there is no control to shape it with — a
67
+ * curve nobody asked for is worse than a plain one. If eased blends are ever
68
+ * wanted they belong as a property ON the window, which is the same call
69
+ * param-track.ts made about bezier keys.
70
+ */
71
+ export function effectState(
72
+ windows: readonly EffectWindow[] | null,
73
+ influence: number,
74
+ sceneTime: number,
75
+ ): EffectState {
76
+ const level = clamp01(influence)
77
+ // UNSCHEDULED RUNS WITH THE SCENE, on the scene's own clock — so an effect
78
+ // nobody has scheduled behaves exactly as it did before windows existed.
79
+ if (!windows || windows.length === 0) return { weight: level, time: sceneTime }
80
+
81
+ // A LANE HOLDS MANY STRIPS, which is what lets one effect fire more than
82
+ // once. Each entry restarts the effect's own clock at its own start, so a hit
83
+ // placed at bar 8 and again at bar 24 plays its opening both times rather
84
+ // than resuming halfway through itself.
85
+ //
86
+ // The LATEST start that contains the time wins. Strips on one lane are not
87
+ // meant to overlap — that is the rule every NLE enforces within a track, and
88
+ // it keeps "which strip am I in" a question with one answer — but a document
89
+ // can be hand-edited, and "the one most recently entered" is the reading that
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
+ }
97
+ if (!window) return SILENT
98
+
99
+ const { start, end } = window
100
+ // The picker above already rejected an empty strip (an end at or before the
101
+ // start, reachable by dragging one edge past the other) and anything the
102
+ // time is outside of, so from here the strip is live and has a real length.
103
+
104
+ // ITS OWN CLOCK, from its entry. This is the reason the window owns the clock
105
+ // rather than being a visibility flag beside it: an effect entering at bar 33
106
+ // should play its own opening, not join the scene four minutes in.
107
+ const time = sceneTime - start
108
+
109
+ let ramp = 1
110
+ const fadeIn = window.blendIn ?? 0
111
+ if (fadeIn > 0) ramp = Math.min(ramp, time / fadeIn)
112
+ const fadeOut = end !== undefined ? (window.blendOut ?? 0) : 0
113
+ if (fadeOut > 0) ramp = Math.min(ramp, (end! - sceneTime) / fadeOut)
114
+
115
+ // The MINIMUM of the two ramps, so blends that overlap degrade instead of
116
+ // break: set both longer than the strip and it becomes a triangle — dimmer
117
+ // than asked for, still smooth, never out of range. Multiplying them would
118
+ // dip toward zero in the middle of a short strip, which is not a fade.
119
+ return { weight: level * clamp01(ramp), time }
120
+ }