Weiter am Editor gearbeitet, unter anderem LOD System, Items, Trees, Modelle
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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<>();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// 0–50 % 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);
|
||||
// 50–100 % 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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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 Rosette 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user