LOD für Palmen und Trauerweiden angepasst

This commit is contained in:
2026-09-14 21:57:41 +02:00
parent 45f7ee1045
commit 1d6f43f9ce
33 changed files with 431 additions and 417 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,10 +43,6 @@ 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 final SharedInput input;
@@ -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) {}
}
@@ -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);
ImpostorUtil.cleanup(app, captureVP, captureFB);
captureVP = null;
}
if (captureFB != null) {
try { captureFB.dispose(); } catch (Exception ignored) {}
captureFB = null;
}
captureReady = false;
captureReady[0] = false;
}
// ── LOD-Control ───────────────────────────────────────────────────────────

View File

@@ -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,10 +42,6 @@ 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(
"blight-assets", "src", "main", "resources");
@@ -61,7 +50,7 @@ public class WillowGeneratorState extends BaseAppState {
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();
@@ -117,7 +106,6 @@ public class WillowGeneratorState extends BaseAppState {
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);
ImpostorUtil.cleanup(app, captureVP, captureFB);
captureVP = null;
}
if (captureFB != null) {
try { captureFB.dispose(); } catch (Exception ignored) {}
captureFB = null;
}
captureReady = false;
captureReady[0] = false;
}
// ── LOD-Control ───────────────────────────────────────────────────────────
// ── LOD-Control (Editor-Vorschau) ─────────────────────────────────────────
private static final class WillowLodControl extends AbstractControl {
private final Camera cam;

View File

@@ -37,7 +37,9 @@ public class ModelLodControl extends AbstractControl {
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

View File

@@ -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<String, RigidBodyControl> 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;

View File

@@ -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

View File

@@ -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