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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9439,25 +9439,6 @@ public class EditorApp extends Application {
|
||||
refreshCharAnimSetCombo();
|
||||
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
|
||||
|
||||
Button embedAnimBtn = new Button("Animationen einbetten");
|
||||
embedAnimBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
embedAnimBtn.setDisable(true);
|
||||
javafx.beans.value.ChangeListener<String> embedEnableListener = (obs, ov, nv) -> {
|
||||
boolean ready = charModelCombo.getValue() != null && !charModelCombo.getValue().isBlank()
|
||||
&& charAnimSetCombo.getValue() != null && !charAnimSetCombo.getValue().isBlank();
|
||||
embedAnimBtn.setDisable(!ready);
|
||||
};
|
||||
charModelCombo.valueProperty().addListener(embedEnableListener);
|
||||
charAnimSetCombo.valueProperty().addListener(embedEnableListener);
|
||||
embedAnimBtn.setOnAction(e -> {
|
||||
String modelPath = charModelCombo.getValue();
|
||||
String setName = charAnimSetCombo.getValue();
|
||||
if (modelPath == null || setName == null) return;
|
||||
if (charEditorStatusLabel != null)
|
||||
charEditorStatusLabel.setText("Bette Animationen ein…");
|
||||
input.animEmbedRequest.set(new SharedInput.AnimEmbedRequest(modelPath, setName));
|
||||
});
|
||||
|
||||
Button stripClipsBtn = new Button("Eingebettete Clips löschen");
|
||||
stripClipsBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
stripClipsBtn.setDisable(true);
|
||||
@@ -9581,7 +9562,6 @@ public class EditorApp extends Application {
|
||||
charEditContainer.getChildren().addAll(
|
||||
new Label("Modell:"), charModelCombo,
|
||||
new Label("Anim-Set:"), charAnimSetCombo,
|
||||
embedAnimBtn,
|
||||
stripClipsBtn
|
||||
);
|
||||
|
||||
|
||||
@@ -668,21 +668,31 @@ public class AnimPreviewState extends BaseAppState {
|
||||
for (Spatial child : n.getChildren()) collectControlTypes(child, out);
|
||||
}
|
||||
|
||||
/** Speichert das aktuelle Modell (inkl. aller AnimClips) zurück auf Disk. */
|
||||
/** Speichert das aktuelle Modell zurück auf Disk – ohne eingebettete AnimClips. */
|
||||
private void saveModel() {
|
||||
if (currentModelPath == null || currentModel == null) return;
|
||||
if (!currentModelPath.endsWith(".j3o")) {
|
||||
LOG.warn("[AnimPreview] Speichern übersprungen – kein .j3o: {}", currentModelPath);
|
||||
return;
|
||||
}
|
||||
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||
List<AnimClip> tempClips = new java.util.ArrayList<>();
|
||||
if (ac != null) {
|
||||
tempClips.addAll(ac.getAnimClips());
|
||||
for (AnimClip c : tempClips) ac.removeAnimClip(c);
|
||||
}
|
||||
Path file = ASSET_ROOT.resolve(currentModelPath.replace('/', java.io.File.separatorChar));
|
||||
try {
|
||||
BinaryExporter.getInstance().save(currentModel, file.toFile());
|
||||
LOG.info("[AnimPreview] Modell gespeichert: {}", currentModelPath);
|
||||
LOG.info("[AnimPreview] Modell gespeichert (ohne Clips): {}", currentModelPath);
|
||||
assets.deleteFromCache(new ModelKey(currentModelPath));
|
||||
} catch (Exception e) {
|
||||
input.animPreviewStatus += " | Speicherfehler: " + e.getMessage();
|
||||
LOG.error("[AnimPreview] Speicherfehler: {}", e.toString());
|
||||
} finally {
|
||||
if (ac != null) {
|
||||
for (AnimClip c : tempClips) ac.addAnimClip(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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