Compare commits
4 Commits
e669e29096
...
914bf6e673
| Author | SHA1 | Date | |
|---|---|---|---|
| 914bf6e673 | |||
| b3b943e588 | |||
| d3c6e8ed77 | |||
| 7a3b2b8733 |
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();
|
refreshCharAnimSetCombo();
|
||||||
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
|
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");
|
Button stripClipsBtn = new Button("Eingebettete Clips löschen");
|
||||||
stripClipsBtn.setMaxWidth(Double.MAX_VALUE);
|
stripClipsBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
stripClipsBtn.setDisable(true);
|
stripClipsBtn.setDisable(true);
|
||||||
@@ -9581,7 +9562,6 @@ public class EditorApp extends Application {
|
|||||||
charEditContainer.getChildren().addAll(
|
charEditContainer.getChildren().addAll(
|
||||||
new Label("Modell:"), charModelCombo,
|
new Label("Modell:"), charModelCombo,
|
||||||
new Label("Anim-Set:"), charAnimSetCombo,
|
new Label("Anim-Set:"), charAnimSetCombo,
|
||||||
embedAnimBtn,
|
|
||||||
stripClipsBtn
|
stripClipsBtn
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -200,9 +200,7 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
currentAction = ac.setCurrentAction(currentClipName);
|
currentAction = ac.setCurrentAction(currentClipName);
|
||||||
if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed);
|
if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed);
|
||||||
} else {
|
} else {
|
||||||
currentAction = null;
|
stopAll();
|
||||||
currentClipName = null;
|
|
||||||
setSkinningEnabled(currentModel, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,11 +225,11 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist));
|
previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist));
|
||||||
c.lookAt(previewTarget, Vector3f.UNIT_Y);
|
c.lookAt(previewTarget, Vector3f.UNIT_Y);
|
||||||
|
|
||||||
// Achsen: Größe proportional zur Kameradistanz, immer am Ursprung
|
// Achsen: Größe proportional zur Kameradistanz, immer am Weltpunkt (0,0,0)
|
||||||
if (axesNode != null) {
|
if (axesNode != null) {
|
||||||
float s = previewCamDist * input.animPreviewZoom * 0.18f;
|
float s = previewCamDist * input.animPreviewZoom * 0.18f;
|
||||||
axesNode.setLocalScale(s);
|
axesNode.setLocalScale(s);
|
||||||
axesNode.setLocalTranslation(previewTarget);
|
axesNode.setLocalTranslation(Vector3f.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
previewScene.updateLogicalState(tpf);
|
previewScene.updateLogicalState(tpf);
|
||||||
@@ -249,10 +247,6 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Spatial model = loadFresh(assetPath);
|
Spatial model = loadFresh(assetPath);
|
||||||
// SkinningControl nur aktiv lassen wenn eine Animation läuft,
|
|
||||||
// sonst kollabiert das Mesh durch uninitalisierte Skin-Matrizen.
|
|
||||||
setSkinningEnabled(model, false);
|
|
||||||
|
|
||||||
// Im Animations-Editor soll der Charakter immer am Ursprung stehen.
|
// Im Animations-Editor soll der Charakter immer am Ursprung stehen.
|
||||||
// Eventuelle Translation die beim GLB-Export eingebacken wurde entfernen.
|
// Eventuelle Translation die beim GLB-Export eingebacken wurde entfernen.
|
||||||
model.setLocalTranslation(Vector3f.ZERO);
|
model.setLocalTranslation(Vector3f.ZERO);
|
||||||
@@ -264,7 +258,12 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
// Alle Clips in-place snappen (verhindert Drift im Preview)
|
// Alle Clips in-place snappen (verhindert Drift im Preview)
|
||||||
AnimComposer previewAC = findControl(model, AnimComposer.class);
|
AnimComposer previewAC = findControl(model, AnimComposer.class);
|
||||||
SkinningControl previewSC = findControl(model, SkinningControl.class);
|
SkinningControl previewSC = findControl(model, SkinningControl.class);
|
||||||
|
LOG.info("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}",
|
||||||
|
previewAC != null ? "gefunden" : "NULL",
|
||||||
|
previewSC != null ? "gefunden" : "NULL",
|
||||||
|
previewAC != null ? previewAC.getAnimClips().size() : "n/a");
|
||||||
if (previewAC != null && previewSC != null) {
|
if (previewAC != null && previewSC != null) {
|
||||||
|
// Eingebettete Clips snappen
|
||||||
for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) {
|
for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) {
|
||||||
AnimClip snapped = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(c, previewSC.getArmature());
|
AnimClip snapped = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(c, previewSC.getArmature());
|
||||||
if (snapped != c) {
|
if (snapped != c) {
|
||||||
@@ -272,18 +271,41 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
previewAC.addAnimClip(snapped);
|
previewAC.addAnimClip(snapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Eingebettete Clips aus der Datei entfernen und speichern (Datei-Größe reduzieren)
|
||||||
|
if (!previewAC.getAnimClips().isEmpty()) {
|
||||||
|
int embedCount = previewAC.getAnimClips().size();
|
||||||
|
for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) {
|
||||||
|
previewAC.removeAnimClip(c);
|
||||||
|
}
|
||||||
|
saveModelStripped(model, assetPath, embedCount);
|
||||||
|
}
|
||||||
|
// T-Pose: AnimClip für alle Joints in Bind-Pose.
|
||||||
|
// Leerer Clip → NPE in ClipAction.doInterpolate (JME3 erwartet tracks != null).
|
||||||
|
// getInitialTransform() liefert die Bind-Pose → SC-Matrix = Bind × Bind⁻¹ = I
|
||||||
|
// → Vertices in Y-up = stehender Charakter.
|
||||||
|
AnimClip tpose = buildTPoseClip(previewSC.getArmature());
|
||||||
|
LOG.info("[AnimPreview] T-Pose Clip: {}", tpose != null ? "erstellt" : "NULL (keine Root-Joints?)");
|
||||||
|
if (tpose != null) {
|
||||||
|
previewAC.addAnimClip(tpose);
|
||||||
|
setSkinningEnabled(model, true);
|
||||||
|
previewAC.setCurrentAction("__tpose__");
|
||||||
|
LOG.info("[AnimPreview] T-Pose aktiviert");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warn("[AnimPreview] T-Pose NICHT möglich: previewAC={}, previewSC={}",
|
||||||
|
previewAC != null ? "ok" : "NULL",
|
||||||
|
previewSC != null ? "ok" : "NULL");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kamera auf Bounding Box ausrichten
|
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox
|
||||||
model.updateGeometricState();
|
model.updateGeometricState();
|
||||||
if (model.getWorldBound() instanceof BoundingBox bb) {
|
if (model.getWorldBound() instanceof BoundingBox bb) {
|
||||||
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||||
previewCamDist = ext * 2.8f;
|
previewCamDist = ext * 2.8f;
|
||||||
previewTarget.set(bb.getCenter());
|
|
||||||
} else {
|
} else {
|
||||||
previewCamDist = 3f;
|
previewCamDist = 3f;
|
||||||
previewTarget.set(0, 1, 0);
|
|
||||||
}
|
}
|
||||||
|
previewTarget.set(0, 1, 0);
|
||||||
input.animPreviewZoom = 1.0f;
|
input.animPreviewZoom = 1.0f;
|
||||||
|
|
||||||
// Clips sammeln und melden
|
// Clips sammeln und melden
|
||||||
@@ -379,7 +401,12 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
currentClipName = null;
|
currentClipName = null;
|
||||||
if (currentModel != null) {
|
if (currentModel != null) {
|
||||||
stopOnSpatial(currentModel);
|
stopOnSpatial(currentModel);
|
||||||
setSkinningEnabled(currentModel, false);
|
// Zurück zur T-Pose: __tpose__ wiedergeben (leerer Clip = Bind-Pose)
|
||||||
|
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||||
|
if (ac != null && ac.getAnimClipsNames().contains("__tpose__")) {
|
||||||
|
setSkinningEnabled(currentModel, true);
|
||||||
|
ac.setCurrentAction("__tpose__");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,21 +695,31 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
for (Spatial child : n.getChildren()) collectControlTypes(child, out);
|
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() {
|
private void saveModel() {
|
||||||
if (currentModelPath == null || currentModel == null) return;
|
if (currentModelPath == null || currentModel == null) return;
|
||||||
if (!currentModelPath.endsWith(".j3o")) {
|
if (!currentModelPath.endsWith(".j3o")) {
|
||||||
LOG.warn("[AnimPreview] Speichern übersprungen – kein .j3o: {}", currentModelPath);
|
LOG.warn("[AnimPreview] Speichern übersprungen – kein .j3o: {}", currentModelPath);
|
||||||
return;
|
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));
|
Path file = ASSET_ROOT.resolve(currentModelPath.replace('/', java.io.File.separatorChar));
|
||||||
try {
|
try {
|
||||||
BinaryExporter.getInstance().save(currentModel, file.toFile());
|
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));
|
assets.deleteFromCache(new ModelKey(currentModelPath));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
input.animPreviewStatus += " | Speicherfehler: " + e.getMessage();
|
input.animPreviewStatus += " | Speicherfehler: " + e.getMessage();
|
||||||
LOG.error("[AnimPreview] Speicherfehler: {}", e.toString());
|
LOG.error("[AnimPreview] Speicherfehler: {}", e.toString());
|
||||||
|
} finally {
|
||||||
|
if (ac != null) {
|
||||||
|
for (AnimClip c : tempClips) ac.addAnimClip(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,6 +1004,70 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt einen AnimClip "__tpose__" mit einem Einzel-Frame-Track für alle Root-Joints.
|
||||||
|
* Hält jeden Root-Joint an seiner lokalen Bind-Pose-Transform → SC-Matrix = I → T-Pose.
|
||||||
|
* Gibt null zurück wenn das Armature keine Root-Joints hat.
|
||||||
|
*/
|
||||||
|
private static AnimClip buildTPoseClip(com.jme3.anim.Armature armature) {
|
||||||
|
int jointCount = armature.getJointCount();
|
||||||
|
if (jointCount == 0) return null;
|
||||||
|
AnimClip clip = new AnimClip("__tpose__");
|
||||||
|
com.jme3.anim.AnimTrack[] tracks = new com.jme3.anim.AnimTrack[jointCount];
|
||||||
|
for (int i = 0; i < jointCount; i++) {
|
||||||
|
com.jme3.anim.Joint joint = armature.getJoint(i);
|
||||||
|
// getInitialTransform() liefert die echte Bind-Pose (nicht den aktuellen Zustand)
|
||||||
|
com.jme3.math.Transform bt = joint.getInitialTransform();
|
||||||
|
tracks[i] = new com.jme3.anim.TransformTrack(
|
||||||
|
joint,
|
||||||
|
new float[]{0f},
|
||||||
|
new com.jme3.math.Vector3f[]{bt.getTranslation().clone()},
|
||||||
|
new com.jme3.math.Quaternion[]{bt.getRotation().clone()},
|
||||||
|
new com.jme3.math.Vector3f[]{bt.getScale().clone()});
|
||||||
|
}
|
||||||
|
clip.setTracks(tracks);
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String[] MODEL_SAVE_ROOTS = {
|
||||||
|
null, // ASSET_ROOT (src) – wird durch ASSET_ROOT ersetzt
|
||||||
|
"blight-assets/bin/main",
|
||||||
|
"blight-assets/build/resources/main",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Speichert das (bereits strip-bereinigte) Modell in allen bekannten Asset-Verzeichnissen. */
|
||||||
|
private void saveModelStripped(Spatial model, String modelPath, int removedCount) {
|
||||||
|
String rel = modelPath.replace('/', java.io.File.separatorChar);
|
||||||
|
int saved = 0;
|
||||||
|
// Zuerst in ASSET_ROOT (= blight-assets/src/main/resources)
|
||||||
|
java.nio.file.Path srcFile = ASSET_ROOT.resolve(rel);
|
||||||
|
if (java.nio.file.Files.exists(srcFile)) {
|
||||||
|
try {
|
||||||
|
BinaryExporter.getInstance().save(model, srcFile.toFile());
|
||||||
|
assets.deleteFromCache(new com.jme3.asset.ModelKey(modelPath));
|
||||||
|
LOG.info("[AnimPreview] {} Clips entfernt, gespeichert: {}", removedCount, srcFile.toAbsolutePath());
|
||||||
|
saved++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("[AnimPreview] Speichern fehlgeschlagen (src): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Auch in Gradle-Output-Verzeichnisse (damit der Laufzeit-Loader die bereinigte Datei sieht)
|
||||||
|
for (String root : new String[]{"blight-assets/bin/main", "blight-assets/build/resources/main"}) {
|
||||||
|
java.nio.file.Path f = java.nio.file.Paths.get(root).resolve(rel);
|
||||||
|
if (!java.nio.file.Files.exists(f)) continue;
|
||||||
|
try {
|
||||||
|
BinaryExporter.getInstance().save(model, f.toFile());
|
||||||
|
LOG.info("[AnimPreview] Auch gespeichert ({}): {}", root, f.toAbsolutePath());
|
||||||
|
saved++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("[AnimPreview] Speichern fehlgeschlagen ({}): {}", root, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (saved == 0) {
|
||||||
|
LOG.warn("[AnimPreview] Modell nicht gespeichert – kein Pfad gefunden für '{}'", modelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void saveClipToFile(AnimClip clip, com.jme3.anim.Armature armature,
|
private void saveClipToFile(AnimClip clip, com.jme3.anim.Armature armature,
|
||||||
java.nio.file.Path outFile) throws Exception {
|
java.nio.file.Path outFile) throws Exception {
|
||||||
Node holder = new Node("clip_" + clip.getName());
|
Node holder = new Node("clip_" + clip.getName());
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
target = snapRootBoneXZ(target, sc.getArmature());
|
|
||||||
ac.addAnimClip(target);
|
ac.addAnimClip(target);
|
||||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
||||||
if (clipName.equals("sit_down")) {
|
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). */
|
/** Wendet alle geladenen Clips auf {@code model} an (nur wenn es ein Rig hat). */
|
||||||
public void applyAllTo(Spatial model) {
|
public void applyAllTo(Spatial model) {
|
||||||
if (RetargetingSystem.findSkinningControl(model) == null) return;
|
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;
|
int applied = 0;
|
||||||
for (String key : clips.keySet()) {
|
for (String key : clips.keySet()) {
|
||||||
if (applyTo(key, model)) applied++;
|
if (applyTo(key, model)) applied++;
|
||||||
@@ -234,6 +239,9 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
|
|
||||||
for (String name : ac.getAnimClipsNames()) {
|
for (String name : ac.getAnimClipsNames()) {
|
||||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||||||
|
if (armature != null) {
|
||||||
|
animClip = snapRootBoneXZ(animClip, armature);
|
||||||
|
}
|
||||||
clips.put(name, animClip);
|
clips.put(name, animClip);
|
||||||
if (armature != null) armatures.put(name, armature);
|
if (armature != null) armatures.put(name, armature);
|
||||||
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
||||||
@@ -260,15 +268,21 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
return null;
|
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.
|
* Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips).
|
||||||
* Y (Höhenachse, JME3 Y-Up) bleibt vollständig erhalten — sit_down / Jump / Bounce laufen korrekt.
|
* 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.
|
* In diesen Mixamo-Exporten ist die Achsenbelegung des Hips-Bones:
|
||||||
* Bei Rigs wo Root (Tiefe 0) selbst Translations hat, wird Root gesnappt.
|
* Local X → seitlich → wird auf 0 eingefroren (kein Seiten-Drift)
|
||||||
* Bei Rigs wo Hips (Tiefe 1) die erste Ebene mit Translations ist (Root hat nur Rotation),
|
* Local Y → vorwärts → wird auf 0 eingefroren für Lauf-Clips (kein Vorwärts-Drift)
|
||||||
* wird Hips gesnappt. So passt der Snap zu beiden Rig-Strukturen.
|
* bleibt frei für Sitz/Stand-Clips (leichte Neigungsbewegung)
|
||||||
* Erstellt einen neuen in-memory-Clip; J3O-Dateien bleiben unverändert.
|
* Local Z → Höhe → IMMER frei lassen (Charakter-Höhe und Setz-Bewegung erhalten)
|
||||||
*/
|
*/
|
||||||
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
|
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
|
||||||
if (clip == null || armature == null) return clip;
|
if (clip == null || armature == null) return clip;
|
||||||
@@ -282,7 +296,9 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
minDepth = Math.min(minDepth, jointDepth(j));
|
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<>();
|
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||||
boolean modified = false;
|
boolean modified = false;
|
||||||
@@ -292,20 +308,45 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Vector3f[] translations = tt.getTranslations();
|
Vector3f[] translations = tt.getTranslations();
|
||||||
|
// Nur den flachsten Bone anfassen – alle anderen unverändert lassen
|
||||||
if (translations == null || translations.length == 0 || jointDepth(j) != minDepth) {
|
if (translations == null || translations.length == 0 || jointDepth(j) != minDepth) {
|
||||||
newTracks.add(track);
|
newTracks.add(track);
|
||||||
continue;
|
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];
|
Vector3f[] snapped = new Vector3f[translations.length];
|
||||||
for (int i = 0; i < translations.length; i++) {
|
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()));
|
newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales()));
|
||||||
modified = true;
|
modified = true;
|
||||||
log.info("[AnimLib] '{}': Tiefe-{}-Joint '{}' XZ={},{} eingefroren, Y frei",
|
|
||||||
clip.getName(), minDepth, j.getName(), f0x, f0z);
|
|
||||||
}
|
}
|
||||||
if (!modified) return clip;
|
if (!modified) return clip;
|
||||||
AnimClip result = new AnimClip(clip.getName());
|
AnimClip result = new AnimClip(clip.getName());
|
||||||
|
|||||||
@@ -579,16 +579,22 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryPlay(String clip) {
|
private boolean tryPlay(String clip) {
|
||||||
if (animComposer == null || !animLib.ensureApplied(clip, visual)) {
|
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
||||||
log.info("[Anim] tryPlay('{}') → ensureApplied FAILED", clip);
|
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Erst Action setzen, DANN SkinningControl aktivieren –
|
||||||
|
// vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter.
|
||||||
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
|
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
|
||||||
log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED");
|
log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED");
|
||||||
if (action != null) {
|
if (action == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (skinningControl != null && !skinningControl.isEnabled()) {
|
||||||
|
skinningControl.setEnabled(true);
|
||||||
|
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
||||||
|
}
|
||||||
runningClip = clip;
|
runningClip = clip;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,9 @@ public class WorldScene extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot());
|
playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot());
|
||||||
|
if (characterVisual != null) {
|
||||||
|
characterVisual.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CharacterControl setzt den Spatial auf den Kapsel-Mittelpunkt: radius=0.4, halfCyl=0.5 → 0.9m über dem Boden.
|
// CharacterControl setzt den Spatial auf den Kapsel-Mittelpunkt: radius=0.4, halfCyl=0.5 → 0.9m über dem Boden.
|
||||||
@@ -319,6 +322,7 @@ public class WorldScene extends BaseAppState {
|
|||||||
if (mc != null && mc.getModelPath() != null) {
|
if (mc != null && mc.getModelPath() != null) {
|
||||||
try {
|
try {
|
||||||
Spatial loaded = assetManager.loadModel(mc.getModelPath());
|
Spatial loaded = assetManager.loadModel(mc.getModelPath());
|
||||||
|
stripEmbeddedClips(loaded, mc.getModelPath());
|
||||||
loaded.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
loaded.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||||
|
|
||||||
// Auf 1.8 m skalieren – Höhe aus Vertex-Daten (zuverlässiger als BoundingBox
|
// Auf 1.8 m skalieren – Höhe aus Vertex-Daten (zuverlässiger als BoundingBox
|
||||||
@@ -342,6 +346,9 @@ public class WorldScene extends BaseAppState {
|
|||||||
Node rotNode = new Node("charRot");
|
Node rotNode = new Node("charRot");
|
||||||
loaded.setLocalTranslation(0, offsetY, 0);
|
loaded.setLocalTranslation(0, offsetY, 0);
|
||||||
rotNode.attachChild(loaded);
|
rotNode.attachChild(loaded);
|
||||||
|
// Verstecken bis idle-Animation läuft: Armature hat Rx(90°) aus GLB-Import
|
||||||
|
// → ohne Animation liegt das Mesh (SkinningControl inaktiv oder Bind-Pose unklar).
|
||||||
|
rotNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
|
||||||
Node wrapper = new Node("character");
|
Node wrapper = new Node("character");
|
||||||
wrapper.attachChild(rotNode);
|
wrapper.attachChild(rotNode);
|
||||||
@@ -358,6 +365,46 @@ public class WorldScene extends BaseAppState {
|
|||||||
return buildCharacter();
|
return buildCharacter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static final String[] MODEL_SAVE_ROOTS = {
|
||||||
|
"blight-assets/src/main/resources",
|
||||||
|
"blight-assets/bin/main",
|
||||||
|
"blight-assets/build/resources/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()) {
|
||||||
|
log.info("[WorldScene] Keine eingebetteten Clips in '{}' ({})",
|
||||||
|
modelPath, ac != null ? "AnimComposer leer" : "kein AnimComposer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int count = ac.getAnimClips().size();
|
||||||
|
log.info("[WorldScene] Entferne {} eingebettete Clips aus '{}'", count, modelPath);
|
||||||
|
for (com.jme3.anim.AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||||||
|
ac.removeAnimClip(c);
|
||||||
|
}
|
||||||
|
String rel = modelPath.replace('/', java.io.File.separatorChar);
|
||||||
|
int saved = 0;
|
||||||
|
for (String root : MODEL_SAVE_ROOTS) {
|
||||||
|
java.nio.file.Path file = java.nio.file.Paths.get(root).resolve(rel);
|
||||||
|
if (!java.nio.file.Files.exists(file)) continue;
|
||||||
|
try {
|
||||||
|
com.jme3.export.binary.BinaryExporter.getInstance().save(model, file.toFile());
|
||||||
|
log.info("[WorldScene] Gespeichert ({}): {}", root, file.toAbsolutePath());
|
||||||
|
saved++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WorldScene] Speichern fehlgeschlagen ({}): {}", file, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (saved == 0) {
|
||||||
|
log.warn("[WorldScene] Modell nicht gespeichert – kein Pfad gefunden für '{}' (CWD={})",
|
||||||
|
modelPath, java.nio.file.Paths.get(".").toAbsolutePath());
|
||||||
|
}
|
||||||
|
assetManager.deleteFromCache(new com.jme3.asset.ModelKey(modelPath));
|
||||||
|
}
|
||||||
|
|
||||||
private MainCharacter findMainCharacter() {
|
private MainCharacter findMainCharacter() {
|
||||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||||
for (GameCharacter c : CharacterIO.loadAll(charDir)) {
|
for (GameCharacter c : CharacterIO.loadAll(charDir)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user