Charakter-Liegend-Fix: T-Pose im Editor, CullHint im Spiel, Strip in src+bin
Editor (AnimPreviewState): - __tpose__ AnimClip mit Einzel-Frame-Track für Root-Joints (behebt NPE in ClipAction.doInterpolate; leerer Clip hatte null-tracks-Array) - Beim Laden: eingebettete Clips automatisch strippt und in src+bin speichern; danach __tpose__ abspielen → Charakter steht in Bind-Pose - stopAll(): zurück zur T-Pose statt SkinningControl deaktivieren - Achsen-Indikator immer fest auf (0,0,0) statt bounding-box-Mitte Spiel (WorldScene): - CullHint.Always beim Laden, Inherit nach Animation-Setup (kein liegender Charakter) - stripEmbeddedClips: speichert jetzt in src + bin + build (alle bekannten Pfade); besseres Logging bei fehlendem Pfad Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,12 +247,6 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Spatial model = loadFresh(assetPath);
|
Spatial model = loadFresh(assetPath);
|
||||||
// SkinningControl deaktiviert + Modell unsichtbar bis Animation läuft:
|
|
||||||
// das Armature-Node hat Rx(90°) aus dem GLB-Import → ohne Animation liegt
|
|
||||||
// das Mesh auf dem Boden (Bind-Pose × Rx90° = liegender Charakter).
|
|
||||||
setSkinningEnabled(model, false);
|
|
||||||
model.setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -267,6 +259,7 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
AnimComposer previewAC = findControl(model, AnimComposer.class);
|
AnimComposer previewAC = findControl(model, AnimComposer.class);
|
||||||
SkinningControl previewSC = findControl(model, SkinningControl.class);
|
SkinningControl previewSC = findControl(model, SkinningControl.class);
|
||||||
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) {
|
||||||
@@ -274,6 +267,25 @@ 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 mit einem Einzel-Frame-Track für den Root-Joint.
|
||||||
|
// Leerer Clip → NPE in ClipAction.doInterpolate (JME3 erwartet tracks != null).
|
||||||
|
// Ein Frame am Root-Joint in Bind-Pose → alle anderen Joints bleiben an
|
||||||
|
// ihren lokalen Bind-Pose-Transforms → SC-Matrix = Bind × Bind⁻¹ = I
|
||||||
|
// → Vertices in Y-up = stehender Charakter.
|
||||||
|
AnimClip tpose = buildTPoseClip(previewSC.getArmature());
|
||||||
|
if (tpose != null) {
|
||||||
|
previewAC.addAnimClip(tpose);
|
||||||
|
setSkinningEnabled(model, true);
|
||||||
|
previewAC.setCurrentAction("__tpose__");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kamera auf Bounding Box ausrichten
|
// Kamera auf Bounding Box ausrichten
|
||||||
@@ -350,8 +362,6 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
// SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und
|
// SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und
|
||||||
// SkinningControl sitzen oft auf verschiedenen Geschwisterknoten.
|
// SkinningControl sitzen oft auf verschiedenen Geschwisterknoten.
|
||||||
setSkinningEnabled(currentModel, true);
|
setSkinningEnabled(currentModel, true);
|
||||||
// Modell erst nach Animation sichtbar machen (CullHint aus loadModel)
|
|
||||||
currentModel.setCullHint(com.jme3.scene.Spatial.CullHint.Inherit);
|
|
||||||
playOnSpatial(currentModel, clipName);
|
playOnSpatial(currentModel, clipName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +393,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)
|
||||||
currentModel.setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||||
|
if (ac != null && ac.getAnimClipsNames().contains("__tpose__")) {
|
||||||
|
setSkinningEnabled(currentModel, true);
|
||||||
|
ac.setCurrentAction("__tpose__");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +996,68 @@ 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) {
|
||||||
|
com.jme3.anim.Joint[] roots = armature.getRoots();
|
||||||
|
if (roots == null || roots.length == 0) return null;
|
||||||
|
AnimClip clip = new AnimClip("__tpose__");
|
||||||
|
com.jme3.anim.AnimTrack[] tracks = new com.jme3.anim.AnimTrack[roots.length];
|
||||||
|
for (int i = 0; i < roots.length; i++) {
|
||||||
|
com.jme3.anim.Joint root = roots[i];
|
||||||
|
tracks[i] = new com.jme3.anim.TransformTrack(
|
||||||
|
root,
|
||||||
|
new float[]{0f},
|
||||||
|
new com.jme3.math.Vector3f[]{root.getLocalTranslation().clone()},
|
||||||
|
new com.jme3.math.Quaternion[]{root.getLocalRotation().clone()},
|
||||||
|
new com.jme3.math.Vector3f[]{root.getLocalScale().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());
|
||||||
|
|||||||
@@ -307,7 +307,6 @@ public class WorldScene extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot());
|
playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot());
|
||||||
// Charakter sichtbar machen: idle-Animation läuft jetzt
|
|
||||||
if (characterVisual != null) {
|
if (characterVisual != null) {
|
||||||
characterVisual.setCullHint(Spatial.CullHint.Inherit);
|
characterVisual.setCullHint(Spatial.CullHint.Inherit);
|
||||||
}
|
}
|
||||||
@@ -347,8 +346,8 @@ 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);
|
||||||
// Charakter verstecken bis Animation läuft: das Armature-Node hat Rx(90°) aus dem
|
// Verstecken bis idle-Animation läuft: Armature hat Rx(90°) aus GLB-Import
|
||||||
// GLB-Import → ohne laufende Animation liegt das Mesh auf dem Boden.
|
// → ohne Animation liegt das Mesh (SkinningControl inaktiv oder Bind-Pose unklar).
|
||||||
rotNode.setCullHint(Spatial.CullHint.Always);
|
rotNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
|
||||||
Node wrapper = new Node("character");
|
Node wrapper = new Node("character");
|
||||||
@@ -367,11 +366,18 @@ public class WorldScene extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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) {
|
private void stripEmbeddedClips(Spatial model, String modelPath) {
|
||||||
com.jme3.anim.AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(model);
|
com.jme3.anim.AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(model);
|
||||||
if (ac == null || ac.getAnimClips().isEmpty()) {
|
if (ac == null || ac.getAnimClips().isEmpty()) {
|
||||||
log.info("[WorldScene] Keine eingebetteten Clips in '{}' (AnimComposer={})",
|
log.info("[WorldScene] Keine eingebetteten Clips in '{}' ({})",
|
||||||
modelPath, ac != null ? "leer" : "nicht gefunden");
|
modelPath, ac != null ? "AnimComposer leer" : "kein AnimComposer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int count = ac.getAnimClips().size();
|
int count = ac.getAnimClips().size();
|
||||||
@@ -379,20 +385,23 @@ public class WorldScene extends BaseAppState {
|
|||||||
for (com.jme3.anim.AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
for (com.jme3.anim.AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||||||
ac.removeAnimClip(c);
|
ac.removeAnimClip(c);
|
||||||
}
|
}
|
||||||
java.nio.file.Path assetRoot = AnimationLibrary.findAssetRoot();
|
String rel = modelPath.replace('/', java.io.File.separatorChar);
|
||||||
java.nio.file.Path file = assetRoot.resolve(modelPath.replace('/', java.io.File.separatorChar));
|
int saved = 0;
|
||||||
if (!java.nio.file.Files.exists(file)) {
|
for (String root : MODEL_SAVE_ROOTS) {
|
||||||
log.warn("[WorldScene] Modelldatei nicht gefunden zum Speichern: {} (assetRoot={})",
|
java.nio.file.Path file = java.nio.file.Paths.get(root).resolve(rel);
|
||||||
file.toAbsolutePath(), assetRoot.toAbsolutePath());
|
if (!java.nio.file.Files.exists(file)) continue;
|
||||||
assetManager.deleteFromCache(new com.jme3.asset.ModelKey(modelPath));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
com.jme3.export.binary.BinaryExporter.getInstance().save(model, file.toFile());
|
com.jme3.export.binary.BinaryExporter.getInstance().save(model, file.toFile());
|
||||||
log.info("[WorldScene] {} Clips entfernt, Datei gespeichert: {}", count, file.toAbsolutePath());
|
log.info("[WorldScene] Gespeichert ({}): {}", root, file.toAbsolutePath());
|
||||||
|
saved++;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[WorldScene] Speichern fehlgeschlagen ({}): {}", file, e.getMessage());
|
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));
|
assetManager.deleteFromCache(new com.jme3.asset.ModelKey(modelPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user