mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-03-03 04:20:32 +00:00
200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
/**
|
|
* Compute world-space positions of Mount0 and weapon Mountpoint from GLB data,
|
|
* then compute the mount transform needed to align them.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
|
|
type Vec3 = [number, number, number];
|
|
type Quat = [number, number, number, number]; // [x, y, z, w]
|
|
|
|
function rotateVec3(v: Vec3, q: Quat): Vec3 {
|
|
const [vx, vy, vz] = v;
|
|
const [qx, qy, qz, qw] = q;
|
|
// q * v * q^(-1), optimized
|
|
const ix = qw * vx + qy * vz - qz * vy;
|
|
const iy = qw * vy + qz * vx - qx * vz;
|
|
const iz = qw * vz + qx * vy - qy * vx;
|
|
const iw = -qx * vx - qy * vy - qz * vz;
|
|
return [
|
|
ix * qw + iw * -qx + iy * -qz - iz * -qy,
|
|
iy * qw + iw * -qy + iz * -qx - ix * -qz,
|
|
iz * qw + iw * -qz + ix * -qy - iy * -qx,
|
|
];
|
|
}
|
|
|
|
function multiplyQuat(a: Quat, b: Quat): Quat {
|
|
const [ax, ay, az, aw] = a;
|
|
const [bx, by, bz, bw] = b;
|
|
return [
|
|
aw * bx + ax * bw + ay * bz - az * by,
|
|
aw * by - ax * bz + ay * bw + az * bx,
|
|
aw * bz + ax * by - ay * bx + az * bw,
|
|
aw * bw - ax * bx - ay * by - az * bz,
|
|
];
|
|
}
|
|
|
|
function inverseQuat(q: Quat): Quat {
|
|
return [-q[0], -q[1], -q[2], q[3]];
|
|
}
|
|
|
|
interface GltfNode {
|
|
name?: string;
|
|
children?: number[];
|
|
translation?: Vec3;
|
|
rotation?: Quat;
|
|
}
|
|
|
|
interface GltfDoc {
|
|
nodes: GltfNode[];
|
|
scenes: { nodes: number[] }[];
|
|
scene?: number;
|
|
}
|
|
|
|
function parseGlb(buffer: Buffer): GltfDoc {
|
|
const magic = buffer.readUInt32LE(0);
|
|
if (magic !== 0x46546c67) throw new Error("Not GLB");
|
|
const chunk0Length = buffer.readUInt32LE(12);
|
|
const jsonStr = buffer.toString("utf-8", 20, 20 + chunk0Length);
|
|
return JSON.parse(jsonStr);
|
|
}
|
|
|
|
function findParent(doc: GltfDoc, nodeIndex: number): number | null {
|
|
for (let i = 0; i < doc.nodes.length; i++) {
|
|
if (doc.nodes[i].children?.includes(nodeIndex)) return i;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getAncestorChain(doc: GltfDoc, nodeIndex: number): number[] {
|
|
const chain: number[] = [];
|
|
let idx: number | null = nodeIndex;
|
|
while (idx != null) {
|
|
chain.unshift(idx);
|
|
idx = findParent(doc, idx);
|
|
}
|
|
return chain;
|
|
}
|
|
|
|
function computeWorldTransform(
|
|
doc: GltfDoc,
|
|
nodeIndex: number,
|
|
): { position: Vec3; quaternion: Quat } {
|
|
const chain = getAncestorChain(doc, nodeIndex);
|
|
let pos: Vec3 = [0, 0, 0];
|
|
let quat: Quat = [0, 0, 0, 1];
|
|
|
|
for (const idx of chain) {
|
|
const node = doc.nodes[idx];
|
|
const t: Vec3 = node.translation ?? [0, 0, 0];
|
|
const r: Quat = node.rotation ?? [0, 0, 0, 1];
|
|
|
|
// World = parent * local
|
|
// new_pos = parent_pos + rotate(local_pos, parent_quat)
|
|
const rotatedT = rotateVec3(t, quat);
|
|
pos = [pos[0] + rotatedT[0], pos[1] + rotatedT[1], pos[2] + rotatedT[2]];
|
|
quat = multiplyQuat(quat, r);
|
|
}
|
|
|
|
return { position: pos, quaternion: quat };
|
|
}
|
|
|
|
function findNodeByName(doc: GltfDoc, name: string): number | null {
|
|
for (let i = 0; i < doc.nodes.length; i++) {
|
|
if (doc.nodes[i].name === name) return i;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function fmt(v: number[]): string {
|
|
return `(${v.map((n) => n.toFixed(4)).join(", ")})`;
|
|
}
|
|
|
|
async function main() {
|
|
const playerBuf = await fs.readFile(
|
|
"docs/base/@vl2/shapes.vl2/shapes/light_male.glb"
|
|
);
|
|
const weaponBuf = await fs.readFile(
|
|
"docs/base/@vl2/shapes.vl2/shapes/weapon_disc.glb"
|
|
);
|
|
|
|
const playerDoc = parseGlb(playerBuf);
|
|
const weaponDoc = parseGlb(weaponBuf);
|
|
|
|
// Compute Mount0 world transform
|
|
const mount0Idx = findNodeByName(playerDoc, "Mount0")!;
|
|
const mount0 = computeWorldTransform(playerDoc, mount0Idx);
|
|
console.log("Mount0 world position (GLB space):", fmt(mount0.position));
|
|
console.log("Mount0 world rotation (GLB space):", fmt(mount0.quaternion));
|
|
|
|
// Compute Mountpoint world transform
|
|
const mpIdx = findNodeByName(weaponDoc, "Mountpoint")!;
|
|
const mp = computeWorldTransform(weaponDoc, mpIdx);
|
|
console.log("\nMountpoint world position (GLB space):", fmt(mp.position));
|
|
console.log("Mountpoint world rotation (GLB space):", fmt(mp.quaternion));
|
|
|
|
// The ShapeRenderer applies a 90° Y rotation to the GLB scene.
|
|
// R90 = quaternion for 90° around Y = (0, sin(45°), 0, cos(45°)) = (0, 0.7071, 0, 0.7071)
|
|
const R90: Quat = [0, Math.SQRT1_2, 0, Math.SQRT1_2];
|
|
const R90_inv = inverseQuat(R90);
|
|
|
|
// Mount0 in entity space (after ShapeRenderer rotation)
|
|
const m0_entity_pos = rotateVec3(mount0.position, R90);
|
|
const m0_entity_quat = multiplyQuat(R90, mount0.quaternion);
|
|
console.log("\nMount0 entity-space position:", fmt(m0_entity_pos));
|
|
console.log("Mount0 entity-space rotation:", fmt(m0_entity_quat));
|
|
|
|
// The mount transform T_mount must satisfy:
|
|
// T_mount * R90 * MP = R90 * M0
|
|
// So: T_mount = R90 * M0 * MP^(-1) * R90^(-1)
|
|
//
|
|
// For position: mount_pos = R90 * (M0_pos - R_M0 * R_MP^(-1) * MP_pos)
|
|
// Wait, let me use the full matrix formula:
|
|
// T_mount = R90 * M0 * MP^(-1) * R90^(-1)
|
|
|
|
// Step 1: MP^(-1)
|
|
const mp_inv_quat = inverseQuat(mp.quaternion);
|
|
const mp_inv_pos: Vec3 = rotateVec3(
|
|
[-mp.position[0], -mp.position[1], -mp.position[2]],
|
|
mp_inv_quat,
|
|
);
|
|
|
|
// Step 2: M0 * MP^(-1)
|
|
const combined_pos: Vec3 = (() => {
|
|
const rotated = rotateVec3(mp_inv_pos, mount0.quaternion);
|
|
return [
|
|
mount0.position[0] + rotated[0],
|
|
mount0.position[1] + rotated[1],
|
|
mount0.position[2] + rotated[2],
|
|
] as Vec3;
|
|
})();
|
|
const combined_quat = multiplyQuat(mount0.quaternion, mp_inv_quat);
|
|
|
|
// Step 3: R90 * combined * R90^(-1)
|
|
const mount_pos = rotateVec3(combined_pos, R90);
|
|
// For rotation: R90 * combined_quat * R90^(-1)
|
|
const mount_quat = multiplyQuat(multiplyQuat(R90, combined_quat), R90_inv);
|
|
|
|
console.log("\n=== CORRECT MOUNT TRANSFORM ===");
|
|
console.log("Position:", fmt(mount_pos));
|
|
console.log("Quaternion:", fmt(mount_quat));
|
|
|
|
// For comparison, show what the current code computes:
|
|
// Current: pos = (mount0Pos.z, mount0Pos.y, -mount0Pos.x)
|
|
// Current: quat = rot90 * mount0Quat
|
|
const current_pos: Vec3 = [
|
|
mount0.position[2],
|
|
mount0.position[1],
|
|
-mount0.position[0],
|
|
];
|
|
const current_quat = multiplyQuat(R90, mount0.quaternion);
|
|
console.log("\n=== CURRENT CODE COMPUTES ===");
|
|
console.log("Position:", fmt(current_pos));
|
|
console.log("Quaternion:", fmt(current_quat));
|
|
|
|
// Show Cam node for reference
|
|
const camIdx = findNodeByName(playerDoc, "Cam")!;
|
|
const cam = computeWorldTransform(playerDoc, camIdx);
|
|
console.log("\nCam world position (GLB space):", fmt(cam.position));
|
|
}
|
|
|
|
main().catch(console.error);
|