feat(assets): elf Mocap-Clips als weapons-mocap.glb für das Mannequin
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa6cde79d3
commit
4422432998
4 files changed
+345
-3
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
@@ -44,3 +45,79 @@ describe.each(WEAPON_SOURCES.map((source) => [source.id, source]))("%s.glb", (id
|
||||
expect(length).toBeLessThanOrEqual(source.length[1]);
|
||||
});
|
||||
});
|
||||
|
||||
/** Die elf Clips, benannt nach den Dateien im Pack. */
|
||||
const CLIP_IDS = [
|
||||
"hunter_bowArrow_tk01",
|
||||
"idle_loadGun-Hand_tk01", "idle_loadGun-Hand_tk02",
|
||||
"idle_loadGun-Rifle_tk04", "idle_loadGun-Rifle_tk05",
|
||||
"leftHandThrow_grenade_tk01", "leftHandThrow_grenade_tk02",
|
||||
"stabTwist_Knife_tk01", "stabTwist_Knife_tk02",
|
||||
"taunt_shootSelfInHead_tk01", "taunt_shootSelfInHead_tk02",
|
||||
];
|
||||
|
||||
describe("weapons-mocap.glb", () => {
|
||||
const file = fileURLToPath(new URL("../public/models/motions/weapons-mocap.glb", import.meta.url));
|
||||
let animations;
|
||||
beforeAll(async () => {
|
||||
const document = await new NodeIO().registerExtensions(ALL_EXTENSIONS).read(file);
|
||||
animations = document.getRoot().listAnimations();
|
||||
}, 30000);
|
||||
|
||||
it("ist kleiner als 3 MB", () => {
|
||||
expect(statSync(file).size).toBeLessThan(3e6);
|
||||
});
|
||||
|
||||
it("enthält die elf Clips", () => {
|
||||
expect(animations.map((animation) => animation.getName()).sort()).toEqual([...CLIP_IDS].sort());
|
||||
});
|
||||
|
||||
// Bruch: ein Knochen ohne Spur — die Figur bliebe dort in der Ruhepose stehen.
|
||||
it("hat je Clip 22 Drehungen und die Beckenposition", () => {
|
||||
for (const animation of animations) {
|
||||
const channels = animation.listChannels();
|
||||
expect(channels.filter((channel) => channel.getTargetPath() === "rotation")).toHaveLength(22);
|
||||
expect(channels.filter((channel) => channel.getTargetPath() === "translation")
|
||||
.map((channel) => channel.getTargetNode().getName())).toEqual(["pelvis"]);
|
||||
}
|
||||
});
|
||||
|
||||
// Bruch: Schleife nicht geschlossen — bei jedem Rücksprung ein sichtbarer Ruck. Bei Drehungen zählt dafür nur
|
||||
// die Drehung selbst, nicht ihr Vorzeichen: q und −q stellen dieselbe Rotation dar, und three.js' slerp
|
||||
// liefert bei Gewicht 1 die dem Ausgangswert näherliegende der beiden Varianten (Quaternion.slerp kennt
|
||||
// keinen Sonderfall für t=1). Darum wird das letzte Element vor dem Vergleich gespiegelt, wenn sein
|
||||
// Skalarprodukt mit dem ersten negativ ist — eine echte Winkelabweichung am Schleifenende bleibt davon
|
||||
// unberührt und schlägt weiterhin an. Bei Positionen (kein Vorzeichen-Zwilling) bleibt der Vergleich exakt.
|
||||
it("endet jeder Kanal dort, wo er beginnt", () => {
|
||||
for (const animation of animations) {
|
||||
for (const channel of animation.listChannels()) {
|
||||
const output = channel.getSampler().getOutput();
|
||||
const first = output.getElement(0, []);
|
||||
const last = output.getElement(output.getCount() - 1, []);
|
||||
const dot = first.reduce((sum, value, i) => sum + value * last[i], 0);
|
||||
const sign = channel.getTargetPath() === "rotation" && dot < 0 ? -1 : 1;
|
||||
for (let i = 0; i < first.length; i++) expect(sign * last[i]).toBeCloseTo(first[i], 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bruch: Drift nicht abgezogen — der Dummy wanderte von seinem Platz.
|
||||
it("beginnt jede Beckenbahn waagrecht bei 0", () => {
|
||||
for (const animation of animations) {
|
||||
const translation = animation.listChannels().find((channel) => channel.getTargetPath() === "translation");
|
||||
const [x, y] = translation.getSampler().getOutput().getElement(0, []);
|
||||
expect(x).toBeCloseTo(0, 5);
|
||||
expect(y).toBeCloseTo(0, 5);
|
||||
}
|
||||
});
|
||||
|
||||
it("tastet mit 30 Bildern je Sekunde ab", () => {
|
||||
for (const animation of animations) {
|
||||
const times = animation.listSamplers()[0].getInput();
|
||||
const values = [...times.getArray()];
|
||||
const duration = values.at(-1);
|
||||
expect(values).toHaveLength(Math.round(duration * 30) + 1);
|
||||
expect(values[0]).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user