reze-engine 0.27.4 → 0.29.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/README.md +27 -10
- package/dist/animation.d.ts +14 -0
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +15 -0
- package/dist/engine.d.ts +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +37 -8
- package/dist/ik-solver.d.ts +1 -1
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +4 -29
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/locomotion.d.ts +90 -0
- package/dist/locomotion.d.ts.map +1 -0
- package/dist/locomotion.js +179 -0
- package/dist/math.d.ts +26 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +328 -0
- package/dist/model.d.ts +51 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +343 -53
- package/package.json +1 -1
- package/src/animation.ts +26 -0
- package/src/engine.ts +46 -8
- package/src/ik-solver.ts +5 -34
- package/src/index.ts +16 -2
- package/src/locomotion.ts +254 -0
- package/src/math.ts +345 -0
- package/src/model.ts +354 -65
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Game-style locomotion over the blend primitive: idle ↔ run ↔ sprint mixed by a
|
|
2
|
+
// smoothed speed level, yaw eased toward the input heading, root motion integrated
|
|
3
|
+
// in code (the clips are in-place). Zero renderer coupling — the controller only
|
|
4
|
+
// talks to Model.setBlendPose and returns the root transform for the host to apply
|
|
5
|
+
// via engine.setModelTransform.
|
|
6
|
+
import { FPS } from "./animation";
|
|
7
|
+
import { Quat, Vec3 } from "./math";
|
|
8
|
+
const TWO_PI = Math.PI * 2;
|
|
9
|
+
function wrapAngle(a) {
|
|
10
|
+
while (a > Math.PI)
|
|
11
|
+
a -= TWO_PI;
|
|
12
|
+
while (a < -Math.PI)
|
|
13
|
+
a += TWO_PI;
|
|
14
|
+
return a;
|
|
15
|
+
}
|
|
16
|
+
export class LocomotionController {
|
|
17
|
+
constructor(model, clips, options) {
|
|
18
|
+
this.inputX = 0;
|
|
19
|
+
this.inputY = 0;
|
|
20
|
+
this.inputSprint = false;
|
|
21
|
+
// Tank mode: forward/steer relative to the CURRENT facing, camera never involved.
|
|
22
|
+
this.tankMode = false;
|
|
23
|
+
this.inputForward = 0;
|
|
24
|
+
this.inputSteer = 0;
|
|
25
|
+
this.speedLevel = 0;
|
|
26
|
+
this.yaw = 0;
|
|
27
|
+
// Movement direction = the INPUT heading, not the body yaw. The body turns
|
|
28
|
+
// cosmetically toward it; translating along the (sweeping) body yaw instead
|
|
29
|
+
// would nudge the character through the forward arc on every L↔R reversal.
|
|
30
|
+
this.dirX = 0;
|
|
31
|
+
this.dirZ = 1;
|
|
32
|
+
this.position = new Vec3(0, 0, 0);
|
|
33
|
+
this.rotation = new Quat(0, 0, 0, 1);
|
|
34
|
+
this.idleTime = 0;
|
|
35
|
+
this.gaitPhase = 0; // 0..1, shared by run and sprint so legs stay aligned across the blend
|
|
36
|
+
this.model = model;
|
|
37
|
+
this.clips = clips;
|
|
38
|
+
this.runSpeed = options?.runSpeed ?? 67;
|
|
39
|
+
this.sprintSpeed = options?.sprintSpeed ?? 92;
|
|
40
|
+
this.speedResponse = options?.speedResponse ?? 5;
|
|
41
|
+
this.turnResponse = options?.turnResponse ?? 10;
|
|
42
|
+
this.cosTurnThreshold = Math.cos(options?.turnInPlaceThreshold ?? Math.PI / 4);
|
|
43
|
+
this.steerRate = options?.steerRate ?? 2.5;
|
|
44
|
+
this.backpedalScale = options?.backpedalScale ?? 0.5;
|
|
45
|
+
this.yawOffset = options?.yawOffset ?? Math.PI;
|
|
46
|
+
this.entries = [
|
|
47
|
+
{ name: clips.idle, time: 0, weight: 1 },
|
|
48
|
+
{ name: clips.run, time: 0, weight: 0 },
|
|
49
|
+
{ name: clips.sprint ?? clips.run, time: 0, weight: 0 },
|
|
50
|
+
];
|
|
51
|
+
this.pose = { position: this.position, yaw: 0, rotation: this.rotation, speedLevel: 0 };
|
|
52
|
+
}
|
|
53
|
+
/** World-vector move input: x = +right (+X), y = +forward (+Z), magnitude clamped
|
|
54
|
+
* to 1. The character turns toward the vector, then runs along it. Call whenever
|
|
55
|
+
* input changes — the value holds between calls. */
|
|
56
|
+
setMove(x, y, sprint = false) {
|
|
57
|
+
const m = Math.hypot(x, y);
|
|
58
|
+
if (m > 1) {
|
|
59
|
+
x /= m;
|
|
60
|
+
y /= m;
|
|
61
|
+
}
|
|
62
|
+
this.tankMode = false;
|
|
63
|
+
this.inputX = x;
|
|
64
|
+
this.inputY = y;
|
|
65
|
+
this.inputSprint = sprint;
|
|
66
|
+
}
|
|
67
|
+
/** Tank-style input relative to the CURRENT facing, camera-independent and fully
|
|
68
|
+
* deterministic: `steer` (−1..1, + = her right) rotates at steerRate whether
|
|
69
|
+
* standing or moving, `forward` (+1 run ahead — curving while steering — or −1
|
|
70
|
+
* backpedal at backpedalScale). Holds between calls, like setMove. */
|
|
71
|
+
setDrive(forward, steer, sprint = false) {
|
|
72
|
+
this.tankMode = true;
|
|
73
|
+
this.inputForward = Math.max(-1, Math.min(1, forward));
|
|
74
|
+
this.inputSteer = Math.max(-1, Math.min(1, steer));
|
|
75
|
+
this.inputSprint = sprint;
|
|
76
|
+
}
|
|
77
|
+
/** Place the character (initial spawn or respawn). */
|
|
78
|
+
teleport(x, y, z, yaw = 0) {
|
|
79
|
+
this.position.setXYZ(x, y, z);
|
|
80
|
+
this.yaw = yaw;
|
|
81
|
+
}
|
|
82
|
+
getPosition() {
|
|
83
|
+
return this.position;
|
|
84
|
+
}
|
|
85
|
+
/** Stop driving the model's pose (the blend is cleared; the single-clip player resumes). */
|
|
86
|
+
detach() {
|
|
87
|
+
this.model.clearBlendPose();
|
|
88
|
+
}
|
|
89
|
+
clipDuration(name) {
|
|
90
|
+
const frames = this.model.getClip(name)?.frameCount ?? 0;
|
|
91
|
+
return frames > 0 ? frames / FPS : 1;
|
|
92
|
+
}
|
|
93
|
+
/** Advance one frame: integrates yaw + position, updates the clip clocks, hands the
|
|
94
|
+
* weighted pose to the model, and returns the root transform to apply. */
|
|
95
|
+
update(dt) {
|
|
96
|
+
if (dt > 0.1)
|
|
97
|
+
dt = 0.1; // tab-switch guard: never integrate a huge step
|
|
98
|
+
const hasSprint = this.clips.sprint !== undefined;
|
|
99
|
+
let moving;
|
|
100
|
+
let align = 1;
|
|
101
|
+
let speedScale = 1;
|
|
102
|
+
if (this.tankMode) {
|
|
103
|
+
// Steering rotates the facing directly — standing or moving — and the travel
|
|
104
|
+
// direction IS the facing, so no pivot gate and no drift by construction.
|
|
105
|
+
this.yaw = wrapAngle(this.yaw + this.inputSteer * this.steerRate * dt);
|
|
106
|
+
const fwd = this.inputForward;
|
|
107
|
+
moving = Math.abs(fwd) > 0.05;
|
|
108
|
+
if (moving) {
|
|
109
|
+
const back = fwd < 0;
|
|
110
|
+
this.dirX = Math.sin(this.yaw) * Math.sign(fwd);
|
|
111
|
+
this.dirZ = Math.cos(this.yaw) * Math.sign(fwd);
|
|
112
|
+
speedScale = Math.abs(fwd) * (back ? this.backpedalScale : 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const m = Math.hypot(this.inputX, this.inputY);
|
|
117
|
+
moving = m > 0.05;
|
|
118
|
+
// Yaw eases toward the input heading only while there is one. Turns are strictly
|
|
119
|
+
// in place: zero translation outside the threshold cone, ramping smoothly to full
|
|
120
|
+
// speed as the body aligns — so direction reversals (L-R-L) cannot drift.
|
|
121
|
+
if (moving) {
|
|
122
|
+
const desired = Math.atan2(this.inputX, this.inputY);
|
|
123
|
+
const err = wrapAngle(desired - this.yaw);
|
|
124
|
+
this.yaw = wrapAngle(this.yaw + err * Math.min(1, this.turnResponse * dt));
|
|
125
|
+
align = Math.max(0, (Math.cos(err) - this.cosTurnThreshold) / (1 - this.cosTurnThreshold));
|
|
126
|
+
this.dirX = this.inputX / m;
|
|
127
|
+
this.dirZ = this.inputY / m;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Speed level ramps linearly toward the target; the pose blend follows it.
|
|
131
|
+
// (No sprint while backpedaling.)
|
|
132
|
+
const sprinting = this.inputSprint && hasSprint && !(this.tankMode && this.inputForward < 0);
|
|
133
|
+
const target = moving ? (sprinting ? 2 : 1) : 0;
|
|
134
|
+
const maxStep = this.speedResponse * dt;
|
|
135
|
+
const d = target - this.speedLevel;
|
|
136
|
+
this.speedLevel += Math.abs(d) <= maxStep ? d : Math.sign(d) * maxStep;
|
|
137
|
+
// Root motion along the travel direction. In-place clips carry no horizontal root.
|
|
138
|
+
const speed = (this.speedLevel <= 1
|
|
139
|
+
? this.runSpeed * this.speedLevel
|
|
140
|
+
: this.runSpeed + (this.sprintSpeed - this.runSpeed) * (this.speedLevel - 1)) *
|
|
141
|
+
align *
|
|
142
|
+
speedScale;
|
|
143
|
+
this.position.x += this.dirX * speed * dt;
|
|
144
|
+
this.position.z += this.dirZ * speed * dt;
|
|
145
|
+
// Clocks: idle free-runs; run/sprint share one normalized gait phase so a
|
|
146
|
+
// mid-blend stride stays on the same feet.
|
|
147
|
+
const idleDur = this.clipDuration(this.clips.idle);
|
|
148
|
+
const runDur = this.clipDuration(this.clips.run);
|
|
149
|
+
const sprintDur = hasSprint ? this.clipDuration(this.clips.sprint) : runDur;
|
|
150
|
+
this.idleTime = (this.idleTime + dt) % idleDur;
|
|
151
|
+
const gaitDur = this.speedLevel <= 1 ? runDur : runDur + (sprintDur - runDur) * (this.speedLevel - 1);
|
|
152
|
+
this.gaitPhase = (this.gaitPhase + dt / gaitDur) % 1;
|
|
153
|
+
// Weights along the 1D speed axis.
|
|
154
|
+
let wIdle, wRun, wSprint;
|
|
155
|
+
if (this.speedLevel <= 1) {
|
|
156
|
+
wIdle = 1 - this.speedLevel;
|
|
157
|
+
wRun = this.speedLevel;
|
|
158
|
+
wSprint = 0;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
wIdle = 0;
|
|
162
|
+
wRun = 2 - this.speedLevel;
|
|
163
|
+
wSprint = this.speedLevel - 1;
|
|
164
|
+
}
|
|
165
|
+
this.entries[0].time = this.idleTime;
|
|
166
|
+
this.entries[0].weight = wIdle;
|
|
167
|
+
this.entries[1].time = this.gaitPhase * runDur;
|
|
168
|
+
this.entries[1].weight = wRun;
|
|
169
|
+
this.entries[2].time = this.gaitPhase * sprintDur;
|
|
170
|
+
this.entries[2].weight = wSprint;
|
|
171
|
+
this.model.setBlendPose(this.entries);
|
|
172
|
+
const ry = this.yaw + this.yawOffset;
|
|
173
|
+
const half = ry * 0.5;
|
|
174
|
+
this.rotation.setXYZW(0, Math.sin(half), 0, Math.cos(half));
|
|
175
|
+
this.pose.yaw = this.yaw;
|
|
176
|
+
this.pose.speedLevel = this.speedLevel;
|
|
177
|
+
return this.pose;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/math.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function easeInOut(t: number): number;
|
|
2
|
+
export type EulerOrder = "XYZ" | "XZY" | "YXZ" | "YZX" | "ZXY" | "ZYX";
|
|
2
3
|
export declare class Vec3 {
|
|
3
4
|
x: number;
|
|
4
5
|
y: number;
|
|
@@ -18,6 +19,8 @@ export declare class Vec3 {
|
|
|
18
19
|
static crossInto(a: Vec3, b: Vec3, out: Vec3): Vec3;
|
|
19
20
|
static setFromMat4Translation(m: Float32Array, out: Vec3): Vec3;
|
|
20
21
|
static transformMat4RotationInto(normal: Vec3, m: Float32Array, out: Vec3): Vec3;
|
|
22
|
+
static mirrorZInto(v: Vec3, out: Vec3): Vec3;
|
|
23
|
+
static mirrorZ(v: Vec3): Vec3;
|
|
21
24
|
normalizeInPlace(): Vec3;
|
|
22
25
|
}
|
|
23
26
|
export declare class Quat {
|
|
@@ -43,6 +46,29 @@ export declare class Quat {
|
|
|
43
46
|
setXYZW(x: number, y: number, z: number, w: number): Quat;
|
|
44
47
|
setIdentity(): Quat;
|
|
45
48
|
static fromEuler(rotX: number, rotY: number, rotZ: number): Quat;
|
|
49
|
+
static dot(a: Quat, b: Quat): number;
|
|
50
|
+
static angleTo(a: Quat, b: Quat): number;
|
|
51
|
+
static conjugateInto(q: Quat, out: Quat): Quat;
|
|
52
|
+
static nlerpInto(a: Quat, b: Quat, t: number, out: Quat): Quat;
|
|
53
|
+
static nlerp(a: Quat, b: Quat, t: number): Quat;
|
|
54
|
+
static rotateVecInto(q: Quat, v: Vec3, out: Vec3): Vec3;
|
|
55
|
+
static rotateVec(q: Quat, v: Vec3): Vec3;
|
|
56
|
+
static rotateVecInvInto(q: Quat, v: Vec3, out: Vec3): Vec3;
|
|
57
|
+
static rotateVecInv(q: Quat, v: Vec3): Vec3;
|
|
58
|
+
static fromUnitVectorsInto(from: Vec3, to: Vec3, out: Quat): Quat;
|
|
59
|
+
static fromUnitVectors(from: Vec3, to: Vec3): Quat;
|
|
60
|
+
static fromBasisInto(x: Vec3, y: Vec3, z: Vec3, out: Quat): Quat;
|
|
61
|
+
static fromBasis(x: Vec3, y: Vec3, z: Vec3): Quat;
|
|
62
|
+
static twistAroundAxisInto(q: Quat, a: Vec3, out: Quat): Quat;
|
|
63
|
+
static twistAroundAxis(q: Quat, a: Vec3): Quat;
|
|
64
|
+
static lookRotationInto(forward: Vec3, up: Vec3, out: Quat): Quat;
|
|
65
|
+
static lookRotation(forward: Vec3, up: Vec3): Quat;
|
|
66
|
+
static mirrorZInto(q: Quat, out: Quat): Quat;
|
|
67
|
+
static mirrorZ(q: Quat): Quat;
|
|
68
|
+
static fromEulerOrderInto(x: number, y: number, z: number, order: EulerOrder, out: Quat): Quat;
|
|
69
|
+
static fromEulerOrder(x: number, y: number, z: number, order: EulerOrder): Quat;
|
|
70
|
+
static toEulerOrderInto(q: Quat, order: EulerOrder, out: Vec3): Vec3;
|
|
71
|
+
static toEulerOrder(q: Quat, order: EulerOrder): Vec3;
|
|
46
72
|
}
|
|
47
73
|
export declare class Mat4 {
|
|
48
74
|
values: Float32Array;
|
package/dist/math.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"math.d.ts","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"AACA,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;
|
|
1
|
+
{"version":3,"file":"math.d.ts","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"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"}
|
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 = [
|