mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-10 11:16:10 +01:00
ZY correct animations
This commit is contained in:
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) {
|
||||||
|
|||||||
Reference in new issue
Block a user