snapRootBoneXZ: Achsen-Fix für Mixamo-Hips (X=0, Y=0/frei, Z=frei)
In diesen Mixamo-Exporten ist Local-Y die Vorwärts-Richtung (nicht Höhe), Local-Z die Höhe. Bisheriger Code fror Z=0 ein → Charakter 1m zu tief. Und Y war frei → Lauf-Drift blieb. Neue Logik: X → 0 (kein Seiten-Drift) Y → 0 für Lauf-Clips (running/walking/sprinting/running_jump), normalisiert sonst Z → vollständig frei (Höhe und Setz/Aufsteh-Bewegung erhalten) Nur der flachste Bone (Hips) wird modifiziert, alle anderen unberührt. Clips vollständig neu importiert mit korrektem Snap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -117,7 +117,6 @@ public class AnimationLibrary extends BaseAppState {
|
||||
return false;
|
||||
}
|
||||
|
||||
target = snapRootBoneXZ(target, sc.getArmature());
|
||||
ac.addAnimClip(target);
|
||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
||||
if (clipName.equals("sit_down")) {
|
||||
@@ -129,6 +128,12 @@ public class AnimationLibrary extends BaseAppState {
|
||||
/** Wendet alle geladenen Clips auf {@code model} an (nur wenn es ein Rig hat). */
|
||||
public void applyAllTo(Spatial model) {
|
||||
if (RetargetingSystem.findSkinningControl(model) == null) return;
|
||||
AnimComposer ac = RetargetingSystem.findAnimComposer(model);
|
||||
if (ac != null) {
|
||||
for (AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||||
ac.removeAnimClip(c);
|
||||
}
|
||||
}
|
||||
int applied = 0;
|
||||
for (String key : clips.keySet()) {
|
||||
if (applyTo(key, model)) applied++;
|
||||
@@ -234,6 +239,9 @@ public class AnimationLibrary extends BaseAppState {
|
||||
|
||||
for (String name : ac.getAnimClipsNames()) {
|
||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||||
if (armature != null) {
|
||||
animClip = snapRootBoneXZ(animClip, armature);
|
||||
}
|
||||
clips.put(name, animClip);
|
||||
if (armature != null) armatures.put(name, armature);
|
||||
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
||||
@@ -260,15 +268,21 @@ public class AnimationLibrary extends BaseAppState {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Clips mit Vorwärts-Root-Motion in Local-Y (rennen, gehen, springen):
|
||||
// Y wird auf 0 eingefroren → kein Drift. Alle anderen Clips: Y bleibt frei (Hinsetzen usw.)
|
||||
private static final java.util.Set<String> LOCOMOTION_CLIPS = java.util.Set.of(
|
||||
"running", "walking", "sprinting", "running_jump"
|
||||
);
|
||||
|
||||
/**
|
||||
* Friert X und Z des "Hüft-Knochens" auf den Wert von Frame 0 ein.
|
||||
* Y (Höhenachse, JME3 Y-Up) bleibt vollständig erhalten — sit_down / Jump / Bounce laufen korrekt.
|
||||
* Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips).
|
||||
* Nur dieser eine Bone wird modifiziert — alle anderen Bones bleiben unverändert.
|
||||
*
|
||||
* Strategie: findet die kleinste Tiefe unter allen Joints die einen Translation-Track haben.
|
||||
* Bei Rigs wo Root (Tiefe 0) selbst Translations hat, wird Root gesnappt.
|
||||
* Bei Rigs wo Hips (Tiefe 1) die erste Ebene mit Translations ist (Root hat nur Rotation),
|
||||
* wird Hips gesnappt. So passt der Snap zu beiden Rig-Strukturen.
|
||||
* Erstellt einen neuen in-memory-Clip; J3O-Dateien bleiben unverändert.
|
||||
* In diesen Mixamo-Exporten ist die Achsenbelegung des Hips-Bones:
|
||||
* Local X → seitlich → wird auf 0 eingefroren (kein Seiten-Drift)
|
||||
* Local Y → vorwärts → wird auf 0 eingefroren für Lauf-Clips (kein Vorwärts-Drift)
|
||||
* bleibt frei für Sitz/Stand-Clips (leichte Neigungsbewegung)
|
||||
* Local Z → Höhe → IMMER frei lassen (Charakter-Höhe und Setz-Bewegung erhalten)
|
||||
*/
|
||||
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
|
||||
if (clip == null || armature == null) return clip;
|
||||
@@ -282,7 +296,9 @@ public class AnimationLibrary extends BaseAppState {
|
||||
minDepth = Math.min(minDepth, jointDepth(j));
|
||||
}
|
||||
}
|
||||
if (minDepth == Integer.MAX_VALUE) return clip; // keine Translation-Tracks vorhanden
|
||||
if (minDepth == Integer.MAX_VALUE) return clip;
|
||||
|
||||
boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName());
|
||||
|
||||
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||
boolean modified = false;
|
||||
@@ -292,20 +308,45 @@ public class AnimationLibrary extends BaseAppState {
|
||||
continue;
|
||||
}
|
||||
Vector3f[] translations = tt.getTranslations();
|
||||
// Nur den flachsten Bone anfassen – alle anderen unverändert lassen
|
||||
if (translations == null || translations.length == 0 || jointDepth(j) != minDepth) {
|
||||
newTracks.add(track);
|
||||
continue;
|
||||
}
|
||||
float f0x = translations[0].x;
|
||||
float f0z = translations[0].z;
|
||||
|
||||
// Y-Normalisierung für Nicht-Lauf-Clips: Frame-0 auf Bind-Pose-Y
|
||||
float bindY = j.getInitialTransform().getTranslation().y;
|
||||
float frame0Y = translations[0].y;
|
||||
float yOffset = bindY - frame0Y;
|
||||
|
||||
// Diagnostik
|
||||
float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE;
|
||||
float xRange = 0, zRange = 0;
|
||||
for (Vector3f t : translations) {
|
||||
if (t.y < yMin) yMin = t.y;
|
||||
if (t.y > yMax) yMax = t.y;
|
||||
xRange = Math.max(xRange, Math.abs(t.x - translations[0].x));
|
||||
zRange = Math.max(zRange, Math.abs(t.z - translations[0].z));
|
||||
}
|
||||
log.info("[AnimLib] snap '{}' root='{}' locomotion={} bindY={} frame0Y={} yOffset={} yRange={} xRange={} zRange={}",
|
||||
clip.getName(), j.getName(), isLocomotion,
|
||||
String.format("%.3f", bindY), String.format("%.3f", frame0Y),
|
||||
String.format("%.3f", yOffset), String.format("%.3f", yMax - yMin),
|
||||
String.format("%.3f", xRange), String.format("%.3f", zRange));
|
||||
|
||||
Vector3f[] snapped = new Vector3f[translations.length];
|
||||
for (int i = 0; i < translations.length; i++) {
|
||||
snapped[i] = new Vector3f(f0x, translations[i].y, f0z);
|
||||
float newY = isLocomotion
|
||||
? 0f // Lauf-Clips: Y einfrieren (Vorwärts-Drift weg)
|
||||
: (translations[i].y + yOffset); // Rest: Y normalisiert (Neigung erhalten)
|
||||
snapped[i] = new Vector3f(
|
||||
0f, // X immer 0 (Seiten-Drift weg)
|
||||
newY,
|
||||
translations[i].z // Z immer frei (Höhe und Setz-Bewegung erhalten)
|
||||
);
|
||||
}
|
||||
newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales()));
|
||||
modified = true;
|
||||
log.info("[AnimLib] '{}': Tiefe-{}-Joint '{}' XZ={},{} eingefroren, Y frei",
|
||||
clip.getName(), minDepth, j.getName(), f0x, f0z);
|
||||
}
|
||||
if (!modified) return clip;
|
||||
AnimClip result = new AnimClip(clip.getName());
|
||||
|
||||
@@ -319,6 +319,7 @@ public class WorldScene extends BaseAppState {
|
||||
if (mc != null && mc.getModelPath() != null) {
|
||||
try {
|
||||
Spatial loaded = assetManager.loadModel(mc.getModelPath());
|
||||
stripEmbeddedClips(loaded, mc.getModelPath());
|
||||
loaded.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
|
||||
// Auf 1.8 m skalieren – Höhe aus Vertex-Daten (zuverlässiger als BoundingBox
|
||||
@@ -358,6 +359,34 @@ public class WorldScene extends BaseAppState {
|
||||
return buildCharacter();
|
||||
}
|
||||
|
||||
private static final String[] ASSET_SEARCH_ROOTS = {
|
||||
"blight-assets/src/main/resources",
|
||||
"blight-assets/build/resources/main",
|
||||
"blight-assets/bin/main",
|
||||
"assets",
|
||||
};
|
||||
|
||||
private void stripEmbeddedClips(Spatial model, String modelPath) {
|
||||
com.jme3.anim.AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(model);
|
||||
if (ac == null || ac.getAnimClips().isEmpty()) return;
|
||||
int count = ac.getAnimClips().size();
|
||||
for (com.jme3.anim.AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||||
ac.removeAnimClip(c);
|
||||
}
|
||||
String rel = modelPath.replace('/', java.io.File.separatorChar);
|
||||
for (String base : ASSET_SEARCH_ROOTS) {
|
||||
java.nio.file.Path file = java.nio.file.Paths.get(base).resolve(rel);
|
||||
if (!java.nio.file.Files.exists(file)) continue;
|
||||
try {
|
||||
com.jme3.export.binary.BinaryExporter.getInstance().save(model, file.toFile());
|
||||
log.info("[WorldScene] {} eingebettete Clips aus '{}' entfernt: {}", count, modelPath, file);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WorldScene] Speichern fehlgeschlagen ({}): {}", file, e.getMessage());
|
||||
}
|
||||
}
|
||||
assetManager.deleteFromCache(new com.jme3.asset.ModelKey(modelPath));
|
||||
}
|
||||
|
||||
private MainCharacter findMainCharacter() {
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||
for (GameCharacter c : CharacterIO.loadAll(charDir)) {
|
||||
|
||||
Reference in New Issue
Block a user