reze-engine 0.50.3 → 0.50.4

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 (93) hide show
  1. package/dist/engine.d.ts +237 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +793 -64
  4. package/dist/physics/autofit.d.ts +147 -0
  5. package/dist/physics/autofit.d.ts.map +1 -0
  6. package/dist/physics/autofit.js +501 -0
  7. package/dist/shaders/cast-api.d.ts +1 -1
  8. package/dist/shaders/cast-api.d.ts.map +1 -1
  9. package/dist/shaders/cast-layout.d.ts +44 -1
  10. package/dist/shaders/cast-layout.d.ts.map +1 -1
  11. package/dist/shaders/cast-layout.js +44 -1
  12. package/dist/shaders/materials/common.d.ts.map +1 -1
  13. package/dist/shaders/materials/common.js +7 -1
  14. package/dist/shaders/materials/nodes.d.ts +1 -1
  15. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  16. package/dist/shaders/materials/nodes.js +17 -9
  17. package/dist/shaders/passes/composite.d.ts +1 -1
  18. package/dist/shaders/passes/composite.d.ts.map +1 -1
  19. package/dist/shaders/passes/depth-prepass.d.ts +1 -1
  20. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  21. package/dist/shaders/passes/depth-prepass.js +52 -12
  22. package/dist/shaders/passes/field-blit.d.ts +26 -0
  23. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  24. package/dist/shaders/passes/field-blit.js +65 -0
  25. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  26. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  27. package/dist/shaders/passes/ground-noise.js +88 -0
  28. package/dist/shaders/passes/ground.d.ts +16 -0
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +131 -27
  31. package/dist/shaders/passes/outline.d.ts +1 -1
  32. package/dist/shaders/passes/outline.d.ts.map +1 -1
  33. package/dist/shaders/passes/outline.js +12 -3
  34. package/dist/shaders/passes/particles.d.ts.map +1 -1
  35. package/dist/shaders/passes/particles.js +6 -2
  36. package/dist/shaders/passes/scene-contract.d.ts +38 -6
  37. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  38. package/dist/shaders/passes/scene-contract.js +53 -16
  39. package/dist/shaders/passes/sim.d.ts +34 -0
  40. package/dist/shaders/passes/sim.d.ts.map +1 -0
  41. package/dist/shaders/passes/sim.js +169 -0
  42. package/dist/shaders/passes/trails.d.ts.map +1 -1
  43. package/dist/shaders/passes/trails.js +36 -8
  44. package/dist/shaders/score-api.d.ts +10 -0
  45. package/dist/shaders/score-api.d.ts.map +1 -0
  46. package/dist/shaders/score-api.js +114 -0
  47. package/package.json +1 -1
  48. package/src/engine.ts +839 -56
  49. package/src/shaders/cast-layout.ts +44 -1
  50. package/src/shaders/materials/common.ts +7 -1
  51. package/src/shaders/materials/nodes.ts +17 -9
  52. package/src/shaders/passes/depth-prepass.ts +53 -12
  53. package/src/shaders/passes/ground.ts +133 -27
  54. package/src/shaders/passes/outline.ts +14 -3
  55. package/src/shaders/passes/particles.ts +6 -2
  56. package/src/shaders/passes/scene-contract.ts +55 -16
  57. package/src/shaders/passes/trails.ts +36 -8
  58. package/dist/physics-debug.d.ts +0 -30
  59. package/dist/physics-debug.d.ts.map +0 -1
  60. package/dist/physics-debug.js +0 -526
  61. package/dist/shaders/materials/body.d.ts +0 -2
  62. package/dist/shaders/materials/body.d.ts.map +0 -1
  63. package/dist/shaders/materials/body.js +0 -95
  64. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  65. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  66. package/dist/shaders/materials/cloth_rough.js +0 -69
  67. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  68. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  69. package/dist/shaders/materials/cloth_smooth.js +0 -61
  70. package/dist/shaders/materials/default.d.ts +0 -2
  71. package/dist/shaders/materials/default.d.ts.map +0 -1
  72. package/dist/shaders/materials/default.js +0 -43
  73. package/dist/shaders/materials/eye.d.ts +0 -2
  74. package/dist/shaders/materials/eye.d.ts.map +0 -1
  75. package/dist/shaders/materials/eye.js +0 -60
  76. package/dist/shaders/materials/face.d.ts +0 -2
  77. package/dist/shaders/materials/face.d.ts.map +0 -1
  78. package/dist/shaders/materials/face.js +0 -95
  79. package/dist/shaders/materials/hair.d.ts +0 -2
  80. package/dist/shaders/materials/hair.d.ts.map +0 -1
  81. package/dist/shaders/materials/hair.js +0 -90
  82. package/dist/shaders/materials/metal.d.ts +0 -2
  83. package/dist/shaders/materials/metal.d.ts.map +0 -1
  84. package/dist/shaders/materials/metal.js +0 -77
  85. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  86. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  87. package/dist/shaders/materials/mmd_classic.js +0 -66
  88. package/dist/shaders/materials/stockings.d.ts +0 -2
  89. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  90. package/dist/shaders/materials/stockings.js +0 -122
  91. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  92. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  93. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -0,0 +1,147 @@
1
+ import { type Rigidbody } from "./types";
2
+ import type { Skinning } from "../model";
3
+ /**
4
+ * Supplementary colliders fitted to the mesh a PMX rigid body fails to cover.
5
+ *
6
+ * PMX body proxies are bodies of revolution — 下半身 is a capsule lying
7
+ * transversely across the hips, so its cross-section is a circle. A pelvis seen
8
+ * from the side is not a circle, and across every rig measured the butt sits
9
+ * 0.18–0.71 units OUTSIDE that capsule while the front is flush or inside. That
10
+ * gap is where a skirt goes through a character: the cloth is resting correctly
11
+ * on the collider it was given, and the collider stops half a unit short of the
12
+ * skin.
13
+ *
14
+ * A rest offset cannot close it (see RigidBodyStore.restOffset) — it is one
15
+ * scalar on a radially symmetric shape, and the error is asymmetric. Measured
16
+ * on 托特's run cycle, the offset that clears the butt is 0.5, which is simply
17
+ * the size of the missing geometry, and at that value the skirt stands off the
18
+ * front and hips by the same 0.5.
19
+ *
20
+ * So fit the missing piece instead: take each bone-following body's own skinned
21
+ * vertices, find the ones that escape its shape in a concentrated lobe, and fit
22
+ * a capsule to that lobe. That is the fix riggers apply by hand, derived from
23
+ * the mesh rather than guessed.
24
+ *
25
+ * Measured on 托特's run cycle, counting butt vertices swallowed by a cloth
26
+ * body: 32.4 per frame with nothing, 30.9 with a 0.02 rest offset, 12.4 with
27
+ * this pass, 10.4 with both.
28
+ */
29
+ export interface AutoFitOptions {
30
+ /** A vertex counts as protruding past this much (model units). Below it the
31
+ * mesh is within authoring tolerance of the proxy and needs no help. */
32
+ minProtrusion?: number;
33
+ /**
34
+ * Ignore vertices further out than this multiple of the source shape's
35
+ * RADIUS (its smallest characteristic dimension).
36
+ *
37
+ * A coarse first cut at keeping hair and loose clothing out — anything
38
+ * standing further off the proxy than the proxy is wide is not skin. The
39
+ * discriminator that actually does the work is minConcentration.
40
+ */
41
+ maxProtrusionRatio?: number;
42
+ /** Fewer protruding vertices than this and it is a spike, not a region. */
43
+ minVertices?: number;
44
+ /**
45
+ * Per-axis quantile the fitted lobe's extent is clipped to, each end. A
46
+ * handful of stray vertices (a weight-painting slip, a seam) would otherwise
47
+ * stretch the capsule far past the surface everyone can see.
48
+ */
49
+ quantile?: number;
50
+ /** Skip the fit unless the capsule actually reaches this far past what is
51
+ * already covered — otherwise it adds solver rows for nothing. */
52
+ minGain?: number;
53
+ /** How tightly the kept lobe's outward directions must agree, 0–1. A loose
54
+ * sanity check; the real discriminator is minConcentration. */
55
+ minDirectionality?: number;
56
+ /** Vertices outside the dominant lobe, by this cosine, are not part of the
57
+ * feature being fitted. */
58
+ lobeCos?: number;
59
+ /**
60
+ * How much deeper the fitted lobe must be than protrusion typically is on
61
+ * this body, averaged over the vertices that protrude at all.
62
+ *
63
+ * This is the guard that separates "the proxy is missing a lobe" from "the
64
+ * proxy is not a proxy for this mesh at all". A pelvis escapes its capsule by
65
+ * 0.53 at the back and 0.14 at the front, so the back lobe stands well clear
66
+ * of the average. Hair escapes the skull by a similar depth in every
67
+ * direction, so its deepest lobe is no deeper than typical and the ratio sits
68
+ * near 1. See the note at the meanProtrusion computation for what this
69
+ * deliberately refuses to fit.
70
+ *
71
+ * Without it the pass fits the hairstyle — measured on 托特, 頭 reports
72
+ * 3653 of 4669 vertices outside its capsule, and the fitted shape was a crate
73
+ * around the whole head. No threshold on depth or vertex count tells those
74
+ * two cases apart; concentration does.
75
+ */
76
+ minConcentration?: number;
77
+ /**
78
+ * How many capsules may be fitted to one body, each against what the previous
79
+ * ones already cover.
80
+ *
81
+ * One capsule inscribed in a lobe does not fill it — on 托特's butt a single
82
+ * fit removed 58% of the clipping and left the rest at the edges the capsule
83
+ * could not reach. Each further pass re-measures protrusion against the
84
+ * original shape UNION everything fitted so far, so it lands on whatever is
85
+ * still exposed instead of re-fitting the same bulge.
86
+ */
87
+ maxShapesPerBody?: number;
88
+ /**
89
+ * Quantile of the lobe's perpendicular spread used as the capsule radius.
90
+ *
91
+ * The clip-versus-float dial, and the one to reach for if a result looks
92
+ * wrong. Lower hugs the body: less standoff in idle, more clipping left. 1.0
93
+ * encloses the lobe entirely, which is what produced a visibly hovering
94
+ * skirt on 托特.
95
+ */
96
+ radiusQuantile?: number;
97
+ /**
98
+ * Fit only the deepest part of each lobe: points at least this fraction of
99
+ * the lobe's own maximum protrusion.
100
+ *
101
+ * A lobe tapers to nothing at its edges, and a capsule sized to span the
102
+ * whole thing has to be fat enough to reach the edges too. Fitting the deep
103
+ * core keeps each capsule small and lets a later pass handle the shallows
104
+ * with its own, which is both tighter and closer to what the surface is
105
+ * actually doing.
106
+ */
107
+ lobeDepthFraction?: number;
108
+ }
109
+ /** What the pass decided for one source body — surfaced so a rig can be
110
+ * inspected rather than silently rebuilt underneath its author. */
111
+ export interface AutoFitResult {
112
+ /** Index of the body in the input array that this supplements. */
113
+ sourceIndex: number;
114
+ sourceName: string;
115
+ /** Vertices dominated by that body's bone, and how many were outside it. */
116
+ vertexCount: number;
117
+ protrudingCount: number;
118
+ /** Furthest any kept vertex sat outside the source shape. */
119
+ maxProtrusion: number;
120
+ /** How far the fitted capsule reaches past what was already covered. */
121
+ gain: number;
122
+ /** Length of the kept lobe's mean outward direction, 0–1. */
123
+ directionality: number;
124
+ /** Lobe depth over the body's average protrusion. See minConcentration. */
125
+ concentration: number;
126
+ /**
127
+ * Furthest the capsule's newly exposed surface sits from the skin it was
128
+ * fitted to — the amount cloth resting on it will stand off the body at the
129
+ * worst point, and the number to watch if a result looks puffy. Only the part
130
+ * reaching outside the source shape is measured; the rest is buried.
131
+ */
132
+ overshoot: number;
133
+ body: Rigidbody;
134
+ }
135
+ /**
136
+ * Fit supplementary colliders for every bone-following body whose own mesh
137
+ * escapes it.
138
+ *
139
+ * `vertices` is the model's interleaved vertex buffer (stride 8, position
140
+ * first) and `skinning` its joints/weights — both in PMX bind pose, the same
141
+ * space as `shapePosition` / `shapeRotation`, so nothing has to be skinned.
142
+ *
143
+ * Returns one entry per body it chose to supplement; the caller appends
144
+ * `.body` to the rigid body list before constructing RezePhysics.
145
+ */
146
+ export declare function fitSupplementaryColliders(rigidbodies: Rigidbody[], vertices: Float32Array, skinning: Skinning, options?: AutoFitOptions): AutoFitResult[];
147
+ //# sourceMappingURL=autofit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autofit.d.ts","sourceRoot":"","sources":["../../src/physics/autofit.ts"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,SAAS,CAAA;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,cAAc;IAC7B;6EACyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;uEACmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;oEACgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;gCAC4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AA2BD;oEACoE;AACpE,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,cAAc,EAAE,MAAM,CAAA;IACtB,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,SAAS,CAAA;CAChB;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,SAAS,EAAE,EACxB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,cAAmB,GAC3B,aAAa,EAAE,CA4CjB"}
@@ -0,0 +1,501 @@
1
+ import { Vec3, Quat, Mat4 } from "../math";
2
+ import { RigidbodyShape, RigidbodyType } from "./types";
3
+ const DEFAULTS = {
4
+ minProtrusion: 0.05,
5
+ maxProtrusionRatio: 1.0,
6
+ minVertices: 24,
7
+ quantile: 0.02,
8
+ minGain: 0.1,
9
+ minDirectionality: 0.5,
10
+ lobeCos: 0.5,
11
+ minConcentration: 1.3,
12
+ maxShapesPerBody: 3,
13
+ radiusQuantile: 0.5,
14
+ lobeDepthFraction: 0.45,
15
+ };
16
+ const VERTEX_STRIDE = 8;
17
+ /**
18
+ * Fit supplementary colliders for every bone-following body whose own mesh
19
+ * escapes it.
20
+ *
21
+ * `vertices` is the model's interleaved vertex buffer (stride 8, position
22
+ * first) and `skinning` its joints/weights — both in PMX bind pose, the same
23
+ * space as `shapePosition` / `shapeRotation`, so nothing has to be skinned.
24
+ *
25
+ * Returns one entry per body it chose to supplement; the caller appends
26
+ * `.body` to the rigid body list before constructing RezePhysics.
27
+ */
28
+ export function fitSupplementaryColliders(rigidbodies, vertices, skinning, options = {}) {
29
+ const opt = { ...DEFAULTS, ...options };
30
+ const vertexCount = vertices.length / VERTEX_STRIDE;
31
+ if (vertexCount === 0)
32
+ return [];
33
+ // Bucket vertices by their dominant bone once. Every body then reads only the
34
+ // mesh it stands in for, instead of the pass being O(bodies × vertices).
35
+ const byBone = new Map();
36
+ for (let i = 0; i < vertexCount; i++) {
37
+ let bestW = -1;
38
+ let bestB = -1;
39
+ for (let k = 0; k < 4; k++) {
40
+ const w = skinning.weights[i * 4 + k];
41
+ if (w > bestW) {
42
+ bestW = w;
43
+ bestB = skinning.joints[i * 4 + k];
44
+ }
45
+ }
46
+ if (bestB < 0)
47
+ continue;
48
+ const list = byBone.get(bestB);
49
+ if (list === undefined)
50
+ byBone.set(bestB, [i]);
51
+ else
52
+ list.push(i);
53
+ }
54
+ const out = [];
55
+ for (let b = 0; b < rigidbodies.length; b++) {
56
+ const rb = rigidbodies[b];
57
+ // Only bone-following proxies. A dynamic body IS the cloth — it has no
58
+ // skinned surface of its own to escape from.
59
+ if (rb.type === RigidbodyType.Dynamic && rb.mass > 0)
60
+ continue;
61
+ if (rb.boneIndex < 0)
62
+ continue;
63
+ const owned = byBone.get(rb.boneIndex);
64
+ if (owned === undefined || owned.length < opt.minVertices)
65
+ continue;
66
+ // Fit repeatedly, each pass measuring against what the earlier ones cover.
67
+ const covered = [];
68
+ for (let pass = 0; pass < opt.maxShapesPerBody; pass++) {
69
+ const fitted = fitOne(rb, b, owned, vertices, opt, covered);
70
+ if (fitted === null)
71
+ break;
72
+ covered.push(fitted.local);
73
+ out.push(fitted.result);
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+ // Scratch, reused across bodies.
79
+ const _local = [];
80
+ function fitOne(rb, index, owned, vertices, opt, covered) {
81
+ const q = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z);
82
+ const radius = shapeRadius(rb);
83
+ if (radius <= 0)
84
+ return null;
85
+ const maxProtrusion = radius * opt.maxProtrusionRatio;
86
+ // Collect protruding vertices in the SOURCE BODY's local frame. Working in
87
+ // that frame rather than the bone's means the fitted box inherits the
88
+ // proxy's own orientation, which is already aligned with the limb — no
89
+ // covariance fit, and no second convention to keep straight.
90
+ //
91
+ // Each one is stored with the direction it escapes in (the gradient of the
92
+ // signed distance), because which way the mesh escapes is what decides
93
+ // whether this is a feature worth fitting at all.
94
+ _local.length = 0;
95
+ let deepest = 0;
96
+ let seedX = 0, seedY = 1, seedZ = 0;
97
+ let sumProtrusion = 0;
98
+ for (let n = 0; n < owned.length; n++) {
99
+ const o = owned[n] * VERTEX_STRIDE;
100
+ _v.x = vertices[o] - rb.shapePosition.x;
101
+ _v.y = vertices[o + 1] - rb.shapePosition.y;
102
+ _v.z = vertices[o + 2] - rb.shapePosition.z;
103
+ // Into the body's local frame: rotate by the conjugate.
104
+ Quat.rotateVecInvInto(q, _v, _r);
105
+ const px = _r.x, py = _r.y, pz = _r.z;
106
+ const sd = coveredSignedDistance(rb, covered, px, py, pz);
107
+ if (sd <= opt.minProtrusion || sd > maxProtrusion)
108
+ continue;
109
+ coveredOutwardDir(rb, covered, px, py, pz, _r);
110
+ // Seed the lobe on the DEEPEST vertex, not the mean direction. A pelvis
111
+ // escapes its capsule at the back by 0.53 and at the front by 0.14, so the
112
+ // mean cancels below any usable threshold and the butt — the whole point —
113
+ // is never fitted. The deepest point is always ON the feature.
114
+ if (sd > deepest) {
115
+ deepest = sd;
116
+ seedX = _r.x;
117
+ seedY = _r.y;
118
+ seedZ = _r.z;
119
+ }
120
+ sumProtrusion += sd;
121
+ _local.push(px, py, pz, _r.x, _r.y, _r.z, sd);
122
+ }
123
+ let found = _local.length / 7;
124
+ if (found < opt.minVertices)
125
+ return null;
126
+ // Averaged over the PROTRUDING vertices, deliberately — the question being
127
+ // asked is "is the deepest lobe deeper than protrusion typically is on this
128
+ // body", which is exactly the test that separates a butt from a hairstyle.
129
+ //
130
+ // Averaging over all the body's vertices instead was tried and is worse: it
131
+ // lifts every ratio, and measured on 托特 it scored the head's hair (2.4–4.2)
132
+ // ABOVE the pelvis (2.85), so no threshold could separate them and the pass
133
+ // fitted 33 bodies including three around the hair.
134
+ //
135
+ // The known blind spot is a mesh that escapes in exactly one place at uniform
136
+ // depth: the lobe IS the whole protruding set, so the ratio is 1 and nothing
137
+ // is fitted. Real skin always has a gradient, and for a pass that adds bodies
138
+ // to someone else's rig, declining when the evidence is ambiguous is the
139
+ // right way to be wrong.
140
+ const meanProtrusion = sumProtrusion / found;
141
+ // Settle the lobe axis: take everything within lobeCos of the seed, recentre
142
+ // on that set's depth-weighted mean direction, repeat. Two passes is enough —
143
+ // the seed already sits on the feature, this only centres it.
144
+ let mdx = seedX, mdy = seedY, mdz = seedZ;
145
+ for (let pass = 0; pass < 2; pass++) {
146
+ let ax = 0, ay = 0, az = 0;
147
+ for (let i = 0; i < found; i++) {
148
+ const s = i * 7;
149
+ if (_local[s + 3] * mdx + _local[s + 4] * mdy + _local[s + 5] * mdz < opt.lobeCos)
150
+ continue;
151
+ const wgt = _local[s + 6];
152
+ ax += _local[s + 3] * wgt;
153
+ ay += _local[s + 4] * wgt;
154
+ az += _local[s + 5] * wgt;
155
+ }
156
+ const len = Math.sqrt(ax * ax + ay * ay + az * az);
157
+ if (len < 1e-9)
158
+ break;
159
+ mdx = ax / len;
160
+ mdy = ay / len;
161
+ mdz = az / len;
162
+ }
163
+ // Keep only the lobe. Fitting the whole protruding shell would wrap the limb
164
+ // and put box corners out in open air on the side that was already covered.
165
+ // Depth floor for the lobe's core, relative to the deepest point IN the lobe
166
+ // (not the body's overall deepest, which may sit in a different direction).
167
+ let lobeDeepest = 0;
168
+ for (let i = 0; i < found; i++) {
169
+ const s = i * 7;
170
+ if (_local[s + 3] * mdx + _local[s + 4] * mdy + _local[s + 5] * mdz < opt.lobeCos)
171
+ continue;
172
+ if (_local[s + 6] > lobeDeepest)
173
+ lobeDeepest = _local[s + 6];
174
+ }
175
+ const depthFloor = lobeDeepest * opt.lobeDepthFraction;
176
+ let w = 0;
177
+ let lobeSum = 0;
178
+ let dirX = 0, dirY = 0, dirZ = 0;
179
+ for (let i = 0; i < found; i++) {
180
+ const s = i * 7;
181
+ if (_local[s + 3] * mdx + _local[s + 4] * mdy + _local[s + 5] * mdz < opt.lobeCos)
182
+ continue;
183
+ if (_local[s + 6] < depthFloor)
184
+ continue;
185
+ lobeSum += _local[s + 6];
186
+ dirX += _local[s + 3];
187
+ dirY += _local[s + 4];
188
+ dirZ += _local[s + 5];
189
+ _local[w * 3 + 0] = _local[s + 0];
190
+ _local[w * 3 + 1] = _local[s + 1];
191
+ _local[w * 3 + 2] = _local[s + 2];
192
+ w++;
193
+ }
194
+ found = w;
195
+ if (found < opt.minVertices)
196
+ return null;
197
+ // Concentration: is this lobe actually a FEATURE, or is the whole mesh
198
+ // uniformly outside its proxy? Hair around a skull escapes by a similar depth
199
+ // in every direction, so its deepest lobe is no deeper than typical and the
200
+ // ratio sits near 1. A butt is markedly deeper than the pelvis average. This
201
+ // is what stops the pass from boxing a hairstyle.
202
+ //
203
+ // Only the FIRST pass is judged on it. The question it answers — is this
204
+ // proxy standing in for this mesh at all — is settled once per body. Later
205
+ // passes are filling in what the first capsule could not reach, and by then
206
+ // the remaining protrusion is evenly spread by construction, so re-applying
207
+ // the test would reject every one of them and cap coverage at a single
208
+ // capsule. They still have to clear minGain and the vertex count.
209
+ const concentration = meanProtrusion > 1e-6 ? lobeSum / found / meanProtrusion : 0;
210
+ if (covered.length === 0 && concentration < opt.minConcentration)
211
+ return null;
212
+ const directionality = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ) / found;
213
+ if (directionality < opt.minDirectionality)
214
+ return null;
215
+ // Quantile-clipped extent per axis.
216
+ const lo = [0, 0, 0];
217
+ const hi = [0, 0, 0];
218
+ const axis = new Array(found);
219
+ for (let a = 0; a < 3; a++) {
220
+ for (let i = 0; i < found; i++)
221
+ axis[i] = _local[i * 3 + a];
222
+ axis.sort((x, y) => x - y);
223
+ const k = Math.min(found - 1, Math.floor(found * opt.quantile));
224
+ lo[a] = axis[k];
225
+ hi[a] = axis[found - 1 - k];
226
+ }
227
+ const hx = (hi[0] - lo[0]) * 0.5;
228
+ const hy = (hi[1] - lo[1]) * 0.5;
229
+ const hz = (hi[2] - lo[2]) * 0.5;
230
+ if (hx <= 0 || hy <= 0 || hz <= 0)
231
+ return null;
232
+ const cx = (hi[0] + lo[0]) * 0.5;
233
+ const cy = (hi[1] + lo[1]) * 0.5;
234
+ const cz = (hi[2] + lo[2]) * 0.5;
235
+ // Fit a CAPSULE, not a box, and keep the source body's own orientation.
236
+ //
237
+ // A box fitted to a curved lobe puts its eight corners out in open air — on
238
+ // 托特's butt the worst corner measured 0.81 units from the nearest skin
239
+ // vertex, and cloth resting on that corner would float further off the body
240
+ // than the clipping it was added to fix. A capsule has no corners.
241
+ //
242
+ // Reusing the source rotation verbatim is what makes this cheap AND correct.
243
+ // PMX capsules are Y-aligned in their own frame, and a proxy's axis already
244
+ // runs along its limb — 下半身 is a transverse pill across the hips, which is
245
+ // exactly the way a butt lobe runs. Inheriting the rotation means no
246
+ // covariance fit and, more to the point, no quaternion→Euler conversion whose
247
+ // convention could disagree with the loader's.
248
+ // Radius from the lobe's own perpendicular spread, at a quantile — NOT from
249
+ // the enclosing half-extent.
250
+ //
251
+ // This is what decides whether the result looks right. A capsule inflates
252
+ // equally in every direction perpendicular to its axis, so sizing the radius
253
+ // to ENCLOSE the lobe makes a wide shallow patch into a fat capsule: it only
254
+ // needed to reach the butt's depth, but it also grew to the butt's width, and
255
+ // the surplus is surface standing off the skin everywhere else. Measured on
256
+ // 托特's pelvis that was 0.68 units of standoff, which reads as a skirt
257
+ // hovering in idle — trading a clipping artifact for a worse one.
258
+ //
259
+ // Taking a quantile of the actual distances puts the capsule's surface
260
+ // through the lobe rather than around it. The deepest points end up slightly
261
+ // outside it, which the next pass picks up.
262
+ const perp = new Array(found);
263
+ for (let i = 0; i < found; i++) {
264
+ const dx = _local[i * 3] - cx;
265
+ const dz = _local[i * 3 + 2] - cz;
266
+ perp[i] = Math.sqrt(dx * dx + dz * dz);
267
+ }
268
+ perp.sort((a, b) => a - b);
269
+ const fitRadius = perp[Math.min(found - 1, Math.floor(found * opt.radiusQuantile))];
270
+ if (fitRadius <= 0)
271
+ return null;
272
+ // size.y is the FULL cylinder length between cap centres, so the capsule
273
+ // spans h + 2r along the axis. Solve for the lobe's own extent.
274
+ const cyl = Math.max(0, hy * 2 - fitRadius * 2);
275
+ // Gain and overshoot at the capsule's six axis extremes: how far it reaches
276
+ // past the shape it supplements, and how far it sits from the surface it was
277
+ // fitted to. Overshoot is the number to watch if a result looks puffy.
278
+ let gain = 0;
279
+ let overshoot = 0;
280
+ const half = cyl * 0.5;
281
+ const probes = [
282
+ [cx + fitRadius, cy, cz], [cx - fitRadius, cy, cz],
283
+ [cx, cy + half + fitRadius, cz], [cx, cy - half - fitRadius, cz],
284
+ [cx, cy, cz + fitRadius], [cx, cy, cz - fitRadius],
285
+ ];
286
+ for (const [px, py, pz] of probes) {
287
+ const sd = coveredSignedDistance(rb, covered, px, py, pz);
288
+ if (sd > gain)
289
+ gain = sd;
290
+ // Only the part of the capsule that reaches OUTSIDE the original shape is
291
+ // new collision surface. A probe pointing inward has no skin vertex near it
292
+ // by construction — the lobe is a patch on the outside — and counting it
293
+ // would report overshoot for geometry that is buried in the character.
294
+ if (sd <= 0)
295
+ continue;
296
+ let nearest = Infinity;
297
+ for (let i = 0; i < found; i++) {
298
+ const dx = _local[i * 3] - px;
299
+ const dy = _local[i * 3 + 1] - py;
300
+ const dz = _local[i * 3 + 2] - pz;
301
+ const d2 = dx * dx + dy * dy + dz * dz;
302
+ if (d2 < nearest)
303
+ nearest = d2;
304
+ }
305
+ const d = Math.sqrt(nearest);
306
+ if (d > overshoot)
307
+ overshoot = d;
308
+ }
309
+ if (gain < opt.minGain)
310
+ return null;
311
+ // Back out to bind-pose world. Orientation is copied verbatim from the source
312
+ // rather than converted — the capsule is expressed in that frame, and
313
+ // re-deriving Euler angles would only introduce a convention to get wrong.
314
+ _v.x = cx;
315
+ _v.y = cy;
316
+ _v.z = cz;
317
+ Quat.rotateVecInto(q, _v, _r);
318
+ const wx = rb.shapePosition.x + _r.x;
319
+ const wy = rb.shapePosition.y + _r.y;
320
+ const wz = rb.shapePosition.z + _r.z;
321
+ const result = {
322
+ sourceIndex: index,
323
+ sourceName: rb.name,
324
+ vertexCount: owned.length,
325
+ protrudingCount: found,
326
+ maxProtrusion: deepest,
327
+ gain,
328
+ directionality,
329
+ concentration,
330
+ overshoot,
331
+ body: {
332
+ name: `${rb.name}__fit`,
333
+ englishName: `${rb.englishName}__fit`,
334
+ boneIndex: rb.boneIndex,
335
+ // Same group and mask as the body it supplements: it must be visible to
336
+ // exactly the cloth that was already meant to collide with this limb, and
337
+ // invisible to whatever the rigger excluded.
338
+ group: rb.group,
339
+ collisionMask: rb.collisionMask,
340
+ shape: RigidbodyShape.Capsule,
341
+ // PMX capsule semantics: size.x is the radius, size.y the FULL cylinder
342
+ // length between cap centres (Bullet halves it internally), size.z unused.
343
+ size: new Vec3(fitRadius, cyl, fitRadius),
344
+ shapePosition: new Vec3(wx, wy, wz),
345
+ shapeRotation: new Vec3(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z),
346
+ // Static: it follows its bone and never integrates. Mass 0 keeps it out
347
+ // of the dynamic path entirely.
348
+ mass: 0,
349
+ linearDamping: 0,
350
+ angularDamping: 0,
351
+ restitution: rb.restitution,
352
+ friction: rb.friction,
353
+ type: RigidbodyType.Static,
354
+ bodyOffsetMatrixInverse: Mat4.identity(),
355
+ },
356
+ };
357
+ return { result, local: { cx, cy, cz, radius: fitRadius, cyl } };
358
+ }
359
+ /** Smallest characteristic dimension — the "how wide is this proxy" scale the
360
+ * protrusion cap is judged against. */
361
+ function shapeRadius(rb) {
362
+ switch (rb.shape) {
363
+ case RigidbodyShape.Sphere:
364
+ return rb.size.x;
365
+ case RigidbodyShape.Capsule:
366
+ return rb.size.x;
367
+ case RigidbodyShape.Box:
368
+ return Math.min(rb.size.x, rb.size.y, rb.size.z);
369
+ default:
370
+ return 0;
371
+ }
372
+ }
373
+ /** Signed distance to a Y-axis capsule centred at (cx,cy,cz), in the same
374
+ * local frame. `cyl` is the full length between cap centres. */
375
+ function capsuleSignedDistance(c, px, py, pz) {
376
+ const half = c.cyl * 0.5;
377
+ const dy = py - c.cy;
378
+ const t = dy > half ? half : dy < -half ? -half : dy;
379
+ const ax = px - c.cx;
380
+ const az = pz - c.cz;
381
+ const by = dy - t;
382
+ return Math.sqrt(ax * ax + by * by + az * az) - c.radius;
383
+ }
384
+ /** Distance to the UNION of the source shape and everything already fitted to
385
+ * it. Union is a min over signed distances, so a later pass sees only what is
386
+ * still exposed. */
387
+ function coveredSignedDistance(rb, covered, px, py, pz) {
388
+ let best = localSignedDistance(rb, px, py, pz);
389
+ for (let i = 0; i < covered.length; i++) {
390
+ const d = capsuleSignedDistance(covered[i], px, py, pz);
391
+ if (d < best)
392
+ best = d;
393
+ }
394
+ return best;
395
+ }
396
+ /** Outward direction from whichever member of the union is nearest — the
397
+ * gradient of coveredSignedDistance. */
398
+ function coveredOutwardDir(rb, covered, px, py, pz, out) {
399
+ let best = localSignedDistance(rb, px, py, pz);
400
+ let nearest = -1;
401
+ for (let i = 0; i < covered.length; i++) {
402
+ const d = capsuleSignedDistance(covered[i], px, py, pz);
403
+ if (d < best) {
404
+ best = d;
405
+ nearest = i;
406
+ }
407
+ }
408
+ if (nearest < 0) {
409
+ localOutwardDir(rb, px, py, pz, out);
410
+ return;
411
+ }
412
+ // Same construction as localOutwardDir, for the winning capsule.
413
+ const c = covered[nearest];
414
+ const half = c.cyl * 0.5;
415
+ const dy = py - c.cy;
416
+ const t = dy > half ? half : dy < -half ? -half : dy;
417
+ const dx = px - c.cx;
418
+ const dz = pz - c.cz;
419
+ const by = dy - t;
420
+ const len = Math.sqrt(dx * dx + by * by + dz * dz);
421
+ if (len > 1e-9) {
422
+ out.x = dx / len;
423
+ out.y = by / len;
424
+ out.z = dz / len;
425
+ }
426
+ else {
427
+ out.x = 0;
428
+ out.y = 1;
429
+ out.z = 0;
430
+ }
431
+ }
432
+ /** Direction a body-local point escapes the shape in — the gradient of the
433
+ * signed distance, i.e. the unit vector from its closest surface point. */
434
+ function localOutwardDir(rb, px, py, pz, out) {
435
+ let dx, dy, dz;
436
+ switch (rb.shape) {
437
+ case RigidbodyShape.Capsule: {
438
+ const half = rb.size.y * 0.5;
439
+ const t = py > half ? half : py < -half ? -half : py;
440
+ dx = px;
441
+ dy = py - t;
442
+ dz = pz;
443
+ break;
444
+ }
445
+ case RigidbodyShape.Box: {
446
+ dx = px - Math.max(-rb.size.x, Math.min(rb.size.x, px));
447
+ dy = py - Math.max(-rb.size.y, Math.min(rb.size.y, py));
448
+ dz = pz - Math.max(-rb.size.z, Math.min(rb.size.z, pz));
449
+ break;
450
+ }
451
+ default: {
452
+ dx = px;
453
+ dy = py;
454
+ dz = pz;
455
+ break;
456
+ }
457
+ }
458
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
459
+ if (len > 1e-9) {
460
+ out.x = dx / len;
461
+ out.y = dy / len;
462
+ out.z = dz / len;
463
+ }
464
+ else {
465
+ out.x = 0;
466
+ out.y = 1;
467
+ out.z = 0;
468
+ }
469
+ }
470
+ /** Signed distance from a body-local point to the shape's surface; > 0 outside. */
471
+ function localSignedDistance(rb, px, py, pz) {
472
+ switch (rb.shape) {
473
+ case RigidbodyShape.Sphere:
474
+ return Math.sqrt(px * px + py * py + pz * pz) - rb.size.x;
475
+ case RigidbodyShape.Capsule: {
476
+ // PMX capsules are Y-aligned and size.y is the FULL cylinder length
477
+ // between cap centres, so the segment runs ±size.y/2.
478
+ const half = rb.size.y * 0.5;
479
+ const t = py > half ? half : py < -half ? -half : py;
480
+ const dy = py - t;
481
+ return Math.sqrt(px * px + dy * dy + pz * pz) - rb.size.x;
482
+ }
483
+ case RigidbodyShape.Box: {
484
+ // size is half-extents for boxes.
485
+ const qx = Math.abs(px) - rb.size.x;
486
+ const qy = Math.abs(py) - rb.size.y;
487
+ const qz = Math.abs(pz) - rb.size.z;
488
+ const ox = Math.max(qx, 0), oy = Math.max(qy, 0), oz = Math.max(qz, 0);
489
+ const outside = Math.sqrt(ox * ox + oy * oy + oz * oz);
490
+ return outside + Math.min(Math.max(qx, qy, qz), 0);
491
+ }
492
+ default:
493
+ return -Infinity;
494
+ }
495
+ }
496
+ // Rotation goes through Quat.rotateVecInto / rotateVecInvInto rather than
497
+ // open-coded component formulas — this pass runs once at load, so there is
498
+ // nothing to win by hand-rolling it and a sign error here would misplace every
499
+ // fitted body.
500
+ const _v = new Vec3(0, 0, 0);
501
+ const _r = new Vec3(0, 0, 0);
@@ -1,2 +1,2 @@
1
- export declare const CAST_API = "\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 8;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth @anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed @anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 108 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
1
+ export declare const CAST_API = "\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 16;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth @anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed @anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 204 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
2
2
  //# sourceMappingURL=cast-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cast-api.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-api.ts"],"names":[],"mappings":"AA+BA,eAAO,MAAM,QAAQ,k4GA2FpB,CAAA"}
1
+ {"version":3,"file":"cast-api.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-api.ts"],"names":[],"mappings":"AA+BA,eAAO,MAAM,QAAQ,m4GA2FpB,CAAA"}