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();
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
@@ -200,9 +200,7 @@ public class AnimPreviewState extends BaseAppState {
|
||||
currentAction = ac.setCurrentAction(currentClipName);
|
||||
if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed);
|
||||
} else {
|
||||
currentAction = null;
|
||||
currentClipName = null;
|
||||
setSkinningEnabled(currentModel, false);
|
||||
stopAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,11 +225,11 @@ public class AnimPreviewState extends BaseAppState {
|
||||
previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist));
|
||||
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) {
|
||||
float s = previewCamDist * input.animPreviewZoom * 0.18f;
|
||||
axesNode.setLocalScale(s);
|
||||
axesNode.setLocalTranslation(previewTarget);
|
||||
axesNode.setLocalTranslation(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
previewScene.updateLogicalState(tpf);
|
||||
@@ -249,10 +247,6 @@ public class AnimPreviewState extends BaseAppState {
|
||||
|
||||
try {
|
||||
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.
|
||||
// Eventuelle Translation die beim GLB-Export eingebacken wurde entfernen.
|
||||
model.setLocalTranslation(Vector3f.ZERO);
|
||||
@@ -264,7 +258,12 @@ public class AnimPreviewState extends BaseAppState {
|
||||
// Alle Clips in-place snappen (verhindert Drift im Preview)
|
||||
AnimComposer previewAC = findControl(model, AnimComposer.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) {
|
||||
// Eingebettete Clips snappen
|
||||
for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) {
|
||||
AnimClip snapped = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(c, previewSC.getArmature());
|
||||
if (snapped != c) {
|
||||
@@ -272,18 +271,41 @@ public class AnimPreviewState extends BaseAppState {
|
||||
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();
|
||||
if (model.getWorldBound() instanceof BoundingBox bb) {
|
||||
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||
previewCamDist = ext * 2.8f;
|
||||
previewTarget.set(bb.getCenter());
|
||||
} else {
|
||||
previewCamDist = 3f;
|
||||
previewTarget.set(0, 1, 0);
|
||||
}
|
||||
previewTarget.set(0, 1, 0);
|
||||
input.animPreviewZoom = 1.0f;
|
||||
|
||||
// Clips sammeln und melden
|
||||
@@ -379,7 +401,12 @@ public class AnimPreviewState extends BaseAppState {
|
||||
currentClipName = null;
|
||||
if (currentModel != null) {
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
java.nio.file.Path outFile) throws Exception {
|
||||
Node holder = new Node("clip_" + clip.getName());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -579,16 +579,22 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private boolean tryPlay(String clip) {
|
||||
if (animComposer == null || !animLib.ensureApplied(clip, visual)) {
|
||||
log.info("[Anim] tryPlay('{}') → ensureApplied FAILED", clip);
|
||||
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||
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);
|
||||
log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED");
|
||||
if (action != null) {
|
||||
runningClip = clip;
|
||||
return true;
|
||||
if (action == null) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (skinningControl != null && !skinningControl.isEnabled()) {
|
||||
skinningControl.setEnabled(true);
|
||||
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
||||
}
|
||||
runningClip = clip;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +307,9 @@ public class WorldScene extends BaseAppState {
|
||||
}
|
||||
}
|
||||
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.
|
||||
@@ -319,6 +322,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
|
||||
@@ -342,6 +346,9 @@ public class WorldScene extends BaseAppState {
|
||||
Node rotNode = new Node("charRot");
|
||||
loaded.setLocalTranslation(0, offsetY, 0);
|
||||
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");
|
||||
wrapper.attachChild(rotNode);
|
||||
@@ -358,6 +365,46 @@ public class WorldScene extends BaseAppState {
|
||||
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() {
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||
for (GameCharacter c : CharacterIO.loadAll(charDir)) {
|
||||
|
||||
Reference in New Issue
Block a user