diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260816_213338.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260816_213338.j3o.thumb.png deleted file mode 100644 index b23127b..0000000 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260816_213338.j3o.thumb.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260816_213341.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260816_213341.j3o.thumb.png deleted file mode 100644 index 57562b7..0000000 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260816_213341.j3o.thumb.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260914_195434.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260914_195434.j3o.thumb.png new file mode 100644 index 0000000..1251a5d Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/trees/palm/palm_20260914_195434.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101850.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101850.j3o.thumb.png deleted file mode 100644 index d9edfa2..0000000 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101850.j3o.thumb.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101856.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101856.j3o.thumb.png deleted file mode 100644 index fd33838..0000000 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101856.j3o.thumb.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260914_210538.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260914_210538.j3o.thumb.png new file mode 100644 index 0000000..59a7476 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260914_210538.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o b/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o deleted file mode 100644 index 2872d62..0000000 Binary files a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o.meta b/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o.meta index 0a46975..77061c9 100644 --- a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o.meta +++ b/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213338.j3o.meta @@ -3,7 +3,7 @@ attachedEmitters.count=0 attachedLights.count=0 castShadow=true category= -cullDistance=120.0 +cullDistance=200.0 footstepSurface= interactableOffsetX=0.0 interactableOffsetY=0.5 diff --git a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o b/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o deleted file mode 100644 index 2d6ab7a..0000000 Binary files a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o.meta b/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o.meta index 26aeda3..80f8523 100644 --- a/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o.meta +++ b/blight-assets/src/main/resources/Models/trees/palm/palm_20260816_213341.j3o.meta @@ -3,7 +3,7 @@ attachedEmitters.count=0 attachedLights.count=0 castShadow=true category= -cullDistance=120.0 +cullDistance=200.0 footstepSurface= interactableOffsetX=0.0 interactableOffsetY=0.5 diff --git a/blight-assets/src/main/resources/Models/trees/palm/palm_20260914_195434.j3o b/blight-assets/src/main/resources/Models/trees/palm/palm_20260914_195434.j3o new file mode 100644 index 0000000..e4cf71b Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/palm/palm_20260914_195434.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/palm/palm_20260914_195434.j3o.meta b/blight-assets/src/main/resources/Models/trees/palm/palm_20260914_195434.j3o.meta new file mode 100644 index 0000000..3b2576f --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/palm/palm_20260914_195434.j3o.meta @@ -0,0 +1,30 @@ +#Mon Sep 14 19:54:34 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category=trees +cullDistance=200.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=PALM +name=palm_20260914_195434 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o deleted file mode 100644 index 527876b..0000000 Binary files a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o.meta index 2812e5c..91302e5 100644 --- a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o.meta +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o.meta @@ -3,7 +3,7 @@ attachedEmitters.count=0 attachedLights.count=0 castShadow=true category= -cullDistance=120.0 +cullDistance=200.0 footstepSurface= interactableOffsetX=0.0 interactableOffsetY=0.5 diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o.meta index 796a69d..6f572ef 100644 --- a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o.meta +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o.meta @@ -3,7 +3,7 @@ attachedEmitters.count=0 attachedLights.count=0 castShadow=true category= -cullDistance=120.0 +cullDistance=200.0 footstepSurface= interactableOffsetX=0.0 interactableOffsetY=0.5 diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_200608.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_200608.j3o.meta new file mode 100644 index 0000000..85b5864 --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_200608.j3o.meta @@ -0,0 +1,30 @@ +#Mon Sep 14 20:06:09 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category=trees +cullDistance=200.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=TREE +name=willow_20260914_200608 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_203247.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_203247.j3o.meta new file mode 100644 index 0000000..664c6a5 --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_203247.j3o.meta @@ -0,0 +1,30 @@ +#Mon Sep 14 20:32:47 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category=trees +cullDistance=200.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=TREE +name=willow_20260914_203247 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_204422.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_204422.j3o.meta new file mode 100644 index 0000000..dcbe0e3 --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_204422.j3o.meta @@ -0,0 +1,30 @@ +#Mon Sep 14 20:44:22 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category=trees +cullDistance=200.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=TREE +name=willow_20260914_204422 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_205754.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_205754.j3o.meta new file mode 100644 index 0000000..4f75d5d --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_205754.j3o.meta @@ -0,0 +1,30 @@ +#Mon Sep 14 20:57:54 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category=trees +cullDistance=200.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=TREE +name=willow_20260914_205754 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_210538.j3o similarity index 54% rename from blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o rename to blight-assets/src/main/resources/Models/trees/willow/willow_20260914_210538.j3o index e8c2df8..e5a2fac 100644 Binary files a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o and b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_210538.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_210538.j3o.meta b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_210538.j3o.meta new file mode 100644 index 0000000..6e61dd1 --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/willow/willow_20260914_210538.j3o.meta @@ -0,0 +1,30 @@ +#Mon Sep 14 21:05:38 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category=trees +cullDistance=200.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=TREE +name=willow_20260914_210538 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java index 679f802..a95e2d8 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java @@ -14,35 +14,28 @@ import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; import com.jme3.bounding.BoundingBox; import com.jme3.export.binary.BinaryExporter; -import com.jme3.light.AmbientLight; -import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; import com.jme3.math.Vector3f; -import com.jme3.post.SceneProcessor; -import com.jme3.profile.AppProfiler; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; -import com.jme3.scene.Mesh; import com.jme3.scene.Node; import com.jme3.scene.Spatial; -import com.jme3.scene.VertexBuffer; import com.jme3.scene.control.AbstractControl; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; -import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; -import de.blight.editor.state.ThumbnailRenderer; import de.blight.editor.tree.PalmMeshBuilder; import de.blight.editor.tree.PalmOptions; +import de.blight.editor.util.ImpostorUtil; +import de.blight.editor.state.ThumbnailRenderer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,11 +43,7 @@ public class PalmGeneratorState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(PalmGeneratorState.class); - private static final int IMPOSTOR_SIZE = 512; - private static final int ATLAS_DIRS = 4; - private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS; // 2048 - private static final int ATLAS_H = IMPOSTOR_SIZE; // 512 - private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); + private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); private final SharedInput input; private SimpleApplication app; @@ -69,9 +58,9 @@ public class PalmGeneratorState extends BaseAppState { private String pendingFileName = null; private ViewPort captureVP = null; private FrameBuffer captureFB = null; - private volatile boolean captureReady = false; + private boolean[] captureReady = new boolean[]{false}; private int capturePass = 0; - private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS]; + private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; public PalmGeneratorState(SharedInput input) { this.input = input; } @@ -92,12 +81,12 @@ public class PalmGeneratorState extends BaseAppState { if (previewHost == null) return; } - if (pendingRequest != null && captureReady) { + if (pendingRequest != null && captureReady[0]) { finishCapture(); return; } - if (pendingRequest != null) return; // capture still in progress + if (pendingRequest != null) return; SharedInput.PalmGenRequest req = input.palmGenQueue.poll(); if (req != null) startGeneration(req); @@ -114,7 +103,7 @@ public class PalmGeneratorState extends BaseAppState { Texture t = assets.loadTexture(opts.leafTexture); int w = t.getImage().getWidth(); int h = t.getImage().getHeight(); - opts.leafTextureAspect = (float) h / w; // Stiel entlang U → Aspekt = Höhe/Breite + opts.leafTextureAspect = (float) h / w; } catch (Exception ignored) {} } @@ -125,7 +114,7 @@ public class PalmGeneratorState extends BaseAppState { BoundingBox bb = lod0.getWorldBound() instanceof BoundingBox b ? b : null; if (bb == null) bb = new BoundingBox(Vector3f.ZERO, 3f, 8f, 3f); - float dist = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f; + float dist = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f; Vector3f target = new Vector3f(0f, bb.getCenter().y, 0f); previewHost.setPreviewContent(lod0, dist, target); @@ -138,17 +127,17 @@ public class PalmGeneratorState extends BaseAppState { pendingBb = bb; pendingFileName = "palm_" + timestamp; capturePass = 0; - capturePixels = new ByteBuffer[ATLAS_DIRS]; + capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; startCapturePass(0); - input.treeGenStatusMsg = "Palme: Rendere Impostor (1/" + ATLAS_DIRS + ")…"; + input.treeGenStatusMsg = "Palme: Rendere Impostor (1/" + ImpostorUtil.DIRS + ")…"; } private Node buildLod1Palm(PalmOptions opts) { PalmOptions ld = opts.copy(); + // frondCount wird NICHT reduziert – nur Stamm-Geometrie vereinfachen ld.trunkSegments = Math.max(4, opts.trunkSegments / 2); ld.trunkSections = Math.max(4, opts.trunkSections / 2); - ld.frondCount = Math.max(8, opts.frondCount / 2); Node n = PalmMeshBuilder.build(ld); applyMaterials(n, ld); return n; @@ -158,32 +147,45 @@ public class PalmGeneratorState extends BaseAppState { @SuppressWarnings("deprecation") private void startCapturePass(int pass) { - Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); - captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); + BoundingBox safeBb = pendingBb != null ? pendingBb : new BoundingBox(Vector3f.ZERO, 3f, 8f, 3f); + + // Palmen haben einen Lean-Offset: bb.center X/Z weicht vom Modell-Ursprung (Stammfuß) + // ab. makeImpostorNode() platziert das Quad bei (0, cy, 0). Damit Textur-Inhalt und + // Quad-Position übereinstimmen, die Kamera auf (0, cy, 0) ausrichten und die XZ-Extents + // um den Lean-Versatz vergrößern, damit die ganze Krone noch im Bild bleibt. + float cx = safeBb.getCenter().x; + float cz = safeBb.getCenter().z; + BoundingBox capBb = new BoundingBox( + new Vector3f(0f, safeBb.getCenter().y, 0f), + safeBb.getXExtent() + Math.abs(cx), + safeBb.getYExtent(), + safeBb.getZExtent() + Math.abs(cz)); + + Texture2D capTex = new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8); + captureFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1); captureFB.addColorTexture(capTex); - captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); - float angle = pass * FastMath.HALF_PI; - captureVP = buildCaptureViewPort(pendingLod0, pendingBb, captureFB, angle); - captureReady = false; + captureFB.setDepthTexture(new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth)); + captureReady[0] = false; + captureVP = ImpostorUtil.startCapturePass(app, pendingLod0, capBb, captureFB, pass, captureReady); } private void finishCapture() { - ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); - app.getRenderer().readFrameBuffer(captureFB, pixels); - capturePixels[capturePass] = pixels; - cleanupCapture(); + capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB); + ImpostorUtil.cleanup(app, captureVP, captureFB); + captureVP = null; + captureFB = null; + captureReady[0] = false; - if (capturePass < ATLAS_DIRS - 1) { + if (capturePass < ImpostorUtil.DIRS - 1) { capturePass++; - input.treeGenStatusMsg = "Palme: Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…"; + input.treeGenStatusMsg = "Palme: Rendere Impostor (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…"; startCapturePass(capturePass); return; } // Alle Richtungen erfasst - String impostorName = "palm_impostor_" + pendingFileName.substring("palm_".length()); - ByteBuffer atlas = combineAtlas(capturePixels); - Texture2D impTex = saveImpostor(atlas, impostorName, ATLAS_W, ATLAS_H); + ByteBuffer atlas = ImpostorUtil.combineAtlas(capturePixels); + Texture2D impTex = ImpostorUtil.buildAtlasTexture(atlas); if (pendingRequest.exportAfter()) { Node lodNode = assembleLodNode(impTex); @@ -197,7 +199,7 @@ public class PalmGeneratorState extends BaseAppState { pendingLod1 = null; pendingBb = null; pendingFileName = null; - capturePixels = new ByteBuffer[ATLAS_DIRS]; + capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; } // ── LOD-Aufbau ──────────────────────────────────────────────────────────── @@ -205,158 +207,22 @@ public class PalmGeneratorState extends BaseAppState { private Node assembleLodNode(Texture2D impostorTex) { Node root = new Node("palm"); root.attachChild(pendingLod0); + pendingLod1.setName("lod1"); root.attachChild(pendingLod1); - Node lod2 = makeImpostorNode(pendingBb, impostorTex); + Node lod2 = ImpostorUtil.makeImpostorNode(pendingBb, impostorTex, assets); root.attachChild(lod2); pendingLod1.setCullHint(Spatial.CullHint.Always); lod2.setCullHint(Spatial.CullHint.Always); lod2.setShadowMode(RenderQueue.ShadowMode.Off); + ImpostorUtil.addViewControl(lod2, app.getCamera()); root.addControl(new PalmLodControl(app.getCamera(), pendingLod0, pendingLod1, lod2, 50f, 150f)); return root; } - private Node makeImpostorNode(BoundingBox bb, Texture2D tex) { - float h = bb.getYExtent() * 2f; - float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f; - float size = Math.max(h, w); - float yOff = bb.getCenter().y + 2f; - - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - if (tex != null) mat.setTexture("ColorMap", tex); - else mat.setColor("Color", new ColorRGBA(0.22f, 0.65f, 0.14f, 0.9f)); - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); - - Node n = new Node("lod2"); - for (int d = 0; d < ATLAS_DIRS; d++) { - float uMin = (float) d / ATLAS_DIRS; - float uMax = (float)(d + 1) / ATLAS_DIRS; - n.attachChild(buildBillboardQuad("quad_" + d, d * FastMath.HALF_PI, - yOff, size, mat.clone(), uMin, uMax)); - } - n.setQueueBucket(RenderQueue.Bucket.Transparent); - return n; - } - - private Geometry buildBillboardQuad(String name, float yRot, float yCent, - float size, Material mat, float uMin, float uMax) { - float hw = size * 0.5f; - float hh = size * 0.5f; - float cos = FastMath.cos(yRot); - float sin = FastMath.sin(yRot); - - Mesh mesh = new Mesh(); - mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{ - -hw*cos, yCent-hh, -hw*sin, - hw*cos, yCent-hh, hw*sin, - hw*cos, yCent+hh, hw*sin, - -hw*cos, yCent+hh, -hw*sin - }); - mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ - uMin, 0, uMax, 0, uMax, 1, uMin, 1 - }); - mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0}); - mesh.updateBound(); - - Geometry g = new Geometry(name, mesh); - g.setMaterial(mat); - return g; - } - - // ── Offscreen-ViewPort ──────────────────────────────────────────────────── - - private ViewPort buildCaptureViewPort(Node palmNode, BoundingBox bb, FrameBuffer fb, float angle) { - Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE); - Vector3f center = bb.getCenter().add(0f, 2f, 0f); - float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); - float dist = extent * 3.0f; - - float camX = FastMath.sin(angle) * dist; - float camZ = FastMath.cos(angle) * dist; - cam.setLocation(center.add(camX, 0f, camZ)); - cam.lookAt(center, Vector3f.UNIT_Y); - cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f); - - ViewPort vp = app.getRenderManager() - .createPostView("palmCap_" + System.nanoTime(), cam); - vp.setOutputFrameBuffer(fb); - vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f)); - vp.setClearFlags(true, true, true); - - Node scene = new Node("palmCapScene"); - scene.addLight(new DirectionalLight( - new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), - new ColorRGBA(2.0f, 1.85f, 1.5f, 1f))); - scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f))); - - Node capPalm = cloneForCapture(palmNode); - scene.attachChild(capPalm); - vp.attachScene(scene); - scene.updateGeometricState(); - - vp.addProcessor(new SceneProcessor() { - @Override public void initialize(RenderManager rm, ViewPort v) {} - @Override public void reshape(ViewPort v, int w, int h) {} - @Override public boolean isInitialized() { return true; } - @Override public void preFrame(float t) {} - @Override public void postQueue(RenderQueue rq) {} - @Override public void cleanup() {} - @Override public void setProfiler(AppProfiler profiler) {} - @Override public void postFrame(FrameBuffer out) { - vp.removeProcessor(this); - captureReady = true; - } - }); - - return vp; - } - - private Node cloneForCapture(Node src) { - Node copy = new Node(src.getName() + "_cap"); - copy.setLocalTranslation(src.getLocalTranslation()); - copy.setLocalScale(src.getLocalScale()); - for (Spatial child : src.getChildren()) { - if (child instanceof Geometry g) { - Geometry gc = new Geometry(g.getName() + "_c", g.getMesh()); - gc.setMaterial(g.getMaterial().clone()); - copy.attachChild(gc); - } - } - return copy; - } - - // ── Atlas kombinieren + Impostor speichern ──────────────────────────────── - - private ByteBuffer combineAtlas(ByteBuffer[] passes) { - ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4); - for (int d = 0; d < ATLAS_DIRS; d++) { - ByteBuffer src = passes[d]; - src.rewind(); - for (int y = 0; y < IMPOSTOR_SIZE; y++) { - for (int x = 0; x < IMPOSTOR_SIZE; x++) { - int srcOff = (y * IMPOSTOR_SIZE + x) * 4; - int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4; - atlas.put(dstOff, src.get(srcOff)); - atlas.put(dstOff + 1, src.get(srcOff + 1)); - atlas.put(dstOff + 2, src.get(srcOff + 2)); - atlas.put(dstOff + 3, src.get(srcOff + 3)); - } - } - } - return atlas; - } - - private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) { - pixels.rewind(); - Image jmeImg = new Image(Image.Format.RGBA8, width, height, - pixels, null, com.jme3.texture.image.ColorSpace.sRGB); - return new Texture2D(jmeImg); - } - // ── Export ──────────────────────────────────────────────────────────────── private void exportPalm(Node lodNode, String fileName) { @@ -364,8 +230,7 @@ public class PalmGeneratorState extends BaseAppState { Path modelDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("palm"); Files.createDirectories(modelDir); File out = modelDir.resolve(fileName + ".j3o").toFile(); - while (lodNode.getNumControls() > 0) - lodNode.removeControl(lodNode.getControl(0)); + stripControlsRecursive(lodNode); byte[] thumb = null; try { thumb = ThumbnailRenderer.render(lodNode.clone(), app.getRenderManager(), app.getRenderer()); @@ -373,6 +238,17 @@ public class PalmGeneratorState extends BaseAppState { } catch (Exception te) { log.warn("[Palme] Thumbnail-Fehler: {}", te.getMessage()); } BinaryExporter.getInstance().save(lodNode, out); if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, out.toPath(), ASSET_ROOT); + de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( + fileName, "trees", "", + 1f, 1f, 1f, true, + 0f, 0f, + true, true, true, + 1f, 1f, + "", "", 30f, 80f, 200f, + java.util.List.of(), java.util.List.of(), java.util.List.of(), + de.blight.common.model.InteractableType.NONE, + 0f, 0.5f, 0f, 0f, "", de.blight.common.ModelCategory.PALM); + de.blight.common.ModelMetaIO.save(meta, out.toPath()); log.info("[Palme] Gespeichert: {}", out.getAbsolutePath()); input.treeGenStatusMsg = "Gespeichert: Models/trees/palm/" + fileName + ".j3o"; input.refreshAssets = true; @@ -382,6 +258,13 @@ public class PalmGeneratorState extends BaseAppState { } } + private static void stripControlsRecursive(Spatial s) { + while (s.getNumControls() > 0) s.removeControl(s.getControl(0)); + if (s instanceof Node n) { + for (Spatial child : n.getChildren()) stripControlsRecursive(child); + } + } + // ── Material-Helpers ────────────────────────────────────────────────────── private void applyMaterials(Node palm, PalmOptions opts) { @@ -484,15 +367,10 @@ public class PalmGeneratorState extends BaseAppState { // ── Aufräumen ───────────────────────────────────────────────────────────── private void cleanupCapture() { - if (captureVP != null) { - app.getRenderManager().removePostView(captureVP); - captureVP = null; - } - if (captureFB != null) { - try { captureFB.dispose(); } catch (Exception ignored) {} - captureFB = null; - } - captureReady = false; + ImpostorUtil.cleanup(app, captureVP, captureFB); + captureVP = null; + captureFB = null; + captureReady[0] = false; } // ── LOD-Control ─────────────────────────────────────────────────────────── diff --git a/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java index 588373f..07f2543 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java @@ -14,34 +14,27 @@ import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; import com.jme3.bounding.BoundingBox; import com.jme3.export.binary.BinaryExporter; -import com.jme3.light.AmbientLight; -import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; import com.jme3.math.Vector3f; -import com.jme3.post.SceneProcessor; -import com.jme3.profile.AppProfiler; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; -import com.jme3.scene.Mesh; import com.jme3.scene.Node; import com.jme3.scene.Spatial; -import com.jme3.scene.VertexBuffer; import com.jme3.scene.control.AbstractControl; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; -import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; import de.blight.editor.tree.WillowMeshBuilder; import de.blight.editor.tree.WillowOptions; +import de.blight.editor.util.ImpostorUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,19 +42,15 @@ public class WillowGeneratorState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(WillowGeneratorState.class); - private static final int IMPOSTOR_SIZE = 512; - private static final int ATLAS_DIRS = 4; - private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS; - private static final int ATLAS_H = IMPOSTOR_SIZE; - private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve( + private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve( "blight-assets", "src", "main", "resources"); - private final SharedInput input; - private SimpleApplication app; - private AssetManager assets; + private final SharedInput input; + private SimpleApplication app; + private AssetManager assets; private TreeGeneratorState previewHost; - // ── Capture-Kontext ─────────────────────────────────────────────────────── + // ── Pending-Capture-Kontext ─────────────────────────────────────────────── private SharedInput.WillowGenRequest pendingRequest = null; private Node pendingLod0 = null; private Node pendingLod1 = null; @@ -69,9 +58,9 @@ public class WillowGeneratorState extends BaseAppState { private String pendingFileName = null; private ViewPort captureVP = null; private FrameBuffer captureFB = null; - private volatile boolean captureReady = false; + private boolean[] captureReady = new boolean[]{false}; private int capturePass = 0; - private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS]; + private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; public WillowGeneratorState(SharedInput input) { this.input = input; } @@ -91,7 +80,7 @@ public class WillowGeneratorState extends BaseAppState { if (previewHost == null) return; } - if (pendingRequest != null && captureReady) { + if (pendingRequest != null && captureReady[0]) { finishCapture(); return; } @@ -102,7 +91,7 @@ public class WillowGeneratorState extends BaseAppState { if (req != null) startGeneration(req); } - // ── Phase 1: Generierung ────────────────────────────────────────────────── + // ── Phase 1: Generierung + Capture-Setup ───────────────────────────────── private void startGeneration(SharedInput.WillowGenRequest req) { cleanupCapture(); @@ -115,9 +104,8 @@ public class WillowGeneratorState extends BaseAppState { BoundingBox bb = lod0.getWorldBound() instanceof BoundingBox b ? b : null; if (bb == null) bb = new BoundingBox(Vector3f.ZERO, 5f, 8f, 5f); - float dist = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f; + float dist = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f; Vector3f target = new Vector3f(0f, bb.getCenter().y, 0f); - previewHost.setPreviewContent(lod0, dist, target); String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); @@ -128,21 +116,17 @@ public class WillowGeneratorState extends BaseAppState { pendingBb = bb; pendingFileName = "willow_" + timestamp; capturePass = 0; - capturePixels = new ByteBuffer[ATLAS_DIRS]; + capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; startCapturePass(0); - input.treeGenStatusMsg = "Trauerweide: Rendere Impostor (1/" + ATLAS_DIRS + ")…"; + input.treeGenStatusMsg = "Trauerweide: Rendere Impostor (1/" + ImpostorUtil.DIRS + ")…"; } private Node buildLod1(WillowOptions opts) { - WillowOptions ld = opts.copy(); - ld.trunkSegments = Math.max(4, opts.trunkSegments / 2); - ld.trunkSections = Math.max(4, opts.trunkSections / 2); - ld.branchSections = Math.max(4, opts.branchSections / 2); - ld.subBranchSections = Math.max(3, opts.subBranchSections / 2); - ld.subBranchCount = Math.max(1, opts.subBranchCount / 2); - Node n = WillowMeshBuilder.build(ld); - applyMaterials(n, ld); + // Keine strukturellen Änderungen: trunkSegments bestimmt Ast-Ansatzpunkte, + // trunkSections die Ast-Anzahl entlang des Stamms – beides ist erkennungsrelevant. + Node n = WillowMeshBuilder.build(opts); + applyMaterials(n, opts); return n; } @@ -150,31 +134,41 @@ public class WillowGeneratorState extends BaseAppState { @SuppressWarnings("deprecation") private void startCapturePass(int pass) { - Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); - captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); + BoundingBox safeBb = pendingBb != null ? pendingBb : new BoundingBox(Vector3f.ZERO, 5f, 8f, 5f); + // XZ-Offset-Ausgleich: Kamera auf (0, cy, 0) ausrichten und Extents um den + // Versatz erweitern, damit der gesamte Kronendurchmesser im Bild bleibt + // (gleiche Logik wie PalmGeneratorState). + float cx = safeBb.getCenter().x; + float cz = safeBb.getCenter().z; + BoundingBox capBb = new BoundingBox( + new com.jme3.math.Vector3f(0f, safeBb.getCenter().y, 0f), + safeBb.getXExtent() + Math.abs(cx), + safeBb.getYExtent(), + safeBb.getZExtent() + Math.abs(cz)); + Texture2D capTex = new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8); + captureFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1); captureFB.addColorTexture(capTex); - captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); - float angle = pass * FastMath.HALF_PI; - captureVP = buildCaptureViewPort(pendingLod0, pendingBb, captureFB, angle); - captureReady = false; + captureFB.setDepthTexture(new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth)); + captureReady[0] = false; + captureVP = ImpostorUtil.startCapturePass(app, pendingLod0, capBb, captureFB, pass, captureReady); } private void finishCapture() { - ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); - app.getRenderer().readFrameBuffer(captureFB, pixels); - capturePixels[capturePass] = pixels; - cleanupCapture(); + capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB); + ImpostorUtil.cleanup(app, captureVP, captureFB); + captureVP = null; + captureFB = null; + captureReady[0] = false; - if (capturePass < ATLAS_DIRS - 1) { + if (capturePass < ImpostorUtil.DIRS - 1) { capturePass++; - input.treeGenStatusMsg = "Trauerweide: Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…"; + input.treeGenStatusMsg = "Trauerweide: Rendere Impostor (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…"; startCapturePass(capturePass); return; } - String impostorName = "willow_impostor_" + pendingFileName.substring("willow_".length()); - ByteBuffer atlas = combineAtlas(capturePixels); - Texture2D impTex = saveImpostor(atlas, impostorName, ATLAS_W, ATLAS_H); + ByteBuffer atlas = ImpostorUtil.combineAtlas(capturePixels); + Texture2D impTex = ImpostorUtil.buildAtlasTexture(atlas); if (pendingRequest.exportAfter()) { Node lodNode = assembleLodNode(impTex); @@ -188,7 +182,7 @@ public class WillowGeneratorState extends BaseAppState { pendingLod1 = null; pendingBb = null; pendingFileName = null; - capturePixels = new ByteBuffer[ATLAS_DIRS]; + capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; } // ── LOD-Aufbau ──────────────────────────────────────────────────────────── @@ -196,154 +190,22 @@ public class WillowGeneratorState extends BaseAppState { private Node assembleLodNode(Texture2D impostorTex) { Node root = new Node("willow"); root.attachChild(pendingLod0); + pendingLod1.setName("lod1"); root.attachChild(pendingLod1); - Node lod2 = makeImpostorNode(pendingBb, impostorTex); + Node lod2 = ImpostorUtil.makeImpostorNode(pendingBb, impostorTex, assets); root.attachChild(lod2); pendingLod1.setCullHint(Spatial.CullHint.Always); lod2.setCullHint(Spatial.CullHint.Always); lod2.setShadowMode(RenderQueue.ShadowMode.Off); + ImpostorUtil.addViewControl(lod2, app.getCamera()); root.addControl(new WillowLodControl(app.getCamera(), pendingLod0, pendingLod1, lod2, 50f, 150f)); return root; } - private Node makeImpostorNode(BoundingBox bb, Texture2D tex) { - float h = bb.getYExtent() * 2f; - float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f; - float size = Math.max(h, w); - float yOff = bb.getCenter().y + 2f; - - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - if (tex != null) mat.setTexture("ColorMap", tex); - else mat.setColor("Color", new ColorRGBA(0.22f, 0.62f, 0.14f, 0.9f)); - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); - - Node n = new Node("lod2"); - for (int d = 0; d < ATLAS_DIRS; d++) { - float uMin = (float) d / ATLAS_DIRS; - float uMax = (float)(d + 1) / ATLAS_DIRS; - n.attachChild(buildBillboardQuad("quad_" + d, d * FastMath.HALF_PI, - yOff, size, mat.clone(), uMin, uMax)); - } - n.setQueueBucket(RenderQueue.Bucket.Transparent); - return n; - } - - private Geometry buildBillboardQuad(String name, float yRot, float yCent, - float size, Material mat, float uMin, float uMax) { - float hw = size * 0.5f; - float hh = size * 0.5f; - float cos = FastMath.cos(yRot); - float sin = FastMath.sin(yRot); - - Mesh mesh = new Mesh(); - mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{ - -hw*cos, yCent-hh, -hw*sin, - hw*cos, yCent-hh, hw*sin, - hw*cos, yCent+hh, hw*sin, - -hw*cos, yCent+hh, -hw*sin - }); - mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ - uMin, 0, uMax, 0, uMax, 1, uMin, 1 - }); - mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0}); - mesh.updateBound(); - - Geometry g = new Geometry(name, mesh); - g.setMaterial(mat); - return g; - } - - // ── Offscreen-ViewPort ──────────────────────────────────────────────────── - - private ViewPort buildCaptureViewPort(Node willowNode, BoundingBox bb, FrameBuffer fb, float angle) { - Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE); - Vector3f center = bb.getCenter().add(0f, 2f, 0f); - float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); - float dist = extent * 3.0f; - - float camX = FastMath.sin(angle) * dist; - float camZ = FastMath.cos(angle) * dist; - cam.setLocation(center.add(camX, 0f, camZ)); - cam.lookAt(center, Vector3f.UNIT_Y); - cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f); - - ViewPort vp = app.getRenderManager().createPostView("willowCap_" + System.nanoTime(), cam); - vp.setOutputFrameBuffer(fb); - vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f)); - vp.setClearFlags(true, true, true); - - Node scene = new Node("willowCapScene"); - scene.addLight(new DirectionalLight( - new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), - new ColorRGBA(2.0f, 1.85f, 1.5f, 1f))); - scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f))); - - scene.attachChild(cloneForCapture(willowNode)); - vp.attachScene(scene); - scene.updateGeometricState(); - - vp.addProcessor(new SceneProcessor() { - @Override public void initialize(RenderManager rm, ViewPort v) {} - @Override public void reshape(ViewPort v, int w, int h) {} - @Override public boolean isInitialized() { return true; } - @Override public void preFrame(float t) {} - @Override public void postQueue(RenderQueue rq) {} - @Override public void cleanup() {} - @Override public void setProfiler(AppProfiler profiler) {} - @Override public void postFrame(FrameBuffer out) { - vp.removeProcessor(this); - captureReady = true; - } - }); - - return vp; - } - - private Node cloneForCapture(Node src) { - Node copy = new Node(src.getName() + "_cap"); - for (Spatial child : src.getChildren()) { - if (child instanceof Geometry g) { - Geometry gc = new Geometry(g.getName() + "_c", g.getMesh()); - gc.setMaterial(g.getMaterial().clone()); - copy.attachChild(gc); - } - } - return copy; - } - - // ── Atlas kombinieren ───────────────────────────────────────────────────── - - private ByteBuffer combineAtlas(ByteBuffer[] passes) { - ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4); - for (int d = 0; d < ATLAS_DIRS; d++) { - ByteBuffer src = passes[d]; - src.rewind(); - for (int y = 0; y < IMPOSTOR_SIZE; y++) { - for (int x = 0; x < IMPOSTOR_SIZE; x++) { - int srcOff = (y * IMPOSTOR_SIZE + x) * 4; - int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4; - atlas.put(dstOff, src.get(srcOff)); - atlas.put(dstOff + 1, src.get(srcOff + 1)); - atlas.put(dstOff + 2, src.get(srcOff + 2)); - atlas.put(dstOff + 3, src.get(srcOff + 3)); - } - } - } - return atlas; - } - - private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) { - pixels.rewind(); - Image jmeImg = new Image(Image.Format.RGBA8, width, height, - pixels, null, com.jme3.texture.image.ColorSpace.sRGB); - return new Texture2D(jmeImg); - } - // ── Export ──────────────────────────────────────────────────────────────── private void exportWillow(Node lodNode, String fileName) { @@ -351,8 +213,7 @@ public class WillowGeneratorState extends BaseAppState { Path modelDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("willow"); Files.createDirectories(modelDir); File out = modelDir.resolve(fileName + ".j3o").toFile(); - while (lodNode.getNumControls() > 0) - lodNode.removeControl(lodNode.getControl(0)); + stripControlsRecursive(lodNode); byte[] thumb = null; try { thumb = ThumbnailRenderer.render(lodNode.clone(), app.getRenderManager(), app.getRenderer()); @@ -360,6 +221,23 @@ public class WillowGeneratorState extends BaseAppState { } catch (Exception te) { log.warn("[Trauerweide] Thumbnail-Fehler: {}", te.getMessage()); } BinaryExporter.getInstance().save(lodNode, out); if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, out.toPath(), ASSET_ROOT); + // pivotOffsetY = Bounding-Box-Minimum der Trauerweide (negativ wenn Äste unter Y=0 hängen). + // ModelEditorState.applyPivot nutzt diesen Wert damit der Stammfuß (Y=0) als Boden gilt, + // nicht die tiefsten Blattspitzen. + float pivotY = (pendingBb != null) + ? pendingBb.getCenter().y - pendingBb.getYExtent() + : 0f; + de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( + fileName, "trees", "", + 1f, 1f, 1f, true, + pivotY, 0f, + true, true, true, + 1f, 1f, + "", "", 30f, 80f, 200f, + java.util.List.of(), java.util.List.of(), java.util.List.of(), + de.blight.common.model.InteractableType.NONE, + 0f, 0.5f, 0f, 0f, "", de.blight.common.ModelCategory.TREE); + de.blight.common.ModelMetaIO.save(meta, out.toPath()); log.info("[Trauerweide] Gespeichert: {}", out.getAbsolutePath()); input.treeGenStatusMsg = "Gespeichert: Models/trees/willow/" + fileName + ".j3o"; input.refreshAssets = true; @@ -369,6 +247,13 @@ public class WillowGeneratorState extends BaseAppState { } } + private static void stripControlsRecursive(Spatial s) { + while (s.getNumControls() > 0) s.removeControl(s.getControl(0)); + if (s instanceof Node n) { + for (Spatial child : n.getChildren()) stripControlsRecursive(child); + } + } + // ── Materialien ─────────────────────────────────────────────────────────── private void applyMaterials(Node willow, WillowOptions opts) { @@ -441,18 +326,13 @@ public class WillowGeneratorState extends BaseAppState { // ── Aufräumen ───────────────────────────────────────────────────────────── private void cleanupCapture() { - if (captureVP != null) { - app.getRenderManager().removePostView(captureVP); - captureVP = null; - } - if (captureFB != null) { - try { captureFB.dispose(); } catch (Exception ignored) {} - captureFB = null; - } - captureReady = false; + ImpostorUtil.cleanup(app, captureVP, captureFB); + captureVP = null; + captureFB = null; + captureReady[0] = false; } - // ── LOD-Control ─────────────────────────────────────────────────────────── + // ── LOD-Control (Editor-Vorschau) ───────────────────────────────────────── private static final class WillowLodControl extends AbstractControl { private final Camera cam; diff --git a/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java b/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java index f1df497..591afd1 100644 --- a/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java +++ b/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java @@ -34,10 +34,12 @@ public class ModelLodControl extends AbstractControl { /** Erzwingt LOD2 für alle Instanzen. Wird per Konsolen-Befehl "set debugLod2 true" gesetzt. */ public static volatile boolean debugLod2 = false; - private final Camera cam; - private final float lod1DistSq; - private final float lod2DistSq; - private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=ausgeblendet + private final Camera cam; + private final float lod1DistSq; + private final float lod2DistSq; + private float[] lodFactorRef = {1.0f}; + private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=ausgeblendet + private boolean firstUpdate = true; // ── Embedded-LOD-Modus ──────────────────────────────────────────────────── private final Spatial embLod0; @@ -93,6 +95,8 @@ public class ModelLodControl extends AbstractControl { this.lodLoadCallback = cb; } + public void setLodFactorRef(float[] ref) { this.lodFactorRef = ref; } + @Override protected void controlRender(RenderManager rm, ViewPort vp) {} @@ -113,6 +117,15 @@ public class ModelLodControl extends AbstractControl { float dz = cam.getLocation().z - wt.z; float distSq = dx*dx + dy*dy + dz*dz; + if (firstUpdate) { + firstUpdate = false; + log.info("[LOD] Control aktiv: {} | embedded={} | lod1={}m lod2={}m | pos=({},{},{})", + spatial.getName(), embLod0 != null, + String.format("%.0f", Math.sqrt(lod1DistSq == Float.MAX_VALUE ? 0 : lod1DistSq)), + String.format("%.0f", Math.sqrt(lod2DistSq == Float.MAX_VALUE ? 0 : lod2DistSq)), + String.format("%.1f", wt.x), String.format("%.1f", wt.y), String.format("%.1f", wt.z)); + } + boolean embedded = embLod0 != null; boolean hasLod1 = embedded ? embLod1 != null : lod1Path != null; @@ -121,13 +134,17 @@ public class ModelLodControl extends AbstractControl { // Sichtbarkeit hängt nur vom LOD-System ab — kein separates Culling. // Fehlt LOD1 aber LOD2 vorhanden: bei lod1Distance direkt zu LOD2 wechseln (nicht ausblenden). // Fehlt beides (z.B. Kaktusfeige): ab lod1Distance ausblenden. + float f = lodFactorRef[0]; + float d1Sq = lod1DistSq == Float.MAX_VALUE ? Float.MAX_VALUE : lod1DistSq * f * f; + float d2Sq = lod2DistSq == Float.MAX_VALUE ? Float.MAX_VALUE : lod2DistSq * f * f; + int targetSlot; if (debugLod2) { // Debug: erzwingt die niedrigste verfügbare LOD-Stufe targetSlot = hasLod2 ? 2 : (hasLod1 ? 1 : 0); - } else if (distSq >= lod2DistSq) { + } else if (distSq >= d2Sq) { targetSlot = hasLod2 ? 2 : -1; - } else if (distSq >= lod1DistSq) { + } else if (distSq >= d1Sq) { if (hasLod1) targetSlot = 1; else if (hasLod2) targetSlot = 2; // kein LOD1, aber LOD2 vorhanden → Impostor früher else targetSlot = -1; // weder LOD1 noch LOD2 → ausblenden diff --git a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java index c20cdd8..1f8ecec 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java @@ -53,9 +53,9 @@ public class WorldObjectsState extends BaseAppState { private static final float IMPOSTOR_CAP_AMB_G = 0.30f; private static final float IMPOSTOR_CAP_AMB_B = 0.32f; private float debugTimer = 0f; - private float lodFactor = 1.0f; + private final float[] lodFactorRef = {1.0f}; - public void setLodFactor(float f) { this.lodFactor = f; } + public void setLodFactor(float f) { this.lodFactorRef[0] = f; } /** RigidBodyControl pro Interactable-ID, damit Kollision während Animationen deaktiviert werden kann. */ private final Map interactableRbcs = new HashMap<>(); @@ -253,6 +253,7 @@ public class WorldObjectsState extends BaseAppState { // Exportiertes Mesh hat Vorrang vor modelPath String path = (m.meshFile() != null && !m.meshFile().isBlank()) ? m.meshFile() : m.modelPath(); + log.info("[WorldObjects-LOD] buildSpatial: {} lod1={}m lod2={}m", path, m.lod1Distance(), m.lod2Distance()); Spatial spatial; if (path.startsWith("@")) { @@ -265,8 +266,29 @@ public class WorldObjectsState extends BaseAppState { } spatial.setName("obj_" + path); + // LOD-Distanzen: ModelMeta zur Laufzeit bevorzugen (überschreibt veraltete Scene-TSV-Werte, + // die 0 sein können wenn das Modell vor Einführung der Meta-Datei platziert wurde). + float lodDist1 = m.lod1Distance(); + float lodDist2 = m.lod2Distance(); + if (!path.startsWith("@")) { + de.blight.common.ModelMeta mm = tryLoadMeta(path); + if (mm != null && (mm.lod1Distance() > 0f || mm.lod2Distance() > 0f)) { + lodDist1 = mm.lod1Distance(); + lodDist2 = mm.lod2Distance(); + } + } + // Fallback für j3os, die eingebettete LOD-Knoten haben aber keine ModelMeta + // (Modelle, die vor Einführung der Meta-Datei erzeugt und platziert wurden). + // Vorhandensein von "lod1" oder "lod2" als Kind beweist, dass LOD beabsichtigt war. + if (lodDist1 == 0f && lodDist2 == 0f && spatial instanceof Node tn + && (tn.getChild("lod1") != null || tn.getChild("lod2") != null)) { + lodDist1 = 50f; + lodDist2 = 150f; + log.debug("[WorldObjects-LOD] {} – keine Meta, LOD-Knoten gefunden → Fallback 50/150m", path); + } + // LOD — kein cullDistance mehr; Objekte ohne LOD-Stufe werden ab der jeweiligen Distanz ausgeblendet - if (m.lod1Distance() > 0f || m.lod2Distance() > 0f) { + if (lodDist1 > 0f || lodDist2 > 0f) { if (spatial instanceof Node treeNode) { Spatial lod0 = treeNode.getChild("lod0"); Spatial lod1 = treeNode.getChild("lod1"); @@ -284,7 +306,36 @@ public class WorldObjectsState extends BaseAppState { path, treeNode.getChildren().size(), lod0.getName(), lod1 != null ? "'" + lod1.getName() + "'" : "null", lod2 != null ? "'" + lod2.getName() + "'" : "null", - m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor); + lodDist1 * lodFactorRef[0], lodDist2 * lodFactorRef[0]); + // Pivot-Korrektur: Falls der Mesh-Ursprung des Generators nicht bei Y=0 liegt, + // treeNode in lodRoot einbetten und lod0 verschieben (wie Fall B). + treeNode.updateGeometricState(); + if (lod0.getWorldBound() instanceof com.jme3.bounding.BoundingBox lod0Bb) { + float groundY = lod0Bb.getCenter().y - lod0Bb.getYExtent(); + log.info("[WorldObjects-LOD] Pivot: {} groundY={}m", path, String.format("%.2f", groundY)); + if (groundY > 0.01f) { + Node lodRoot = new Node("lodRoot_" + path); + lodRoot.attachChild(treeNode); + treeNode.setLocalTranslation(0f, -groundY, 0f); + lod0.setCullHint(Spatial.CullHint.Inherit); + if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always); + if (lod2 != null) { + lod2.setCullHint(Spatial.CullHint.Always); + if (lod2 instanceof Node lod2Node && !lod2Node.getChildren().isEmpty() + && isImpostorNode(lod2Node)) { + initImpostor(lod2Node); + } + } + if (lod1 != null || lod2 != null) { + ModelLodControl lodCtrl = new ModelLodControl( + app.getCamera(), lod0, lod1, lod2, + lodDist1, lodDist2); + lodCtrl.setLodFactorRef(lodFactorRef); + treeNode.addControl(lodCtrl); + } + return lodRoot; + } + } lod0.setCullHint(Spatial.CullHint.Inherit); if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always); if (lod2 != null) { @@ -297,9 +348,11 @@ public class WorldObjectsState extends BaseAppState { // ModelLodControl nur wenn tatsächliche LOD-Varianten vorhanden — sonst // würde das Objekt ab lod1Distance komplett ausgeblendet (targetSlot=-1). if (lod1 != null || lod2 != null) { - treeNode.addControl(new ModelLodControl( + ModelLodControl lodCtrl = new ModelLodControl( app.getCamera(), lod0, lod1, lod2, - m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor)); + lodDist1, lodDist2); + lodCtrl.setLodFactorRef(lodFactorRef); + treeNode.addControl(lodCtrl); } return treeNode; } @@ -316,7 +369,7 @@ public class WorldObjectsState extends BaseAppState { if (lod2 != null) { log.info("[WorldObjects-LOD] Fall B: {} | Kinder={} | lod2='{}' | dist=0/{}m", path, treeNode.getChildren().size(), lod2.getName(), - m.lod2Distance() * lodFactor); + lodDist2 * lodFactorRef[0]); treeNode.detachChild(lod2); Node lodRoot = new Node("lodRoot_" + path); lodRoot.attachChild(treeNode); // index 0 = lod0 (volle Qualität) @@ -340,9 +393,11 @@ public class WorldObjectsState extends BaseAppState { && isImpostorNode(lod2Node)) { initImpostor(lod2Node); } - lodRoot.addControl(new ModelLodControl( + ModelLodControl lodCtrl = new ModelLodControl( app.getCamera(), treeNode, null, lod2, - m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor)); + lodDist1, lodDist2); + lodCtrl.setLodFactorRef(lodFactorRef); + lodRoot.addControl(lodCtrl); return lodRoot; } } @@ -358,7 +413,8 @@ public class WorldObjectsState extends BaseAppState { ModelLodControl ctrl = new ModelLodControl( assets, app.getCamera(), lp1, lp2, - m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor); + lodDist1, lodDist2); + ctrl.setLodFactorRef(lodFactorRef); ctrl.setLodLoadCallback(lod -> { convertUnshadedToPbr(lod); collectSceneLitMaterials(lod); @@ -371,6 +427,17 @@ public class WorldObjectsState extends BaseAppState { return spatial; } + private de.blight.common.ModelMeta tryLoadMeta(String assetPath) { + try { + java.nio.file.Path root = de.blight.game.animation.AnimationLibrary.findAssetRoot(); + java.nio.file.Path j3o = root.resolve(assetPath); + if (!java.nio.file.Files.exists(j3o)) return null; + return de.blight.common.ModelMetaIO.load(j3o); + } catch (Exception e) { + return null; + } + } + private boolean isImpostorNode(Node n) { if (n.getControl(BillboardControl.class) != null) { return true; diff --git a/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench b/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench index c54e975..8a5a0f0 100644 --- a/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench +++ b/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench @@ -2,7 +2,7 @@ "id": "9ac9943d-0e12-4d5c-8323-0e2b92eebdec", "benchType": "Simple", "sitzX": 236.63928, - "sitzY": -5.81074, + "sitzY": 1.90243, "sitzZ": -888.1745, "sitzRotY": 4.7967663, "sitzSet": true diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 9b17644..487b489 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index bf82a4f..29a6300 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 042eb5b..17677d0 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -1,16 +1,6 @@ # modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow lod1Path lod2Path lod1Distance lod2Distance cullDistance interactableType interactableId modelCategory Models/northcoast/wrack1.j3o 286.06750 -3.50554 -947.28595 -1.76657 1.00000 -0.00000 -0.50184 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213338.j3o 277.36694 2.74803 -956.39905 0.43292 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213338.j3o 280.27863 2.66144 -928.60248 -1.51992 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213341.j3o 249.12691 6.49606 -895.80035 -0.96942 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213341.j3o 243.51527 1.40601 -894.84039 -2.48591 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213338.j3o 233.49467 1.43232 -892.32526 2.66339 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213341.j3o 247.05431 6.44288 -888.47949 1.23606 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213341.j3o 239.43253 0.94988 -886.23218 -2.49448 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec NONE -Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE +Models/imported/bank1.j3o 236.63928 1.40243 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec NONE Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6 NONE Models/trees/pine/medium/pine_medium_20260706_190939.j3o 157.82524 2.00000 -793.89911 -2.44322 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE Models/trees/pine/medium/pine_medium_20260706_190939.j3o 170.43440 2.00000 -784.16235 2.81244 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE @@ -22,3 +12,5 @@ Models/northcoast/alte_fischerhytte.j3o 317.92999 2.01000 -863.58002 0.00000 1.0 Models/northcoast/alte_fischerhytte2.j3o 312.98001 2.00000 -851.37000 -3.50811 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/northcoast/alte_fischerhytte.j3o 322.73001 2.00000 -856.09003 -1.83155 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/imported/feuerstelle1.j3o 1.46137 2.00000 -1.08840 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 NONE +Models/trees/palm/palm_20260914_195434.j3o 226.76038 1.25643 -893.77020 -1.59663 1.00000 -0.00000 0.00000 true true true 50.00000 150.00000 300.00000 PALM +Models/trees/willow/willow_20260914_210538.j3o 256.04678 5.84959 -887.66675 0.00000 1.00000 0.00000 0.00000 true true true 50.00000 150.00000 300.00000 TREE diff --git a/blight-map/src/main/map/chunks/chunk_09_00.blc b/blight-map/src/main/map/chunks/chunk_09_00.blc index 495cdf4..481f2f5 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_00.blc and b/blight-map/src/main/map/chunks/chunk_09_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_01.blc b/blight-map/src/main/map/chunks/chunk_09_01.blc index ce7eb30..520a792 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_01.blc and b/blight-map/src/main/map/chunks/chunk_09_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_00.blc b/blight-map/src/main/map/chunks/chunk_10_00.blc index 1dc115c..b192199 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_00.blc and b/blight-map/src/main/map/chunks/chunk_10_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_01.blc b/blight-map/src/main/map/chunks/chunk_10_01.blc index d87e9d1..0147ece 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_01.blc and b/blight-map/src/main/map/chunks/chunk_10_01.blc differ