reze-engine 0.27.3 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/math.js CHANGED
@@ -90,6 +90,16 @@ export class Vec3 {
90
90
  out.z = m[2] * nx + m[6] * ny + m[10] * nz;
91
91
  return out;
92
92
  }
93
+ // out = (x, y, -z): right-handed Y-up ↔ left-handed Y-up (involutive). Safe when out === v.
94
+ static mirrorZInto(v, out) {
95
+ out.x = v.x;
96
+ out.y = v.y;
97
+ out.z = -v.z;
98
+ return out;
99
+ }
100
+ static mirrorZ(v) {
101
+ return new Vec3(v.x, v.y, -v.z);
102
+ }
93
103
  // In-place normalize returning length squared info via Vec3. Alias for normalize() but explicit.
94
104
  normalizeInPlace() {
95
105
  return this.normalize();
@@ -286,6 +296,318 @@ export class Quat {
286
296
  const z = cy * cx * sz - sy * sx * cz;
287
297
  return new Quat(x, y, z, w).normalize();
288
298
  }
299
+ // 4D dot product. Negative means a and b are on opposite hemispheres (same rotation
300
+ // when |dot| ≈ 1 either way — quaternion double cover).
301
+ static dot(a, b) {
302
+ return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
303
+ }
304
+ // Rotation angle between a and b in radians, insensitive to double cover.
305
+ static angleTo(a, b) {
306
+ const d = Math.abs(a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w);
307
+ return 2 * Math.acos(Math.min(1, d));
308
+ }
309
+ // out = conjugate of q (inverse for unit quaternions), without mutating q. Safe when out === q.
310
+ static conjugateInto(q, out) {
311
+ out.x = -q.x;
312
+ out.y = -q.y;
313
+ out.z = -q.z;
314
+ out.w = q.w;
315
+ return out;
316
+ }
317
+ // out = normalized lerp from a to b, taking the shorter path (negates b when dot < 0).
318
+ // Cheaper than slerp; non-constant angular velocity. Safe when out === a or out === b.
319
+ static nlerpInto(a, b, t, out) {
320
+ const d = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
321
+ const s = d < 0 ? -1 : 1;
322
+ const x = a.x + (b.x * s - a.x) * t;
323
+ const y = a.y + (b.y * s - a.y) * t;
324
+ const z = a.z + (b.z * s - a.z) * t;
325
+ const w = a.w + (b.w * s - a.w) * t;
326
+ const invLen = 1 / Math.hypot(x, y, z, w);
327
+ out.x = x * invLen;
328
+ out.y = y * invLen;
329
+ out.z = z * invLen;
330
+ out.w = w * invLen;
331
+ return out;
332
+ }
333
+ static nlerp(a, b, t) {
334
+ return Quat.nlerpInto(a, b, t, new Quat(0, 0, 0, 1));
335
+ }
336
+ // out = v rotated by unit quaternion q (no matrix, no allocation). Safe when out === v.
337
+ static rotateVecInto(q, v, out) {
338
+ // v' = v + 2*qw*(qv × v) + 2*(qv × (qv × v))
339
+ const qx = q.x, qy = q.y, qz = q.z, qw = q.w;
340
+ const vx = v.x, vy = v.y, vz = v.z;
341
+ // t = 2 * (qv × v)
342
+ const tx = 2 * (qy * vz - qz * vy);
343
+ const ty = 2 * (qz * vx - qx * vz);
344
+ const tz = 2 * (qx * vy - qy * vx);
345
+ out.x = vx + qw * tx + qy * tz - qz * ty;
346
+ out.y = vy + qw * ty + qz * tx - qx * tz;
347
+ out.z = vz + qw * tz + qx * ty - qy * tx;
348
+ return out;
349
+ }
350
+ static rotateVec(q, v) {
351
+ return Quat.rotateVecInto(q, v, new Vec3(0, 0, 0));
352
+ }
353
+ // out = v rotated by the inverse (conjugate) of unit quaternion q. Safe when out === v.
354
+ static rotateVecInvInto(q, v, out) {
355
+ const qx = -q.x, qy = -q.y, qz = -q.z, qw = q.w;
356
+ const vx = v.x, vy = v.y, vz = v.z;
357
+ const tx = 2 * (qy * vz - qz * vy);
358
+ const ty = 2 * (qz * vx - qx * vz);
359
+ const tz = 2 * (qx * vy - qy * vx);
360
+ out.x = vx + qw * tx + qy * tz - qz * ty;
361
+ out.y = vy + qw * ty + qz * tx - qx * tz;
362
+ out.z = vz + qw * tz + qx * ty - qy * tx;
363
+ return out;
364
+ }
365
+ static rotateVecInv(q, v) {
366
+ return Quat.rotateVecInvInto(q, v, new Vec3(0, 0, 0));
367
+ }
368
+ // out = shortest-arc rotation taking unit vector `from` to unit vector `to`.
369
+ // Matches Babylon's FromUnitVectorsToRef exactly, including the near-antiparallel
370
+ // branch (w = 1 + dot < 0.001 → 180° about a perpendicular picked the same way).
371
+ static fromUnitVectorsInto(from, to, out) {
372
+ const r = from.x * to.x + from.y * to.y + from.z * to.z + 1;
373
+ if (r < 0.001) {
374
+ if (Math.abs(from.x) > Math.abs(from.z)) {
375
+ out.setXYZW(-from.y, from.x, 0, 0);
376
+ }
377
+ else {
378
+ out.setXYZW(0, -from.z, from.y, 0);
379
+ }
380
+ }
381
+ else {
382
+ // q = (from × to, 1 + from·to)
383
+ out.setXYZW(from.y * to.z - from.z * to.y, from.z * to.x - from.x * to.z, from.x * to.y - from.y * to.x, r);
384
+ }
385
+ const invLen = 1 / Math.sqrt(out.x * out.x + out.y * out.y + out.z * out.z + out.w * out.w);
386
+ out.setXYZW(out.x * invLen, out.y * invLen, out.z * invLen, out.w * invLen);
387
+ return out;
388
+ }
389
+ static fromUnitVectors(from, to) {
390
+ return Quat.fromUnitVectorsInto(from, to, new Quat(0, 0, 0, 1));
391
+ }
392
+ // out = rotation taking the standard basis onto the orthonormal axes x, y, z
393
+ // (the columns of the column-major rotation matrix): rotateVec(out, (1,0,0)) = x, etc.
394
+ static fromBasisInto(x, y, z, out) {
395
+ // Shepperd's method on the 3x3 with columns x, y, z.
396
+ const m00 = x.x, m01 = x.y, m02 = x.z;
397
+ const m10 = y.x, m11 = y.y, m12 = y.z;
398
+ const m20 = z.x, m21 = z.y, m22 = z.z;
399
+ const trace = m00 + m11 + m22;
400
+ if (trace > 0) {
401
+ const s = 0.5 / Math.sqrt(trace + 1);
402
+ out.setXYZW((m12 - m21) * s, (m20 - m02) * s, (m01 - m10) * s, 0.25 / s);
403
+ }
404
+ else if (m00 > m11 && m00 > m22) {
405
+ const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
406
+ out.setXYZW(0.25 * s, (m10 + m01) / s, (m20 + m02) / s, (m12 - m21) / s);
407
+ }
408
+ else if (m11 > m22) {
409
+ const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
410
+ out.setXYZW((m10 + m01) / s, 0.25 * s, (m21 + m12) / s, (m20 - m02) / s);
411
+ }
412
+ else {
413
+ const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
414
+ out.setXYZW((m20 + m02) / s, (m21 + m12) / s, 0.25 * s, (m01 - m10) / s);
415
+ }
416
+ return out;
417
+ }
418
+ static fromBasis(x, y, z) {
419
+ return Quat.fromBasisInto(x, y, z, new Quat(0, 0, 0, 1));
420
+ }
421
+ // out = twist component of q around unit axis `a`, so that q = swing · twist
422
+ // (swing = Quat.multiply(q, conjugate(twist))). Singular when q is ~180° about an
423
+ // axis perpendicular to `a` — returns identity there.
424
+ static twistAroundAxisInto(q, a, out) {
425
+ const d = q.x * a.x + q.y * a.y + q.z * a.z;
426
+ const px = a.x * d;
427
+ const py = a.y * d;
428
+ const pz = a.z * d;
429
+ const len = Math.sqrt(px * px + py * py + pz * pz + q.w * q.w);
430
+ if (len < 1e-8) {
431
+ out.setIdentity();
432
+ return out;
433
+ }
434
+ out.setXYZW(px / len, py / len, pz / len, q.w / len);
435
+ return out;
436
+ }
437
+ static twistAroundAxis(q, a) {
438
+ return Quat.twistAroundAxisInto(q, a, new Quat(0, 0, 0, 1));
439
+ }
440
+ // out = orientation looking along `forward` (mapped to local +Z, the engine's LH forward)
441
+ // with local +Y toward `up`. Falls back to a reference up when forward ∥ up.
442
+ static lookRotationInto(forward, up, out) {
443
+ let zx = forward.x, zy = forward.y, zz = forward.z;
444
+ const zl = Math.sqrt(zx * zx + zy * zy + zz * zz);
445
+ if (zl === 0)
446
+ return out.setIdentity();
447
+ const zi = 1 / zl;
448
+ zx *= zi;
449
+ zy *= zi;
450
+ zz *= zi;
451
+ // x = up × z
452
+ let xx = up.y * zz - up.z * zy;
453
+ let xy = up.z * zx - up.x * zz;
454
+ let xz = up.x * zy - up.y * zx;
455
+ let xl = Math.sqrt(xx * xx + xy * xy + xz * xz);
456
+ if (xl < 1e-8) {
457
+ // forward ∥ up: substitute a reference up not parallel to forward
458
+ const uy = Math.abs(zy) < 0.99 ? 1 : 0;
459
+ const ux = 1 - uy;
460
+ xx = uy * zz;
461
+ xy = -ux * zz;
462
+ xz = ux * zy - uy * zx;
463
+ xl = Math.sqrt(xx * xx + xy * xy + xz * xz);
464
+ }
465
+ const xi = 1 / xl;
466
+ xx *= xi;
467
+ xy *= xi;
468
+ xz *= xi;
469
+ // y = z × x (unit: z ⊥ x)
470
+ const yx = zy * xz - zz * xy;
471
+ const yy = zz * xx - zx * xz;
472
+ const yz = zx * xy - zy * xx;
473
+ _lookX.setXYZ(xx, xy, xz);
474
+ _lookY.setXYZ(yx, yy, yz);
475
+ _lookZ.setXYZ(zx, zy, zz);
476
+ return Quat.fromBasisInto(_lookX, _lookY, _lookZ, out);
477
+ }
478
+ static lookRotation(forward, up) {
479
+ return Quat.lookRotationInto(forward, up, new Quat(0, 0, 0, 1));
480
+ }
481
+ // out = q with the Z axis mirrored: right-handed Y-up ↔ left-handed Y-up
482
+ // (conjugation by the Z-mirror; involutive). Pairs with Vec3.mirrorZInto. Safe when out === q.
483
+ static mirrorZInto(q, out) {
484
+ out.x = -q.x;
485
+ out.y = -q.y;
486
+ out.z = q.z;
487
+ out.w = q.w;
488
+ return out;
489
+ }
490
+ static mirrorZ(q) {
491
+ return new Quat(-q.x, -q.y, q.z, q.w);
492
+ }
493
+ // out = quaternion from euler angles (radians) applied in the given intrinsic order.
494
+ // fromEulerOrderInto(x, y, z, "YXZ", out) matches Quat.fromEuler (the MMD/PMX convention).
495
+ static fromEulerOrderInto(x, y, z, order, out) {
496
+ out.setIdentity();
497
+ for (let i = 0; i < 3; i++) {
498
+ const axis = order.charCodeAt(i) - 88; // "X" → 0, "Y" → 1, "Z" → 2
499
+ const angle = axis === 0 ? x : axis === 1 ? y : z;
500
+ const half = angle * 0.5;
501
+ const s = Math.sin(half);
502
+ const c = Math.cos(half);
503
+ const ax = out.x, ay = out.y, az = out.z, aw = out.w;
504
+ if (axis === 0) {
505
+ out.x = aw * s + ax * c;
506
+ out.y = ay * c + az * s;
507
+ out.z = az * c - ay * s;
508
+ out.w = aw * c - ax * s;
509
+ }
510
+ else if (axis === 1) {
511
+ out.x = ax * c - az * s;
512
+ out.y = aw * s + ay * c;
513
+ out.z = az * c + ax * s;
514
+ out.w = aw * c - ay * s;
515
+ }
516
+ else {
517
+ out.x = ax * c + ay * s;
518
+ out.y = ay * c - ax * s;
519
+ out.z = aw * s + az * c;
520
+ out.w = aw * c - az * s;
521
+ }
522
+ }
523
+ return out;
524
+ }
525
+ static fromEulerOrder(x, y, z, order) {
526
+ return Quat.fromEulerOrderInto(x, y, z, order, new Quat(0, 0, 0, 1));
527
+ }
528
+ // Extract euler angles (radians) in the given intrinsic order into out (out.x/y/z = rotation
529
+ // about X/Y/Z). At gimbal lock (middle angle ±90°) the split between first and third angle is
530
+ // ambiguous; the third is set to 0.
531
+ static toEulerOrderInto(q, order, out) {
532
+ Mat4.fromQuatInto(q.x, q.y, q.z, q.w, _eulerMat, 0);
533
+ const m = _eulerMat;
534
+ const m11 = m[0], m12 = m[4], m13 = m[8];
535
+ const m21 = m[1], m22 = m[5], m23 = m[9];
536
+ const m31 = m[2], m32 = m[6], m33 = m[10];
537
+ const clamp = (v) => (v < -1 ? -1 : v > 1 ? 1 : v);
538
+ switch (order) {
539
+ case "XYZ":
540
+ out.y = Math.asin(clamp(m13));
541
+ if (Math.abs(m13) < 0.9999999) {
542
+ out.x = Math.atan2(-m23, m33);
543
+ out.z = Math.atan2(-m12, m11);
544
+ }
545
+ else {
546
+ out.x = Math.atan2(m32, m22);
547
+ out.z = 0;
548
+ }
549
+ break;
550
+ case "YXZ":
551
+ out.x = Math.asin(-clamp(m23));
552
+ if (Math.abs(m23) < 0.9999999) {
553
+ out.y = Math.atan2(m13, m33);
554
+ out.z = Math.atan2(m21, m22);
555
+ }
556
+ else {
557
+ out.y = Math.atan2(-m31, m11);
558
+ out.z = 0;
559
+ }
560
+ break;
561
+ case "ZXY":
562
+ out.x = Math.asin(clamp(m32));
563
+ if (Math.abs(m32) < 0.9999999) {
564
+ out.y = Math.atan2(-m31, m33);
565
+ out.z = Math.atan2(-m12, m22);
566
+ }
567
+ else {
568
+ out.y = 0;
569
+ out.z = Math.atan2(m21, m11);
570
+ }
571
+ break;
572
+ case "ZYX":
573
+ out.y = Math.asin(-clamp(m31));
574
+ if (Math.abs(m31) < 0.9999999) {
575
+ out.x = Math.atan2(m32, m33);
576
+ out.z = Math.atan2(m21, m11);
577
+ }
578
+ else {
579
+ out.x = 0;
580
+ out.z = Math.atan2(-m12, m22);
581
+ }
582
+ break;
583
+ case "YZX":
584
+ out.z = Math.asin(clamp(m21));
585
+ if (Math.abs(m21) < 0.9999999) {
586
+ out.x = Math.atan2(-m23, m22);
587
+ out.y = Math.atan2(-m31, m11);
588
+ }
589
+ else {
590
+ out.x = 0;
591
+ out.y = Math.atan2(m13, m33);
592
+ }
593
+ break;
594
+ case "XZY":
595
+ out.z = Math.asin(-clamp(m12));
596
+ if (Math.abs(m12) < 0.9999999) {
597
+ out.x = Math.atan2(m32, m22);
598
+ out.y = Math.atan2(m13, m11);
599
+ }
600
+ else {
601
+ out.x = Math.atan2(-m23, m33);
602
+ out.y = 0;
603
+ }
604
+ break;
605
+ }
606
+ return out;
607
+ }
608
+ static toEulerOrder(q, order) {
609
+ return Quat.toEulerOrderInto(q, order, new Vec3(0, 0, 0));
610
+ }
289
611
  }
290
612
  export class Mat4 {
291
613
  constructor(values) {
@@ -762,6 +1084,12 @@ export class Mat4 {
762
1084
  return new Mat4(out);
763
1085
  }
764
1086
  }
1087
+ // Module-private scratch for Quat.toEulerOrderInto / lookRotationInto (never handed out,
1088
+ // so no cross-call stomping with the public pools below).
1089
+ const _eulerMat = new Float32Array(16);
1090
+ const _lookX = new Vec3(0, 0, 0);
1091
+ const _lookY = new Vec3(0, 0, 0);
1092
+ const _lookZ = new Vec3(0, 0, 0);
765
1093
  // Preallocated scratch instances for hot paths. Each subsystem should use its own
766
1094
  // slot to avoid cross-call stomping. Bump the count if more call sites need scratch.
767
1095
  export const scratchMat4Values = [
@@ -1,2 +1,2 @@
1
- export declare const SHADOW_DEPTH_SHADER_WGSL = "\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {\n let pos4 = vec4f(position, 1.0);\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n return lp.viewProj * vec4f(sp.xyz, 1.0);\n}\n";
1
+ export declare const SHADOW_DEPTH_SHADER_WGSL = "\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@group(0) @binding(2) var texSampler: sampler;\n@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;\nstruct MaterialDiffuse { diffuse: vec4f, };\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n};\n\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {\n let pos4 = vec4f(position, 1.0);\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n var out: VSOut;\n out.position = lp.viewProj * vec4f(sp.xyz, 1.0);\n out.uv = uv;\n return out;\n}\n\n@fragment fn fs(in: VSOut) {\n let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;\n if (alpha < 0.5) { discard; }\n}\n";
2
2
  //# sourceMappingURL=shadow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,+tBAcpC,CAAA"}
1
+ {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,wBAAwB,otCAgCpC,CAAA"}
@@ -1,16 +1,39 @@
1
- // Shadow map depth-only pass. Skinned VS, no FS (depth-only attachment).
1
+ // Shadow map depth pass. Skinned VS + alpha-test FS (depth-only attachment, no
2
+ // color targets): texels where diffuse-texture alpha × material alpha fall below
3
+ // the cutoff are discarded, so lace casts lace-shaped shadows and a true veil
4
+ // casts nothing — per texel, with no per-material sheerness classification.
5
+ // Group 1 is the main pass's per-material bind group reused as-is; only
6
+ // bindings 0/1 are declared here (a layout may carry bindings a shader ignores).
2
7
  export const SHADOW_DEPTH_SHADER_WGSL = /* wgsl */ `
3
8
  struct LightVP { viewProj: mat4x4f, };
4
9
  @group(0) @binding(0) var<uniform> lp: LightVP;
5
10
  @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
11
+ @group(0) @binding(2) var texSampler: sampler;
12
+ @group(1) @binding(0) var diffuseTexture: texture_2d<f32>;
13
+ struct MaterialDiffuse { diffuse: vec4f, };
14
+ @group(1) @binding(1) var<uniform> material: MaterialDiffuse;
15
+
16
+ struct VSOut {
17
+ @builtin(position) position: vec4f,
18
+ @location(0) uv: vec2f,
19
+ };
20
+
6
21
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
7
- @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
22
+ @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {
8
23
  let pos4 = vec4f(position, 1.0);
9
24
  let ws = weights0.x + weights0.y + weights0.z + weights0.w;
10
25
  let inv = select(1.0, 1.0 / ws, ws > 0.0001);
11
26
  let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
12
27
  var sp = vec4f(0.0);
13
28
  for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
14
- return lp.viewProj * vec4f(sp.xyz, 1.0);
29
+ var out: VSOut;
30
+ out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
31
+ out.uv = uv;
32
+ return out;
33
+ }
34
+
35
+ @fragment fn fs(in: VSOut) {
36
+ let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;
37
+ if (alpha < 0.5) { discard; }
15
38
  }
16
39
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.27.3",
3
+ "version": "0.28.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",
package/src/engine.ts CHANGED
@@ -385,7 +385,8 @@ interface DrawCall {
385
385
  // Present only for material draw calls (opaque/transparent) — the grouping walk skips
386
386
  // any draw call without it.
387
387
  baseBindGroupEntries?: GPUBindGroupEntry[]
388
- /** Material draws only: false = excluded from the shadow map (fully sheer). */
388
+ /** Material draws only: false = excluded from the shadow map (PMX cast-shadow
389
+ * flag off). Sheer texels are cut per fragment by the shadow pass's alpha test. */
389
390
  castsShadow?: boolean
390
391
  /** Edge-flagged materials: interleaved inverted-hull outline drawn right after
391
392
  * this material with the outline pipeline. Shares this call's index range;
@@ -490,7 +491,7 @@ function buildAlphaSampler(
490
491
  * neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
491
492
  * Bucketing itself is binary (babylon-mmd parity): ANY translucent coverage
492
493
  * routes to the alpha-blend bucket. `avg` below this threshold additionally
493
- * marks a material as fully sheer (a veil), which vetoes shadow casting. */
494
+ * marks a material as fully sheer (a veil). */
494
495
  const SHEER_ALPHA_THRESHOLD = 0.7
495
496
  function materialAlphaStats(
496
497
  verts: Float32Array,
@@ -1770,6 +1771,7 @@ export class Engine {
1770
1771
  entries: [
1771
1772
  { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
1772
1773
  { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
1774
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
1773
1775
  ],
1774
1776
  })
1775
1777
  const shadowShader = this.device.createShaderModule({
@@ -1778,8 +1780,13 @@ export class Engine {
1778
1780
  })
1779
1781
  this.shadowDepthPipeline = this.device.createRenderPipeline({
1780
1782
  label: "shadow depth pipeline",
1781
- layout: this.device.createPipelineLayout({ bindGroupLayouts: [shadowBindGroupLayout] }),
1783
+ // Group 1 is the main pass's per-material layout so each shadow draw can
1784
+ // rebind the draw call's existing material bind group for the alpha test.
1785
+ layout: this.device.createPipelineLayout({
1786
+ bindGroupLayouts: [shadowBindGroupLayout, this.mainPerMaterialBindGroupLayout],
1787
+ }),
1782
1788
  vertex: { module: shadowShader, entryPoint: "vs", buffers: fullVertexBuffers as GPUVertexBufferLayout[] },
1789
+ fragment: { module: shadowShader, entryPoint: "fs", targets: [] },
1783
1790
  primitive: { cullMode: "none" },
1784
1791
  depthStencil: {
1785
1792
  format: "depth32float",
@@ -3357,6 +3364,7 @@ export class Engine {
3357
3364
  entries: [
3358
3365
  { binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
3359
3366
  { binding: 1, resource: { buffer: skinMatrixBuffer } },
3367
+ { binding: 2, resource: this.materialSampler },
3360
3368
  ],
3361
3369
  })
3362
3370
 
@@ -3687,9 +3695,12 @@ export class Engine {
3687
3695
  // only guards against centroid-sampling noise on genuinely solid cloth.
3688
3696
  const sheer = stats.avg < SHEER_ALPHA_THRESHOLD
3689
3697
  const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || stats.translucentFrac > 0.02
3690
- // Shadow casting: the PMX author's own flag (bit 0x04, cast self-shadow),
3691
- // still vetoed for fully sheer cloth a veil must not cast a solid sheet.
3692
- const castsShadow = (mat.edgeFlag & 0x04) !== 0 && !sheer
3698
+ // Shadow casting: the PMX author's own flag (bit 0x04, cast self-shadow)
3699
+ // exactly what MMD honors. Sheerness is handled per texel by the shadow
3700
+ // pass's alpha test, not by a per-material veto: a threshold on avg alpha
3701
+ // misclassified fully-worn opaque dresses (avg 0.69) as veils and stripped
3702
+ // their shadows, while any lower cliff would strand the next model.
3703
+ const castsShadow = (mat.edgeFlag & 0x04) !== 0
3693
3704
  // Load-time classification log — one line per material, cheap and
3694
3705
  // invaluable when a model renders wrong (bucket/outline/shadow disputes).
3695
3706
  console.info(
@@ -4726,7 +4737,9 @@ export class Engine {
4726
4737
  sp.setVertexBuffer(2, inst.weightsBuffer)
4727
4738
  sp.setIndexBuffer(inst.indexBuffer, "uint32")
4728
4739
  for (const draw of inst.shadowDrawCalls) {
4729
- if (this.shouldRenderDrawCall(inst, draw)) sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
4740
+ if (!this.shouldRenderDrawCall(inst, draw)) continue
4741
+ sp.setBindGroup(1, draw.bindGroup)
4742
+ sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
4730
4743
  }
4731
4744
  }
4732
4745
 
package/src/ik-solver.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // IK solver (MMD-style; see Saba MMDIkSolver.cpp)
2
2
 
3
- import { Mat4, Quat, Vec3 } from "./math"
3
+ import { Mat4, Quat, Vec3, type EulerOrder } from "./math"
4
4
  import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
5
5
 
6
6
  // Callback type for updating world matrix (provided by model to handle append transformations)
@@ -80,7 +80,6 @@ const _ikVec: Vec3[] = [
80
80
  ]
81
81
  const _ikQuat: Quat[] = [
82
82
  new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1),
83
- new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1), new Quat(0, 0, 0, 1),
84
83
  ]
85
84
  const _ikMat: Float32Array[] = [
86
85
  new Float32Array(16), new Float32Array(16), new Float32Array(16), new Float32Array(16),
@@ -215,7 +214,7 @@ export class IKSolverSystem {
215
214
  // _ikVec[0]=chainPos, [1]=ikPos, [2]=targetPos, [3]=chainTargetVec, [4]=chainIkVec,
216
215
  // [5]=rotAxis, [6]=finalAxis, [7]=eulerTmp, [8]=limitedEuler
217
216
  // _ikMat[0]=parentRot, [1]=invParentRot, [2]=quatToMatTmp
218
- // _ikQuat[0]=ikRotation, [1]=combinedRot, [2]=localRotConj, [3..5]=axisAngleTmp
217
+ // _ikQuat[0]=ikRotation, [1]=combinedRot, [2]=localRotConj
219
218
 
220
219
  const chainPos = Vec3.setFromMat4Translation(worldMatrices[chainBoneIndex].values, _ikVec[0])
221
220
  const ikPos = Vec3.setFromMat4Translation(worldMatrices[ikBoneIndex].values, _ikVec[1])
@@ -340,16 +339,8 @@ export class IKSolverSystem {
340
339
  return Math.sqrt(dx * dx + dy * dy + dz * dz)
341
340
  }
342
341
 
343
- // Euler axis triples for each rotation order (indexed by order enum).
344
- // Reused to avoid allocations in reconstructQuatFromEulerInto.
345
- private static readonly EULER_AXES: readonly [number, number, number][][] = [
346
- // YXZ: Y, X, Z
347
- [[0, 1, 0], [1, 0, 0], [0, 0, 1]],
348
- // ZYX: Z, Y, X
349
- [[0, 0, 1], [0, 1, 0], [1, 0, 0]],
350
- // XZY: X, Z, Y
351
- [[1, 0, 0], [0, 0, 1], [0, 1, 0]],
352
- ]
342
+ // Maps InternalEulerRotationOrder to the shared math-layer order names.
343
+ private static readonly EULER_ORDER_NAMES: readonly EulerOrder[] = ["YXZ", "ZYX", "XZY"]
353
344
 
354
345
  private static extractEulerAnglesInto(quat: Quat, order: InternalEulerRotationOrder, out: Vec3): void {
355
346
  Mat4.fromQuatInto(quat.x, quat.y, quat.z, quat.w, _ikMat[2], 0)
@@ -396,27 +387,7 @@ export class IKSolverSystem {
396
387
  }
397
388
 
398
389
  private static reconstructQuatFromEulerInto(euler: Vec3, order: InternalEulerRotationOrder, out: Quat): void {
399
- const axes = this.EULER_AXES[order]
400
- const a1 = axes[0], a2 = axes[1], a3 = axes[2]
401
- const ang1 =
402
- order === InternalEulerRotationOrder.YXZ ? euler.y
403
- : order === InternalEulerRotationOrder.ZYX ? euler.z
404
- : euler.x
405
- const ang2 =
406
- order === InternalEulerRotationOrder.YXZ ? euler.x
407
- : order === InternalEulerRotationOrder.ZYX ? euler.y
408
- : euler.z
409
- const ang3 =
410
- order === InternalEulerRotationOrder.YXZ ? euler.z
411
- : order === InternalEulerRotationOrder.ZYX ? euler.x
412
- : euler.y
413
-
414
- // result = axisAngle(a1, ang1); then *= axisAngle(a2, ang2); then *= axisAngle(a3, ang3)
415
- Quat.fromAxisAngleInto(a1[0], a1[1], a1[2], ang1, out)
416
- Quat.fromAxisAngleInto(a2[0], a2[1], a2[2], ang2, _ikQuat[3])
417
- Quat.multiplyInto(out, _ikQuat[3], out)
418
- Quat.fromAxisAngleInto(a3[0], a3[1], a3[2], ang3, _ikQuat[3])
419
- Quat.multiplyInto(out, _ikQuat[3], out)
390
+ Quat.fromEulerOrderInto(euler.x, euler.y, euler.z, this.EULER_ORDER_NAMES[order], out)
420
391
  }
421
392
 
422
393
  // Write parent's world rotation (translation stripped) into out Float32Array.
package/src/index.ts CHANGED
@@ -53,7 +53,7 @@ export { STOCKINGS_GRAPH } from "./graph/presets/stockings"
53
53
  export { EYE_GRAPH } from "./graph/presets/eye"
54
54
  export { FACE_GRAPH } from "./graph/presets/face"
55
55
  export { Model } from "./model"
56
- export { Vec3, Quat, Mat4 } from "./math"
56
+ export { Vec3, Quat, Mat4, easeInOut, type EulerOrder } from "./math"
57
57
  export type {
58
58
  AnimationClip,
59
59
  AnimationPlayOptions,
@@ -64,7 +64,12 @@ export type {
64
64
  BoneInterpolation,
65
65
  ControlPoint,
66
66
  } from "./animation"
67
- export { FPS } from "./animation"
67
+ export {
68
+ FPS,
69
+ bezierInterpolate,
70
+ interpolateControlPoints,
71
+ rawInterpolationToBoneInterpolation,
72
+ } from "./animation"
68
73
  export { VMDLoader, type CameraKeyframe, type IkFrame } from "./vmd-loader"
69
74
  export { CameraAnimation, type CameraPose } from "./camera-animation"
70
75
  export { RezePhysics } from "./physics"