// Ein Dummy der Animations-Testseite: geklontes Mannequin, Mocap-Clip in Dauerschleife, dazu eine feste // Fingerhaltung und wahlweise eine Waffe im Handknochen // (docs/superpowers/specs/2026-09-17-animationen-design.md, "Dummies"). import { AnimationClip, AnimationMixer, LoopRepeat, MathUtils, QuaternionKeyframeTrack } from "three"; import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js"; /** Fingerknochen des Mannequins ohne die Blattknochen. Die Mocap-Daten haben keine Finger. */ export const FINGER_BONE = /^(thumb|index|middle|ring|pinky)_0[1-3]_[lr]$/; /** * Macht aus einem Clip des Mannequins (Pistol_Idle_Loop für die Waffenhand, Idle_Loop für leere Hände) eine feste * Fingerhaltung: nur die Drehungen der Fingerknochen, eingefroren auf ihren ersten Keyframe. Läuft als zweite * Aktion auf demselben Mixer — sie berührt keinen Knochen, den der Mocap-Clip bewegt. */ export function fingerPoseClip(source, name = `${source.name}_Finger`) { const tracks = source.tracks .filter((track) => track.name.endsWith(".quaternion") && FINGER_BONE.test(track.name.slice(0, track.name.lastIndexOf(".")))) .map((track) => { const first = [...track.values.slice(0, 4)]; return new QuaternionKeyframeTrack(track.name, [0, 1], [...first, ...first]); }); if (tracks.length === 0) throw new Error(`${source.name}: keine Fingerspuren`); return new AnimationClip(name, 1, tracks); } /** Lage der Waffe im Handknochen: Position in Metern, Drehung in Grad (XYZ). */ export function applyGrip(object, grip) { object.position.fromArray(grip.position); object.rotation.set(...grip.rotation.map((degrees) => MathUtils.degToRad(degrees)), "XYZ"); } /** Baut einen Dummy. template ist das geladene Mannequin, weapon die geladene Waffenszene (wird geklont). */ export function createDummy({ template, clip, fingers, weapon = null, hand = "r", grip = null }) { const root = cloneSkeleton(template); root.traverse((object) => { if (!object.isMesh) return; object.castShadow = true; object.receiveShadow = true; // Für ein Skinned Mesh ist die Hülle der Ruhepose ohnehin die falsche Grundlage, sobald Knochen die // Figur bewegen — besonders sichtbar beim Bogenschützen, der bis 3 m von seinem Platz wandert. Deshalb // für alle Dummies aus, nicht nur für ihn. object.frustumCulled = false; }); const mixer = new AnimationMixer(root); mixer.clipAction(clip).setLoop(LoopRepeat, Infinity).play(); mixer.clipAction(fingers).play(); let held = null; if (weapon) { if (!grip) throw new Error("Dummy mit Waffe ohne Griff"); const boneName = hand === "l" ? "hand_l" : "hand_r"; const bone = root.getObjectByName(boneName); if (!bone) throw new Error(`Dummy ohne Knochen ${boneName}`); held = weapon.clone(true); held.traverse((object) => { if (!object.isMesh) return; object.castShadow = true; object.receiveShadow = true; }); applyGrip(held, grip); bone.add(held); } return { root, mixer, clip, weapon: held }; }