feat(assets): Blickrichtung, Drift und Schleifennaht der Mocap-Clips
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
070bb79593
commit
aa6cde79d3
2 files changed
+165
-2
No files matched your search
@@ -6,7 +6,7 @@
|
||||
//
|
||||
// Das Zip bleibt außerhalb des Repos. Die Rechenschritte sind reine Funktionen (tests/motions.test.js), main()
|
||||
// liest nur Dateien und schreibt das Ergebnis.
|
||||
import { Matrix4, Quaternion, Vector3 } from "three";
|
||||
import { MathUtils, Matrix4, Quaternion, Vector3 } from "three";
|
||||
|
||||
/**
|
||||
* MVN-Knochen -> Mannequin-Knochen, zugeordnet nach gemessener Höhe in der Ruhepose. Spine2 (1,187 m) hat kein
|
||||
@@ -112,3 +112,75 @@ export function createRetargeter(rig, { boneMap = BONE_MAP, hipsScale = 1 } = {}
|
||||
return { local, hips };
|
||||
};
|
||||
}
|
||||
|
||||
export const SAMPLE_RATE = 30;
|
||||
/** Überblendzeit am Schleifenende: Naht in Grad ÷ BLEND_SPEED (°/s), begrenzt auf BLEND_MIN bis BLEND_MAX Sekunden. */
|
||||
export const BLEND_SPEED = 90;
|
||||
export const BLEND_MIN = 0.25;
|
||||
export const BLEND_MAX = 1.5;
|
||||
|
||||
/** n = round((end − start) · rate) + 1 gleichmäßig verteilte Zeitpunkte von start bis end, beide eingeschlossen. */
|
||||
export function sampleTimes(start, end, rate = SAMPLE_RATE) {
|
||||
if (!(end > start)) throw new Error(`Leerer Zeitraum ${start}–${end}`);
|
||||
const count = Math.round((end - start) * rate) + 1;
|
||||
return Array.from({ length: count }, (_, i) => start + ((end - start) * i) / (count - 1));
|
||||
}
|
||||
|
||||
/** Winkel des Vorwärtsvektors (+Z) einer Drehung gegen +Z in der Waagrechten, im Bogenmaß, positiv zu +X. */
|
||||
export function headingOf(quaternion) {
|
||||
const forward = new Vector3(0, 0, 1).applyQuaternion(quaternion);
|
||||
return Math.atan2(forward.x, forward.z);
|
||||
}
|
||||
|
||||
/** Dreht alle Bilder um die Hochachse, bis die Hüfte im ersten Bild nach +Z schaut. */
|
||||
export function faceForward(frames) {
|
||||
const angle = -headingOf(frames[0].rotations.get(SOURCE_HIPS));
|
||||
const turn = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), angle);
|
||||
return frames.map((frame) => ({
|
||||
rotations: new Map([...frame.rotations].map(([name, rotation]) => [name, turn.clone().multiply(rotation)])),
|
||||
hips: frame.hips.clone().applyQuaternion(turn),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Legt die Hüfte im ersten Bild waagrecht auf 0 und zieht die Drift linear ab, sodass das letzte Bild dort endet. */
|
||||
export function removeDrift(frames) {
|
||||
const first = frames[0].hips;
|
||||
const last = frames.at(-1).hips;
|
||||
return frames.map((frame, i) => {
|
||||
const along = frames.length > 1 ? i / (frames.length - 1) : 0;
|
||||
const hips = frame.hips.clone();
|
||||
hips.x -= first.x + along * (last.x - first.x);
|
||||
hips.z -= first.z + along * (last.z - first.z);
|
||||
return { rotations: frame.rotations, hips };
|
||||
});
|
||||
}
|
||||
|
||||
/** Größte Abweichung der Weltdrehung zwischen letztem und erstem Bild über alle Knochen, in Grad. */
|
||||
export function seamAngle(frames) {
|
||||
const last = frames.at(-1).rotations;
|
||||
let largest = 0;
|
||||
for (const [name, rotation] of frames[0].rotations) largest = Math.max(largest, rotation.angleTo(last.get(name)));
|
||||
return MathUtils.radToDeg(largest);
|
||||
}
|
||||
|
||||
export function blendDuration(seamDegrees) {
|
||||
return MathUtils.clamp(seamDegrees / BLEND_SPEED, BLEND_MIN, BLEND_MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt die Schleife: In den letzten `blend` Sekunden werden lokale Drehungen (slerp) und die Hüfte (lerp) mit
|
||||
* smoothstep-Gewicht zum ersten Bild gezogen. Das letzte Bild gleicht danach dem ersten. poses: [{ local, hips }].
|
||||
*/
|
||||
export function closeLoop(times, poses, blend) {
|
||||
const end = times.at(-1);
|
||||
const first = poses[0];
|
||||
return poses.map((pose, i) => {
|
||||
if (times[i] <= end - blend) return pose;
|
||||
const weight = MathUtils.smoothstep(times[i], end - blend, end);
|
||||
return {
|
||||
local: new Map([...pose.local].map(([name, rotation]) =>
|
||||
[name, rotation.clone().slerp(first.local.get(name), weight)])),
|
||||
hips: pose.hips.clone().lerp(first.hips, weight),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user