diff --git a/blight-assets/src/main/resources/Textures/menu/Gemini_Generated_Image_.png b/blight-assets/src/main/resources/Textures/menu/Gemini_Generated_Image_.png new file mode 100644 index 0000000..86e390d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/Gemini_Generated_Image_.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/button_quit.png b/blight-assets/src/main/resources/Textures/menu/button_quit.png index 047bb42..967bcf4 100644 Binary files a/blight-assets/src/main/resources/Textures/menu/button_quit.png and b/blight-assets/src/main/resources/Textures/menu/button_quit.png differ diff --git a/blight-fbx-test/build.gradle b/blight-fbx-test/build.gradle new file mode 100644 index 0000000..3529a60 --- /dev/null +++ b/blight-fbx-test/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'java' +} + +ext { + jmeVersion = '3.9.0-stable' + mainClassName = 'de.blight.fbxtest.FbxTestApp' +} + +dependencies { + implementation "org.jmonkeyengine:jme3-core:${jmeVersion}" + implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}" + implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}" + implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}" + implementation 'com.github.stephengold:MonkeyWrench:1.0.0' + runtimeOnly 'ch.qos.logback:logback-classic:1.5.18' + implementation 'org.slf4j:slf4j-api:2.0.17' +} + +tasks.register('extractNatives', Copy) { + def nativeConf = configurations.runtimeClasspath.resolvedConfiguration + .resolvedArtifacts + .findAll { it.name.contains('natives') } + .collect { zipTree(it.file) } + from nativeConf + into "${buildDir}/natives" + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.register('run', JavaExec) { + group = 'application' + description = 'Startet den FBX-Tester' + mainClass = mainClassName + classpath = sourceSets.main.runtimeClasspath + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(26) + } + jvmArgs = [ + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED', + '--enable-native-access=ALL-UNNAMED', + "-Djava.library.path=${buildDir}/natives", + ] + dependsOn extractNatives + workingDir = rootDir +} diff --git a/blight-fbx-test/src/main/java/de/blight/fbxtest/FbxTestApp.java b/blight-fbx-test/src/main/java/de/blight/fbxtest/FbxTestApp.java new file mode 100644 index 0000000..0be1637 --- /dev/null +++ b/blight-fbx-test/src/main/java/de/blight/fbxtest/FbxTestApp.java @@ -0,0 +1,1152 @@ +package de.blight.fbxtest; + +import com.github.stephengold.wrench.LwjglAssetLoader; +import com.jme3.anim.AnimClip; +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.ScreenshotAppState; +import com.jme3.asset.plugins.FileLocator; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.*; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.material.MatParam; +import com.jme3.math.*; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.*; +import com.jme3.scene.control.Control; +import com.jme3.system.AppSettings; +import com.jme3.texture.Texture; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.util.*; +import java.util.List; + +public class FbxTestApp extends SimpleApplication { + + // ── Zustand ────────────────────────────────────────────────────────────── + private Spatial currentModel; + private AnimComposer animComposer; + private List animNames = new ArrayList<>(); + private int animIndex = -1; + private boolean playing = false; + private String modelName = ""; + + // ── Sequenz-Playback ────────────────────────────────────────────────────── + private final List playSequence = new ArrayList<>(); + private int seqIndex = -1; + private boolean sequencePlaying = false; + private float seqElapsed = 0f; + + // ── Orbit-Kamera ────────────────────────────────────────────────────────── + private float orbitYaw = 0f; + private float orbitPitch = 20f; + private float orbitDist = 3f; + private boolean mouseLeft, mouseRight; + private float lastMouseX, lastMouseY; + + // ── HUD / Panel ────────────────────────────────────────────────────────── + private static final int PANEL_W = 280; + private static final float ITEM_H = 20f; + private static final int LIST_VISIBLE = 24; + + private BitmapText hudTitle; + private BitmapText hudModelInfo; + private BitmapText hudAnimList; + private BitmapText hudHelp; + private int listScrollOffset = 0; + + // ── Letztes FBX-Verzeichnis für den Datei-Dialog ───────────────────────── + private File lastDir = new File(System.getProperty("user.home")); + + // ── Log-Datei ───────────────────────────────────────────────────────────── + private static final java.io.File LOG_FILE = + new java.io.File(System.getProperty("user.home") + "/.blight/fbxtest/debug.log"); + + private static void log(String msg) { + System.out.println(msg); + try (java.io.FileWriter fw = new java.io.FileWriter(LOG_FILE, true)) { + fw.write(msg + "\n"); + } catch (java.io.IOException ignored) {} + } + + // ========================================================================= + + public static void main(String[] args) { + AppSettings s = new AppSettings(true); + s.setTitle("FBX-Tester"); + s.setWidth(1280); + s.setHeight(720); + s.setFrameRate(60); + + FbxTestApp app = new FbxTestApp(); + app.setSettings(s); + app.setShowSettings(false); + app.start(); + } + + // ========================================================================= + + @Override + public void simpleInitApp() { + // MonkeyWrench als Loader registrieren + assetManager.registerLoader(LwjglAssetLoader.class, + "fbx", "glb", "gltf", "dae", "obj", "3ds", "bvh"); + + flyCam.setEnabled(false); + + // Log-Datei leeren + LOG_FILE.getParentFile().mkdirs(); + try { new java.io.FileWriter(LOG_FILE, false).close(); } catch (java.io.IOException ignored) {} + log("=== FBX-Test gestartet ==="); + + // Screenshot mit F12 → ~/.blight/fbxtest/fbxtest_YYYY-MM-DD_HH-mm-ss.png + String screenshotDir = System.getProperty("user.home") + "/.blight/fbxtest"; + new java.io.File(screenshotDir).mkdirs(); + ScreenshotAppState screenshotState = new ScreenshotAppState("", "fbxtest"); + stateManager.attach(screenshotState); + inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12)); + inputManager.addListener((ActionListener)(name, pressed, tpf) -> { + if (pressed) { + String ts = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); + screenshotState.setFileName(screenshotDir + java.io.File.separator + "fbxtest_" + ts + "_"); + screenshotState.takeScreenshot(); + } + }, "Screenshot"); + + setupLights(); + setupOrbitInput(); + setupHud(); + updateHud(); + + // Bodenraster als Orientierungshilfe + buildGrid(); + + // Sitz-Sequenz automatisch laden + enqueue(this::autoLoadSitSequence); + } + + // ── Szene ───────────────────────────────────────────────────────────────── + + private void setupLights() { + AmbientLight ambient = new AmbientLight(new ColorRGBA(0.4f, 0.4f, 0.4f, 1f)); + rootNode.addLight(ambient); + + DirectionalLight sun = new DirectionalLight(); + sun.setDirection(new Vector3f(-1f, -2f, -1f).normalizeLocal()); + sun.setColor(new ColorRGBA(1.1f, 1.0f, 0.9f, 1f)); + rootNode.addLight(sun); + + DirectionalLight fill = new DirectionalLight(); + fill.setDirection(new Vector3f(1f, -0.5f, 1f).normalizeLocal()); + fill.setColor(new ColorRGBA(0.3f, 0.35f, 0.4f, 1f)); + rootNode.addLight(fill); + } + + private void buildGrid() { + Node grid = new Node("grid"); + com.jme3.material.Material mat = new com.jme3.material.Material( + assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.4f, 0.4f, 0.4f, 1f)); + + int size = 10; + for (int i = -size; i <= size; i++) { + com.jme3.scene.shape.Line h = new com.jme3.scene.shape.Line( + new Vector3f(-size, 0, i), new Vector3f(size, 0, i)); + com.jme3.scene.shape.Line v = new com.jme3.scene.shape.Line( + new Vector3f(i, 0, -size), new Vector3f(i, 0, size)); + Geometry gh = new Geometry("", h); gh.setMaterial(mat); + Geometry gv = new Geometry("", v); gv.setMaterial(mat); + grid.attachChild(gh); + grid.attachChild(gv); + } + rootNode.attachChild(grid); + } + + // ── Modell laden ────────────────────────────────────────────────────────── + + private void openFileChooser() { + SwingUtilities.invokeLater(() -> { + JFileChooser fc = new JFileChooser(lastDir); + fc.setDialogTitle("FBX-Modell wählen"); + fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter( + "3D-Modelle (FBX, GLB, GLTF, DAE, OBJ)", "fbx", "glb", "gltf", "dae", "obj")); + fc.setPreferredSize(new Dimension(800, 500)); + int result = fc.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + lastDir = file.getParentFile(); + enqueue(() -> loadModel(file)); + } + }); + } + + private void openAnimFileChooser() { + if (currentModel == null) { + System.out.println("[FBX-Test] Erst ein Modell laden (O), dann Animation importieren (A)."); + return; + } + SwingUtilities.invokeLater(() -> { + JFileChooser fc = new JFileChooser(lastDir); + fc.setDialogTitle("Animations-FBX wählen"); + fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter( + "Animations-Dateien (FBX, BVH, DAE)", "fbx", "bvh", "dae")); + fc.setPreferredSize(new Dimension(800, 500)); + int result = fc.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + lastDir = file.getParentFile(); + enqueue(() -> importAnimation(file)); + } + }); + } + + private void openTextureChooser() { + if (currentModel == null) { + System.out.println("[FBX-Test] Erst ein Modell laden (O), dann Textur importieren (T)."); + return; + } + SwingUtilities.invokeLater(() -> { + JFileChooser fc = new JFileChooser(lastDir); + fc.setDialogTitle("Textur-Quelle wählen (GLB, OBJ, FBX oder Bild)"); + fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter( + "3D-Modelle & Bilder", "glb", "gltf", "fbx", "dae", "obj", "png", "jpg", "jpeg")); + fc.setPreferredSize(new Dimension(800, 500)); + int result = fc.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + lastDir = file.getParentFile(); + enqueue(() -> importTexture(file)); + } + }); + } + + private void importTexture(File file) { + String ext = file.getName().toLowerCase(); + + // Direktes Bild: als DiffuseMap laden + if (ext.endsWith(".png") || ext.endsWith(".jpg") || ext.endsWith(".jpeg")) { + try { + assetManager.registerLocator(file.getParent(), FileLocator.class); + Texture tex = assetManager.loadTexture(file.getName()); + applyTextureToModel(tex); + } catch (Exception e) { + System.err.println("[FBX-Test] Fehler beim Laden der Textur: " + e.getMessage()); + } + return; + } + + // 3D-Datei: erste Diffuse/BaseColor-Textur extrahieren + try { + assetManager.registerLocator(file.getParent(), FileLocator.class); + Spatial src = assetManager.loadModel(file.getName()); + Texture tex = extractFirstTexture(src); + if (tex == null) { + System.out.println("[FBX-Test] Keine Textur in der Datei gefunden: " + file.getName()); + return; + } + applyTextureToModel(tex); + System.out.println("[FBX-Test] Textur übernommen aus: " + file.getName()); + } catch (Exception e) { + System.err.println("[FBX-Test] Fehler beim Textur-Import: " + e.getMessage()); + e.printStackTrace(); + } + } + + private Texture extractFirstTexture(Spatial spatial) { + if (spatial instanceof Geometry geo && geo.getMaterial() != null) { + Material mat = geo.getMaterial(); + for (String key : new String[]{"BaseColorMap", "DiffuseMap", "ColorMap"}) { + MatParam p = mat.getParam(key); + if (p != null && p.getValue() instanceof Texture t) return t; + } + } + if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + Texture t = extractFirstTexture(child); + if (t != null) return t; + } + } + return null; + } + + private void applyTextureToModel(Texture tex) { + applyTextureRecursive(currentModel, tex); + System.out.println("[FBX-Test] Textur auf Modell angewendet."); + } + + private void applyTextureRecursive(Spatial spatial, Texture tex) { + if (spatial instanceof Geometry geo && geo.getMaterial() != null) { + Material mat = geo.getMaterial(); + String defName = mat.getMaterialDef().getName(); + if (defName.contains("Lighting")) { + mat.setTexture("DiffuseMap", tex); + } else if (defName.contains("PBR") || defName.contains("Pbr")) { + mat.setTexture("BaseColorMap", tex); + } + } else if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + applyTextureRecursive(child, tex); + } + } + } + + private void loadModel(File file) { + // Altes Modell entfernen + if (currentModel != null) { + rootNode.detachChild(currentModel); + currentModel = null; + animComposer = null; + animNames.clear(); + animIndex = -1; + playing = false; + } + + fixMaterialsLogged = false; + try { + // FileLocator für FBX-Verzeichnis und das begleitende .fbm-Unterverzeichnis + assetManager.registerLocator(file.getParent(), FileLocator.class); + String baseName = file.getName().replaceAll("\\.[^.]+$", ""); + java.io.File fbmDir = new java.io.File(file.getParentFile(), baseName + ".fbm"); + log("[FBX-Test] FBX geladen aus: " + file.getAbsolutePath()); + log("[FBX-Test] .fbm-Verzeichnis gesucht: " + fbmDir.getAbsolutePath() + " (isDir=" + fbmDir.isDirectory() + ")"); + if (fbmDir.isDirectory()) { + assetManager.registerLocator(fbmDir.getAbsolutePath(), FileLocator.class); + log("[FBX-Test] .fbm-Verzeichnis registriert: " + fbmDir.getAbsolutePath()); + } + // Texturdateien auch direkt im FBX-Verzeichnis suchen (Fallback) + assetManager.registerLocator(file.getParentFile().getAbsolutePath(), FileLocator.class); + + // Verbose-Logging für MonkeyWrench: zeigt ob Z-up erkannt wird und interne Struktur + com.github.stephengold.wrench.LwjglAssetKey lwjglKey = + new com.github.stephengold.wrench.LwjglAssetKey(file.getName()); + lwjglKey.setVerboseLogging(true); + Spatial model = (Spatial) assetManager.loadAsset(lwjglKey); + + // FBX (Mixamo): exportiert in cm → 0.01f; GLB/GLTF/DAE/OBJ: in Metern → 1.0f + String ext = file.getName().toLowerCase(); + float modelScale = ext.endsWith(".fbx") ? 0.01f : 1.0f; + model.setLocalScale(modelScale); + model.setLocalTranslation(0, 0, 0); + model.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + + // Embedded Texturen aus dem FBX-Binär extrahieren (Mixamo embedded PNGs) + java.io.File embeddedDir = extractEmbeddedTextures(file, model); + if (embeddedDir != null) { + assetManager.registerLocator(embeddedDir.getAbsolutePath(), FileLocator.class); + log("[FBX-Test] Embedded-Tex-Dir registriert: " + embeddedDir.getAbsolutePath()); + } + + fixMeshAndSkeleton(model); + fixBrokenTexturePaths(model, file.getParentFile()); + + // Kein Wrapper – direkt an rootNode hängen + rootNode.attachChild(model); + currentModel = model; + modelName = file.getName(); + + // AnimComposer suchen + animComposer = findAnimComposer(model); + animNames.clear(); + if (animComposer != null) { + for (AnimClip clip : animComposer.getAnimClips()) { + animNames.add(clip.getName()); + } + Collections.sort(animNames); + } + animIndex = animNames.isEmpty() ? -1 : 0; + playing = false; + + // Kamera auf Modell ausrichten + orbitDist = 2.5f; + orbitYaw = 0f; + orbitPitch = 15f; + + log("[FBX-Test] === Szenegraph ==="); + printSceneGraph(model, " "); + log("[FBX-Test] === Ende Szenegraph ==="); + log(String.format("[FBX-Test] Geladen: %s | %d Animationen", file.getName(), animNames.size())); + if (animComposer == null) { + log("[FBX-Test] Kein AnimComposer gefunden."); + } else { + animNames.forEach(n -> log(" - " + n)); + } + + // Alle Armature-Joints ausgeben – besonders $AssimpFbx$_Rotation Nodes + SkinningControl skinCtrl = findSkinningControl(model); + if (skinCtrl != null) { + com.jme3.anim.Armature arm = skinCtrl.getArmature(); + log("[FBX-Test] Armature: " + arm.getJointCount() + " Joints"); + for (int i = 0; i < arm.getJointCount(); i++) { + com.jme3.anim.Joint j = arm.getJoint(i); + String parent = j.getParent() != null ? j.getParent().getName() : "—"; + com.jme3.math.Quaternion r = j.getLocalRotation(); + boolean nonIdentRot = Math.abs(r.getW() - 1f) > 0.001f || Math.abs(r.getX()) > 0.001f + || Math.abs(r.getY()) > 0.001f || Math.abs(r.getZ()) > 0.001f; + com.jme3.math.Vector3f t = j.getLocalTranslation(); + boolean nonZeroT = t.lengthSquared() > 0.0001f; + // Immer ausgeben wenn $AssimpFbx$ im Namen, sonst nur wenn nicht-trivial + if (j.getName().contains("$Assimp") || nonIdentRot || nonZeroT || j.getParent() == null) { + log(String.format(" [%2d] %-45s par=%-30s T=%s R=%s", + i, j.getName(), parent, t, r)); + } + } + } else { + log("[FBX-Test] Kein SkinningControl gefunden."); + } + + updateHud(); + + } catch (Exception e) { + System.err.println("[FBX-Test] Fehler beim Laden: " + e.getMessage()); + e.printStackTrace(); + } + } + + private com.jme3.anim.AnimClip findClipByName(String name) { + if (animComposer == null) return null; + for (com.jme3.anim.AnimClip c : animComposer.getAnimClips()) { + if (name.equals(c.getName())) return c; + } + return null; + } + + private void startSequence(List names) { + playSequence.clear(); + for (String n : names) { + if (findClipByName(n) != null) playSequence.add(n); + } + if (playSequence.isEmpty()) { log("[FBX-Test] Sequenz: keine Clips gefunden"); return; } + seqIndex = 0; + seqElapsed = 0f; + sequencePlaying = true; + String first = playSequence.get(0); + animComposer.setCurrentAction(first); + animIndex = animNames.indexOf(first); + playing = true; + log("[FBX-Test] Sequenz gestartet: " + playSequence); + updateHud(); + } + + /** + * Erstellt einen neuen AnimClip, dessen TransformTracks auf Joints der + * targetArm zeigen statt auf Joints der Quell-Armature (Retargeting). + */ + private com.jme3.anim.AnimClip retargetClip( + com.jme3.anim.AnimClip src, + com.jme3.anim.Armature targetArm, + String newName) { + com.jme3.anim.AnimClip result = new com.jme3.anim.AnimClip(newName); + java.util.List> newTracks = new java.util.ArrayList<>(); + for (com.jme3.anim.AnimTrack track : src.getTracks()) { + if (!(track instanceof com.jme3.anim.TransformTrack tt)) continue; + Object tgt = tt.getTarget(); + if (!(tgt instanceof com.jme3.anim.Joint srcJoint)) continue; + com.jme3.anim.Joint destJoint = targetArm.getJoint(srcJoint.getName()); + if (destJoint == null) continue; + newTracks.add(new com.jme3.anim.TransformTrack( + destJoint, tt.getTimes(), + tt.getTranslations(), tt.getRotations(), tt.getScales())); + } + result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0])); + log("[FBX-Test] Retarget '" + newName + "': " + newTracks.size() + " Tracks"); + return result; + } + + /** Importiert ersten Clip aus file, retargetet ihn auf targetArm und legt ihn als newName an. */ + private void importAnimationAs(java.io.File file, String newName, com.jme3.anim.Armature targetArm) { + if (animComposer == null) return; + try { + assetManager.registerLocator(file.getParent(), FileLocator.class); + Spatial src = assetManager.loadModel(file.getName()); + AnimComposer srcComp = findAnimComposer(src); + if (srcComp == null) { log("[FBX-Test] Kein AnimComposer in: " + file.getName()); return; } + com.jme3.anim.AnimClip[] clips = srcComp.getAnimClips() + .toArray(new com.jme3.anim.AnimClip[0]); + if (clips.length == 0) { log("[FBX-Test] Keine Clips in: " + file.getName()); return; } + com.jme3.anim.AnimClip imported = retargetClip(clips[0], targetArm, newName); + animComposer.addAnimClip(imported); + if (!animNames.contains(newName)) animNames.add(newName); + log("[FBX-Test] Importiert als '" + newName + "' (" + String.format("%.2f", clips[0].getLength()) + "s)"); + } catch (Exception e) { + log("[FBX-Test] Import fehlgeschlagen (" + file.getName() + "): " + e.getMessage()); + } + } + + private void autoLoadSitSequence() { + java.io.File dir = new java.io.File("/home/mario/Test"); + + // Modell laden + loadModel(new java.io.File(dir, "Stand To Sit.fbx")); + SkinningControl mainSkin = findSkinningControl(currentModel); + com.jme3.anim.Armature mainArm = mainSkin != null ? mainSkin.getArmature() : null; + + // Ersten Clip (mixamo.com) auf stand_to_sit umbenennen + com.jme3.anim.AnimClip firstClip = animNames.isEmpty() ? null : findClipByName(animNames.get(0)); + if (firstClip != null) { + com.jme3.anim.AnimClip renamed = new com.jme3.anim.AnimClip("stand_to_sit"); + renamed.setTracks(firstClip.getTracks()); + animComposer.addAnimClip(renamed); + animNames.clear(); + animNames.add("stand_to_sit"); + } + + // Weitere Animationen importieren, auf Haupt-Armature retargeten, 25 cm nach hinten (-Z) + importAnimationAs(new java.io.File(dir, "Sitting Idle.fbx"), "sitting_idle", mainArm); + offsetRootTranslation(findClipByName("sitting_idle"), 0, 0, -25f); + + importAnimationAs(new java.io.File(dir, "Sit To Stand.fbx"), "sit_to_stand", mainArm); + offsetRootTranslation(findClipByName("sit_to_stand"), 0, 0, -25f); + + Collections.sort(animNames); + List seq = List.of("stand_to_sit", "sitting_idle", "sit_to_stand"); + log("[FBX-Test] Sequenz: " + seq); + startSequence(seq); + } + + /** Verschiebt alle Root-Joint-Keyframes eines Clips (neuer Track, kein In-Place). */ + private void offsetRootTranslation(com.jme3.anim.AnimClip clip, float dx, float dy, float dz) { + if (clip == null) return; + com.jme3.anim.AnimTrack[] tracks = clip.getTracks(); + for (int i = 0; i < tracks.length; i++) { + if (!(tracks[i] instanceof com.jme3.anim.TransformTrack tt)) continue; + if (!(tt.getTarget() instanceof com.jme3.anim.Joint j)) continue; + if (j.getParent() != null) continue; + com.jme3.math.Vector3f[] orig = tt.getTranslations(); + if (orig == null) { log("[FBX-Test] Root '" + j.getName() + "': keine Translations-KF"); break; } + com.jme3.math.Vector3f[] shifted = new com.jme3.math.Vector3f[orig.length]; + for (int k = 0; k < orig.length; k++) shifted[k] = orig[k].add(dx, dy, dz); + tracks[i] = new com.jme3.anim.TransformTrack( + j, tt.getTimes(), shifted, tt.getRotations(), tt.getScales()); + clip.setTracks(tracks); + log("[FBX-Test] Root verschoben: '" + clip.getName() + "' " + orig.length + + " KF (dx=" + dx + " dy=" + dy + " dz=" + dz + ")"); + break; + } + } + + private void importAnimation(File file) { + if (animComposer == null) { + System.out.println("[FBX-Test] Modell hat keinen AnimComposer – Animation kann nicht importiert werden."); + return; + } + try { + assetManager.registerLocator(file.getParent(), FileLocator.class); + Spatial animSource = assetManager.loadModel(file.getName()); + animSource.setLocalScale(0.01f); + + AnimComposer srcComposer = findAnimComposer(animSource); + if (srcComposer == null) { + System.out.println("[FBX-Test] Keine Animationen in Datei gefunden: " + file.getName()); + return; + } + + int imported = 0; + for (AnimClip clip : srcComposer.getAnimClips()) { + String name = clip.getName(); + animComposer.addAnimClip(clip); + if (!animNames.contains(name)) { + animNames.add(name); + imported++; + System.out.println("[FBX-Test] Animation importiert: " + name); + } + } + Collections.sort(animNames); + if (animIndex < 0 && !animNames.isEmpty()) animIndex = 0; + + System.out.printf("[FBX-Test] %d neue Animation(en) aus %s importiert.%n", + imported, file.getName()); + updateHud(); + + } catch (Exception e) { + System.err.println("[FBX-Test] Fehler beim Animationsimport: " + e.getMessage()); + e.printStackTrace(); + } + } + + // ── Animation steuern ──────────────────────────────────────────────────── + + private void playCurrentAnim() { + sequencePlaying = false; + if (animComposer == null || animIndex < 0) return; + String name = animNames.get(animIndex); + animComposer.setCurrentAction(name); + playing = true; + System.out.println("[FBX-Test] Spiele: " + name); + updateHud(); + } + + private void stopAnim() { + if (animComposer == null) return; + animComposer.reset(); + playing = false; + updateHud(); + } + + private void nextAnim(int dir) { + if (animNames.isEmpty()) return; + animIndex = (animIndex + dir + animNames.size()) % animNames.size(); + if (playing) playCurrentAnim(); + else updateHud(); + } + + // ── HUD ────────────────────────────────────────────────────────────────── + + private void setupHud() { + int W = settings.getWidth(); + int H = settings.getHeight(); + int px = W - PANEL_W + 8; + + // Panel-Hintergrund + com.jme3.scene.shape.Quad panelQuad = new com.jme3.scene.shape.Quad(PANEL_W, H); + Geometry panelBg = new Geometry("panel-bg", panelQuad); + Material panelMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + panelMat.setColor("Color", new ColorRGBA(0.08f, 0.08f, 0.10f, 1f)); + panelBg.setMaterial(panelMat); + panelBg.setLocalTranslation(W - PANEL_W, 0, -2); + guiNode.attachChild(panelBg); + + // Trennlinie + com.jme3.scene.shape.Quad divQuad = new com.jme3.scene.shape.Quad(1, H); + Geometry divLine = new Geometry("panel-div", divQuad); + Material divMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + divMat.setColor("Color", new ColorRGBA(0.3f, 0.3f, 0.35f, 1f)); + divLine.setMaterial(divMat); + divLine.setLocalTranslation(W - PANEL_W, 0, -1); + guiNode.attachChild(divLine); + + BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt"); + float fs = font.getCharSet().getRenderedSize(); + + // Panel-Titel + hudTitle = new BitmapText(font); + hudTitle.setSize(fs * 1.1f); + hudTitle.setColor(ColorRGBA.Yellow); + hudTitle.setLocalTranslation(px, H - 10, 0); + guiNode.attachChild(hudTitle); + + // Modell-Info (Animationsanzahl, Status) + hudModelInfo = new BitmapText(font); + hudModelInfo.setSize(fs * 0.85f); + hudModelInfo.setColor(new ColorRGBA(0.6f, 0.6f, 0.7f, 1f)); + hudModelInfo.setLocalTranslation(px, H - 32, 0); + guiNode.attachChild(hudModelInfo); + + // Animations-Liste + hudAnimList = new BitmapText(font); + hudAnimList.setSize(fs); + hudAnimList.setColor(ColorRGBA.White); + hudAnimList.setLocalTranslation(px, H - 56, 0); + guiNode.attachChild(hudAnimList); + + // Hilfetext unten links + hudHelp = new BitmapText(font); + hudHelp.setSize(fs * 0.85f); + hudHelp.setColor(new ColorRGBA(0.55f, 0.55f, 0.55f, 1f)); + hudHelp.setText("[O] Modell [T] Textur [A] Animation [Leertaste] Play/Stop [F12] Screenshot\n" + + "[Mausrad] Zoom [LMB] Drehen (Panel: Klick/Scroll)"); + hudHelp.setLocalTranslation(8, 38, 0); + guiNode.attachChild(hudHelp); + } + + private void updateHud() { + if (currentModel == null) { + hudTitle.setText("Kein Modell geladen"); + hudModelInfo.setText("[O] FBX/GLB öffnen"); + hudAnimList.setText(""); + return; + } + + String status = playing ? " [läuft]" : ""; + hudTitle.setText(modelName + status); + hudModelInfo.setText(animNames.size() + " Animation(en)" + + (animIndex >= 0 ? " – " + animNames.get(animIndex) : "")); + + if (animNames.isEmpty()) { + hudAnimList.setText("Keine Animationen\n[A] Animation importieren"); + return; + } + + // Scroll-Offset so anpassen, dass animIndex sichtbar bleibt + if (animIndex >= 0) { + if (animIndex < listScrollOffset) { + listScrollOffset = animIndex; + } else if (animIndex >= listScrollOffset + LIST_VISIBLE) { + listScrollOffset = animIndex - LIST_VISIBLE + 1; + } + } + + int visEnd = Math.min(animNames.size(), listScrollOffset + LIST_VISIBLE); + StringBuilder sb = new StringBuilder(); + for (int i = listScrollOffset; i < visEnd; i++) { + if (i == animIndex) { + sb.append(playing ? "► " : "» "); + } else { + sb.append(" "); + } + String name = animNames.get(i); + // Lange Namen kürzen damit sie ins Panel passen + if (name.length() > 26) name = name.substring(0, 24) + ".."; + sb.append(name).append('\n'); + } + if (animNames.size() > LIST_VISIBLE) { + sb.append(" … ").append(listScrollOffset + LIST_VISIBLE) + .append('/').append(animNames.size()); + } + hudAnimList.setText(sb.toString()); + } + + // ── Orbit-Kamera ────────────────────────────────────────────────────────── + + private void setupOrbitInput() { + inputManager.addMapping("Open", new KeyTrigger(KeyInput.KEY_O)); + inputManager.addMapping("ImportAnim", new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("ImportTex", new KeyTrigger(KeyInput.KEY_T)); + inputManager.addMapping("AnimNext", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addMapping("AnimPrev", new KeyTrigger(KeyInput.KEY_LEFT)); + inputManager.addMapping("PlayStop", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addMapping("MouseL", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + inputManager.addMapping("MouseR", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false)); + inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)); + + ActionListener actions = (name, pressed, tpf) -> { + switch (name) { + case "Open" -> { if (pressed) openFileChooser(); } + case "ImportAnim" -> { if (pressed) openAnimFileChooser(); } + case "ImportTex" -> { if (pressed) openTextureChooser(); } + case "AnimNext" -> { if (pressed) nextAnim(+1); } + case "AnimPrev" -> { if (pressed) nextAnim(-1); } + case "PlayStop" -> { if (pressed) { if (playing) stopAnim(); else playCurrentAnim(); } } + case "MouseL" -> { + if (pressed) { + Vector2f cur = inputManager.getCursorPosition(); + if (cur.x >= settings.getWidth() - PANEL_W) { + handlePanelClick(cur); + } else { + mouseLeft = true; + } + } else { + mouseLeft = false; + } + } + case "MouseR" -> mouseRight = pressed; + } + }; + inputManager.addListener(actions, + "Open", "ImportAnim", "ImportTex", "AnimNext", "AnimPrev", "PlayStop", "MouseL", "MouseR"); + + AnalogListener analog = (name, value, tpf) -> { + Vector2f cur = inputManager.getCursorPosition(); + boolean inPanel = cur.x >= settings.getWidth() - PANEL_W; + switch (name) { + case "ZoomIn" -> { if (inPanel) scrollList(-1); else orbitDist = Math.max(0.5f, orbitDist - value * 10f); } + case "ZoomOut" -> { if (inPanel) scrollList(+1); else orbitDist = Math.min(50f, orbitDist + value * 10f); } + } + }; + inputManager.addListener(analog, "ZoomIn", "ZoomOut"); + } + + @Override + public void simpleUpdate(float tpf) { + // Sequenz-Weiterschaltung + if (sequencePlaying && animComposer != null && !playSequence.isEmpty()) { + seqElapsed += tpf; + com.jme3.anim.AnimClip clip = findClipByName(playSequence.get(seqIndex)); + if (clip != null && seqElapsed >= clip.getLength()) { + seqIndex = (seqIndex + 1) % playSequence.size(); + String next = playSequence.get(seqIndex); + animComposer.setCurrentAction(next); + animIndex = animNames.indexOf(next); + seqElapsed = 0f; + updateHud(); + } + } + + Vector2f cur = inputManager.getCursorPosition(); + // Orbit-Kamera nur in der 3D-Ansicht (nicht im Panel) + if ((mouseLeft || mouseRight) && cur.x < settings.getWidth() - PANEL_W) { + float dx = cur.x - lastMouseX; + float dy = cur.y - lastMouseY; + lastMouseX = cur.x; + lastMouseY = cur.y; + if (mouseLeft) { + orbitYaw -= dx * 0.4f; + orbitPitch = FastMath.clamp(orbitPitch + dy * 0.4f, -89f, 89f); + } + } else { + lastMouseX = cur.x; + lastMouseY = cur.y; + } + + // Kameraposition berechnen + float yawRad = orbitYaw * FastMath.DEG_TO_RAD; + float pitchRad = orbitPitch * FastMath.DEG_TO_RAD; + float cx = orbitDist * FastMath.cos(pitchRad) * FastMath.sin(yawRad); + float cy = orbitDist * FastMath.sin(pitchRad); + float cz = orbitDist * FastMath.cos(pitchRad) * FastMath.cos(yawRad); + + float targetY = (currentModel != null) ? 1.0f : 0f; + cam.setLocation(new Vector3f(cx, cy + targetY, cz)); + cam.lookAt(new Vector3f(0, targetY, 0), Vector3f.UNIT_Y); + } + + // ── Hilfsmethoden ──────────────────────────────────────────────────────── + + private void handlePanelClick(Vector2f cursor) { + if (animNames.isEmpty()) return; + // Liste beginnt bei Y = height - 56 (s. setupHud) und läuft nach unten + float listTop = settings.getHeight() - 56; + float relY = listTop - cursor.y; // Pixel unterhalb des Listen-Starts + if (relY < 0) return; // Klick oberhalb der Liste (Titel/Info) + int clicked = listScrollOffset + (int)(relY / ITEM_H); + if (clicked >= 0 && clicked < animNames.size()) { + animIndex = clicked; + playCurrentAnim(); + } + } + + private void scrollList(int dir) { + int maxOffset = Math.max(0, animNames.size() - LIST_VISIBLE); + listScrollOffset = Math.max(0, Math.min(maxOffset, listScrollOffset + dir)); + updateHud(); + } + + private void printSceneGraph(Spatial s, String indent) { + String type = s.getClass().getSimpleName(); + String rot = s.getLocalRotation().toString(); + String ctrl = ""; + for (int i = 0; i < s.getNumControls(); i++) { + ctrl += " [" + s.getControl(i).getClass().getSimpleName() + "]"; + } + log(indent + type + " '" + s.getName() + "' rot=" + rot + ctrl); + if (s instanceof Node n) { + for (Spatial child : n.getChildren()) { + printSceneGraph(child, indent + " "); + } + } + } + + private AnimComposer findAnimComposer(Spatial spatial) { + AnimComposer c = spatial.getControl(AnimComposer.class); + if (c != null) return c; + if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + c = findAnimComposer(child); + if (c != null) return c; + } + } + return null; + } + + private SkinningControl findSkinningControl(Spatial spatial) { + SkinningControl c = spatial.getControl(SkinningControl.class); + if (c != null) return c; + if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + c = findSkinningControl(child); + if (c != null) return c; + } + } + return null; + } + + /** + * Setzt Rotation von Mesh-Container-Nodes auf identity. + * Nodes mit AnimComposer oder SkinningControl werden NICHT angefasst. + * Gibt true zurück wenn mindestens eine Rotation genullt wurde + * (→ Quelle war Z-up, Root-Node braucht -90°X Korrektur). + */ + private boolean zeroMeshNodeRotations(Spatial spatial) { + if (!(spatial instanceof Node node)) return false; + boolean hasSkeleton = node.getControl(AnimComposer.class) != null + || node.getControl(SkinningControl.class) != null; + boolean zeroed = false; + if (!hasSkeleton) { + Quaternion rot = node.getLocalRotation(); + boolean isIdentity = Math.abs(rot.getW() - 1f) < 0.01f + && Math.abs(rot.getX()) < 0.01f + && Math.abs(rot.getY()) < 0.01f + && Math.abs(rot.getZ()) < 0.01f; + if (!isIdentity) { + log("[FBX-Test] zeroMeshNodeRotation: '" + node.getName() + "' war " + rot); + node.setLocalRotation(Quaternion.IDENTITY); + zeroed = true; + } + } + for (Spatial child : node.getChildren()) { + if (zeroMeshNodeRotations(child)) zeroed = true; + } + return zeroed; + } + + /** + * Kombinierter Fix für Mesh + Skelett: + * 1. Mesh-Container-Rotation nullen (Mesh-Vertices sind in Y-up, der +90°X war falsch) + * 2. Root-Joint-Position von Z-up → Y-up konvertieren (Hips Z=57cm → Y=57cm) + * 3. Inverse Bind Matrices aus der korrigierten Pose neu berechnen + */ + private void fixMeshAndSkeleton(Spatial model) { + fixMaterials(model); + zeroMeshNodeRotations(model); + + SkinningControl skinCtrl = findSkinningControl(model); + if (skinCtrl == null) return; + com.jme3.anim.Armature arm = skinCtrl.getArmature(); + + // Root-Joint-Position: Z-up (x,y,z) → Y-up (x,z,-y) + boolean fixed = false; + for (int i = 0; i < arm.getJointCount(); i++) { + com.jme3.anim.Joint j = arm.getJoint(i); + if (j.getParent() == null) { + com.jme3.math.Vector3f t = j.getLocalTransform().getTranslation(); + float ox = t.x, oy = t.y, oz = t.z; + t.set(ox, oz, -oy); + log("[FBX-Test] Root-Joint '" + j.getName() + "': (" + ox + "," + oy + "," + oz + ") → " + t); + fixed = true; + } + } + if (!fixed) return; + + // Inverse Bind Matrices aus der neuen Pose neu berechnen + java.util.HashMap modelMats = new java.util.HashMap<>(); + for (int i = 0; i < arm.getJointCount(); i++) { + buildJointModelMatrix(arm.getJoint(i), modelMats); + } + for (int i = 0; i < arm.getJointCount(); i++) { + com.jme3.anim.Joint j = arm.getJoint(i); + com.jme3.math.Matrix4f ibm = j.getInverseModelBindMatrix(); + if (ibm != null) { + ibm.set(modelMats.get(j).invert()); + } + } + // Bind-Pose sichern (initialTransform = neuer localTransform) + arm.saveBindPose(); + log("[FBX-Test] Inverse Bind Matrices Y-up neu gesetzt."); + } + + private com.jme3.math.Matrix4f buildJointModelMatrix( + com.jme3.anim.Joint j, + java.util.HashMap cache) { + com.jme3.math.Matrix4f cached = cache.get(j); + if (cached != null) return cached; + com.jme3.math.Matrix4f local = j.getLocalTransform().toTransformMatrix(); + com.jme3.math.Matrix4f result = (j.getParent() != null) + ? buildJointModelMatrix(j.getParent(), cache).mult(local) + : local; + cache.put(j, result); + return result; + } + + /** Ersetzt alle Materialien durch Lighting.j3md (PBR braucht LightProbe, den wir nicht haben). */ + private boolean fixMaterialsLogged = false; + + private void fixMaterials(Spatial spatial) { + if (spatial instanceof Geometry geo) { + Material mat = geo.getMaterial(); + if (mat == null) return; + + // Einmalig alle Original-Param-Namen loggen + if (!fixMaterialsLogged) { + fixMaterialsLogged = true; + StringBuilder sb = new StringBuilder("[FBX-Test] Material '") + .append(mat.getMaterialDef().getName()).append("' params: "); + for (MatParam p : mat.getParams()) { + Object v = p.getValue(); + sb.append(p.getName()).append('=') + .append(v instanceof Texture t ? "Tex(" + t.getKey() + ")" : v) + .append(" "); + } + log(sb.toString()); + } + + String defName = mat.getMaterialDef().getName(); + + if (defName.contains("Lighting")) { + // Bereits Lighting.j3md – Material NICHT ersetzen, nur Diffuse auf White + // setzen damit eine vorhandene DiffuseMap nicht abgedunkelt wird. + MatParam dm = mat.getParam("DiffuseMap"); + if (dm != null && dm.getValue() instanceof Texture) { + mat.setColor("Diffuse", ColorRGBA.White.clone()); + mat.setColor("Ambient", new ColorRGBA(0.45f, 0.45f, 0.45f, 1f)); + } + return; + } + + // PBR oder anderes Format → Lighting.j3md bauen und Texturen übernehmen + Texture diffuseTex = null; + Texture normalTex = null; + ColorRGBA baseColor = null; + + for (String name : new String[]{"BaseColorMap", "DiffuseMap", "Albedo", "ColorMap"}) { + MatParam p = mat.getParam(name); + if (p != null && p.getValue() instanceof Texture t) { diffuseTex = t; break; } + } + for (String name : new String[]{"NormalMap", "NormalCamera"}) { + MatParam p = mat.getParam(name); + if (p != null && p.getValue() instanceof Texture t) { normalTex = t; break; } + } + for (String name : new String[]{"BaseColor", "Diffuse"}) { + MatParam p = mat.getParam(name); + if (p != null && p.getValue() instanceof ColorRGBA c) { baseColor = c.clone(); break; } + } + + Material lit = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + lit.setBoolean("UseMaterialColors", true); + if (diffuseTex != null) { + lit.setTexture("DiffuseMap", diffuseTex); + lit.setColor("Diffuse", ColorRGBA.White.clone()); + lit.setColor("Ambient", new ColorRGBA(0.45f, 0.45f, 0.45f, 1f)); + } else { + ColorRGBA col = baseColor != null ? baseColor : ColorRGBA.LightGray.clone(); + lit.setColor("Diffuse", col); + lit.setColor("Ambient", col.mult(0.4f).setAlpha(1f)); + } + if (normalTex != null) lit.setTexture("NormalMap", normalTex); + lit.setColor("Specular", new ColorRGBA(0.1f, 0.1f, 0.1f, 1f)); + lit.setFloat("Shininess", 24f); + geo.setMaterial(lit); + + } else if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + fixMaterials(child); + } + } + } + + /** + * Ersetzt kaputte Textur-Pfade (z.B. Mixamo-Server-Absolut-Pfade) durch einen + * Reload per Dateiname. Die FBX-/fbm-Verzeichnisse müssen bereits als + * FileLocator registriert sein. + */ + /** Sammelt alle Dateinamen aus Textur-Params eines Spatial-Baums. */ + private java.util.Set collectTextureFilenames(Spatial spatial) { + java.util.Set names = new java.util.LinkedHashSet<>(); + if (spatial instanceof Geometry geo) { + Material mat = geo.getMaterial(); + if (mat != null) { + for (MatParam p : mat.getParams()) { + if (p.getValue() instanceof Texture tex && tex.getKey() != null) { + String name = new java.io.File(tex.getKey().getName()).getName(); + if (!name.isEmpty()) names.add(name); + } + } + } + } else if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) names.addAll(collectTextureFilenames(child)); + } + return names; + } + + /** + * Sucht eingebettete PNG-Texturen im FBX-Binär: Dateiname → PNG-Magic → IEND. + * Schreibt gefundene PNGs in ein temporäres Verzeichnis und gibt es zurück. + */ + private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) { + java.util.Set filenames = collectTextureFilenames(model); + if (filenames.isEmpty()) return null; + try { + byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath()); + java.io.File tempDir = new java.io.File( + System.getProperty("java.io.tmpdir"), + "fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode())); + tempDir.mkdirs(); + + byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}; + byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82}; + + boolean any = false; + for (String filename : filenames) { + byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + int namePos = binaryIndexOf(data, nameBytes, 0); + if (namePos < 0) { log("[FBX-Test] Embedded: '" + filename + "' nicht im FBX"); continue; } + + int pngPos = binaryIndexOf(data, pngMagic, namePos); + if (pngPos < 0 || pngPos - namePos > 65536) { + log("[FBX-Test] Embedded: PNG für '" + filename + "' zu weit weg oder fehlt"); + continue; + } + int endPos = binaryIndexOf(data, iend, pngPos + 8); + if (endPos < 0) { log("[FBX-Test] Embedded: IEND für '" + filename + "' fehlt"); continue; } + + byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length); + java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png); + log("[FBX-Test] Embedded-Textur extrahiert: " + filename + " (" + png.length + " B)"); + any = true; + } + return any ? tempDir : null; + } catch (Exception e) { + log("[FBX-Test] Embedded-Extraktion fehlgeschlagen: " + e.getMessage()); + return null; + } + } + + private static int binaryIndexOf(byte[] data, byte[] pattern, int from) { + outer: + for (int i = from, max = data.length - pattern.length; i <= max; i++) { + for (int j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) continue outer; + } + return i; + } + return -1; + } + + private void fixBrokenTexturePaths(Spatial spatial, java.io.File baseDir) { + if (spatial instanceof Geometry geo) { + Material mat = geo.getMaterial(); + if (mat == null) return; + List params = new ArrayList<>(mat.getParams()); + for (MatParam mp : params) { + if (!(mp.getValue() instanceof Texture tex)) continue; + com.jme3.asset.AssetKey key = tex.getKey(); + if (key == null) continue; + String filename = new java.io.File(key.getName()).getName(); + if (filename.isEmpty()) continue; + + // Versuch 1: direkt per Dateiname (aus bereits registrierten Locators) + try { + com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true); + texKey.setGenerateMips(true); + Texture reloaded = assetManager.loadTexture(texKey); + mat.setTexture(mp.getName(), reloaded); + log("[FBX-Test] Textur geladen: " + mp.getName() + " = " + filename); + continue; + } catch (Exception ignored) {} + + // Versuch 2: Dateisystemsuche bis Tiefe 3 ab FBX-Verzeichnis + try { + final String fn = filename; + java.nio.file.Path found = java.nio.file.Files.walk(baseDir.toPath(), 3) + .filter(p -> p.getFileName().toString().equalsIgnoreCase(fn)) + .findFirst().orElse(null); + if (found != null) { + String dir = found.getParent().toAbsolutePath().toString(); + assetManager.registerLocator(dir, FileLocator.class); + com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true); + texKey.setGenerateMips(true); + Texture reloaded = assetManager.loadTexture(texKey); + mat.setTexture(mp.getName(), reloaded); + log("[FBX-Test] Textur gefunden per Suche: " + mp.getName() + " in " + dir); + } else { + log("[FBX-Test] Textur nicht gefunden: " + filename); + } + } catch (Exception e) { + log("[FBX-Test] Textur-Suche fehlgeschlagen: " + filename + " – " + e.getMessage()); + } + } + } else if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + fixBrokenTexturePaths(child, baseDir); + } + } + } +} diff --git a/blight-fbx-test/src/main/resources/logback.xml b/blight-fbx-test/src/main/resources/logback.xml new file mode 100644 index 0000000..8beb649 --- /dev/null +++ b/blight-fbx-test/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{HH:mm:ss} %-5level [%logger{20}] %msg%n + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 4e55789..c54eedc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,3 +7,4 @@ include 'blight-lang' include 'blight-editor' include 'blight-game' include 'blight-vegetation-generator' +include 'blight-fbx-test'