ZY correct animations

This commit is contained in:
tresabhi committed 2026-11-09 22:01:05 -06:00
1 parent ddd0e049c6
commit c883f79025
3 files changed
+110 -59

No files matched your search

@@ -0,0 +1,59 @@
import type { mat4, vec3, vec4 } from "@gltf-transform/core";
import { Matrix4 } from "three";
/**
* SC2 is Z-up; glTF is Y-up. The basis change C is a -90° rotation about X:
* (x, y, z) -> (x, z, -y). Points and directions map through C, rotations and
* matrices are conjugated (C M C⁻¹), and scales swap their Y and Z factors.
*/
const C = new Matrix4().makeRotationX(-Math.PI / 2);
const C_INVERSE = new Matrix4().makeRotationX(Math.PI / 2);
export function sc2ToGltfVector(vector: readonly number[]): vec3 {
return [vector[0], vector[2], -vector[1]];
}
/** (x, y, z, w) quaternion */
export function sc2ToGltfQuaternion(q: readonly number[]): vec4 {
return [q[0], q[2], -q[1], q[3]];
}
export function sc2ToGltfScale(scale: readonly number[]): vec3 {
return [scale[0], scale[2], scale[1]];
}
/** anything indexable by number, e.g. a plain array or a typed array */
type NumberArray = { [index: number]: number; length: number };
/** column-major 4x4 */
export function sc2ToGltfMatrix(matrix: readonly number[]): mat4 {
return new Matrix4()
.multiplyMatrices(C, new Matrix4().fromArray(matrix))
.multiply(C_INVERSE)
.toArray() as mat4;
}
/** Converts a flat list of tightly packed vec3s in place. */
export function sc2ToGltfVectors<T extends NumberArray>(array: T) {
for (let i = 0; i < array.length; i += 3) {
const y = array[i + 1];
array[i + 1] = array[i + 2];
array[i + 2] = -y;
}
return array;
}
/** Converts a flat list of tightly packed quaternions in place. */
export function sc2ToGltfQuaternions<T extends NumberArray>(array: T) {
for (let i = 0; i < array.length; i += 4) {
const y = array[i + 1];
array[i + 1] = array[i + 2];
array[i + 2] = -y;
}
return array;
}
@@ -8,7 +8,7 @@ import {
type Hierarchy, type Hierarchy,
type PolygonGroup, type PolygonGroup,
type SC2, type SC2,
type Scg, type ScgPolygonGroups,
} from "@blitzkit/core"; } from "@blitzkit/core";
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract"; import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import { import {
@@ -20,6 +20,12 @@ import {
type Skin, type Skin,
} from "@gltf-transform/core"; } from "@gltf-transform/core";
import { sortBy, times } from "lodash-es"; import { sortBy, times } from "lodash-es";
import {
sc2ToGltfQuaternion,
sc2ToGltfScale,
sc2ToGltfVector,
sc2ToGltfVectors,
} from "./sc2Axes";
import type { Motion } from "./skinAnimations/motion"; import type { Motion } from "./skinAnimations/motion";
import { import {
bindSocket, bindSocket,
@@ -54,22 +60,6 @@ export const vertexAttributeGltfVectorSizes = {
type RenderComponent = ComponentOf<"RenderComponent">; type RenderComponent = ComponentOf<"RenderComponent">;
/**
* SC2 is Z-up (and right handed); glTF is Y-up. (x, y, z) -> (x, z, -y).
*/
export function sc2ToGltfVector<T extends [number, number, number]>(
vector: T,
): [number, number, number] {
return [vector[0], vector[2], -vector[1]];
}
/** Same conversion for a quaternion's (x, y, z, w). */
export function sc2ToGltfQuaternion(
q: [number, number, number, number],
): [number, number, number, number] {
return [q[0], q[2], -q[1], q[3]];
}
const SPATIAL_ATTRIBUTES = new Set<VertexAttribute>([ const SPATIAL_ATTRIBUTES = new Set<VertexAttribute>([
VertexAttribute.VERTEX, VertexAttribute.VERTEX,
VertexAttribute.NORMAL, VertexAttribute.NORMAL,
@@ -108,15 +98,7 @@ export function createAttributeAccessors(
const vertexSize = vertexAttributeGltfVectorSizes[attribute]; const vertexSize = vertexAttributeGltfVectorSizes[attribute];
const array = new Float32Array(value.flat()); const array = new Float32Array(value.flat());
if (SPATIAL_ATTRIBUTES.has(attribute)) { if (SPATIAL_ATTRIBUTES.has(attribute)) sc2ToGltfVectors(array);
for (let i = 0; i < array.length; i += 3) {
const y = array[i + 1];
const z = array[i + 2];
array[i + 1] = z;
array[i + 2] = -y;
}
}
accessors.set( accessors.set(
name, name,
@@ -178,7 +160,7 @@ export type Sc2RenderMode =
export interface Sc2ComponentHandlerOptions { export interface Sc2ComponentHandlerOptions {
document: Document; document: Document;
buffer: Buffer; buffer: Buffer;
scg: Scg; scg: ScgPolygonGroups;
render: Sc2RenderMode; render: Sc2RenderMode;
/** enables skeletons, motion clips and joint bindings */ /** enables skeletons, motion clips and joint bindings */
skeleton?: { skeleton?: {
@@ -240,7 +222,7 @@ export class Sc2ComponentHandler {
TransformComponent: (component, { node }) => { TransformComponent: (component, { node }) => {
node.setTranslation(sc2ToGltfVector(component["tc.localTranslation"])); node.setTranslation(sc2ToGltfVector(component["tc.localTranslation"]));
node.setRotation(sc2ToGltfQuaternion(component["tc.localRotation"])); node.setRotation(sc2ToGltfQuaternion(component["tc.localRotation"]));
node.setScale(component["tc.localScale"]); node.setScale(sc2ToGltfScale(component["tc.localScale"]));
}, },
StateSwitcherComponent: (component, { hierarchy, node }) => { StateSwitcherComponent: (component, { hierarchy, node }) => {
const garage = this.options.skeleton?.garage; const garage = this.options.skeleton?.garage;
@@ -262,13 +244,7 @@ export class Sc2ComponentHandler {
(other): other is ComponentOf<"MotionComponent"> => (other): other is ComponentOf<"MotionComponent"> =>
other["comp.typename"] === "MotionComponent", other["comp.typename"] === "MotionComponent",
); );
const created = createSkeleton( const created = createSkeleton(document, buffer, node, component, motion);
document,
buffer,
node,
component,
motion,
);
this.skeletons.set(hierarchy, created); this.skeletons.set(hierarchy, created);
@@ -341,7 +317,8 @@ export class Sc2ComponentHandler {
private dispatch(component: Component, context: HierarchyContext) { private dispatch(component: Component, context: HierarchyContext) {
const name = component["comp.typename"]; const name = component["comp.typename"];
// the table is exhaustive per name, but indexing by a union loses that // the table is exhaustive per name, but indexing by a union loses that
const handler = this.handlers[name] as AnyComponentHandler | null | undefined; const handler = this.handlers[name] as
AnyComponentHandler | null | undefined;
if (handler === undefined) { if (handler === undefined) {
if (this.options.strict) { if (this.options.strict) {
@@ -472,22 +449,19 @@ export class Sc2ComponentHandler {
); );
hardJointIndices.forEach((hardJointIndex) => { hardJointIndices.forEach((hardJointIndex) => {
const primitive = document const primitive = document.createPrimitive().setIndices(
.createPrimitive() document
.setIndices( .createAccessor()
document .setType("SCALAR")
.createAccessor() .setArray(
.setType("SCALAR") new Uint16Array(
.setArray( polygonGroup.indices.filter(
new Uint16Array( (index) => vertexHardJointIndices.get(index) === hardJointIndex,
polygonGroup.indices.filter(
(index) =>
vertexHardJointIndices.get(index) === hardJointIndex,
),
), ),
) ),
.setBuffer(buffer), )
); .setBuffer(buffer),
);
accessors.forEach((accessor, name) => accessors.forEach((accessor, name) =>
primitive.setAttribute(name, accessor), primitive.setAttribute(name, accessor),
@@ -496,7 +470,9 @@ export class Sc2ComponentHandler {
node.addChild( node.addChild(
document document
.createNode(`${hierarchy.name}_armor_${hardJointIndex}`) .createNode(`${hierarchy.name}_armor_${hardJointIndex}`)
.setMesh(document.createMesh(batch["##name"]).addPrimitive(primitive)), .setMesh(
document.createMesh(batch["##name"]).addPrimitive(primitive),
),
); );
}); });
} }
@@ -15,6 +15,13 @@ import {
import { isEqual, times } from "lodash-es"; import { isEqual, times } from "lodash-es";
import { dirname, normalize } from "path/posix"; import { dirname, normalize } from "path/posix";
import { Matrix4 } from "three"; import { Matrix4 } from "three";
import {
sc2ToGltfMatrix,
sc2ToGltfQuaternion,
sc2ToGltfQuaternions,
sc2ToGltfVector,
sc2ToGltfVectors,
} from "../sc2Axes";
import { MotionChannel, MotionReadStream, type Motion } from "./motion"; import { MotionChannel, MotionReadStream, type Motion } from "./motion";
export interface Skeleton { export interface Skeleton {
@@ -79,9 +86,15 @@ export function createSkeleton(
const scale: vec3 = [1, 1, 1]; const scale: vec3 = [1, 1, 1];
const track = motion?.tracks.get(joint["joint.uid"]); const track = motion?.tracks.get(joint["joint.uid"]);
const trackScale = track?.get(MotionChannel.Scale)?.values[0]; const trackScale = track?.get(MotionChannel.Scale)?.values[0];
const trackTranslation = track
?.get(MotionChannel.Translation)
?.values.slice(0, 3);
const trackRotation = track
?.get(MotionChannel.Rotation)
?.values.slice(0, 4);
MathUtils.decompose( MathUtils.decompose(
joint["joint.bindPose"].flat() as mat4, sc2ToGltfMatrix(joint["joint.bindPose"].flat()),
translation, translation,
rotation, rotation,
scale, scale,
@@ -91,12 +104,10 @@ export function createSkeleton(
return document return document
.createNode(joint["joint.name"]) .createNode(joint["joint.name"])
.setTranslation( .setTranslation(
(track?.get(MotionChannel.Translation)?.values.slice(0, 3) as vec3) ?? trackTranslation ? sc2ToGltfVector(trackTranslation) : translation,
translation,
) )
.setRotation( .setRotation(
(track?.get(MotionChannel.Rotation)?.values.slice(0, 4) as vec4) ?? trackRotation ? sc2ToGltfQuaternion(trackRotation) : rotation,
rotation,
) )
.setScale( .setScale(
trackScale === undefined ? scale : [trackScale, trackScale, trackScale], trackScale === undefined ? scale : [trackScale, trackScale, trackScale],
@@ -119,7 +130,9 @@ export function createSkeleton(
buffer, buffer,
"MAT4", "MAT4",
new Float32Array( new Float32Array(
joints.flatMap((joint) => joint["joint.invBindPose"].flat()), joints.flatMap((joint) =>
sc2ToGltfMatrix(joint["joint.invBindPose"].flat()),
),
), ),
), ),
); );
@@ -153,10 +166,13 @@ export function createMotionClip(
for (const [type, { times, values }] of track) { for (const [type, { times, values }] of track) {
const keyTimes = times.map((time) => time / playbackRate); const keyTimes = times.map((time) => time / playbackRate);
// scale keys are uniform, so they need no axis conversion
const keyValues = const keyValues =
type === MotionChannel.Scale type === MotionChannel.Scale
? values.flatMap((value) => [value, value, value]) ? values.flatMap((value) => [value, value, value])
: [...values]; : type === MotionChannel.Rotation
? sc2ToGltfQuaternions([...values])
: sc2ToGltfVectors([...values]);
// the game loops over the clip's duration, which can outlast its keys // the game loops over the clip's duration, which can outlast its keys
if (keyTimes[keyTimes.length - 1] < duration) { if (keyTimes[keyTimes.length - 1] < duration) {