LOD für Palmen und Trauerweiden angepasst
This commit is contained in:
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.
|
Before Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1010 B |
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user