reze-engine 0.41.4 → 0.42.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.
@@ -14,7 +14,15 @@
14
14
  * Compare a particle's own distance against it and the model
15
15
  * occludes it; fog needs no comparison at all, its alpha simply IS
16
16
  * a function of distance.
17
- * - `bgResolution()` — canvas size in pixels, for aspect correction.
17
+ * - `rzResolution()` — canvas size in pixels, for aspect correction.
18
+ * - `rzCameraPos()`, `rzWorldPos(ray, depth)` — the lens, and the place a pixel
19
+ * was drawn.
20
+ * - `rzSubjectCount()`, `rzSubjectHip(i)` — the cast, at HIP height (see the
21
+ * function; it is not the floor, and reading it as the floor is a
22
+ * mistake this API's own comment used to invite).
23
+ * - `rzProject(p)` — a world point as uv + view-axis distance; the cheap way to
24
+ * anchor anything, and `z` compares directly against `depth`.
25
+ * - the bg* spellings of all of the above still resolve, permanently.
18
26
  * - declared params arrive as `params.<name>` (f32 or vec3f), shared by both.
19
27
  *
20
28
  * Return display-space sRGB + alpha, 0..1. Both mounts are alpha-composited
@@ -23,6 +31,43 @@
23
31
  * 0 lets it through, which is how a starfield is stars over the user's color;
24
32
  * a foreground at alpha 1 covers the frame. No mode flag anywhere — the alpha
25
33
  * channel already says it. */
34
+ /**
35
+ * The bones an effect asked for, in declaration order — the slots rzAnchor reads.
36
+ *
37
+ * // @anchor 左手首 trail
38
+ * // @anchor 頭
39
+ *
40
+ * A declaration in the source, like the mounts: what a file names is what gets
41
+ * resolved and uploaded, so naming none costs nothing and nobody pays for a
42
+ * rig's other five hundred bones. Anchored to the start of a line so that
43
+ * writing the word @anchor in ordinary prose does not silently add a slot —
44
+ * which would shift every slot after it.
45
+ *
46
+ * `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
47
+ * because a path is two orders of magnitude more data than a point, and most
48
+ * anchors want a point.
49
+ *
50
+ * Names are passed through verbatim: any bone the rig has works, and one it does
51
+ * not have simply reports invalid.
52
+ */
53
+ export declare function parseEffectAnchors(wgsl: string, max: number): {
54
+ bone: string;
55
+ trail: boolean;
56
+ }[];
57
+ /**
58
+ * The caps the cast buffer is built to, shared by the shader below and by the
59
+ * engine that fills it. Interpolated into the WGSL rather than written twice:
60
+ * the layout arithmetic on both sides has to agree exactly, and two literals
61
+ * that must match are two literals that eventually will not.
62
+ *
63
+ * All three are MINIMUMS. Raising one breaks nothing, because effects read
64
+ * through accessors and loop to the count functions; lowering one does.
65
+ */
66
+ export declare const EFFECT_SUBJECTS = 4;
67
+ export declare const EFFECT_ANCHORS = 8;
68
+ export declare const EFFECT_TRAIL_SAMPLES = 128;
69
+ /** vec4 slot where the trails begin — after the subjects and the anchors. */
70
+ export declare const EFFECT_TRAIL_BASE: number;
26
71
  export type CompositeEffectSource = {
27
72
  /** The user's WGSL verbatim: helpers plus whichever entry points it defines. */
28
73
  wgsl: string;
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;+BAwB+B;AAC/B,MAAM,MAAM,qBAAqB,GAAG;IAClC,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;CACvB,CAAA;AA4XD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+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;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAChC,eAAO,MAAM,cAAc,IAAI,CAAA;AAC/B,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,QAA6D,CAAA;AAE3F,MAAM,MAAM,qBAAqB,GAAG;IAClC,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;CACvB,CAAA;AA2hBD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -12,6 +12,77 @@
12
12
  //
13
13
  // Both composite in display space, so neither affects lighting, bloom, or
14
14
  // tonemapping, and both are captured by offline export like any background.
15
+ /** What user effect WGSL may define, documented once. A file declares its own
16
+ * mounts by which of these it defines — defining both is how one file is one
17
+ * weather system (dark sky behind, rain in front):
18
+ *
19
+ * fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
20
+ * fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f
21
+ *
22
+ * - `ray` — normalized world-space view direction of this pixel (left-handed,
23
+ * +Z forward; identical to what the 360 skybox samples by).
24
+ * - `uv` — 0..1 across the canvas, origin bottom-left (shadertoy-style).
25
+ * - `time` — seconds since the effect was applied.
26
+ * - `depth` — FOREGROUND ONLY. Camera-space distance in metres of whatever the
27
+ * scene drew at this pixel, the far plane where it drew nothing.
28
+ * Compare a particle's own distance against it and the model
29
+ * occludes it; fog needs no comparison at all, its alpha simply IS
30
+ * a function of distance.
31
+ * - `rzResolution()` — canvas size in pixels, for aspect correction.
32
+ * - `rzCameraPos()`, `rzWorldPos(ray, depth)` — the lens, and the place a pixel
33
+ * was drawn.
34
+ * - `rzSubjectCount()`, `rzSubjectHip(i)` — the cast, at HIP height (see the
35
+ * function; it is not the floor, and reading it as the floor is a
36
+ * mistake this API's own comment used to invite).
37
+ * - `rzProject(p)` — a world point as uv + view-axis distance; the cheap way to
38
+ * anchor anything, and `z` compares directly against `depth`.
39
+ * - the bg* spellings of all of the above still resolve, permanently.
40
+ * - declared params arrive as `params.<name>` (f32 or vec3f), shared by both.
41
+ *
42
+ * Return display-space sRGB + alpha, 0..1. Both mounts are alpha-composited
43
+ * LAYERS, so alpha is what decides how much they replace: a background effect
44
+ * at alpha 1 covers the base (solid color / 360 equirect / transparent) and at
45
+ * 0 lets it through, which is how a starfield is stars over the user's color;
46
+ * a foreground at alpha 1 covers the frame. No mode flag anywhere — the alpha
47
+ * channel already says it. */
48
+ /**
49
+ * The bones an effect asked for, in declaration order — the slots rzAnchor reads.
50
+ *
51
+ * // @anchor 左手首 trail
52
+ * // @anchor 頭
53
+ *
54
+ * A declaration in the source, like the mounts: what a file names is what gets
55
+ * resolved and uploaded, so naming none costs nothing and nobody pays for a
56
+ * rig's other five hundred bones. Anchored to the start of a line so that
57
+ * writing the word @anchor in ordinary prose does not silently add a slot —
58
+ * which would shift every slot after it.
59
+ *
60
+ * `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
61
+ * because a path is two orders of magnitude more data than a point, and most
62
+ * anchors want a point.
63
+ *
64
+ * Names are passed through verbatim: any bone the rig has works, and one it does
65
+ * not have simply reports invalid.
66
+ */
67
+ export function parseEffectAnchors(wgsl, max) {
68
+ return [...wgsl.matchAll(/^[ \t]*\/\/[ \t]*@anchor[ \t]+(\S+)([ \t]+trail)?[ \t]*$/gm)]
69
+ .map((m) => ({ bone: m[1], trail: m[2] !== undefined }))
70
+ .slice(0, max);
71
+ }
72
+ /**
73
+ * The caps the cast buffer is built to, shared by the shader below and by the
74
+ * engine that fills it. Interpolated into the WGSL rather than written twice:
75
+ * the layout arithmetic on both sides has to agree exactly, and two literals
76
+ * that must match are two literals that eventually will not.
77
+ *
78
+ * All three are MINIMUMS. Raising one breaks nothing, because effects read
79
+ * through accessors and loop to the count functions; lowering one does.
80
+ */
81
+ export const EFFECT_SUBJECTS = 4;
82
+ export const EFFECT_ANCHORS = 8;
83
+ export const EFFECT_TRAIL_SAMPLES = 128;
84
+ /** vec4 slot where the trails begin — after the subjects and the anchors. */
85
+ export const EFFECT_TRAIL_BASE = EFFECT_SUBJECTS * 3 + EFFECT_ANCHORS * EFFECT_SUBJECTS * 3;
15
86
  const COMPOSITE_HEAD = /* wgsl */ `
16
87
  // Pipeline-override constant: the engine creates two composite pipelines, one
17
88
  // with APPLY_GAMMA=false (gamma=1 fast path) and one with APPLY_GAMMA=true.
@@ -70,6 +141,13 @@ override APPLY_GAMMA: bool = true;
70
141
  // Blender's AgX, as the 57³ lookup it ships as rather than a reconstruction of
71
142
  // it. Sampled in the log-encoded E-Gamut space the cube expects — see agxTransform.
72
143
  @group(0) @binding(10) var agxLut: texture_3d<f32>;
144
+ // The cast, as data. Read through rzSubject/rzAnchor below — the LAYOUT IS NOT
145
+ // STABLE and never will be, because it depends on what each effect declared.
146
+ // Reading it directly is the one thing that would freeze it forever.
147
+ //
148
+ // vec4 slots: [0 .. 11] four subjects, three each (root+valid, hip, bounds);
149
+ // then MAX_ANCHORS × four subjects, three each (pos+valid, vel, fwd).
150
+ @group(0) @binding(11) var<storage, read> _rzCast: array<vec4f>;
73
151
 
74
152
  // Must match FILMIC_LUT_WIDTH in engine.ts (bakeFilmicLut).
75
153
  const FILMIC_LUT_W: f32 = 256.0;
@@ -165,29 +243,179 @@ fn viewTransform(c: vec3f) -> vec3f {
165
243
  return vec3f(filmic(c.r), filmic(c.g), filmic(c.b));
166
244
  }
167
245
 
168
- /** Canvas size in pixels — for user effects (aspect correction). */
169
- fn bgResolution() -> vec2f { return viewU[6].zw; }
246
+ // ── The effect API ────────────────────────────────────────────────────────────
247
+ //
248
+ // Named rz*, for the engine. The prefix earns its place twice: user code is
249
+ // concatenated into THIS module, so an unprefixed rzAnchor() would collide with
250
+ // exactly the helper an author would write, and the old bg* prefix stopped being
251
+ // true in 0.41.0 when effects gained a mount over the finished frame.
252
+ //
253
+ // The bg* names below are permanent aliases, not a deprecation with an end date.
254
+ // A published link is immutable, so a scene pinned to a bg* effect has to keep
255
+ // compiling forever. They are one-line and inlined; no new function gets one.
256
+
257
+ /** Canvas size in pixels — for aspect correction. */
258
+ fn rzResolution() -> vec2f { return viewU[6].zw; }
170
259
 
171
260
  /** The camera's world position. */
172
- fn bgCameraPos() -> vec3f { return viewU[10].xyz; }
261
+ fn rzCameraPos() -> vec3f { return viewU[10].xyz; }
173
262
 
174
263
  /** How many characters are in the scene, up to four. */
175
- fn bgSubjectCount() -> i32 { return i32(viewU[10].w); }
264
+ fn rzSubjectCount() -> i32 { return i32(viewU[10].w); }
265
+
266
+ /**
267
+ * A world point as the camera sees it: xy the uv it lands on, z its distance
268
+ * along the VIEW AXIS in metres.
269
+ *
270
+ * The exact inverse of the ray this pass builds per pixel, so it is the cheap way
271
+ * to work with anything anchored in the world. Marching a curve or a trail in 3D
272
+ * costs a distance evaluation per sample per pixel; projecting its points once
273
+ * and measuring in 2D costs a subtraction, which is the difference between a
274
+ * ribbon that runs at 4K and one that does not.
275
+ *
276
+ * z is directly comparable to the depth handed to foreground(), so occlusion is
277
+ * a single test: draw where your z is nearer than the scene's. It is returned
278
+ * SIGNED and unclamped — behind the camera is negative, and worth rejecting
279
+ * before you use the uv, which is meaningless there.
280
+ */
281
+ fn rzProject(p: vec3f) -> vec3f {
282
+ let d = p - viewU[10].xyz;
283
+ let z = dot(d, viewU[5].xyz);
284
+ // Guard only the divide. z itself is returned as it is, so the caller can see
285
+ // the sign; clamping it here would put points behind the lens on the horizon.
286
+ let inv = 1.0 / select(z, 1e-4, z < 1e-4);
287
+ let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);
288
+ return vec3f(ndc * 0.5 + 0.5, z);
289
+ }
290
+
291
+ /** A character, as much of one as a shader needs. */
292
+ struct RzSubject {
293
+ /** On the FLOOR, under the body — where a ring or a magic circle belongs. */
294
+ root: vec3f,
295
+ /** At the hips, the middle of the body — where an aura belongs. */
296
+ center: vec3f,
297
+ /** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
298
+ bounds: vec4f,
299
+ /** False past the end of the cast, and every field is then zero. */
300
+ valid: bool,
301
+ }
302
+
303
+ /** One bone an effect asked for, by name, at the top of its own source. */
304
+ struct RzAnchor {
305
+ pos: vec3f,
306
+ /** World units per second, from the previous frame. Direction for a trail,
307
+ * magnitude for anything that should react to how hard someone is moving. */
308
+ vel: vec3f,
309
+ /** The bone's forward axis — which way a foot points, where a head looks. */
310
+ fwd: vec3f,
311
+ /** False when this rig has no such bone. Check it: the alternative is drawing
312
+ * a hand effect at the world origin on every model that spells it differently. */
313
+ valid: bool,
314
+ }
315
+
316
+ const RZ_MAX_ANCHORS: i32 = ${EFFECT_ANCHORS};
176
317
 
177
318
  /**
178
- * Where a character is standing, in world space.
319
+ * Character i. Loop to rzSubjectCount(), never to a constant — the caps here are
320
+ * MINIMUMS and are free to grow, which is only true while nobody hardcodes them.
321
+ */
322
+ fn rzSubject(i: i32) -> RzSubject {
323
+ var s: RzSubject;
324
+ s.valid = i >= 0 && i < rzSubjectCount();
325
+ if (!s.valid) { return s; }
326
+ let b = i * 3;
327
+ s.root = _rzCast[b].xyz;
328
+ s.center = _rzCast[b + 1].xyz;
329
+ s.bounds = _rzCast[b + 2];
330
+ return s;
331
+ }
332
+
333
+ /**
334
+ * The slot-th bone this effect declared, on character subject.
335
+ *
336
+ * Slots are the order of the declarations at the top of your source:
337
+ *
338
+ * // @anchor 左手首
339
+ * // @anchor 頭
340
+ *
341
+ * gives you slot 0 and slot 1. Any bone name the model has works; valid is
342
+ * false when it does not have it, which is the normal case across rigs that
343
+ * spell things differently.
344
+ */
345
+ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
346
+ var a: RzAnchor;
347
+ a.valid = false;
348
+ if (subject < 0 || subject >= rzSubjectCount() || slot < 0 || slot >= RZ_MAX_ANCHORS) { return a; }
349
+ let b = ${EFFECT_SUBJECTS * 3} + (slot * ${EFFECT_SUBJECTS} + subject) * 3;
350
+ a.valid = _rzCast[b].w > 0.5;
351
+ a.pos = _rzCast[b].xyz;
352
+ a.vel = _rzCast[b + 1].xyz;
353
+ a.fwd = _rzCast[b + 2].xyz;
354
+ return a;
355
+ }
356
+
357
+ const RZ_TRAIL_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
358
+
359
+ /**
360
+ * How many path samples this anchor has. Zero unless it was declared with
361
+ * trail, and it climbs from zero as the trail fills after the effect loads.
362
+ *
363
+ * Loop to THIS, never to RZ_TRAIL_SAMPLES: the cap is a minimum and is free to
364
+ * grow, which stays true only while nobody hardcodes it.
365
+ */
366
+ fn rzTrailCount(subject: i32, slot: i32) -> i32 {
367
+ if (subject < 0 || subject >= rzSubjectCount() || slot < 0 || slot >= RZ_MAX_ANCHORS) { return 0; }
368
+ return i32(_rzCast[${EFFECT_SUBJECTS * 3} + (slot * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
369
+ }
370
+
371
+ /**
372
+ * Sample i of an anchor's path: xyz where it was, w how many seconds ago.
373
+ *
374
+ * i = 0 is NOW and they run backwards in time, so a ribbon is drawn by walking i
375
+ * upward and fading on .w. Sampled at a fixed rate on the SCENE clock, not the
376
+ * display's — so the path is identical in the editor, in an export, and in a
377
+ * re-export, and its spacing does not change with framerate.
378
+ *
379
+ * This is what a hand trail wants instead of position and velocity. One position
380
+ * and one velocity is a straight segment that jitters, because a velocity is a
381
+ * difference between two frames; a path is what actually happened.
382
+ */
383
+ fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
384
+ let n = rzTrailCount(subject, slot);
385
+ if (i < 0 || i >= n) { return vec4f(0.0); }
386
+ let base = ${EFFECT_TRAIL_BASE} + (slot * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
387
+ return _rzCast[base + i];
388
+ }
389
+
390
+ fn bgResolution() -> vec2f { return rzResolution(); }
391
+ fn bgCameraPos() -> vec3f { return rzCameraPos(); }
392
+ fn bgSubjectCount() -> i32 { return rzSubjectCount(); }
393
+
394
+ /**
395
+ * Where a character IS, in world space — at the hips, not on the floor.
396
+ *
397
+ * An effect that wants to RESPOND to the cast — a glow that follows someone,
398
+ * dust kicked up where they are — needs to know where they are, and the ray and
399
+ * the depth cannot tell it: they describe the pixel, not the scene.
179
400
  *
180
- * An effect that wants to RESPOND to the cast ripples under the feet, a glow
181
- * that follows someone, dust kicked up where they are needs to know where
182
- * they are, and the ray and the depth cannot tell it: they describe the pixel,
183
- * not the scene. This is the model's root, which for a PMX is between the feet
184
- * on the floor, so it is already the contact point a ripple wants.
401
+ * The value is model.position + センター + 全ての親. センター sits at hip
402
+ * height on every standard MMD rig, so this is a point in the middle of the
403
+ * body. It is NOT the contact point: a ripple drawn here appears at the waist.
404
+ * Ground effects want the .xz of this and their own floor height, which is what
405
+ * the effects that shipped against it already do.
406
+ *
407
+ * The comment here used to claim it was "between the feet on the floor", which
408
+ * is where that habit came from. Left as it is regardless of the name: a
409
+ * published link is immutable, so every shared scene pinning an effect that
410
+ * reads this depends on it meaning exactly what it has always meant.
185
411
  *
186
412
  * Clamped rather than bounds-checked: an effect looping past the count reads the
187
413
  * last subject instead of sampling whatever follows the array, which is a wrong
188
414
  * ripple rather than an undefined one.
189
415
  */
190
- fn bgSubjectPos(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
416
+ fn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
417
+
418
+ fn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }
191
419
 
192
420
  /** Where in the WORLD the scene drew this pixel — the depth handed to
193
421
  * foreground() turned into a place. Without it an effect can only think in
@@ -198,11 +426,13 @@ fn bgSubjectPos(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
198
426
  * the ray's projection onto camera-forward before being walked out. At the far
199
427
  * plane (nothing drawn) this lands a very long way off, which is what a sky
200
428
  * should do to anything reading it. */
201
- fn bgWorldPos(ray: vec3f, depth: f32) -> vec3f {
429
+ fn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {
202
430
  let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);
203
- return bgCameraPos() + normalize(ray) * (depth / axis);
431
+ return rzCameraPos() + normalize(ray) * (depth / axis);
204
432
  }
205
433
 
434
+ fn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }
435
+
206
436
  /** Color grading, applied to the tonemapped SCENE (not the background — see the
207
437
  * call site). The core is ASC CDL, the film-industry interchange standard:
208
438
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.41.4",
3
+ "version": "0.42.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",