Die Spielfigur wirft jetzt einen Schatten (player.init durchlief applyCharacter nie); drei stille Fehlerpfade meckern jetzt statt roh abzubrechen (fehlender Clip fuer die Fingerhaltung, Waffe ohne Griff, mit neuem Test); die Waffenauswahl im Panel zeigt Beschriftungen statt IDs; der Waffen-Umbau prueft jetzt die Meshzahl je Datei; stageTransform ist aus main.js nach lineup.js gezogen und mit dem im Browser gemessenen Fall getestet; slotPosition nimmt DUMMIES als Argument statt es aus dem Modul zu lesen; dazu Kommentar- und Entwurfskorrekturen (Docstring, Beckenspur-Test, Frustum-Culling, Port/Kamera/ VIEW_DISTANCE im Entwurf). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
3.2 KiB
JavaScript
66 lines
3.2 KiB
JavaScript
// 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 };
|
|
}
|