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/src/math.ts CHANGED
@@ -3,6 +3,10 @@ export function easeInOut(t: number): number {
3
3
  return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
4
4
  }
5
5
 
6
+ // Euler rotation order, read as intrinsic axes left to right:
7
+ // "YXZ" = rotate about Y, then local X, then local Z (equals extrinsic ZXY — the MMD/PMX convention).
8
+ export type EulerOrder = "XYZ" | "XZY" | "YXZ" | "YZX" | "ZXY" | "ZYX"
9
+
6
10
  export class Vec3 {
7
11
  x: number
8
12
  y: number
@@ -113,6 +117,18 @@ export class Vec3 {
113
117
  return out
114
118
  }
115
119
 
120
+ // out = (x, y, -z): right-handed Y-up ↔ left-handed Y-up (involutive). Safe when out === v.
121
+ static mirrorZInto(v: Vec3, out: Vec3): Vec3 {
122
+ out.x = v.x
123
+ out.y = v.y
124
+ out.z = -v.z
125
+ return out
126
+ }
127
+
128
+ static mirrorZ(v: Vec3): Vec3 {
129
+ return new Vec3(v.x, v.y, -v.z)
130
+ }
131
+
116
132
  // In-place normalize returning length squared info via Vec3. Alias for normalize() but explicit.
117
133
  normalizeInPlace(): Vec3 {
118
134
  return this.normalize()
@@ -333,6 +349,328 @@ export class Quat {
333
349
 
334
350
  return new Quat(x, y, z, w).normalize()
335
351
  }
352
+
353
+ // 4D dot product. Negative means a and b are on opposite hemispheres (same rotation
354
+ // when |dot| ≈ 1 either way — quaternion double cover).
355
+ static dot(a: Quat, b: Quat): number {
356
+ return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
357
+ }
358
+
359
+ // Rotation angle between a and b in radians, insensitive to double cover.
360
+ static angleTo(a: Quat, b: Quat): number {
361
+ const d = Math.abs(a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w)
362
+ return 2 * Math.acos(Math.min(1, d))
363
+ }
364
+
365
+ // out = conjugate of q (inverse for unit quaternions), without mutating q. Safe when out === q.
366
+ static conjugateInto(q: Quat, out: Quat): Quat {
367
+ out.x = -q.x
368
+ out.y = -q.y
369
+ out.z = -q.z
370
+ out.w = q.w
371
+ return out
372
+ }
373
+
374
+ // out = normalized lerp from a to b, taking the shorter path (negates b when dot < 0).
375
+ // Cheaper than slerp; non-constant angular velocity. Safe when out === a or out === b.
376
+ static nlerpInto(a: Quat, b: Quat, t: number, out: Quat): Quat {
377
+ const d = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
378
+ const s = d < 0 ? -1 : 1
379
+ const x = a.x + (b.x * s - a.x) * t
380
+ const y = a.y + (b.y * s - a.y) * t
381
+ const z = a.z + (b.z * s - a.z) * t
382
+ const w = a.w + (b.w * s - a.w) * t
383
+ const invLen = 1 / Math.hypot(x, y, z, w)
384
+ out.x = x * invLen
385
+ out.y = y * invLen
386
+ out.z = z * invLen
387
+ out.w = w * invLen
388
+ return out
389
+ }
390
+
391
+ static nlerp(a: Quat, b: Quat, t: number): Quat {
392
+ return Quat.nlerpInto(a, b, t, new Quat(0, 0, 0, 1))
393
+ }
394
+
395
+ // out = v rotated by unit quaternion q (no matrix, no allocation). Safe when out === v.
396
+ static rotateVecInto(q: Quat, v: Vec3, out: Vec3): Vec3 {
397
+ // v' = v + 2*qw*(qv × v) + 2*(qv × (qv × v))
398
+ const qx = q.x, qy = q.y, qz = q.z, qw = q.w
399
+ const vx = v.x, vy = v.y, vz = v.z
400
+ // t = 2 * (qv × v)
401
+ const tx = 2 * (qy * vz - qz * vy)
402
+ const ty = 2 * (qz * vx - qx * vz)
403
+ const tz = 2 * (qx * vy - qy * vx)
404
+ out.x = vx + qw * tx + qy * tz - qz * ty
405
+ out.y = vy + qw * ty + qz * tx - qx * tz
406
+ out.z = vz + qw * tz + qx * ty - qy * tx
407
+ return out
408
+ }
409
+
410
+ static rotateVec(q: Quat, v: Vec3): Vec3 {
411
+ return Quat.rotateVecInto(q, v, new Vec3(0, 0, 0))
412
+ }
413
+
414
+ // out = v rotated by the inverse (conjugate) of unit quaternion q. Safe when out === v.
415
+ static rotateVecInvInto(q: Quat, v: Vec3, out: Vec3): Vec3 {
416
+ const qx = -q.x, qy = -q.y, qz = -q.z, qw = q.w
417
+ const vx = v.x, vy = v.y, vz = v.z
418
+ const tx = 2 * (qy * vz - qz * vy)
419
+ const ty = 2 * (qz * vx - qx * vz)
420
+ const tz = 2 * (qx * vy - qy * vx)
421
+ out.x = vx + qw * tx + qy * tz - qz * ty
422
+ out.y = vy + qw * ty + qz * tx - qx * tz
423
+ out.z = vz + qw * tz + qx * ty - qy * tx
424
+ return out
425
+ }
426
+
427
+ static rotateVecInv(q: Quat, v: Vec3): Vec3 {
428
+ return Quat.rotateVecInvInto(q, v, new Vec3(0, 0, 0))
429
+ }
430
+
431
+ // out = shortest-arc rotation taking unit vector `from` to unit vector `to`.
432
+ // Matches Babylon's FromUnitVectorsToRef exactly, including the near-antiparallel
433
+ // branch (w = 1 + dot < 0.001 → 180° about a perpendicular picked the same way).
434
+ static fromUnitVectorsInto(from: Vec3, to: Vec3, out: Quat): Quat {
435
+ const r = from.x * to.x + from.y * to.y + from.z * to.z + 1
436
+ if (r < 0.001) {
437
+ if (Math.abs(from.x) > Math.abs(from.z)) {
438
+ out.setXYZW(-from.y, from.x, 0, 0)
439
+ } else {
440
+ out.setXYZW(0, -from.z, from.y, 0)
441
+ }
442
+ } else {
443
+ // q = (from × to, 1 + from·to)
444
+ out.setXYZW(
445
+ from.y * to.z - from.z * to.y,
446
+ from.z * to.x - from.x * to.z,
447
+ from.x * to.y - from.y * to.x,
448
+ r
449
+ )
450
+ }
451
+ const invLen = 1 / Math.sqrt(out.x * out.x + out.y * out.y + out.z * out.z + out.w * out.w)
452
+ out.setXYZW(out.x * invLen, out.y * invLen, out.z * invLen, out.w * invLen)
453
+ return out
454
+ }
455
+
456
+ static fromUnitVectors(from: Vec3, to: Vec3): Quat {
457
+ return Quat.fromUnitVectorsInto(from, to, new Quat(0, 0, 0, 1))
458
+ }
459
+
460
+ // out = rotation taking the standard basis onto the orthonormal axes x, y, z
461
+ // (the columns of the column-major rotation matrix): rotateVec(out, (1,0,0)) = x, etc.
462
+ static fromBasisInto(x: Vec3, y: Vec3, z: Vec3, out: Quat): Quat {
463
+ // Shepperd's method on the 3x3 with columns x, y, z.
464
+ const m00 = x.x, m01 = x.y, m02 = x.z
465
+ const m10 = y.x, m11 = y.y, m12 = y.z
466
+ const m20 = z.x, m21 = z.y, m22 = z.z
467
+ const trace = m00 + m11 + m22
468
+ if (trace > 0) {
469
+ const s = 0.5 / Math.sqrt(trace + 1)
470
+ out.setXYZW((m12 - m21) * s, (m20 - m02) * s, (m01 - m10) * s, 0.25 / s)
471
+ } else if (m00 > m11 && m00 > m22) {
472
+ const s = 2 * Math.sqrt(1 + m00 - m11 - m22)
473
+ out.setXYZW(0.25 * s, (m10 + m01) / s, (m20 + m02) / s, (m12 - m21) / s)
474
+ } else if (m11 > m22) {
475
+ const s = 2 * Math.sqrt(1 + m11 - m00 - m22)
476
+ out.setXYZW((m10 + m01) / s, 0.25 * s, (m21 + m12) / s, (m20 - m02) / s)
477
+ } else {
478
+ const s = 2 * Math.sqrt(1 + m22 - m00 - m11)
479
+ out.setXYZW((m20 + m02) / s, (m21 + m12) / s, 0.25 * s, (m01 - m10) / s)
480
+ }
481
+ return out
482
+ }
483
+
484
+ static fromBasis(x: Vec3, y: Vec3, z: Vec3): Quat {
485
+ return Quat.fromBasisInto(x, y, z, new Quat(0, 0, 0, 1))
486
+ }
487
+
488
+ // out = twist component of q around unit axis `a`, so that q = swing · twist
489
+ // (swing = Quat.multiply(q, conjugate(twist))). Singular when q is ~180° about an
490
+ // axis perpendicular to `a` — returns identity there.
491
+ static twistAroundAxisInto(q: Quat, a: Vec3, out: Quat): Quat {
492
+ const d = q.x * a.x + q.y * a.y + q.z * a.z
493
+ const px = a.x * d
494
+ const py = a.y * d
495
+ const pz = a.z * d
496
+ const len = Math.sqrt(px * px + py * py + pz * pz + q.w * q.w)
497
+ if (len < 1e-8) {
498
+ out.setIdentity()
499
+ return out
500
+ }
501
+ out.setXYZW(px / len, py / len, pz / len, q.w / len)
502
+ return out
503
+ }
504
+
505
+ static twistAroundAxis(q: Quat, a: Vec3): Quat {
506
+ return Quat.twistAroundAxisInto(q, a, new Quat(0, 0, 0, 1))
507
+ }
508
+
509
+ // out = orientation looking along `forward` (mapped to local +Z, the engine's LH forward)
510
+ // with local +Y toward `up`. Falls back to a reference up when forward ∥ up.
511
+ static lookRotationInto(forward: Vec3, up: Vec3, out: Quat): Quat {
512
+ let zx = forward.x, zy = forward.y, zz = forward.z
513
+ const zl = Math.sqrt(zx * zx + zy * zy + zz * zz)
514
+ if (zl === 0) return out.setIdentity()
515
+ const zi = 1 / zl
516
+ zx *= zi; zy *= zi; zz *= zi
517
+ // x = up × z
518
+ let xx = up.y * zz - up.z * zy
519
+ let xy = up.z * zx - up.x * zz
520
+ let xz = up.x * zy - up.y * zx
521
+ let xl = Math.sqrt(xx * xx + xy * xy + xz * xz)
522
+ if (xl < 1e-8) {
523
+ // forward ∥ up: substitute a reference up not parallel to forward
524
+ const uy = Math.abs(zy) < 0.99 ? 1 : 0
525
+ const ux = 1 - uy
526
+ xx = uy * zz
527
+ xy = -ux * zz
528
+ xz = ux * zy - uy * zx
529
+ xl = Math.sqrt(xx * xx + xy * xy + xz * xz)
530
+ }
531
+ const xi = 1 / xl
532
+ xx *= xi; xy *= xi; xz *= xi
533
+ // y = z × x (unit: z ⊥ x)
534
+ const yx = zy * xz - zz * xy
535
+ const yy = zz * xx - zx * xz
536
+ const yz = zx * xy - zy * xx
537
+ _lookX.setXYZ(xx, xy, xz)
538
+ _lookY.setXYZ(yx, yy, yz)
539
+ _lookZ.setXYZ(zx, zy, zz)
540
+ return Quat.fromBasisInto(_lookX, _lookY, _lookZ, out)
541
+ }
542
+
543
+ static lookRotation(forward: Vec3, up: Vec3): Quat {
544
+ return Quat.lookRotationInto(forward, up, new Quat(0, 0, 0, 1))
545
+ }
546
+
547
+ // out = q with the Z axis mirrored: right-handed Y-up ↔ left-handed Y-up
548
+ // (conjugation by the Z-mirror; involutive). Pairs with Vec3.mirrorZInto. Safe when out === q.
549
+ static mirrorZInto(q: Quat, out: Quat): Quat {
550
+ out.x = -q.x
551
+ out.y = -q.y
552
+ out.z = q.z
553
+ out.w = q.w
554
+ return out
555
+ }
556
+
557
+ static mirrorZ(q: Quat): Quat {
558
+ return new Quat(-q.x, -q.y, q.z, q.w)
559
+ }
560
+
561
+ // out = quaternion from euler angles (radians) applied in the given intrinsic order.
562
+ // fromEulerOrderInto(x, y, z, "YXZ", out) matches Quat.fromEuler (the MMD/PMX convention).
563
+ static fromEulerOrderInto(x: number, y: number, z: number, order: EulerOrder, out: Quat): Quat {
564
+ out.setIdentity()
565
+ for (let i = 0; i < 3; i++) {
566
+ const axis = order.charCodeAt(i) - 88 // "X" → 0, "Y" → 1, "Z" → 2
567
+ const angle = axis === 0 ? x : axis === 1 ? y : z
568
+ const half = angle * 0.5
569
+ const s = Math.sin(half)
570
+ const c = Math.cos(half)
571
+ const ax = out.x, ay = out.y, az = out.z, aw = out.w
572
+ if (axis === 0) {
573
+ out.x = aw * s + ax * c
574
+ out.y = ay * c + az * s
575
+ out.z = az * c - ay * s
576
+ out.w = aw * c - ax * s
577
+ } else if (axis === 1) {
578
+ out.x = ax * c - az * s
579
+ out.y = aw * s + ay * c
580
+ out.z = az * c + ax * s
581
+ out.w = aw * c - ay * s
582
+ } else {
583
+ out.x = ax * c + ay * s
584
+ out.y = ay * c - ax * s
585
+ out.z = aw * s + az * c
586
+ out.w = aw * c - az * s
587
+ }
588
+ }
589
+ return out
590
+ }
591
+
592
+ static fromEulerOrder(x: number, y: number, z: number, order: EulerOrder): Quat {
593
+ return Quat.fromEulerOrderInto(x, y, z, order, new Quat(0, 0, 0, 1))
594
+ }
595
+
596
+ // Extract euler angles (radians) in the given intrinsic order into out (out.x/y/z = rotation
597
+ // about X/Y/Z). At gimbal lock (middle angle ±90°) the split between first and third angle is
598
+ // ambiguous; the third is set to 0.
599
+ static toEulerOrderInto(q: Quat, order: EulerOrder, out: Vec3): Vec3 {
600
+ Mat4.fromQuatInto(q.x, q.y, q.z, q.w, _eulerMat, 0)
601
+ const m = _eulerMat
602
+ const m11 = m[0], m12 = m[4], m13 = m[8]
603
+ const m21 = m[1], m22 = m[5], m23 = m[9]
604
+ const m31 = m[2], m32 = m[6], m33 = m[10]
605
+ const clamp = (v: number) => (v < -1 ? -1 : v > 1 ? 1 : v)
606
+ switch (order) {
607
+ case "XYZ":
608
+ out.y = Math.asin(clamp(m13))
609
+ if (Math.abs(m13) < 0.9999999) {
610
+ out.x = Math.atan2(-m23, m33)
611
+ out.z = Math.atan2(-m12, m11)
612
+ } else {
613
+ out.x = Math.atan2(m32, m22)
614
+ out.z = 0
615
+ }
616
+ break
617
+ case "YXZ":
618
+ out.x = Math.asin(-clamp(m23))
619
+ if (Math.abs(m23) < 0.9999999) {
620
+ out.y = Math.atan2(m13, m33)
621
+ out.z = Math.atan2(m21, m22)
622
+ } else {
623
+ out.y = Math.atan2(-m31, m11)
624
+ out.z = 0
625
+ }
626
+ break
627
+ case "ZXY":
628
+ out.x = Math.asin(clamp(m32))
629
+ if (Math.abs(m32) < 0.9999999) {
630
+ out.y = Math.atan2(-m31, m33)
631
+ out.z = Math.atan2(-m12, m22)
632
+ } else {
633
+ out.y = 0
634
+ out.z = Math.atan2(m21, m11)
635
+ }
636
+ break
637
+ case "ZYX":
638
+ out.y = Math.asin(-clamp(m31))
639
+ if (Math.abs(m31) < 0.9999999) {
640
+ out.x = Math.atan2(m32, m33)
641
+ out.z = Math.atan2(m21, m11)
642
+ } else {
643
+ out.x = 0
644
+ out.z = Math.atan2(-m12, m22)
645
+ }
646
+ break
647
+ case "YZX":
648
+ out.z = Math.asin(clamp(m21))
649
+ if (Math.abs(m21) < 0.9999999) {
650
+ out.x = Math.atan2(-m23, m22)
651
+ out.y = Math.atan2(-m31, m11)
652
+ } else {
653
+ out.x = 0
654
+ out.y = Math.atan2(m13, m33)
655
+ }
656
+ break
657
+ case "XZY":
658
+ out.z = Math.asin(-clamp(m12))
659
+ if (Math.abs(m12) < 0.9999999) {
660
+ out.x = Math.atan2(m32, m22)
661
+ out.y = Math.atan2(m13, m11)
662
+ } else {
663
+ out.x = Math.atan2(-m23, m33)
664
+ out.y = 0
665
+ }
666
+ break
667
+ }
668
+ return out
669
+ }
670
+
671
+ static toEulerOrder(q: Quat, order: EulerOrder): Vec3 {
672
+ return Quat.toEulerOrderInto(q, order, new Vec3(0, 0, 0))
673
+ }
336
674
  }
337
675
 
338
676
  export class Mat4 {
@@ -837,6 +1175,13 @@ export class Mat4 {
837
1175
  }
838
1176
  }
839
1177
 
1178
+ // Module-private scratch for Quat.toEulerOrderInto / lookRotationInto (never handed out,
1179
+ // so no cross-call stomping with the public pools below).
1180
+ const _eulerMat = new Float32Array(16)
1181
+ const _lookX = new Vec3(0, 0, 0)
1182
+ const _lookY = new Vec3(0, 0, 0)
1183
+ const _lookZ = new Vec3(0, 0, 0)
1184
+
840
1185
  // Preallocated scratch instances for hot paths. Each subsystem should use its own
841
1186
  // slot to avoid cross-call stomping. Bump the count if more call sites need scratch.
842
1187
  export const scratchMat4Values: Float32Array[] = [
@@ -1,17 +1,40 @@
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
 
3
8
  export const SHADOW_DEPTH_SHADER_WGSL = /* wgsl */ `
4
9
  struct LightVP { viewProj: mat4x4f, };
5
10
  @group(0) @binding(0) var<uniform> lp: LightVP;
6
11
  @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
12
+ @group(0) @binding(2) var texSampler: sampler;
13
+ @group(1) @binding(0) var diffuseTexture: texture_2d<f32>;
14
+ struct MaterialDiffuse { diffuse: vec4f, };
15
+ @group(1) @binding(1) var<uniform> material: MaterialDiffuse;
16
+
17
+ struct VSOut {
18
+ @builtin(position) position: vec4f,
19
+ @location(0) uv: vec2f,
20
+ };
21
+
7
22
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
8
- @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
23
+ @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {
9
24
  let pos4 = vec4f(position, 1.0);
10
25
  let ws = weights0.x + weights0.y + weights0.z + weights0.w;
11
26
  let inv = select(1.0, 1.0 / ws, ws > 0.0001);
12
27
  let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
13
28
  var sp = vec4f(0.0);
14
29
  for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
15
- return lp.viewProj * vec4f(sp.xyz, 1.0);
30
+ var out: VSOut;
31
+ out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
32
+ out.uv = uv;
33
+ return out;
34
+ }
35
+
36
+ @fragment fn fs(in: VSOut) {
37
+ let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;
38
+ if (alpha < 0.5) { discard; }
16
39
  }
17
40
  `