reze-engine 0.41.3 → 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.
package/src/model.ts CHANGED
@@ -92,6 +92,12 @@ export interface Bone {
92
92
  appendRatio?: number // 0..1
93
93
  appendRotate?: boolean
94
94
  appendMove?: boolean
95
+ /** 軸制限: this bone rotates ONLY about this axis (the twist bones use it). */
96
+ fixedAxis?: [number, number, number]
97
+ /** 変形階層: MMD poses whole layers in order, not bones in array order.
98
+ * Parsed but not yet honoured — distinct from Model's `deformOrder`, which is
99
+ * this engine's parent-before-child traversal. */
100
+ deformLayer?: number
95
101
  ikTargetIndex?: number // IK target bone index (if this bone is an IK effector)
96
102
  ikIteration?: number // IK iteration count
97
103
  ikLimitAngle?: number // IK rotation constraint (radians)
@@ -477,6 +483,9 @@ export class Model {
477
483
  this.applyMorphs()
478
484
  }
479
485
 
486
+ /** 軸制限 bones and their normalised axes — see applyFixedAxes. */
487
+ private fixedAxisBones: { index: number; x: number; y: number; z: number }[] = []
488
+
480
489
  private initializeRuntimeSkeleton(): void {
481
490
  const boneCount = this.skeleton.bones.length
482
491
 
@@ -490,6 +499,18 @@ export class Model {
490
499
  worldMatrices[i] = Mat4.identity()
491
500
  }
492
501
 
502
+ // 軸制限 bones, gathered once and normalised. A standard rig has four —
503
+ // the two arm twists and the two wrist twists — so this is a four-element
504
+ // walk per pose rather than a branch on every bone.
505
+ this.fixedAxisBones = []
506
+ for (let i = 0; i < boneCount; i++) {
507
+ const a = this.skeleton.bones[i].fixedAxis
508
+ if (!a) continue
509
+ const len = Math.hypot(a[0], a[1], a[2])
510
+ if (len < 1e-8) continue
511
+ this.fixedAxisBones.push({ index: i, x: a[0] / len, y: a[1] / len, z: a[2] / len })
512
+ }
513
+
493
514
  this.runtimeSkeleton = {
494
515
  localRotations,
495
516
  localTranslations,
@@ -673,19 +694,42 @@ export class Model {
673
694
  * Stages are the reason this exists: a door or a lift is rigged as a bone
674
695
  * morph and there is no VMD anywhere that drives it.
675
696
  */
697
+ /**
698
+ * Take the last frame's bone-morph offsets back off, before any pose source
699
+ * runs.
700
+ *
701
+ * This cannot live at the top of applyBoneMorphs, which is where it used to
702
+ * be. A pose source writes only the bones its clip keys, and it writes them
703
+ * from the clip — so restoring afterwards overwrote the freshly animated pose
704
+ * with a snapshot taken a frame earlier, and the re-snapshot immediately
705
+ * below then saved that same stale value again. The result was not a drift or
706
+ * a lag: every bone any bone morph touched stayed pinned to the first frame
707
+ * the model was posed at, permanently, and at ANY weight — the restore ran
708
+ * over the touched set before the weight test, so a morph sitting at 0 froze
709
+ * its bones just as hard as one at 1.
710
+ *
711
+ * It reads as "the arms don't animate" because arms are what these morphs
712
+ * touch: character models routinely ship T-Pose / A-Pose / ShouderBlend /
713
+ * ElbowBlend adjusters on 腕 and ひじ, and nothing else in the rig is a
714
+ * comparably popular bone-morph target.
715
+ */
716
+ private undoBoneMorphs(): void {
717
+ if (!this.boneMorphApplied) return
718
+ for (let i = 0; i < this.boneMorphBones.length; i++) {
719
+ const b = this.boneMorphBones[i]
720
+ this.runtimeSkeleton.localRotations[b].set(this.boneMorphRestoreR[i])
721
+ this.runtimeSkeleton.localTranslations[b].set(this.boneMorphRestoreT[i])
722
+ }
723
+ this.boneMorphApplied = false
724
+ }
725
+
676
726
  private applyBoneMorphs(): void {
677
727
  if (this.boneMorphPlan.length === 0) return
678
728
 
679
- // Undo the previous application first. With a clip running this is a no-op
680
- // in effect the pose source already overwrote these bones but a stage
681
- // has no clip, so without it the same offset would be re-added every frame.
682
- if (this.boneMorphApplied) {
683
- for (let i = 0; i < this.boneMorphBones.length; i++) {
684
- const b = this.boneMorphBones[i]
685
- this.runtimeSkeleton.localRotations[b].set(this.boneMorphRestoreR[i])
686
- this.runtimeSkeleton.localTranslations[b].set(this.boneMorphRestoreT[i])
687
- }
688
- }
729
+ // Snapshot the pose as the sources left it, so undoBoneMorphs can hand these
730
+ // bones back unmorphed at the top of the next frame. The two halves are a
731
+ // pair: without the undo a stage which has no clip to rewrite its bones —
732
+ // would compound the same offset every frame.
689
733
  for (let i = 0; i < this.boneMorphBones.length; i++) {
690
734
  const b = this.boneMorphBones[i]
691
735
  this.boneMorphRestoreR[i].set(this.runtimeSkeleton.localRotations[b])
@@ -924,6 +968,23 @@ export class Model {
924
968
  return this.runtimeSkeleton.worldMatrices[idx].getPosition()
925
969
  }
926
970
 
971
+ /**
972
+ * A bone's forward axis, normalised — which way a foot points, where a head
973
+ * looks. Model space, like getBoneWorldPosition: the caller composes the model
974
+ * transform on top if it wants world space.
975
+ *
976
+ * Column 2 of the world matrix. Null for a name this rig does not have, which
977
+ * is the ordinary case across rigs that spell bones differently.
978
+ */
979
+ getBoneWorldForward(boneName: string): Vec3 | null {
980
+ const idx = this.runtimeSkeleton.nameIndex[boneName]
981
+ if (idx === undefined || idx < 0) return null
982
+ const m = this.runtimeSkeleton.worldMatrices[idx].values
983
+ const len = Math.hypot(m[8], m[9], m[10])
984
+ if (len < 1e-8) return null
985
+ return new Vec3(m[8] / len, m[9] / len, m[10] / len)
986
+ }
987
+
927
988
  getSkinning(): Skinning {
928
989
  return this.skinning
929
990
  }
@@ -1922,6 +1983,39 @@ export class Model {
1922
1983
  : frameA.weight
1923
1984
  }
1924
1985
 
1986
+ /**
1987
+ * Hold 軸制限 bones to their own axis.
1988
+ *
1989
+ * A twist bone (腕捩 / 手捩) exists to rotate about the length of the limb and
1990
+ * nothing else, and PMX says so by giving it a fixed axis. A VMD still keys it
1991
+ * with a full quaternion, so without the constraint the bone bends as well as
1992
+ * twists — and because the elbow is parented to 腕捩 and 腕捩1..3 inherit a
1993
+ * fraction of it, that error is carried into the whole forearm and multiplied
1994
+ * three ways down the arm. It is the most visible thing on the model.
1995
+ *
1996
+ * Projecting the quaternion's vector part onto the axis and renormalising is
1997
+ * what keeps the twist and drops everything else; the same operation MMD and
1998
+ * every faithful runtime performs.
1999
+ *
2000
+ * Applied to the STORED local rotation rather than inside a matrix path,
2001
+ * because the append children read that array directly: constrain it once and
2002
+ * the bone, its inheritors and a hand-posed gizmo all agree.
2003
+ */
2004
+ private applyFixedAxes(): void {
2005
+ for (const a of this.fixedAxisBones) {
2006
+ const q = this.runtimeSkeleton.localRotations[a.index]
2007
+ const dot = q.x * a.x + q.y * a.y + q.z * a.z
2008
+ const x = a.x * dot
2009
+ const y = a.y * dot
2010
+ const z = a.z * dot
2011
+ const len = Math.sqrt(x * x + y * y + z * z + q.w * q.w)
2012
+ if (len > 1e-8) {
2013
+ const inv = 1 / len
2014
+ q.setXYZW(x * inv, y * inv, z * inv, q.w * inv)
2015
+ }
2016
+ }
2017
+ }
2018
+
1925
2019
  private applyPoseFromClip(clip: AnimationClip | null, frame: number): void {
1926
2020
  if (!clip) return
1927
2021
  this.applyIkFromClip(clip, frame)
@@ -1941,6 +2035,7 @@ export class Model {
1941
2035
  this.runtimeSkeleton.localRotations[boneIdx].set(_animSlerp)
1942
2036
  this.runtimeSkeleton.localTranslations[boneIdx].set(localTranslation)
1943
2037
  }
2038
+ this.applyFixedAxes()
1944
2039
 
1945
2040
  for (const [morphName, keyFrames] of clip.morphTracks.entries()) {
1946
2041
  const weight = this.sampleMorphTrack(morphName, keyFrames, frame, this.morphTrackIndices)
@@ -2084,6 +2179,7 @@ export class Model {
2084
2179
  this.runtimeSkeleton.localRotations[i].set(_blendQ)
2085
2180
  this.runtimeSkeleton.localTranslations[i].set(localTranslation)
2086
2181
  }
2182
+ this.applyFixedAxes()
2087
2183
 
2088
2184
  const morphCount = morphAcc.length
2089
2185
  for (let i = 0; i < morphCount; i++) {
@@ -2237,6 +2333,13 @@ export class Model {
2237
2333
  const evWatch = this.clipEvents.size > 0 ? this.animationState.getCurrentAnimation() : null
2238
2334
  const evPrevFrame = evWatch !== null ? this.animationState.getCurrentFrame() : 0
2239
2335
  this.animationState.update(deltaTime)
2336
+
2337
+ // Hand the pose sources their bones unmorphed. A bone morph is an offset on
2338
+ // top of the pose, so last frame's offset has to come off before this
2339
+ // frame's pose goes on — taking it off afterwards is what froze every
2340
+ // morph-touched bone at its first posed frame.
2341
+ this.undoBoneMorphs()
2342
+
2240
2343
  if (!this.clipApplySuspended) {
2241
2344
  if (this.oneShot !== null) {
2242
2345
  this.applyOneShot(deltaTime)
@@ -2378,7 +2481,9 @@ export class Model {
2378
2481
 
2379
2482
  // Handle append transformations (same logic as computeWorldMatrices)
2380
2483
  const appendParentIdx = b.appendParentIndex
2381
- const hasAppend = b.appendRotate &&
2484
+ // `|| appendMove`: a move-only append (no rotation) was skipped altogether,
2485
+ // because the guard demanded appendRotate before either branch could run.
2486
+ const hasAppend = (b.appendRotate || b.appendMove) &&
2382
2487
  appendParentIdx !== undefined &&
2383
2488
  appendParentIdx >= 0 &&
2384
2489
  appendParentIdx < bones.length
@@ -2405,12 +2510,15 @@ export class Model {
2405
2510
  scratchQuat[2].setIdentity()
2406
2511
  Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
2407
2512
 
2408
- // finalRot = slerpResult * finalRot (rotation composition as quat mul)
2513
+ // MMD composes the append on the RIGHT: own animated rotation first, then
2514
+ // the inherited one (saba's `r = r * appendRotate`). We had it on the
2515
+ // left, which is a different rotation whenever a bone carries BOTH its
2516
+ // own key and an append — the twist bones, if a motion keys them.
2409
2517
  const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
2410
- const nx = sw * fx + sx * fw + sy * fz - sz * fy
2411
- const ny = sw * fy - sx * fz + sy * fw + sz * fx
2412
- const nz = sw * fz + sx * fy - sy * fx + sz * fw
2413
- const nw = sw * fw - sx * fx - sy * fy - sz * fz
2518
+ const nx = fw * sx + fx * sw + fy * sz - fz * sy
2519
+ const ny = fw * sy - fx * sz + fy * sw + fz * sx
2520
+ const nz = fw * sz + fx * sy - fy * sx + fz * sw
2521
+ const nw = fw * sw - fx * sx - fy * sy - fz * sz
2414
2522
  fx = nx; fy = ny; fz = nz; fw = nw
2415
2523
  }
2416
2524
 
@@ -2469,7 +2577,7 @@ export class Model {
2469
2577
 
2470
2578
  const appendParentIdx = b.appendParentIndex
2471
2579
  const hasAppend =
2472
- b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
2580
+ (b.appendRotate || b.appendMove) && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
2473
2581
 
2474
2582
  if (hasAppend) {
2475
2583
  const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
@@ -2487,12 +2595,15 @@ export class Model {
2487
2595
  scratchQuat[2].setIdentity()
2488
2596
  Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
2489
2597
 
2490
- // finalRot = slerpResult * finalRot (quat mul)
2598
+ // MMD composes the append on the RIGHT: own animated rotation first, then
2599
+ // the inherited one (saba's `r = r * appendRotate`). We had it on the
2600
+ // left, which is a different rotation whenever a bone carries BOTH its
2601
+ // own key and an append — the twist bones, if a motion keys them.
2491
2602
  const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
2492
- const nx = sw * fx + sx * fw + sy * fz - sz * fy
2493
- const ny = sw * fy - sx * fz + sy * fw + sz * fx
2494
- const nz = sw * fz + sx * fy - sy * fx + sz * fw
2495
- const nw = sw * fw - sx * fx - sy * fy - sz * fz
2603
+ const nx = fw * sx + fx * sw + fy * sz - fz * sy
2604
+ const ny = fw * sy - fx * sz + fy * sw + fz * sx
2605
+ const nz = fw * sz + fx * sy - fy * sx + fz * sw
2606
+ const nw = fw * sw - fx * sx - fy * sy - fz * sz
2496
2607
  fx = nx; fy = ny; fz = nz; fw = nw
2497
2608
  }
2498
2609
 
package/src/pmx-loader.ts CHANGED
@@ -354,6 +354,8 @@ export class PmxLoader {
354
354
  ikIteration?: number
355
355
  ikLimitAngle?: number
356
356
  ikLinks?: IKLink[]
357
+ fixedAxis?: [number, number, number]
358
+ deformLayer?: number
357
359
  }
358
360
  const abs: AbsBone[] = new Array(count)
359
361
  // PMX 2.x bone flags (best-effort common masks)
@@ -372,7 +374,7 @@ export class PmxLoader {
372
374
  const y = this.getFloat32()
373
375
  const z = this.getFloat32()
374
376
  const parentIndex = this.getNonVertexIndex(this.boneIndexSize)
375
- this.getInt32() // transform order (skip)
377
+ const deformLayer = this.getInt32() // 変形階層
376
378
  const flags = this.getUint16()
377
379
 
378
380
  // Tail: bone index or offset vector3
@@ -397,11 +399,10 @@ export class PmxLoader {
397
399
  appendMove = (flags & FLAG_APPEND_MOVE) !== 0
398
400
  }
399
401
 
400
- // Axis limit
402
+ // Axis limit (軸制限)
403
+ let fixedAxis: [number, number, number] | undefined = undefined
401
404
  if ((flags & FLAG_AXIS_LIMIT) !== 0) {
402
- this.getFloat32()
403
- this.getFloat32()
404
- this.getFloat32()
405
+ fixedAxis = [this.getFloat32(), this.getFloat32(), this.getFloat32()]
405
406
  }
406
407
 
407
408
  // Local axis (two vectors x and z)
@@ -467,6 +468,8 @@ export class PmxLoader {
467
468
  appendRatio,
468
469
  appendRotate,
469
470
  appendMove,
471
+ fixedAxis,
472
+ deformLayer,
470
473
  ikTargetIndex,
471
474
  ikIteration,
472
475
  ikLimitAngle,
@@ -487,6 +490,8 @@ export class PmxLoader {
487
490
  appendRatio: a.appendRatio,
488
491
  appendRotate: a.appendRotate,
489
492
  appendMove: a.appendMove,
493
+ fixedAxis: a.fixedAxis,
494
+ deformLayer: a.deformLayer,
490
495
  ikTargetIndex: a.ikTargetIndex,
491
496
  ikIteration: a.ikIteration,
492
497
  ikLimitAngle: a.ikLimitAngle,
@@ -29,7 +29,15 @@
29
29
  * Compare a particle's own distance against it and the model
30
30
  * occludes it; fog needs no comparison at all, its alpha simply IS
31
31
  * a function of distance.
32
- * - `bgResolution()` — canvas size in pixels, for aspect correction.
32
+ * - `rzResolution()` — canvas size in pixels, for aspect correction.
33
+ * - `rzCameraPos()`, `rzWorldPos(ray, depth)` — the lens, and the place a pixel
34
+ * was drawn.
35
+ * - `rzSubjectCount()`, `rzSubjectHip(i)` — the cast, at HIP height (see the
36
+ * function; it is not the floor, and reading it as the floor is a
37
+ * mistake this API's own comment used to invite).
38
+ * - `rzProject(p)` — a world point as uv + view-axis distance; the cheap way to
39
+ * anchor anything, and `z` compares directly against `depth`.
40
+ * - the bg* spellings of all of the above still resolve, permanently.
33
41
  * - declared params arrive as `params.<name>` (f32 or vec3f), shared by both.
34
42
  *
35
43
  * Return display-space sRGB + alpha, 0..1. Both mounts are alpha-composited
@@ -38,6 +46,46 @@
38
46
  * 0 lets it through, which is how a starfield is stars over the user's color;
39
47
  * a foreground at alpha 1 covers the frame. No mode flag anywhere — the alpha
40
48
  * channel already says it. */
49
+ /**
50
+ * The bones an effect asked for, in declaration order — the slots rzAnchor reads.
51
+ *
52
+ * // @anchor 左手首 trail
53
+ * // @anchor 頭
54
+ *
55
+ * A declaration in the source, like the mounts: what a file names is what gets
56
+ * resolved and uploaded, so naming none costs nothing and nobody pays for a
57
+ * rig's other five hundred bones. Anchored to the start of a line so that
58
+ * writing the word @anchor in ordinary prose does not silently add a slot —
59
+ * which would shift every slot after it.
60
+ *
61
+ * `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
62
+ * because a path is two orders of magnitude more data than a point, and most
63
+ * anchors want a point.
64
+ *
65
+ * Names are passed through verbatim: any bone the rig has works, and one it does
66
+ * not have simply reports invalid.
67
+ */
68
+ export function parseEffectAnchors(wgsl: string, max: number): { bone: string; trail: boolean }[] {
69
+ return [...wgsl.matchAll(/^[ \t]*\/\/[ \t]*@anchor[ \t]+(\S+)([ \t]+trail)?[ \t]*$/gm)]
70
+ .map((m) => ({ bone: m[1], trail: m[2] !== undefined }))
71
+ .slice(0, max)
72
+ }
73
+
74
+ /**
75
+ * The caps the cast buffer is built to, shared by the shader below and by the
76
+ * engine that fills it. Interpolated into the WGSL rather than written twice:
77
+ * the layout arithmetic on both sides has to agree exactly, and two literals
78
+ * that must match are two literals that eventually will not.
79
+ *
80
+ * All three are MINIMUMS. Raising one breaks nothing, because effects read
81
+ * through accessors and loop to the count functions; lowering one does.
82
+ */
83
+ export const EFFECT_SUBJECTS = 4
84
+ export const EFFECT_ANCHORS = 8
85
+ export const EFFECT_TRAIL_SAMPLES = 128
86
+ /** vec4 slot where the trails begin — after the subjects and the anchors. */
87
+ export const EFFECT_TRAIL_BASE = EFFECT_SUBJECTS * 3 + EFFECT_ANCHORS * EFFECT_SUBJECTS * 3
88
+
41
89
  export type CompositeEffectSource = {
42
90
  /** The user's WGSL verbatim: helpers plus whichever entry points it defines. */
43
91
  wgsl: string
@@ -107,6 +155,13 @@ override APPLY_GAMMA: bool = true;
107
155
  // Blender's AgX, as the 57³ lookup it ships as rather than a reconstruction of
108
156
  // it. Sampled in the log-encoded E-Gamut space the cube expects — see agxTransform.
109
157
  @group(0) @binding(10) var agxLut: texture_3d<f32>;
158
+ // The cast, as data. Read through rzSubject/rzAnchor below — the LAYOUT IS NOT
159
+ // STABLE and never will be, because it depends on what each effect declared.
160
+ // Reading it directly is the one thing that would freeze it forever.
161
+ //
162
+ // vec4 slots: [0 .. 11] four subjects, three each (root+valid, hip, bounds);
163
+ // then MAX_ANCHORS × four subjects, three each (pos+valid, vel, fwd).
164
+ @group(0) @binding(11) var<storage, read> _rzCast: array<vec4f>;
110
165
 
111
166
  // Must match FILMIC_LUT_WIDTH in engine.ts (bakeFilmicLut).
112
167
  const FILMIC_LUT_W: f32 = 256.0;
@@ -202,29 +257,179 @@ fn viewTransform(c: vec3f) -> vec3f {
202
257
  return vec3f(filmic(c.r), filmic(c.g), filmic(c.b));
203
258
  }
204
259
 
205
- /** Canvas size in pixels — for user effects (aspect correction). */
206
- fn bgResolution() -> vec2f { return viewU[6].zw; }
260
+ // ── The effect API ────────────────────────────────────────────────────────────
261
+ //
262
+ // Named rz*, for the engine. The prefix earns its place twice: user code is
263
+ // concatenated into THIS module, so an unprefixed rzAnchor() would collide with
264
+ // exactly the helper an author would write, and the old bg* prefix stopped being
265
+ // true in 0.41.0 when effects gained a mount over the finished frame.
266
+ //
267
+ // The bg* names below are permanent aliases, not a deprecation with an end date.
268
+ // A published link is immutable, so a scene pinned to a bg* effect has to keep
269
+ // compiling forever. They are one-line and inlined; no new function gets one.
270
+
271
+ /** Canvas size in pixels — for aspect correction. */
272
+ fn rzResolution() -> vec2f { return viewU[6].zw; }
207
273
 
208
274
  /** The camera's world position. */
209
- fn bgCameraPos() -> vec3f { return viewU[10].xyz; }
275
+ fn rzCameraPos() -> vec3f { return viewU[10].xyz; }
210
276
 
211
277
  /** How many characters are in the scene, up to four. */
212
- fn bgSubjectCount() -> i32 { return i32(viewU[10].w); }
278
+ fn rzSubjectCount() -> i32 { return i32(viewU[10].w); }
213
279
 
214
280
  /**
215
- * Where a character is standing, in world space.
281
+ * A world point as the camera sees it: xy the uv it lands on, z its distance
282
+ * along the VIEW AXIS in metres.
216
283
  *
217
- * An effect that wants to RESPOND to the cast ripples under the feet, a glow
218
- * that follows someone, dust kicked up where they are needs to know where
219
- * they are, and the ray and the depth cannot tell it: they describe the pixel,
220
- * not the scene. This is the model's root, which for a PMX is between the feet
221
- * on the floor, so it is already the contact point a ripple wants.
284
+ * The exact inverse of the ray this pass builds per pixel, so it is the cheap way
285
+ * to work with anything anchored in the world. Marching a curve or a trail in 3D
286
+ * costs a distance evaluation per sample per pixel; projecting its points once
287
+ * and measuring in 2D costs a subtraction, which is the difference between a
288
+ * ribbon that runs at 4K and one that does not.
289
+ *
290
+ * z is directly comparable to the depth handed to foreground(), so occlusion is
291
+ * a single test: draw where your z is nearer than the scene's. It is returned
292
+ * SIGNED and unclamped — behind the camera is negative, and worth rejecting
293
+ * before you use the uv, which is meaningless there.
294
+ */
295
+ fn rzProject(p: vec3f) -> vec3f {
296
+ let d = p - viewU[10].xyz;
297
+ let z = dot(d, viewU[5].xyz);
298
+ // Guard only the divide. z itself is returned as it is, so the caller can see
299
+ // the sign; clamping it here would put points behind the lens on the horizon.
300
+ let inv = 1.0 / select(z, 1e-4, z < 1e-4);
301
+ let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);
302
+ return vec3f(ndc * 0.5 + 0.5, z);
303
+ }
304
+
305
+ /** A character, as much of one as a shader needs. */
306
+ struct RzSubject {
307
+ /** On the FLOOR, under the body — where a ring or a magic circle belongs. */
308
+ root: vec3f,
309
+ /** At the hips, the middle of the body — where an aura belongs. */
310
+ center: vec3f,
311
+ /** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
312
+ bounds: vec4f,
313
+ /** False past the end of the cast, and every field is then zero. */
314
+ valid: bool,
315
+ }
316
+
317
+ /** One bone an effect asked for, by name, at the top of its own source. */
318
+ struct RzAnchor {
319
+ pos: vec3f,
320
+ /** World units per second, from the previous frame. Direction for a trail,
321
+ * magnitude for anything that should react to how hard someone is moving. */
322
+ vel: vec3f,
323
+ /** The bone's forward axis — which way a foot points, where a head looks. */
324
+ fwd: vec3f,
325
+ /** False when this rig has no such bone. Check it: the alternative is drawing
326
+ * a hand effect at the world origin on every model that spells it differently. */
327
+ valid: bool,
328
+ }
329
+
330
+ const RZ_MAX_ANCHORS: i32 = ${EFFECT_ANCHORS};
331
+
332
+ /**
333
+ * Character i. Loop to rzSubjectCount(), never to a constant — the caps here are
334
+ * MINIMUMS and are free to grow, which is only true while nobody hardcodes them.
335
+ */
336
+ fn rzSubject(i: i32) -> RzSubject {
337
+ var s: RzSubject;
338
+ s.valid = i >= 0 && i < rzSubjectCount();
339
+ if (!s.valid) { return s; }
340
+ let b = i * 3;
341
+ s.root = _rzCast[b].xyz;
342
+ s.center = _rzCast[b + 1].xyz;
343
+ s.bounds = _rzCast[b + 2];
344
+ return s;
345
+ }
346
+
347
+ /**
348
+ * The slot-th bone this effect declared, on character subject.
349
+ *
350
+ * Slots are the order of the declarations at the top of your source:
351
+ *
352
+ * // @anchor 左手首
353
+ * // @anchor 頭
354
+ *
355
+ * gives you slot 0 and slot 1. Any bone name the model has works; valid is
356
+ * false when it does not have it, which is the normal case across rigs that
357
+ * spell things differently.
358
+ */
359
+ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
360
+ var a: RzAnchor;
361
+ a.valid = false;
362
+ if (subject < 0 || subject >= rzSubjectCount() || slot < 0 || slot >= RZ_MAX_ANCHORS) { return a; }
363
+ let b = ${EFFECT_SUBJECTS * 3} + (slot * ${EFFECT_SUBJECTS} + subject) * 3;
364
+ a.valid = _rzCast[b].w > 0.5;
365
+ a.pos = _rzCast[b].xyz;
366
+ a.vel = _rzCast[b + 1].xyz;
367
+ a.fwd = _rzCast[b + 2].xyz;
368
+ return a;
369
+ }
370
+
371
+ const RZ_TRAIL_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
372
+
373
+ /**
374
+ * How many path samples this anchor has. Zero unless it was declared with
375
+ * trail, and it climbs from zero as the trail fills after the effect loads.
376
+ *
377
+ * Loop to THIS, never to RZ_TRAIL_SAMPLES: the cap is a minimum and is free to
378
+ * grow, which stays true only while nobody hardcodes it.
379
+ */
380
+ fn rzTrailCount(subject: i32, slot: i32) -> i32 {
381
+ if (subject < 0 || subject >= rzSubjectCount() || slot < 0 || slot >= RZ_MAX_ANCHORS) { return 0; }
382
+ return i32(_rzCast[${EFFECT_SUBJECTS * 3} + (slot * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
383
+ }
384
+
385
+ /**
386
+ * Sample i of an anchor's path: xyz where it was, w how many seconds ago.
387
+ *
388
+ * i = 0 is NOW and they run backwards in time, so a ribbon is drawn by walking i
389
+ * upward and fading on .w. Sampled at a fixed rate on the SCENE clock, not the
390
+ * display's — so the path is identical in the editor, in an export, and in a
391
+ * re-export, and its spacing does not change with framerate.
392
+ *
393
+ * This is what a hand trail wants instead of position and velocity. One position
394
+ * and one velocity is a straight segment that jitters, because a velocity is a
395
+ * difference between two frames; a path is what actually happened.
396
+ */
397
+ fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
398
+ let n = rzTrailCount(subject, slot);
399
+ if (i < 0 || i >= n) { return vec4f(0.0); }
400
+ let base = ${EFFECT_TRAIL_BASE} + (slot * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
401
+ return _rzCast[base + i];
402
+ }
403
+
404
+ fn bgResolution() -> vec2f { return rzResolution(); }
405
+ fn bgCameraPos() -> vec3f { return rzCameraPos(); }
406
+ fn bgSubjectCount() -> i32 { return rzSubjectCount(); }
407
+
408
+ /**
409
+ * Where a character IS, in world space — at the hips, not on the floor.
410
+ *
411
+ * An effect that wants to RESPOND to the cast — a glow that follows someone,
412
+ * dust kicked up where they are — needs to know where they are, and the ray and
413
+ * the depth cannot tell it: they describe the pixel, not the scene.
414
+ *
415
+ * The value is model.position + センター + 全ての親. センター sits at hip
416
+ * height on every standard MMD rig, so this is a point in the middle of the
417
+ * body. It is NOT the contact point: a ripple drawn here appears at the waist.
418
+ * Ground effects want the .xz of this and their own floor height, which is what
419
+ * the effects that shipped against it already do.
420
+ *
421
+ * The comment here used to claim it was "between the feet on the floor", which
422
+ * is where that habit came from. Left as it is regardless of the name: a
423
+ * published link is immutable, so every shared scene pinning an effect that
424
+ * reads this depends on it meaning exactly what it has always meant.
222
425
  *
223
426
  * Clamped rather than bounds-checked: an effect looping past the count reads the
224
427
  * last subject instead of sampling whatever follows the array, which is a wrong
225
428
  * ripple rather than an undefined one.
226
429
  */
227
- fn bgSubjectPos(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
430
+ fn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
431
+
432
+ fn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }
228
433
 
229
434
  /** Where in the WORLD the scene drew this pixel — the depth handed to
230
435
  * foreground() turned into a place. Without it an effect can only think in
@@ -235,11 +440,13 @@ fn bgSubjectPos(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
235
440
  * the ray's projection onto camera-forward before being walked out. At the far
236
441
  * plane (nothing drawn) this lands a very long way off, which is what a sky
237
442
  * should do to anything reading it. */
238
- fn bgWorldPos(ray: vec3f, depth: f32) -> vec3f {
443
+ fn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {
239
444
  let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);
240
- return bgCameraPos() + normalize(ray) * (depth / axis);
445
+ return rzCameraPos() + normalize(ray) * (depth / axis);
241
446
  }
242
447
 
448
+ fn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }
449
+
243
450
  /** Color grading, applied to the tonemapped SCENE (not the background — see the
244
451
  * call site). The core is ASC CDL, the film-industry interchange standard:
245
452
  *