Weiter am Editor gearbeitet, unter anderem LOD System, Items, Trees, Modelle

This commit is contained in:
2026-06-08 22:25:47 +02:00
parent 1297869dfa
commit 5e85051716
1113 changed files with 3665 additions and 529 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture2D;
import de.blight.editor.state.AnimPreviewState;
import de.blight.editor.state.ItemPlacementState;
import de.blight.editor.state.AreaState;
import de.blight.editor.state.ModelEditorState;
import de.blight.editor.state.EmitterState;
@@ -18,6 +19,7 @@ import de.blight.editor.state.SoundAreaState;
import de.blight.editor.state.WaterBodyState;
import de.blight.editor.state.EzTreeState;
import de.blight.editor.state.LightState;
import de.blight.editor.state.FernGeneratorState;
import de.blight.editor.state.PalmGeneratorState;
import de.blight.editor.state.SceneObjectState;
import de.blight.editor.state.TerrainEditorState;
@@ -91,6 +93,7 @@ public class JmeEditorApp extends SimpleApplication {
stateManager.attach(new TreeGeneratorState(input));
stateManager.attach(new EzTreeState(input));
stateManager.attach(new PalmGeneratorState(input));
stateManager.attach(new FernGeneratorState(input));
stateManager.attach(new LightState(input));
stateManager.attach(new EmitterState(input));
stateManager.attach(new WaterBodyState(input));
@@ -101,6 +104,7 @@ public class JmeEditorApp extends SimpleApplication {
stateManager.attach(new PlayToolState(input));
stateManager.attach(new AnimPreviewState(input));
stateManager.attach(new ModelEditorState(input));
stateManager.attach(new ItemPlacementState(input));
input.loadingStatus = "Initialisiere Konsole...";
jmeConsole = new JmeConsole(false);

View File

@@ -76,6 +76,10 @@ public class SharedInput {
public record GrassVertexEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<GrassVertexEdit> grassVertexEditQueue = new ConcurrentLinkedQueue<>();
// ── Farn-Generator ────────────────────────────────────────────────────────
public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {}
public final ConcurrentLinkedQueue<FernGenRequest> fernGenQueue = new ConcurrentLinkedQueue<>();
// ── Gras-Einstellungen (JavaFX → JME3) ───────────────────────────────────
/** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */
public volatile String grassTexturePath = "";
@@ -582,4 +586,23 @@ public class SharedInput {
/** JFX → JME: Model-Editor schließen. */
public volatile boolean modelEditorCloseRequest = false;
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
public volatile boolean modelEditorHasEmbeddedLods = false;
/** JFX → JME: welche LOD-Stufe anzeigen (0=LOD0, 1=LOD1, 2=LOD2). */
public volatile int modelEditorLodPreview = 0;
public volatile String modelEditorLod1Path = "";
public volatile String modelEditorLod2Path = "";
public volatile boolean modelEditorLodChanged = false;
// ── Item-Platzierung ──────────────────────────────────────────────────────
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
public static final int LAYER_ITEMS = 21;
/** JFX → JME: itemId des zu platzierenden Items (aus .item-Datei). */
public volatile String pendingItemId = null;
/** Klick-Events für Item-Platzierung (analoges Format zu objectClickQueue). */
public final ConcurrentLinkedQueue<ObjectClick> itemClickQueue = new ConcurrentLinkedQueue<>();
}

View File

@@ -19,6 +19,11 @@ public class SceneObject extends PlacedObject {
public String texturePath = "";
public String normalMapPath = "";
public String materialPath = "";
public String lod1Path = "";
public String lod2Path = "";
public float lod1Distance = 30f;
public float lod2Distance = 80f;
public float cullDistance = 120f;
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
boolean solid) {

View File

@@ -61,6 +61,9 @@ public class EzTreeState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(EzTreeState.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");
private static final Path TOOLS_DIR = de.blight.editor.ProjectRoot.resolve("tools");
private static final Gson GSON = new Gson();
@@ -72,10 +75,14 @@ public class EzTreeState extends BaseAppState {
// ── Capture-Phase ────────────────────────────────────────────────────────
private SharedInput.EzTreeGenRequest pendingRequest = null;
private Node pendingTreeNode = null;
private Node pendingHdNode = null;
private Node pendingLd1Node = null;
private BoundingBox pendingBb = null;
private ViewPort captureVP = null;
private FrameBuffer captureFB = null;
private volatile boolean captureReady = false;
private int capturePass = 0;
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
public EzTreeState(SharedInput input) { this.input = input; }
@@ -113,14 +120,15 @@ public class EzTreeState extends BaseAppState {
private void startGeneration(SharedInput.EzTreeGenRequest req) {
cleanupCapture();
Node treeNode = tryNodeJsGeneration(req);
if (treeNode == null) {
treeNode = javaFallback(req);
}
final Node finalNode = treeNode;
finalNode.updateGeometricState();
Node hdNode = tryNodeJsGeneration(req);
if (hdNode == null) hdNode = javaFallback(req);
hdNode.setLocalScale(1f / 3f);
hdNode.updateGeometricState();
BoundingBox bb = boundsOf(finalNode);
Node ld1Node = buildLod1Node(req.options());
ld1Node.setLocalScale(1f / 3f);
BoundingBox bb = boundsOf(hdNode);
float camDist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
: 20f;
@@ -128,22 +136,45 @@ public class EzTreeState extends BaseAppState {
? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 5f, 0f);
final Node finalHd = hdNode;
final Node finalLd1 = ld1Node;
final BoundingBox finalBb = bb;
final float dist = camDist;
final Vector3f tgt = target;
app.enqueue(() -> {
previewHost.setPreviewContent(finalNode, dist, tgt);
previewHost.setPreviewContent(finalHd, dist, tgt);
if (req.exportAfter()) {
setupCapture(finalNode, boundsOf(finalNode), req);
pendingRequest = req;
pendingHdNode = finalHd;
pendingLd1Node = finalLd1;
pendingBb = finalBb;
capturePass = 0;
capturePixels = new ByteBuffer[ATLAS_DIRS];
startCapturePass(0, finalHd, finalBb);
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (1/" + ATLAS_DIRS + ")…";
}
});
if (!req.exportAfter()) {
input.treeGenStatusMsg = "EZ-Tree Vorschau: " + resolveSubPath(req.presetName());
} else {
input.treeGenStatusMsg = "EZ-Tree: generiere…";
}
}
private Node buildLod1Node(TreeOptions opts) {
TreeOptions ld = opts.copy();
// Levels NICHT reduzieren gleiche Baumform und -größe wie LOD0 behalten.
// Nur Polygon-Anzahl pro Ast halbieren (sections/segments) und Blattanzahl reduzieren.
for (int lv = 0; lv <= ld.branch.levels; lv++) {
ld.branch.sections.put(lv, Math.max(3, opts.branch.getSections(lv) / 2));
ld.branch.segments.put(lv, Math.max(3, opts.branch.getSegments(lv) / 2));
}
ld.leaves.count = Math.max(0, opts.leaves.count / 2);
Tree tree = new Tree(ld);
tree.generate();
applyMaterials(tree, opts);
return treeToNode(tree, "EzTree_ld1");
}
// ── Node.js-Generierung ───────────────────────────────────────────────────
private Node tryNodeJsGeneration(SharedInput.EzTreeGenRequest req) {
@@ -328,45 +359,71 @@ public class EzTreeState extends BaseAppState {
Tree tree = new Tree(req.options());
tree.generate();
applyMaterials(tree, req.options());
return tree;
return treeToNode(tree, "EzTree");
}
// ── Phase 2: Impostor-Capture ─────────────────────────────────────────────
/** Überträgt alle Kinder eines Tree in einen plain Node, damit kein Tree-Objekt im j3o landet. */
private static Node treeToNode(Tree tree, String name) {
Node n = new Node(name);
n.setLocalScale(tree.getLocalScale());
n.setLocalTranslation(tree.getLocalTranslation());
for (Spatial child : new java.util.ArrayList<>(tree.getChildren())) {
tree.detachChild(child);
n.attachChild(child);
}
return n;
}
private void setupCapture(Node treeNode, BoundingBox bb, SharedInput.EzTreeGenRequest req) {
// ── Phase 2: Impostor-Capture (4-Richtungen) ─────────────────────────────
private void startCapturePass(int pass, Node treeNode, BoundingBox bb) {
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
captureFB.addColorTexture(capTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB);
float angle = pass * com.jme3.math.FastMath.HALF_PI;
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB, angle);
captureReady = false;
pendingRequest = req;
pendingTreeNode = treeNode;
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
}
private void finishCapture() {
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
app.getRenderer().readFrameBuffer(captureFB, pixels);
SharedInput.EzTreeGenRequest req = pendingRequest;
Node treeNode = pendingTreeNode;
capturePixels[capturePass] = pixels;
cleanupCapture();
if (capturePass < ATLAS_DIRS - 1) {
capturePass++;
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…";
startCapturePass(capturePass, pendingHdNode, pendingBb);
return;
}
// Alle Richtungen erfasst
SharedInput.EzTreeGenRequest req = pendingRequest;
Node hdNode = pendingHdNode;
Node ld1Node = pendingLd1Node;
BoundingBox bb = pendingBb;
ByteBuffer[] savedPixels = capturePixels;
pendingRequest = null;
pendingHdNode = null;
pendingLd1Node = null;
pendingBb = null;
capturePixels = new ByteBuffer[ATLAS_DIRS];
String subPath = resolveSubPath(req.presetName());
String namePart = req.presetName() != null
? req.presetName().toLowerCase().replace(" ", "_")
: subPath;
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
String exportName = namePart + "_" + timestamp;
saveImpostor(pixels, "ez_impostor_" + exportName);
exportTree(treeNode, exportName, subPath);
pendingRequest = null;
pendingTreeNode = null;
ByteBuffer atlas = combineAtlas(savedPixels);
Texture2D atlasT2d = saveImpostor(atlas, "ez_impostor_" + exportName, ATLAS_W, ATLAS_H);
Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d);
exportTree(lodRoot, exportName, subPath);
}
// ── Material-Aufbau ───────────────────────────────────────────────────────
@@ -440,15 +497,22 @@ public class EzTreeState extends BaseAppState {
// ── Offscreen-Viewport für Impostor ───────────────────────────────────────
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb) {
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
float dist = extent * 3f;
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb, float angle) {
Vector3f center = bb.getCenter();
float yExt = bb.getYExtent();
float hExt = Math.max(bb.getXExtent(), bb.getZExtent());
float extent = Math.max(yExt, hExt);
float dist = extent * 3.5f;
float camX = com.jme3.math.FastMath.sin(angle) * dist;
float camZ = com.jme3.math.FastMath.cos(angle) * dist;
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
cam.setLocation(center.add(0f, 0f, dist));
cam.setLocation(center.add(camX, 0f, camZ));
cam.lookAt(center, Vector3f.UNIT_Y);
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
// FOV groß genug, um den vollen Baum (+ 15 % Rand) zu erfassen
float fovY = (float) Math.toDegrees(2.0 * Math.atan2(yExt * 1.15, dist));
float fovH = (float) Math.toDegrees(2.0 * Math.atan2(hExt * 1.15, dist));
cam.setFrustumPerspective(Math.max(fovY, fovH), 1f, 0.1f, dist * 4f);
ViewPort vp = app.getRenderManager()
.createPostView("ezCapture_" + System.nanoTime(), cam);
@@ -524,34 +588,123 @@ public class EzTreeState extends BaseAppState {
captureReady = false;
}
private void saveImpostor(ByteBuffer pixels, String name) {
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
try {
pixels.rewind();
BufferedImage img = new BufferedImage(
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = pixels.get() & 0xFF, g = pixels.get() & 0xFF,
b = pixels.get() & 0xFF, a = pixels.get() & 0xFF;
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
img.setRGB(x, height - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
}
}
Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
Files.createDirectories(texDir);
ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
File pngFile = texDir.resolve(name + ".png").toFile();
ImageIO.write(img, "PNG", pngFile);
try {
return (Texture2D) assets.loadTexture("Textures/impostor/" + name + ".png");
} catch (Exception ignored) {
pixels.rewind();
Image jmeImg = new Image(Image.Format.RGBA8, width, height, pixels, null,
com.jme3.texture.image.ColorSpace.sRGB);
return new Texture2D(jmeImg);
}
} catch (IOException e) {
log.error("[EzTree] Impostor-Fehler: {}", e.getMessage());
return null;
}
}
private void exportTree(Node treeNode, String fileName, String subPath) {
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 Node assembleLodNode(String name, Node hd, Node ld1, BoundingBox bb, Texture2D atlasTex) {
Node root = new Node(name != null ? name : "ez_tree");
root.attachChild(hd);
root.attachChild(ld1);
Node lod2 = makeImpostorNode(bb, atlasTex);
root.attachChild(lod2);
hd.setCullHint(Spatial.CullHint.Inherit);
ld1.setCullHint(Spatial.CullHint.Always);
lod2.setCullHint(Spatial.CullHint.Always);
root.addControl(new EzTreeLodControl(app.getCamera(), hd, ld1, lod2, 40f, 120f));
return root;
}
private Node makeImpostorNode(BoundingBox bb, Texture2D tex) {
float h = bb != null ? bb.getYExtent() * 2f : 10f;
float w = bb != null ? Math.max(bb.getXExtent(), bb.getZExtent()) * 2f : 4f;
float size = Math.max(h, w);
float yOff = bb != null ? bb.getCenter().y : 5f;
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
if (tex != null) mat.setTexture("ColorMap", tex);
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
mat.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(com.jme3.material.RenderState.FaceCullMode.Off);
Node n = new Node("lod2");
for (int d = 0; d < ATLAS_DIRS; d++) {
float angle = d * com.jme3.math.FastMath.HALF_PI;
float uMin = (float) d / ATLAS_DIRS;
float uMax = (float)(d + 1) / ATLAS_DIRS;
n.attachChild(buildBillboardQuad("quad_" + d, angle, 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 = com.jme3.math.FastMath.cos(yRot);
float sin = com.jme3.math.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, 0f, uMax, 0f, uMax, 1f, uMin, 1f
});
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;
}
private void exportTree(Node lodRoot, String fileName, String subPath) {
try {
treeNode.setLocalScale(0.33f);
treeNode.updateGeometricState();
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
Files.createDirectories(baseDir);
File out = baseDir.resolve(fileName + ".j3o").toFile();
BinaryExporter.getInstance().save(treeNode, out);
// Controls vor Export entfernen (nicht serialisierbar über BinaryExporter)
while (lodRoot.getNumControls() > 0) lodRoot.removeControl(lodRoot.getControl(0));
BinaryExporter.getInstance().save(lodRoot, out);
log.info("[EZ-Tree] Gespeichert: {}", out.getAbsolutePath());
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + subPath + "/" + fileName + ".j3o";
input.refreshAssets = true;
@@ -562,6 +715,32 @@ public class EzTreeState extends BaseAppState {
}
}
// ── LOD-Control ───────────────────────────────────────────────────────────
private static final class EzTreeLodControl extends com.jme3.scene.control.AbstractControl {
private final Camera cam;
private final Node lod0, lod1, lod2;
private final float d01sq, d12sq;
EzTreeLodControl(Camera cam, Node l0, Node l1, Node l2, float d01, float d12) {
this.cam = cam;
this.lod0 = l0; this.lod1 = l1; this.lod2 = l2;
this.d01sq = d01 * d01;
this.d12sq = d12 * d12;
}
@Override
protected void controlUpdate(float tpf) {
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
lod1.setCullHint(dSq >= d01sq && dSq < d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
@Override protected void controlRender(com.jme3.renderer.RenderManager rm,
com.jme3.renderer.ViewPort vp) {}
}
private static String resolveSubPath(String presetName) {
if (presetName == null) return "unknown";
String lo = presetName.toLowerCase();

View File

@@ -0,0 +1,130 @@
package de.blight.editor.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
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.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.texture.Texture;
import de.blight.editor.SharedInput;
import de.blight.editor.tree.FernMeshBuilder;
import de.blight.editor.tree.FernOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FernGeneratorState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(FernGeneratorState.class);
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve(
"blight-assets", "src", "main", "resources");
private final SharedInput input;
private SimpleApplication app;
private AssetManager assets;
private TreeGeneratorState previewHost;
public FernGeneratorState(SharedInput input) { this.input = input; }
@Override protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assets = app.getAssetManager();
}
@Override protected void cleanup(Application app) {}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void update(float tpf) {
if (previewHost == null) {
previewHost = getStateManager().getState(TreeGeneratorState.class);
if (previewHost == null) return;
}
SharedInput.FernGenRequest req = input.fernGenQueue.poll();
if (req == null) return;
FernOptions opts = req.options();
Node fern = FernMeshBuilder.build(opts);
applyMaterial(fern, opts);
fern.updateGeometricState();
BoundingBox bb = fern.getWorldBound() instanceof BoundingBox b ? b : null;
float dist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 4f
: 3f;
Vector3f target = bb != null ? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 0.4f, 0f);
final Node finalFern = fern;
final float finalDist = dist;
final Vector3f finalTarget = target;
final boolean doExport = req.exportAfter();
app.enqueue(() -> {
previewHost.setPreviewContent(finalFern, finalDist, finalTarget);
if (doExport) exportFern(finalFern);
});
input.treeGenStatusMsg = doExport ? "Farn: exportiere…" : "Farn: Vorschau";
}
private void applyMaterial(Node fern, FernOptions opts) {
Material mat;
try {
mat = new Material(assets, "MatDefs/Fern.j3md");
mat.setFloat("WindStrength", opts.windStrength);
mat.setFloat("WindSpeed", opts.windSpeed);
try {
Texture diff = assets.loadTexture("Textures/fern/Fern02_Diffuse.tga");
mat.setTexture("DiffuseMap", diff);
mat.setBoolean("HasDiffuseMap", true);
} catch (Exception ignored) {}
try {
Texture norm = assets.loadTexture("Textures/fern/Fern02_Normal.tga");
mat.setTexture("NormalMap", norm);
mat.setBoolean("HasNormalMap", true);
} catch (Exception ignored) {}
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
} catch (Exception e) {
mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new com.jme3.math.ColorRGBA(0.1f, 0.55f, 0.1f, 1f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
}
for (Spatial child : fern.getChildren()) {
if (child instanceof Geometry g) {
g.setMaterial(mat.clone());
g.setQueueBucket(RenderQueue.Bucket.Transparent);
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
}
}
private void exportFern(Node fern) {
try {
Path dir = ASSET_ROOT.resolve("Models").resolve("plants").resolve("fern");
Files.createDirectories(dir);
String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
Path out = dir.resolve("fern_" + ts + ".j3o");
BinaryExporter.getInstance().save(fern, out.toFile());
log.info("[Farn] Gespeichert: {}", out);
input.treeGenStatusMsg = "Gespeichert: Models/plants/fern/fern_" + ts + ".j3o";
input.refreshAssets = true;
} catch (IOException e) {
log.error("[Farn] Export-Fehler: {}", e.getMessage());
input.treeGenStatusMsg = "Farn Export-Fehler: " + e.getMessage();
}
}
}

View File

@@ -45,10 +45,14 @@ public class GrassVertexState extends BaseAppState {
static final float WIDTH_FACTOR = 0.05f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
static final float BEND_FACTOR = 0.15f; // max. Krümmungsversatz an der Spitze
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f);
static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
// 050 % Trockenheit: Grün → Goldgelb
static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f);
static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
// 50100 % Trockenheit: Goldgelb → Dunkelbraun
static final ColorRGBA VERY_DRY_ROOT_COLOR = new ColorRGBA(0.18f, 0.09f, 0.02f, 1f);
static final ColorRGBA VERY_DRY_TIP_COLOR = new ColorRGBA(0.38f, 0.20f, 0.05f, 1f);
// ── Zustand ───────────────────────────────────────────────────────────────
private final SharedInput input;
@@ -156,20 +160,53 @@ public class GrassVertexState extends BaseAppState {
private final Random rng = new Random();
private void addBlades(Vector3f center) {
float radius = (float) input.grassVertexTool.brushRadius.getValue();
float height = (float) input.grassVertexTool.bladeHeight.getValue();
int density = (int) input.grassVertexTool.density.getValue();
/** Quadratischer Falloff: 1.0 im Zentrum, 0.5 am Rand. */
private static float brushFalloff(float distRatio) {
return 1f - 0.5f * distRatio * distRatio;
}
private void addBlades(Vector3f center) {
float radius = (float) input.grassVertexTool.brushRadius.getValue();
float height = (float) input.grassVertexTool.bladeHeight.getValue();
int density = (int) input.grassVertexTool.density.getValue();
float witherPct = (float) input.grassVertexTool.dryness.getValue() / 100f;
float uniformity = (float) input.grassVertexTool.uniformity.getValue();
float variation = (1f - uniformity) * 0.25f; // max ±25 % bei uniformity=0
float radSq = radius * radius;
// Kleinere Rand-Halme im Pinselbereich durch größere ersetzen
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
List<GrassVertexBlade> list = chunkBlades[ci];
boolean changed = false;
for (int i = 0; i < list.size(); i++) {
GrassVertexBlade b = list.get(i);
float dx = b.x() - center.x, dz = b.z() - center.z;
float distSq = dx*dx + dz*dz;
if (distSq > radSq) continue;
float distRatio = (float) Math.sqrt(distSq) / radius;
float idealH = height * brushFalloff(distRatio);
// Halm ist deutlich kleiner als die aktuelle Pinselposition erlaubt → ersetzen
if (b.height() < idealH * 0.88f) {
float newH = idealH * (1f + variation * (rng.nextFloat() * 2f - 1f));
list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), b.dryness()));
changed = true;
}
}
if (changed) dirtyChunks[ci] = true;
}
// Neue Halme mit Falloff und Gleichmäßigkeits-Variation setzen
for (int i = 0; i < density; i++) {
float angle = rng.nextFloat() * (float) (Math.PI * 2);
float r = rng.nextFloat() * radius;
float bx = center.x + (float) Math.cos(angle) * r;
float bz = center.z + (float) Math.sin(angle) * r;
float by = terrain.getHeight(new Vector2f(bx, bz));
float angle = rng.nextFloat() * (float) (Math.PI * 2);
float r = rng.nextFloat() * radius;
float bx = center.x + (float) Math.cos(angle) * r;
float bz = center.z + (float) Math.sin(angle) * r;
float by = terrain.getHeight(new Vector2f(bx, bz));
if (Float.isNaN(by)) continue;
float h = height * (0.75f + rng.nextFloat() * 0.5f);
float witherPct = (float) input.grassVertexTool.dryness.getValue() / 100f;
float distRatio = r / radius;
float h = height * brushFalloff(distRatio)
* (1f + variation * (rng.nextFloat() * 2f - 1f));
h = Math.max(0.05f, h);
float bladeDryness = rng.nextFloat() < witherPct
? 0.5f + rng.nextFloat() * 0.5f : 0f;
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness);
@@ -381,10 +418,23 @@ public class GrassVertexState extends BaseAppState {
float dr = DRY_ROOT_COLOR.r + (DRY_TIP_COLOR.r - DRY_ROOT_COLOR.r) * wf;
float dg = DRY_ROOT_COLOR.g + (DRY_TIP_COLOR.g - DRY_ROOT_COLOR.g) * wf;
float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
col[ci] = gr + (dr - gr) * dryness;
col[ci+1] = gg + (dg - gg) * dryness;
col[ci+2] = gb + (db - gb) * dryness;
col[ci+3] = 1f;
float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
// Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun
float fr, fg, fb;
if (dryness <= 0.5f) {
float t = dryness * 2f;
fr = gr + (dr - gr) * t;
fg = gg + (dg - gg) * t;
fb = gb + (db - gb) * t;
} else {
float t = (dryness - 0.5f) * 2f;
fr = dr + (vr - dr) * t;
fg = dg + (vg - dg) * t;
fb = db + (vb - db) * t;
}
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
int ti = vi * 2;
tex[ti] = wf; tex[ti+1] = 0f;

View File

@@ -0,0 +1,188 @@
package de.blight.editor.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.scene.*;
import com.jme3.scene.shape.Box;
import com.jme3.terrain.geomipmap.TerrainQuad;
import de.blight.common.PlacedItem;
import de.blight.common.PlacedItemIO;
import de.blight.editor.SharedInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor.
* Aktiv wenn {@code input.activeLayer == LAYER_ITEMS}.
*/
public class ItemPlacementState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(ItemPlacementState.class);
private final SharedInput input;
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private Node rootNode;
private TerrainQuad terrain;
private final List<PlacedItem> items = new ArrayList<>();
private final List<Node> nodes = new ArrayList<>();
private Node itemRoot;
private Node previewNode;
public ItemPlacementState(SharedInput input) {
this.input = input;
}
public void setTerrain(TerrainQuad terrain) {
this.terrain = terrain;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.cam = app.getCamera();
this.assets = app.getAssetManager();
this.rootNode = this.app.getRootNode();
itemRoot = new Node("itemRoot");
previewNode = new Node("itemPreview");
previewNode.setCullHint(Spatial.CullHint.Always);
Geometry prev = new Geometry("prev", new Box(0.15f, 0.15f, 0.15f));
Material pm = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
pm.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 0.6f));
prev.setMaterial(pm);
prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines);
previewNode.attachChild(prev);
}
@Override
protected void onEnable() {
items.clear();
nodes.clear();
itemRoot.detachAllChildren();
try {
items.addAll(PlacedItemIO.load());
} catch (Exception e) {
log.warn("[ItemPlacement] Laden fehlgeschlagen: {}", e.getMessage());
}
for (PlacedItem pi : items) {
Node n = buildItemNode(pi.itemId());
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
itemRoot.attachChild(n);
nodes.add(n);
}
rootNode.attachChild(itemRoot);
rootNode.attachChild(previewNode);
}
@Override
protected void onDisable() {
itemRoot.removeFromParent();
previewNode.removeFromParent();
}
@Override
protected void cleanup(Application app) {}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
updatePreview();
SharedInput.ObjectClick click;
while ((click = input.itemClickQueue.poll()) != null) {
if (click.rightButton()) {
input.pendingItemId = null;
input.activeLayer = 0;
} else {
handlePlace(click);
}
}
}
private void updatePreview() {
if (input.activeLayer != SharedInput.LAYER_ITEMS
|| input.pendingItemId == null
|| terrain == null
|| input.mouseScreenX < 0) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
float jx = input.mouseScreenX * (float) input.viewportScaleX;
float jy = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
Ray ray = screenToRay(jx, jy);
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
Vector3f pt = hits.getClosestCollision().getContactPoint();
previewNode.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
previewNode.setCullHint(Spatial.CullHint.Inherit);
}
private void handlePlace(SharedInput.ObjectClick click) {
if (terrain == null || input.pendingItemId == null) return;
float jx = click.screenX() * (float) input.viewportScaleX;
float jy = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Ray ray = screenToRay(jx, jy);
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
PlacedItem pi = new PlacedItem(input.pendingItemId, pt.x, pt.y, pt.z);
items.add(pi);
Node n = buildItemNode(pi.itemId());
n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
itemRoot.attachChild(n);
nodes.add(n);
try {
PlacedItemIO.save(items);
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})",
pi.itemId(), pt.x, pt.y, pt.z);
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private Node buildItemNode(String itemId) {
Node n = new Node("item_" + itemId);
Geometry g = new Geometry("itemGeo_" + itemId, new Box(0.15f, 0.15f, 0.15f));
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 1f));
g.setMaterial(mat);
n.attachChild(g);
n.setUserData("itemId", itemId);
return n;
}
private Ray screenToRay(float jmeX, float jmeY) {
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
return new Ray(near, far.subtract(near).normalizeLocal());
}
}

View File

@@ -50,7 +50,12 @@ public class ModelEditorState extends BaseAppState {
private Quaternion savedCamRot;
private final SharedInput input;
private String currentPath = null;
private String currentPath = null;
private String mainModelPath = null;
// Eingebettete LOD-Stufen (falls j3o ein LOD-Node-Baum ist)
private boolean hasEmbeddedLods = false;
private Spatial[] embeddedLodSpatials = null;
// Mittelpunkt für Orbit (Bounding-Box-Zentrum des Modells)
private Vector3f orbitCenter = Vector3f.ZERO.clone();
@@ -91,9 +96,34 @@ public class ModelEditorState extends BaseAppState {
String openPath = input.modelEditorOpenPath;
if (openPath != null) {
input.modelEditorOpenPath = null;
mainModelPath = openPath;
input.modelEditorLodPreview = 0;
loadModel(openPath);
}
if (input.modelEditorLodChanged) {
input.modelEditorLodChanged = false;
if (hasEmbeddedLods && embeddedLodSpatials != null) {
// Eingebettete LOD-Kinder direkt ein-/ausblenden
int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1);
for (int i = 0; i < embeddedLodSpatials.length; i++) {
embeddedLodSpatials[i].setCullHint(
i == lodIdx ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
} else {
String path = switch (input.modelEditorLodPreview) {
case 1 -> input.modelEditorLod1Path;
case 2 -> input.modelEditorLod2Path;
default -> mainModelPath;
};
if (path == null || path.isBlank()) {
path = mainModelPath;
input.modelEditorLodPreview = 0;
}
if (path != null) loadModel(path);
}
}
if (previewRoot == null) return;
// Schließen
@@ -152,12 +182,31 @@ public class ModelEditorState extends BaseAppState {
currentPath = assetPath;
modelWrapper = new Node("model_wrapper");
hasEmbeddedLods = false;
embeddedLodSpatials = null;
input.modelEditorHasEmbeddedLods = false;
try {
Spatial model = app.getAssetManager().loadModel(assetPath);
stripControls(model);
modelWrapper.attachChild(model);
// Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei Kind 0 sichtbar
// und alle weiteren mit CullHint.Always gesetzt sind (EZ-Tree / TreeGenerator-Muster).
if (model instanceof Node rootNode && rootNode.getChildren().size() >= 2) {
var children = rootNode.getChildren();
boolean lodPattern = children.stream().skip(1)
.allMatch(s -> s.getCullHint() == Spatial.CullHint.Always);
if (lodPattern) {
embeddedLodSpatials = children.toArray(new Spatial[0]);
hasEmbeddedLods = true;
input.modelEditorHasEmbeddedLods = true;
}
}
} catch (Exception e) {
// Fallback: roter Quader
System.err.println("[ModelEditor] Fehler beim Laden von '" + assetPath + "': " + e.getMessage());
e.printStackTrace();
input.modelEditorBoundsReady = false;
// Fallback: roter Quader als visuelles Signal
Geometry box = new Geometry("error_box", new Box(0.5f, 0.5f, 0.5f));
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Red);
@@ -243,6 +292,9 @@ public class ModelEditorState extends BaseAppState {
modelWrapper = null;
gridGeo = null;
}
hasEmbeddedLods = false;
embeddedLodSpatials = null;
input.modelEditorHasEmbeddedLods = false;
app.getRootNode().setCullHint(Spatial.CullHint.Inherit);
if (savedCamPos != null) {

View File

@@ -144,7 +144,9 @@ public class SceneObjectState extends BaseAppState {
so.getScale(), so.solid,
so.getTexturePath(), so.getNormalMapPath(), so.getMaterialPath(),
meshFile, animClips.get(i),
so.castShadow, so.receiveShadow));
so.castShadow, so.receiveShadow,
so.lod1Path, so.lod2Path,
so.lod1Distance, so.lod2Distance, so.cullDistance));
}
return list;
}
@@ -168,6 +170,11 @@ public class SceneObjectState extends BaseAppState {
so.setMaterialPath(pm.materialPath());
so.castShadow = pm.castShadow();
so.receiveShadow = pm.receiveShadow();
so.lod1Path = pm.lod1Path() != null ? pm.lod1Path() : "";
so.lod2Path = pm.lod2Path() != null ? pm.lod2Path() : "";
so.lod1Distance = pm.lod1Distance();
so.lod2Distance = pm.lod2Distance();
so.cullDistance = pm.cullDistance();
objects.add(so);
animClips.add(pm.animClip() != null ? pm.animClip() : "");
@@ -566,6 +573,13 @@ public class SceneObjectState extends BaseAppState {
}
SceneObject so = new SceneObject(modelPath, wx, wz, wy + placementOffY, defaultSolid);
if (meta != null) {
so.lod1Path = meta.lod1Path() != null ? meta.lod1Path() : "";
so.lod2Path = meta.lod2Path() != null ? meta.lod2Path() : "";
so.lod1Distance = meta.lod1Distance();
so.lod2Distance = meta.lod2Distance();
so.cullDistance = meta.cullDistance();
}
so.setRotation(0f, rotY, 0f);
so.setScale(defaultScale);
so.castShadow = defaultCast;
@@ -1217,6 +1231,7 @@ public class SceneObjectState extends BaseAppState {
try {
Spatial model = assets.loadModel(req.assetPath());
if (!req.keepControls()) stripControlsRecursive(model);
com.jme3.util.TangentBinormalGenerator.generate(model);
if (req.centerOrigin()) {
model.setLocalTranslation(0f, 0f, 0f);

View File

@@ -97,6 +97,7 @@ public class TerrainEditorState extends BaseAppState {
private PlacedObjectState placedObjectState;
private GrassVertexState grassVertexState;
private SceneObjectState sceneObjState;
private ItemPlacementState itemPlacementState;
private LightState lightState;
private EmitterState emitterState;
private WaterBodyState waterBodyState;
@@ -231,6 +232,14 @@ public class TerrainEditorState extends BaseAppState {
sceneObjState = app.getStateManager().getState(SceneObjectState.class);
if (sceneObjState != null) {
sceneObjState.setTerrain(terrain);
}
itemPlacementState = app.getStateManager().getState(ItemPlacementState.class);
if (itemPlacementState != null) {
itemPlacementState.setTerrain(terrain);
}
if (sceneObjState != null) {
try {
var placed = PlacedModelIO.load();
if (!placed.isEmpty()) sceneObjState.loadPlacedModels(placed);
@@ -880,6 +889,14 @@ public class TerrainEditorState extends BaseAppState {
catch (IOException e) { log.error("Vertex-Gras nicht speicherbar", e); }
}
MapIO.save(data);
if (heightSnap != null) {
try {
de.blight.common.ChunkTerrainIO.exportFromEditorHeightMap(heightSnap, TOTAL_SIZE);
log.info("Chunk-Dateien exportiert.");
} catch (IOException e) {
log.error("Chunk-Export fehlgeschlagen", e);
}
}
if (models != null) PlacedModelIO.save(models);
if (lights != null) LightIO.save(lights);
if (emitters != null) EmitterIO.save(emitters);
@@ -1193,8 +1210,7 @@ public class TerrainEditorState extends BaseAppState {
private void updateCamera(float tpf) {
if (input.activeLayer == SharedInput.LAYER_MODEL_EDITOR) {
input.consumeMouseDelta(); // konsumieren ohne zu verarbeiten
return;
return; // ModelEditorState konsumiert das Delta selbst
}
int[] delta = input.consumeMouseDelta();
if (delta[0] != 0 || delta[1] != 0) {

View File

@@ -69,6 +69,9 @@ public class TreeGeneratorState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(TreeGeneratorState.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 int PREVIEW_SIZE = 1024;
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
@@ -97,10 +100,12 @@ public class TreeGeneratorState extends BaseAppState {
private TreeMeshBuilder.MeshResult pendingHdResult = null;
private Material pendingBarkMat = null;
private Material pendingLeafMat = null;
private ViewPort captureVP = null;
private FrameBuffer captureFB = null;
private Texture2D captureTex = null;
private volatile boolean captureReady = false; // vom SceneProcessor gesetzt
private ViewPort captureVP = null;
private FrameBuffer captureFB = null;
private Texture2D captureTex = null;
private volatile boolean captureReady = false;
private int capturePass = 0;
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
public TreeGeneratorState(SharedInput input) { this.input = input; }
@@ -123,7 +128,7 @@ public class TreeGeneratorState extends BaseAppState {
previewVP = this.app.getRenderManager().createPostView("treePreview", previewCam);
previewVP.setOutputFrameBuffer(previewFB);
previewVP.setBackgroundColor(new ColorRGBA(0.50f, 0.72f, 0.95f, 1f));
previewVP.setBackgroundColor(new ColorRGBA(0.15f, 0.15f, 0.18f, 1f));
previewVP.setClearFlags(true, true, true);
previewScene = new Node("previewScene");
@@ -137,8 +142,7 @@ public class TreeGeneratorState extends BaseAppState {
previewScene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.60f, 1f)));
previewTreeHolder = new Node("treeHolder");
previewScene.attachChild(previewTreeHolder);
previewScene.attachChild(buildPreviewGround());
previewScene.attachChild(buildPreviewSky());
previewScene.attachChild(buildPreviewGrid());
previewVP.attachScene(previewScene);
DirectionalLightShadowRenderer shadowRenderer =
@@ -148,6 +152,19 @@ public class TreeGeneratorState extends BaseAppState {
shadowRenderer.setShadowIntensity(0.25f);
shadowRenderer.setShadowZExtend(80f);
previewVP.addProcessor(shadowRenderer);
// Stellt sicher, dass previewScene direkt vor dem Rendern immer aktuell ist
// unabhängig davon, welche AppStates nach TreeGeneratorState die Szene noch ändern.
previewVP.addProcessor(new SceneProcessor() {
private boolean inited = false;
@Override public void initialize(RenderManager rm, ViewPort v) { inited = true; }
@Override public void reshape(ViewPort v, int w, int h) {}
@Override public boolean isInitialized() { return inited; }
@Override public void preFrame(float tpf) { previewScene.updateGeometricState(); }
@Override public void postQueue(RenderQueue rq) {}
@Override public void postFrame(FrameBuffer out) {}
@Override public void cleanup() {}
@Override public void setProfiler(AppProfiler profiler) {}
});
previewTransfer = new FrameTransfer(input.treePreviewImage);
previewVP.addProcessor(previewTransfer);
}
@@ -193,8 +210,8 @@ public class TreeGeneratorState extends BaseAppState {
resizePreviewViewport(reqW, reqH);
}
// 3. Kamera-Orbit + updateGeometricState immer zuletzt
// nach allen Szenenänderungen dieser Frame
// 3. Kamera-Orbit updateGeometricState wird jetzt per preFrame-SceneProcessor
// direkt vor dem Rendern des previewVP aufgerufen (nach allen State-Updates).
if (previewVP != null) {
float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD;
float rotX = input.treePreviewRotX * FastMath.DEG_TO_RAD;
@@ -206,7 +223,6 @@ public class TreeGeneratorState extends BaseAppState {
previewTarget.y + FastMath.sin(rotX) * dist,
FastMath.cos(rotY) * cosX * dist));
c.lookAt(previewTarget, Vector3f.UNIT_Y);
previewScene.updateGeometricState();
}
}
@@ -266,36 +282,41 @@ public class TreeGeneratorState extends BaseAppState {
Node hdNode = makeTreeNode(hd, barkMat, leafMat, "hd");
Node ldNode = makeTreeNode(ld, barkMat.clone(), leafMat.clone(), "ld");
// Capture-Viewport aufbauen
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
captureFB.addColorTexture(captureTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
captureVP = buildCaptureViewPort(hdNode, hd.bounds(), captureFB);
captureReady = false;
pendingRequest = req;
pendingHdNode = hdNode;
pendingLdNode = ldNode;
pendingRequest = req;
pendingHdNode = hdNode;
pendingLdNode = ldNode;
pendingHdResult = hd;
pendingBarkMat = barkMat;
pendingLeafMat = leafMat;
pendingBarkMat = barkMat;
pendingLeafMat = leafMat;
capturePass = 0;
capturePixels = new ByteBuffer[ATLAS_DIRS];
input.treeGenStatusMsg = "Rendere Impostor…";
startCapturePass(0);
input.treeGenStatusMsg = "Rendere Impostor (1/" + ATLAS_DIRS + ")…";
}
// ── Phase 2: Capture abschließen ──────────────────────────────────────────
private void finishCapture() {
// Pixel aus Framebuffer lesen
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
app.getRenderer().readFrameBuffer(captureFB, pixels);
capturePixels[capturePass] = pixels;
cleanupCapture();
if (capturePass < ATLAS_DIRS - 1) {
capturePass++;
input.treeGenStatusMsg = "Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…";
startCapturePass(capturePass);
return;
}
// Alle Richtungen erfasst Atlas zusammensetzen
String treeType = pendingRequest.treeType();
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + treeType + "_" + timestamp);
ByteBuffer atlas = combineAtlas(capturePixels);
Texture2D impostorTex = saveImpostor(atlas, "impostor_" + treeType + "_" + timestamp,
ATLAS_W, ATLAS_H);
Node previewTree = makeTreeNode(pendingHdResult,
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
@@ -321,6 +342,7 @@ public class TreeGeneratorState extends BaseAppState {
pendingHdResult = null;
pendingBarkMat = null;
pendingLeafMat = null;
capturePixels = new ByteBuffer[ATLAS_DIRS];
}
// ── LOD-Aufbau ────────────────────────────────────────────────────────────
@@ -346,7 +368,7 @@ public class TreeGeneratorState extends BaseAppState {
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; // passt zur Baum-Offset-Y
float yOff = bb.getCenter().y + 2f;
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
if (tex != null) mat.setTexture("ColorMap", tex);
@@ -355,14 +377,18 @@ public class TreeGeneratorState extends BaseAppState {
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
Node n = new Node("lod2");
n.attachChild(buildBillboardQuad("quad_a", 0f, yOff, size, mat));
n.attachChild(buildBillboardQuad("quad_b", FastMath.HALF_PI, yOff, size, mat.clone()));
for (int d = 0; d < ATLAS_DIRS; d++) {
float angle = d * FastMath.HALF_PI;
float uMin = (float) d / ATLAS_DIRS;
float uMax = (float)(d + 1) / ATLAS_DIRS;
n.attachChild(buildBillboardQuad("quad_" + d, angle, 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 size, Material mat, float uMin, float uMax) {
float hw = size * 0.5f;
float hh = size * 0.5f;
float cos = FastMath.cos(yRot);
@@ -375,8 +401,10 @@ public class TreeGeneratorState extends BaseAppState {
hw*cos, yCent+hh, hw*sin,
-hw*cos, yCent+hh, -hw*sin
});
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ 0,0, 1,0, 1,1, 0,1 });
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
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);
@@ -384,6 +412,37 @@ public class TreeGeneratorState extends BaseAppState {
return g;
}
/** Kombiniert 4 einzelne 512×512-Puffer zu einem horizontalen 2048×512-Atlas. */
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;
}
/** Startet einen einzelnen Capture-Durchlauf für die gegebene Richtung (Pass 0..3). */
private void startCapturePass(int pass) {
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
captureFB.addColorTexture(captureTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
float angle = pass * FastMath.HALF_PI;
captureVP = buildCaptureViewPort(pendingHdNode, pendingHdResult.bounds(), captureFB, angle);
captureReady = false;
}
// ── Material-Factories ────────────────────────────────────────────────────
private Material buildBarkMaterial(TreeParams p) {
@@ -458,13 +517,15 @@ public class TreeGeneratorState extends BaseAppState {
// ── Offscreen-ViewPort ────────────────────────────────────────────────────
private ViewPort buildCaptureViewPort(Node treeNode, BoundingBox bb, FrameBuffer fb) {
private ViewPort buildCaptureViewPort(Node treeNode, 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;
cam.setLocation(center.add(0f, 0f, dist));
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);
@@ -519,18 +580,17 @@ public class TreeGeneratorState extends BaseAppState {
// ── Impostor-PNG speichern ────────────────────────────────────────────────
private Texture2D saveImpostor(ByteBuffer pixels, String name) {
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
try {
pixels.rewind();
BufferedImage img = new BufferedImage(
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = pixels.get() & 0xFF;
int g = pixels.get() & 0xFF;
int b = pixels.get() & 0xFF;
int a = pixels.get() & 0xFF;
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
img.setRGB(x, height - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
}
}
Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
@@ -543,12 +603,12 @@ public class TreeGeneratorState extends BaseAppState {
return (Texture2D) assets.loadTexture("Textures/impostor/" + name + ".png");
} catch (Exception loadEx) {
pixels.rewind();
Image jmeImg = new Image(Image.Format.RGBA8, IMPOSTOR_SIZE, IMPOSTOR_SIZE,
Image jmeImg = new Image(Image.Format.RGBA8, width, height,
pixels, null, com.jme3.texture.image.ColorSpace.sRGB);
return new Texture2D(jmeImg);
}
} catch (IOException e) {
System.err.println("[TreeGenerator] Impostor-Fehler: " + e.getMessage());
log.error("[Blight-Baum] Impostor-Fehler: {}", e.getMessage());
return null;
}
}
@@ -615,45 +675,47 @@ public class TreeGeneratorState extends BaseAppState {
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
}
// ── Vorschau-Boden (groß, Gras-Textur) ──────────────────────────────────
// ── Vorschau-Raster (Offscreen-VP, analog zum Objekt-Editor) ────────────
// Rendert in previewFB — völlig isoliert vom Haupt-Viewport des Welt-Editors.
private Geometry buildPreviewGround() {
float size = 600f;
float tiles = 30f; // UV-Wiederholungen
private Geometry buildPreviewGrid() {
float halfSize = 20f;
int n = (int)(halfSize * 2);
int lines = (n + 1) * 2;
// Eigenes Mesh mit gekachelten UVs (Quad unterstützt kein Tiling)
com.jme3.scene.Mesh mesh = new com.jme3.scene.Mesh();
float h = size * 0.5f;
mesh.setBuffer(VertexBuffer.Type.Position, 3,
new float[]{ -h,0,-h, h,0,-h, h,0,h, -h,0,h });
mesh.setBuffer(VertexBuffer.Type.Normal, 3,
new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 });
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
new float[]{ 0,0, tiles,0, tiles,tiles, 0,tiles });
mesh.setBuffer(VertexBuffer.Type.Index, 3,
new int[]{ 0,2,1, 0,3,2 });
java.nio.FloatBuffer pos = BufferUtils.createFloatBuffer(lines * 2 * 3);
java.nio.FloatBuffer col = BufferUtils.createFloatBuffer(lines * 2 * 4);
for (int i = 0; i <= n; i++) {
float coord = -halfSize + i;
boolean major5 = (Math.abs(Math.round(coord)) % 5) == 0;
boolean major10 = (Math.abs(Math.round(coord)) % 10) == 0;
float bright = major10 ? 0.70f : major5 ? 0.45f : 0.22f;
pos.put(coord).put(0).put(-halfSize);
pos.put(coord).put(0).put( halfSize);
col.put(bright).put(bright).put(bright).put(1f);
col.put(bright).put(bright).put(bright).put(1f);
pos.put(-halfSize).put(0).put(coord);
pos.put( halfSize).put(0).put(coord);
col.put(bright).put(bright).put(bright).put(1f);
col.put(bright).put(bright).put(bright).put(1f);
}
pos.flip(); col.flip();
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Color, 4, col);
mesh.updateBound();
Geometry ground = new Geometry("previewGround", mesh);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setBoolean("VertexColor", true);
Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.45f, 0.14f, 1f));
mat.setColor("Ambient", new ColorRGBA(0.11f, 0.18f, 0.06f, 1f));
mat.setColor("Specular", ColorRGBA.Black);
mat.setFloat("Shininess", 0f);
try {
Texture grassTex = assets.loadTexture("Textures/gras.png");
grassTex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("DiffuseMap", grassTex);
} catch (Exception ignored) {
// Fallback auf Farbe, wenn Textur fehlt
}
ground.setMaterial(mat);
ground.setShadowMode(RenderQueue.ShadowMode.Receive);
return ground;
Geometry geo = new Geometry("previewGrid", mesh);
geo.setMaterial(mat);
return geo;
}
// ── Skybox (Kuppel) ───────────────────────────────────────────────────────

View File

@@ -4,10 +4,12 @@ import java.util.List;
public class GrassVertexTool extends EditorTool {
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0);
public final ToolParameter density = new ToolParameter("Dichte", 5.0, 1.0, 100.0);
public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0);
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0);
public final ToolParameter density = new ToolParameter("Dichte", 5.0, 1.0, 100.0);
public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0);
/** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */
public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0);
@Override public String getName() { return "Gras (Vertices)"; }
@@ -15,5 +17,5 @@ public class GrassVertexTool extends EditorTool {
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
@Override
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness); }
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness, uniformity); }
}

View File

@@ -0,0 +1,174 @@
package de.blight.editor.tree;
import com.jme3.math.FastMath;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Random;
/**
* Baut einen prozeduralen Farn als Roset­te von Band-Fächern.
*
* UV-Atlas (aus FernPlantV2.obj abgeleitet):
* 5 Fächer-Varianten nebeneinander in U-Richtung:
* Spalte 0: U ∈ [0.081, 0.249]
* Spalte 1: U ∈ [0.249, 0.416]
* Spalte 2: U ∈ [0.416, 0.585]
* Spalte 3: U ∈ [0.585, 0.750]
* Spalte 4: U ∈ [0.750, 0.914]
* V-Richtung: V=0 = Wurzel, V=1 = Spitze
*
* Pro Scheitelpunktpaar:
* linke Kante = (U_spalteLinks, t)
* rechte Kante = (U_spalteRechts, t)
*
* Breiten-Richtung: senkrecht zur Wirbelsäule, in der vertikalen Fächer-Ebene
* W = normalize(dly·cosAz, 1, dly·sinAz) → Fächer stehen aufrecht
*
* Vertex-Farbe R = Wind-Gewicht (0 = Wurzel → 1 = Spitze).
*/
public class FernMeshBuilder {
// UV-Atlas: 2 Fächer-Varianten (Spalten 0+1 und Spalten 3+4).
// Spalte 2 [0.416..0.585] enthält in der Textur kein Blatt → nur 2 Varianten.
private static final float[] FROND_UL = { 0.081f, 0.585f }; // linke Spitze
private static final float[] FROND_SPINE = { 0.249f, 0.750f }; // Mittelrippe
private static final float[] FROND_UR = { 0.416f, 0.914f }; // rechte Spitze
public static Node build(FernOptions opts) {
Random rng = new Random(opts.seed);
Node fern = new Node("fern");
float baseAng = rng.nextFloat() * FastMath.TWO_PI;
for (int i = 0; i < opts.frondCount; i++) {
float az = baseAng + i * (FastMath.TWO_PI / opts.frondCount)
+ (rng.nextFloat() - 0.5f) * 0.55f;
float lScale = 0.75f + rng.nextFloat() * 0.50f;
float drScale = 0.80f + rng.nextFloat() * 0.40f;
float til = opts.tiltMin + rng.nextFloat() * (opts.tiltMax - opts.tiltMin);
int variant = rng.nextInt(FROND_UL.length);
Geometry g = buildFrond(opts, az, lScale, drScale, til, variant);
g.setName("frond_" + i);
fern.attachChild(g);
}
return fern;
}
private static Geometry buildFrond(FernOptions opts, float az,
float lScale, float drScale, float til, int variant) {
int S = Math.max(2, opts.frondSegments);
float len = opts.frondLength * lScale;
float drp = opts.droop * drScale;
float tilRad = til * FastMath.DEG_TO_RAD;
float cosA = FastMath.cos(az);
float sinA = FastMath.sin(az);
float cosTil = FastMath.cos(tilRad);
float sinTil = FastMath.sin(tilRad);
float uL = FROND_UL[variant];
float uSp = FROND_SPINE[variant];
float uR = FROND_UR[variant];
// Breiten-Richtung: horizontal, senkrecht zur Azimut-Richtung
float wx = sinA, wz = -cosA; // wy = 0
// 3 Vertex-Spalten pro Zeile: linke Spitze, Mittelrippe, rechte Spitze
int vCount = (S + 1) * 3;
int iCount = S * 12; // 4 Dreiecke × 3 Indices
FloatBuffer pos = BufferUtils.createFloatBuffer(vCount * 3);
FloatBuffer norm = BufferUtils.createFloatBuffer(vCount * 3);
FloatBuffer uv = BufferUtils.createFloatBuffer(vCount * 2);
FloatBuffer tan = BufferUtils.createFloatBuffer(vCount * 4);
FloatBuffer col4 = BufferUtils.createFloatBuffer(vCount * 4);
IntBuffer idx = BufferUtils.createIntBuffer(iCount);
// Breite aus UV-Aspektverhältnis (quadratische Textur vorausgesetzt)
float hw = len * (uR - uL) * 0.5f;
for (int j = 0; j <= S; j++) {
float t = (float) j / S;
// s = Bogenlänge entlang der Wirbelsäule (korrekte Winkel für alle Neigungen)
float s = t * len;
// eff = vertikale Ableitung d(cy)/d(s) inkl. Droop
float eff = sinTil - 2f * drp * s / len;
float cx = cosA * cosTil * s;
float cy = sinTil * s - drp * s * s / len;
float cz = sinA * cosTil * s;
// Normale: N = T × W (T = Tangente, W = Breitenrichtung)
// ergibt (-eff·cosA, cosTil, -eff·sinA), dann normieren
float nLen = FastMath.sqrt(eff * eff + cosTil * cosTil);
float nx = -eff * cosA / nLen, ny = cosTil / nLen, nz = -eff * sinA / nLen;
// Tangente entlang der Wirbelsäule (normiert durch nLen = |T|)
float tx = cosA * cosTil / nLen, ty = eff / nLen, tz = sinA * cosTil / nLen;
int vi = j * 3; // linke Spitze=vi, Mittelrippe=vi+1, rechte Spitze=vi+2
// linke Spitze (+W)
putVec3(pos, vi*3, cx + hw*wx, cy, cz + hw*wz);
putVec3(norm, vi*3, nx, ny, nz);
putVec2(uv, vi*2, uL, t);
putVec4(tan, vi*4, tx, ty, tz, 1f);
putVec4(col4, vi*4, t, 0f, 0f, 1f);
// Mittelrippe
putVec3(pos, vi*3+3, cx, cy, cz);
putVec3(norm, vi*3+3, nx, ny, nz);
putVec2(uv, vi*2+2, uSp, t);
putVec4(tan, vi*4+4, tx, ty, tz, 1f);
putVec4(col4, vi*4+4, t, 0f, 0f, 1f);
// rechte Spitze (W)
putVec3(pos, vi*3+6, cx - hw*wx, cy, cz - hw*wz);
putVec3(norm, vi*3+6, nx, ny, nz);
putVec2(uv, vi*2+4, uR, t);
putVec4(tan, vi*4+8, tx, ty, tz, 1f);
putVec4(col4, vi*4+8, t, 0f, 0f, 1f);
if (j < S) {
int base = j * 12;
int vj = j * 3, vj1 = (j+1) * 3;
// Linkes Panel: L(j), C(j), L(j+1) und L(j+1), C(j), C(j+1)
idx.put(base, vj); idx.put(base+1, vj+1); idx.put(base+2, vj1);
idx.put(base+3, vj1); idx.put(base+4, vj+1); idx.put(base+5, vj1+1);
// Rechtes Panel: C(j), R(j), C(j+1) und C(j+1), R(j), R(j+1)
idx.put(base+6, vj+1); idx.put(base+7, vj+2); idx.put(base+8, vj1+1);
idx.put(base+9, vj1+1); idx.put(base+10, vj+2); idx.put(base+11, vj1+2);
}
}
Mesh mesh = new Mesh();
pos.rewind(); norm.rewind(); uv.rewind(); tan.rewind(); col4.rewind(); idx.rewind();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Normal, 3, norm);
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv);
mesh.setBuffer(VertexBuffer.Type.Tangent, 4, tan);
mesh.setBuffer(VertexBuffer.Type.Color, 4, col4);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
mesh.updateCounts();
return new Geometry("frond", mesh);
}
private static void putVec2(FloatBuffer b, int pos, float x, float y) {
b.put(pos, x); b.put(pos+1, y);
}
private static void putVec3(FloatBuffer b, int pos, float x, float y, float z) {
b.put(pos, x); b.put(pos+1, y); b.put(pos+2, z);
}
private static void putVec4(FloatBuffer b, int pos, float x, float y, float z, float w) {
b.put(pos, x); b.put(pos+1, y); b.put(pos+2, z); b.put(pos+3, w);
}
}

View File

@@ -0,0 +1,24 @@
package de.blight.editor.tree;
public class FernOptions {
public int seed = 42;
public int frondCount = 15;
public float frondLength = 0.90f;
public float droop = 0.60f;
public float tiltMin = 30f; // Grad vom Boden (0 = flach, 90 = senkrecht)
public float tiltMax = 60f;
public int frondSegments = 6;
public float heightVariation = 0.4f;
public float windStrength = 0.15f;
public float windSpeed = 0.60f;
public FernOptions copy() {
FernOptions c = new FernOptions();
c.seed = seed; c.frondCount = frondCount; c.frondLength = frondLength;
c.droop = droop;
c.tiltMin = tiltMin; c.tiltMax = tiltMax;
c.frondSegments = frondSegments; c.heightVariation = heightVariation;
c.windStrength = windStrength; c.windSpeed = windSpeed;
return c;
}
}

View File

@@ -21,6 +21,8 @@
<logger name="com.jme3" level="WARN"/>
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" bekanntes JME-Verhalten, kein Fehler -->
<logger name="com.jme3.scene.plugins.gltf" level="ERROR"/>
<!-- TangentBinormalGenerator warnt bei UV-Nähten und harten Kanten erwartet, kein Fehler -->
<logger name="com.jme3.util.TangentBinormalGenerator" level="ERROR"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>