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.
- package/dist/effect-schedule.d.ts +67 -0
- package/dist/effect-schedule.d.ts.map +1 -0
- package/dist/effect-schedule.js +96 -0
- package/dist/engine.d.ts +152 -21
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +399 -204
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/shaders/anchor-table.d.ts +1 -1
- package/dist/shaders/anchor-table.js +3 -3
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-api.js +2 -2
- package/dist/shaders/directives.d.ts +73 -0
- package/dist/shaders/directives.d.ts.map +1 -0
- package/dist/shaders/directives.js +238 -0
- package/dist/shaders/lights.d.ts +0 -10
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +11 -19
- package/dist/shaders/passes/composite.d.ts +5 -5
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +12 -5
- package/dist/shaders/passes/grid.d.ts +0 -2
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +0 -9
- package/dist/shaders/passes/particles.d.ts +0 -6
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +15 -19
- package/dist/shaders/passes/scene-contract.d.ts +1 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +9 -4
- package/package.json +2 -2
- package/src/effect-schedule.ts +120 -0
- package/src/engine.ts +398 -173
- package/src/index.ts +17 -1
- package/src/shaders/anchor-table.ts +3 -3
- package/src/shaders/cast-api.ts +2 -2
- package/src/shaders/directives.ts +289 -0
- package/src/shaders/lights.ts +11 -19
- package/src/shaders/passes/composite.ts +13 -6
- package/src/shaders/passes/grid.ts +0 -9
- package/src/shaders/passes/particles.ts +15 -21
- package/src/shaders/passes/scene-contract.ts +1 -1
- 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,
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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]
|
|
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,
|
|
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;
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
364
|
-
o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0,
|
|
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.
|
|
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
|
+
}
|