reze-engine 0.57.2 → 0.57.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.
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA4BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAsVR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA4BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAyZR"}
@@ -69,6 +69,10 @@ const SUB: i32 = ${TRAIL_SUBDIVISIONS};
69
69
  // scaling width pinches the strip shut at exactly the moments a hand decelerates,
70
70
  // which is every turn — and a ribbon that closes to nothing at each turn reads as
71
71
  // dark breaks in the middle of it.
72
+ /** How far a bone may travel between two trail samples, as a fraction of its
73
+ * owner's bounding radius. Generous on purpose: a hard swing covers a fraction
74
+ * of this, and the only things above it are cuts. */
75
+ const RZ_TRAIL_MAX_STEP: f32 = 0.5;
72
76
  const RZ_REF_SPAN: f32 = 0.37;
73
77
 
74
78
  struct CameraU {
@@ -124,18 +128,62 @@ fn _rzCastLive() -> i32 {
124
128
  }
125
129
  return n;
126
130
  }
131
+ /**
132
+ * CENTRIPETAL knot spacing: the square root of the chord.
133
+ *
134
+ * The whole reason the spline below is not the textbook one-liner. Uniform
135
+ * Catmull-Rom takes the tangent at p1 to be (p2 - p0)/2 — a quantity that knows
136
+ * nothing about how far apart those samples actually are. Trails are sampled at
137
+ * a FIXED RATE, so a hand that accelerates leaves one segment far longer than
138
+ * its neighbours, and a tangent sized for the long one applied over the short
139
+ * one swings the curve far outside the control polygon: the ribbon bulges out
140
+ * to a place no sample ever was, which reads as the trail teleporting, and it
141
+ * happens precisely on the quick moves.
142
+ *
143
+ * Spacing the knots by sqrt(chord) — alpha = 0.5, the centripetal variant — is
144
+ * the standard cure, and it is the only alpha proven to produce no cusp and no
145
+ * self-intersection whatever the samples do.
146
+ */
147
+ fn rzKnot(a: vec3f, b: vec3f) -> f32 {
148
+ return max(sqrt(distance(a, b)), 1e-4);
149
+ }
150
+
151
+ /** The two Hermite tangents of the segment p1..p2, sized by real chord lengths. */
152
+ fn rzSplineTangents(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f) -> mat2x3f {
153
+ let d0 = rzKnot(p0, p1);
154
+ let d1 = rzKnot(p1, p2);
155
+ let d2 = rzKnot(p2, p3);
156
+ let m1 = ((p1 - p0) / d0 - (p2 - p0) / (d0 + d1) + (p2 - p1) / d1) * d1;
157
+ let m2 = ((p2 - p1) / d1 - (p3 - p1) / (d1 + d2) + (p3 - p2) / d2) * d1;
158
+ return mat2x3f(m1, m2);
159
+ }
160
+
127
161
  /** Catmull-Rom through four samples — passes through p1 and p2. */
128
162
  fn rzSpline(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
163
+ let m = rzSplineTangents(p0, p1, p2, p3);
129
164
  let t2 = t * t;
130
165
  let t3 = t2 * t;
131
- return 0.5 * ((2.0 * p1) + (-p0 + p2) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
132
- (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
166
+ return (2.0 * t3 - 3.0 * t2 + 1.0) * p1
167
+ + (t3 - 2.0 * t2 + t) * m[0]
168
+ + (-2.0 * t3 + 3.0 * t2) * p2
169
+ + (t3 - t2) * m[1];
133
170
  }
134
- /** Its derivative — the ribbon's direction, smooth by construction. */
171
+
172
+ /**
173
+ * Its derivative — the ribbon's direction, smooth by construction.
174
+ *
175
+ * Still exact at the knots, which is what the shared edge depends on: at t = 1
176
+ * this is m2 and at t = 0 it is m1, and the two segments meeting at a sample
177
+ * derive those from the same three points. They differ only by a positive
178
+ * scale, and this normalises, so both quads get the identical direction.
179
+ */
135
180
  fn rzSplineTangent(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
181
+ let m = rzSplineTangents(p0, p1, p2, p3);
136
182
  let t2 = t * t;
137
- let d = 0.5 * ((-p0 + p2) + 2.0 * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t +
138
- 3.0 * (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t2);
183
+ let d = (6.0 * t2 - 6.0 * t) * p1
184
+ + (3.0 * t2 - 4.0 * t + 1.0) * m[0]
185
+ + (-6.0 * t2 + 6.0 * t) * p2
186
+ + (3.0 * t2 - 2.0 * t) * m[1];
139
187
  if (length(d) < 1e-6) { return vec3f(0.0, 1.0, 0.0); }
140
188
  return normalize(d);
141
189
  }
@@ -262,9 +310,28 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
262
310
  // where the hand slows — aggregation the width taper never clamps, because
263
311
  // width follows AGE, not speed. Where the hand is not moving there is no path,
264
312
  // and a path effect should draw nothing there at all.
313
+ // AND A GATE AT THE OTHER END, for the same reason. The minimum says "the
314
+ // hand is not moving"; this says "the hand did not move THERE". A bone cannot
315
+ // cross a sizeable fraction of its owner between two samples, so a span that
316
+ // large is not a motion — it is a cut: a seek, a clip boundary, a model
317
+ // repositioned, a motion that jumps. Joined anyway, the two samples either
318
+ // side are bridged by one enormous quad flung across the gap and drawn at full
319
+ // strength, which is the "trail teleports on a fast move" report. A path
320
+ // effect should draw nothing across a discontinuity, exactly as it draws
321
+ // nothing where there is no path.
322
+ //
323
+ // Measured against the subject's own bounding radius rather than in world
324
+ // units, so it holds for a doll and for a giant, and it is checked in WORLD
325
+ // space rather than in pixels — a camera cut moves every projected point at
326
+ // once and must not shred every ribbon on screen.
327
+ //
328
+ // Zero radius means the cast slot is not live, and then there is nothing to
329
+ // measure against: the test stands down rather than rejecting everything.
330
+ let reach = rzSubject(subject).bounds.w * RZ_TRAIL_MAX_STEP;
265
331
  let c1 = cam.proj * cam.view * vec4f(p1, 1.0);
266
332
  let c2 = cam.proj * cam.view * vec4f(p2, 1.0);
267
333
  if (c1.w <= 0.01 || c2.w <= 0.01 ||
334
+ (reach > 0.0 && distance(p1, p2) > reach) ||
268
335
  distance(c1.xy / c1.w * toPx, c2.xy / c2.w * toPx) < 0.7 * (H / 1080.0)) {
269
336
  out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
270
337
  out.uv = vec2f(0.0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.57.2",
3
+ "version": "0.57.4",
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",
@@ -120,6 +120,10 @@ const SUB: i32 = ${TRAIL_SUBDIVISIONS};
120
120
  // scaling width pinches the strip shut at exactly the moments a hand decelerates,
121
121
  // which is every turn — and a ribbon that closes to nothing at each turn reads as
122
122
  // dark breaks in the middle of it.
123
+ /** How far a bone may travel between two trail samples, as a fraction of its
124
+ * owner's bounding radius. Generous on purpose: a hard swing covers a fraction
125
+ * of this, and the only things above it are cuts. */
126
+ const RZ_TRAIL_MAX_STEP: f32 = 0.5;
123
127
  const RZ_REF_SPAN: f32 = 0.37;
124
128
 
125
129
  struct CameraU {
@@ -175,18 +179,62 @@ fn _rzCastLive() -> i32 {
175
179
  }
176
180
  return n;
177
181
  }
182
+ /**
183
+ * CENTRIPETAL knot spacing: the square root of the chord.
184
+ *
185
+ * The whole reason the spline below is not the textbook one-liner. Uniform
186
+ * Catmull-Rom takes the tangent at p1 to be (p2 - p0)/2 — a quantity that knows
187
+ * nothing about how far apart those samples actually are. Trails are sampled at
188
+ * a FIXED RATE, so a hand that accelerates leaves one segment far longer than
189
+ * its neighbours, and a tangent sized for the long one applied over the short
190
+ * one swings the curve far outside the control polygon: the ribbon bulges out
191
+ * to a place no sample ever was, which reads as the trail teleporting, and it
192
+ * happens precisely on the quick moves.
193
+ *
194
+ * Spacing the knots by sqrt(chord) — alpha = 0.5, the centripetal variant — is
195
+ * the standard cure, and it is the only alpha proven to produce no cusp and no
196
+ * self-intersection whatever the samples do.
197
+ */
198
+ fn rzKnot(a: vec3f, b: vec3f) -> f32 {
199
+ return max(sqrt(distance(a, b)), 1e-4);
200
+ }
201
+
202
+ /** The two Hermite tangents of the segment p1..p2, sized by real chord lengths. */
203
+ fn rzSplineTangents(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f) -> mat2x3f {
204
+ let d0 = rzKnot(p0, p1);
205
+ let d1 = rzKnot(p1, p2);
206
+ let d2 = rzKnot(p2, p3);
207
+ let m1 = ((p1 - p0) / d0 - (p2 - p0) / (d0 + d1) + (p2 - p1) / d1) * d1;
208
+ let m2 = ((p2 - p1) / d1 - (p3 - p1) / (d1 + d2) + (p3 - p2) / d2) * d1;
209
+ return mat2x3f(m1, m2);
210
+ }
211
+
178
212
  /** Catmull-Rom through four samples — passes through p1 and p2. */
179
213
  fn rzSpline(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
214
+ let m = rzSplineTangents(p0, p1, p2, p3);
180
215
  let t2 = t * t;
181
216
  let t3 = t2 * t;
182
- return 0.5 * ((2.0 * p1) + (-p0 + p2) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
183
- (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
217
+ return (2.0 * t3 - 3.0 * t2 + 1.0) * p1
218
+ + (t3 - 2.0 * t2 + t) * m[0]
219
+ + (-2.0 * t3 + 3.0 * t2) * p2
220
+ + (t3 - t2) * m[1];
184
221
  }
185
- /** Its derivative — the ribbon's direction, smooth by construction. */
222
+
223
+ /**
224
+ * Its derivative — the ribbon's direction, smooth by construction.
225
+ *
226
+ * Still exact at the knots, which is what the shared edge depends on: at t = 1
227
+ * this is m2 and at t = 0 it is m1, and the two segments meeting at a sample
228
+ * derive those from the same three points. They differ only by a positive
229
+ * scale, and this normalises, so both quads get the identical direction.
230
+ */
186
231
  fn rzSplineTangent(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
232
+ let m = rzSplineTangents(p0, p1, p2, p3);
187
233
  let t2 = t * t;
188
- let d = 0.5 * ((-p0 + p2) + 2.0 * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t +
189
- 3.0 * (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t2);
234
+ let d = (6.0 * t2 - 6.0 * t) * p1
235
+ + (3.0 * t2 - 4.0 * t + 1.0) * m[0]
236
+ + (-6.0 * t2 + 6.0 * t) * p2
237
+ + (3.0 * t2 - 2.0 * t) * m[1];
190
238
  if (length(d) < 1e-6) { return vec3f(0.0, 1.0, 0.0); }
191
239
  return normalize(d);
192
240
  }
@@ -313,9 +361,28 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
313
361
  // where the hand slows — aggregation the width taper never clamps, because
314
362
  // width follows AGE, not speed. Where the hand is not moving there is no path,
315
363
  // and a path effect should draw nothing there at all.
364
+ // AND A GATE AT THE OTHER END, for the same reason. The minimum says "the
365
+ // hand is not moving"; this says "the hand did not move THERE". A bone cannot
366
+ // cross a sizeable fraction of its owner between two samples, so a span that
367
+ // large is not a motion — it is a cut: a seek, a clip boundary, a model
368
+ // repositioned, a motion that jumps. Joined anyway, the two samples either
369
+ // side are bridged by one enormous quad flung across the gap and drawn at full
370
+ // strength, which is the "trail teleports on a fast move" report. A path
371
+ // effect should draw nothing across a discontinuity, exactly as it draws
372
+ // nothing where there is no path.
373
+ //
374
+ // Measured against the subject's own bounding radius rather than in world
375
+ // units, so it holds for a doll and for a giant, and it is checked in WORLD
376
+ // space rather than in pixels — a camera cut moves every projected point at
377
+ // once and must not shred every ribbon on screen.
378
+ //
379
+ // Zero radius means the cast slot is not live, and then there is nothing to
380
+ // measure against: the test stands down rather than rejecting everything.
381
+ let reach = rzSubject(subject).bounds.w * RZ_TRAIL_MAX_STEP;
316
382
  let c1 = cam.proj * cam.view * vec4f(p1, 1.0);
317
383
  let c2 = cam.proj * cam.view * vec4f(p2, 1.0);
318
384
  if (c1.w <= 0.01 || c2.w <= 0.01 ||
385
+ (reach > 0.0 && distance(p1, p2) > reach) ||
319
386
  distance(c1.xy / c1.w * toPx, c2.xy / c2.w * toPx) < 0.7 * (H / 1080.0)) {
320
387
  out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
321
388
  out.uv = vec2f(0.0);