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:
2026-06-22 18:28:51 +02:00
parent e669e29096
commit 7a3b2b8733
21 changed files with 96 additions and 36 deletions

View File

@@ -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
);

View File

@@ -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);
}
}
}