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

View File

@@ -0,0 +1,79 @@
MaterialDef Fern {
MaterialParameters {
Color Diffuse (Color) : 0.18 0.60 0.10 1.0
Float WindStrength : 0.15
Float WindSpeed : 0.6
Texture2D DiffuseMap
Boolean HasDiffuseMap : false
Texture2D NormalMap -LINEAR
Boolean HasNormalMap : false
Int BoundDrawBuffer
Int FilterMode
Boolean HardwareShadows
Texture2D ShadowMap0
Texture2D ShadowMap1
Texture2D ShadowMap2
Texture2D ShadowMap3
Float ShadowIntensity : 1.0
Vector4 Splits
Vector2 FadeInfo
Matrix4 LightViewProjectionMatrix0
Matrix4 LightViewProjectionMatrix1
Matrix4 LightViewProjectionMatrix2
Matrix4 LightViewProjectionMatrix3
Vector3 LightDir
Float PCFEdge
Float ShadowMapSize
Boolean BackfaceShadows : false
}
Technique {
VertexShader GLSL150 : Shaders/Fern.vert
FragmentShader GLSL150 : Shaders/Fern.frag
WorldParameters {
WorldViewProjectionMatrix
WorldMatrix
Time
}
RenderState {
FaceCull Off
}
}
Technique PreShadow {
VertexShader GLSL150 : Shaders/LeafPreShadow.vert
FragmentShader GLSL150 : Shaders/LeafPreShadow.frag
WorldParameters {
WorldViewProjectionMatrix
}
ForcedRenderState {
FaceCull Off
DepthTest On
DepthWrite On
PolyOffset 5 3
ColorWrite Off
}
}
Technique PostShadow {
VertexShader GLSL150 : Shaders/LeafPostShadow.vert
FragmentShader GLSL150 : Shaders/LeafPostShadow.frag
WorldParameters {
WorldViewProjectionMatrix
WorldMatrix
}
ForcedRenderState {
Blend Modulate
FaceCull Off
DepthWrite Off
}
}
}

View File

@@ -0,0 +1,15 @@
#Mon Jun 08 11:09:04 CEST 2026
castShadow=true
category=
name=FernPlantV2
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=0.002
scaleY=0.002
scaleZ=0.002
solid=false
tags=
uniformScale=true

View File

@@ -0,0 +1,20 @@
#Mon Jun 08 22:18:33 CEST 2026
castShadow=true
category=
cullDistance=120.0
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
name=kaktusfeige
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=2.5
scaleY=2.5
scaleZ=2.5
solid=true
tags=
uniformScale=true

View File

@@ -0,0 +1,42 @@
#import "Common/ShaderLib/GLSLCompat.glsllib"
uniform vec4 m_Diffuse;
uniform sampler2D m_DiffuseMap;
uniform bool m_HasDiffuseMap;
uniform sampler2D m_NormalMap;
uniform bool m_HasNormalMap;
in vec2 texCoord;
in vec3 vNormal;
in vec3 vTangent;
in float vHandedness;
void main() {
vec3 baseColor;
if (m_HasDiffuseMap) {
vec4 tex = texture2D(m_DiffuseMap, texCoord);
if (tex.a < 0.5) discard;
baseColor = tex.rgb * m_Diffuse.rgb;
} else {
vec2 uv = texCoord * 2.0 - 1.0;
if (dot(uv, uv) > 0.95) discard;
baseColor = m_Diffuse.rgb;
}
vec3 N = normalize(vNormal);
if (m_HasNormalMap) {
vec3 T = normalize(vTangent);
vec3 B = normalize(cross(N, T) * vHandedness);
mat3 TBN = mat3(T, B, N);
vec3 nm = texture2D(m_NormalMap, texCoord).rgb * 2.0 - 1.0;
N = normalize(TBN * nm);
}
vec3 sun = normalize(vec3(0.4, 1.0, 0.3));
float diff = max(0.0, dot(N, sun));
// two-sided: underside receives half brightness
diff = max(diff, max(0.0, dot(-N, sun)) * 0.5);
float lit = 0.45 + diff * 0.55;
gl_FragColor = vec4(baseColor * lit, 1.0);
}

View File

@@ -0,0 +1,34 @@
#import "Common/ShaderLib/GLSLCompat.glsllib"
uniform mat4 g_WorldViewProjectionMatrix;
uniform mat4 g_WorldMatrix;
uniform float g_Time;
uniform float m_WindStrength;
uniform float m_WindSpeed;
in vec3 inPosition;
in vec3 inNormal;
in vec2 inTexCoord;
in vec4 inColor; // R = wind weight (0=base, 1=tip)
in vec4 inTangent; // xyz=tangent, w=handedness
out vec2 texCoord;
out vec3 vNormal;
out vec3 vTangent;
out float vHandedness;
void main() {
float windW = inColor.r;
float t = g_Time * m_WindSpeed;
vec4 wp = g_WorldMatrix * vec4(inPosition, 1.0);
float phase = wp.x * 0.08 + wp.z * 0.06;
float swayX = sin(t + phase) * windW * m_WindStrength;
float swayZ = cos(t*0.73 + phase) * windW * m_WindStrength * 0.55;
vec3 anim = inPosition + vec3(swayX, 0.0, swayZ);
gl_Position = g_WorldViewProjectionMatrix * vec4(anim, 1.0);
texCoord = inTexCoord;
vNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz);
vTangent = normalize((g_WorldMatrix * vec4(inTangent.xyz, 0.0)).xyz);
vHandedness = inTangent.w;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

View File

@@ -0,0 +1,11 @@
{
"itemId": "neues_item_1780949443027",
"category": "CONSUMABLES",
"name": {
"id": "bloddagave.name"
},
"description": {
"id": "bloddagave.description"
},
"worthGold": 50
}

View File

@@ -0,0 +1,254 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.zip.*;
/**
* Konstanten, Datei-I/O und Hilfsmethoden für das chunk-basierte Terrain-System.
*
* Die Welt (4096 × 4096 m) wird in CHUNKS_PER_AXIS² = 32² = 1024 quadratische
* Chunks à CHUNK_SIZE = 128 m unterteilt. Jeder Chunk speichert CHUNK_VERTS² = 129²
* Höhenwerte bei 1 m Auflösung (native Editor-Auflösung).
*
* LOD-Stufen (3):
* LOD 0 1 m/Vertex (129×129) nah, Chebyshev-Dist ≤ LOD0_RANGE
* LOD 1 4 m/Vertex (33×33) mittel, Chebyshev-Dist ≤ LOD1_RANGE
* LOD 2 16 m/Vertex (9×9) fern, Rest
*
* Kanten-IDs (EDGE_*): werden für Seam-Stitching verwendet.
*/
public final class ChunkTerrainIO {
// ── Welt / Chunk-Konfiguration ─────────────────────────────────────────────
public static final int CHUNK_SIZE = 128;
public static final int WORLD_SIZE = 4096;
public static final int CHUNKS_PER_AXIS = WORLD_SIZE / CHUNK_SIZE; // 32
public static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS; // 1024
/** Vertices pro Kante bei nativer 1-m-Auflösung (inklusiv). */
public static final int CHUNK_VERTS = CHUNK_SIZE + 1; // 129
// ── LOD-Konfiguration ──────────────────────────────────────────────────────
public static final int LOD_COUNT = 3;
/** Meter pro Vertex je LOD-Stufe. */
public static final float[] LOD_SPACING = { 1f, 4f, 16f };
/** Vertices pro Kante je LOD-Stufe. */
public static final int[] LOD_VERTS = { 129, 33, 9 };
/** Chebyshev-Distanz (in Chunks) bis inkl. dieser Stufe aktiv ist. */
public static final int LOD0_RANGE = 1;
public static final int LOD1_RANGE = 3;
/** Chebyshev-Distanz ≤ PHYSICS_RANGE: Physik-Collider aktiv. */
public static final int PHYSICS_RANGE = 1;
// ── Kanten-Konstanten ──────────────────────────────────────────────────────
public static final int EDGE_NORTH = 0; // höchste Zeile (row = VERTS-1)
public static final int EDGE_SOUTH = 1; // unterste Zeile (row = 0)
public static final int EDGE_EAST = 2; // rechteste Spalte (col = VERTS-1)
public static final int EDGE_WEST = 3; // linkeste Spalte (col = 0)
private static final int MAGIC = 0x424C4354; // "BLCT"
private static final int VERSION = 1;
private ChunkTerrainIO() {}
// ── Dateipfade ─────────────────────────────────────────────────────────────
public static Path chunksDir() {
return MapIO.getMapPath().resolveSibling("chunks");
}
public static Path getChunkPath(int cx, int cz) {
return chunksDir().resolve(String.format("chunk_%02d_%02d.blc", cx, cz));
}
public static boolean chunkExists(int cx, int cz) {
return Files.exists(getChunkPath(cx, cz));
}
public static boolean allChunksExist() {
for (int cz = 0; cz < CHUNKS_PER_AXIS; cz++)
for (int cx = 0; cx < CHUNKS_PER_AXIS; cx++)
if (!chunkExists(cx, cz)) return false;
return true;
}
// ── I/O ───────────────────────────────────────────────────────────────────
public static void saveChunk(int cx, int cz, float[] heights) throws IOException {
if (heights.length != CHUNK_VERTS * CHUNK_VERTS)
throw new IllegalArgumentException("heights.length muss " + (CHUNK_VERTS * CHUNK_VERTS) + " sein");
Path p = getChunkPath(cx, cz);
Path tmp = p.resolveSibling(p.getFileName() + ".tmp");
Files.createDirectories(p.getParent());
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new GZIPOutputStream(Files.newOutputStream(tmp))))) {
out.writeInt(MAGIC);
out.writeInt(VERSION);
out.writeInt(cx);
out.writeInt(cz);
for (float h : heights) out.writeFloat(h);
}
try {
Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING);
}
}
public static float[] loadChunk(int cx, int cz) throws IOException {
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new GZIPInputStream(
Files.newInputStream(getChunkPath(cx, cz)))))) {
if (in.readInt() != MAGIC) throw new IOException("Ungültiges Chunk-Format");
int ver = in.readInt();
if (ver != VERSION) throw new IOException("Unbekannte Chunk-Version: " + ver);
in.readInt(); // cx (ignoriert, Dateiname ist maßgeblich)
in.readInt(); // cz
float[] h = new float[CHUNK_VERTS * CHUNK_VERTS];
for (int i = 0; i < h.length; i++) h[i] = in.readFloat();
return h;
}
}
// ── Export ────────────────────────────────────────────────────────────────
/**
* Teilt die Editor-Heightmap (4097 × 4097, 1 m/Vertex) in 1024 Chunk-Dateien auf.
* Direkte Kopie ohne Interpolation jeder Chunk enthält exakt die Editor-Vertices
* seines Bereichs.
*/
public static void exportFromEditorHeightMap(float[] editorH, int editorVerts) throws IOException {
for (int cz = 0; cz < CHUNKS_PER_AXIS; cz++) {
for (int cx = 0; cx < CHUNKS_PER_AXIS; cx++) {
float[] chunk = new float[CHUNK_VERTS * CHUNK_VERTS];
for (int row = 0; row < CHUNK_VERTS; row++) {
int srcRow = cz * CHUNK_SIZE + row;
for (int col = 0; col < CHUNK_VERTS; col++) {
chunk[row * CHUNK_VERTS + col] = editorH[srcRow * editorVerts + cx * CHUNK_SIZE + col];
}
}
saveChunk(cx, cz, chunk);
}
}
}
/**
* Migriert eine alte MapData (16385 × 16385 Höhenwerte, 0,25 m/Vertex)
* in das Chunk-Format. Jeder Chunk enthält 129 × 129 Werte (jeder 4. Quell-Vertex).
*/
public static void exportFromMapData(MapData data) throws IOException {
int srcVerts = MapData.TERRAIN_VERTS; // 16385
int srcPerChunk = (srcVerts - 1) / CHUNKS_PER_AXIS; // 512
int step = srcPerChunk / (CHUNK_VERTS - 1); // 4
for (int cz = 0; cz < CHUNKS_PER_AXIS; cz++) {
for (int cx = 0; cx < CHUNKS_PER_AXIS; cx++) {
float[] chunk = new float[CHUNK_VERTS * CHUNK_VERTS];
for (int row = 0; row < CHUNK_VERTS; row++) {
int srcRow = Math.min(cz * srcPerChunk + row * step, srcVerts - 1);
for (int col = 0; col < CHUNK_VERTS; col++) {
int srcCol = Math.min(cx * srcPerChunk + col * step, srcVerts - 1);
chunk[row * CHUNK_VERTS + col] = data.terrainHeight[srcRow * srcVerts + srcCol];
}
}
saveChunk(cx, cz, chunk);
}
}
}
/** Erzeugt leere (flache) Chunk-Dateien mit Höhe 1.0 für alle 1024 Chunks. */
public static void exportBlankChunks() throws IOException {
float[] flat = new float[CHUNK_VERTS * CHUNK_VERTS];
java.util.Arrays.fill(flat, 1f);
for (int cz = 0; cz < CHUNKS_PER_AXIS; cz++)
for (int cx = 0; cx < CHUNKS_PER_AXIS; cx++)
saveChunk(cx, cz, flat);
}
// ── Downsample ────────────────────────────────────────────────────────────
/** Bilineare Verkleinerung eines quadratischen srcVerts²-Arrays auf dstVerts². */
public static float[] downsample(float[] src, int srcVerts, int dstVerts) {
if (srcVerts == dstVerts) return src.clone();
float[] dst = new float[dstVerts * dstVerts];
float step = (float) (srcVerts - 1) / (dstVerts - 1);
for (int dz = 0; dz < dstVerts; dz++) {
float sz = dz * step;
int sz0 = Math.min((int) sz, srcVerts - 2), sz1 = sz0 + 1;
float fz = sz - sz0;
for (int dx = 0; dx < dstVerts; dx++) {
float sx = dx * step;
int sx0 = Math.min((int) sx, srcVerts - 2), sx1 = sx0 + 1;
float fx = sx - sx0;
dst[dz * dstVerts + dx] =
(src[sz0 * srcVerts + sx0] * (1 - fx) + src[sz0 * srcVerts + sx1] * fx) * (1 - fz)
+ (src[sz1 * srcVerts + sx0] * (1 - fx) + src[sz1 * srcVerts + sx1] * fx) * fz;
}
}
return dst;
}
// ── Seam-Stitching ────────────────────────────────────────────────────────
/**
* Extrahiert den Rand eines quadratischen height-Arrays (verts × verts).
* Werte in Leserichtung: West→Ost für NORTH/SOUTH, Nord→Süd für EAST/WEST.
*/
public static float[] extractEdge(float[] heights, int verts, int edge) {
float[] e = new float[verts];
switch (edge) {
case EDGE_NORTH -> { for (int i = 0; i < verts; i++) e[i] = heights[(verts - 1) * verts + i]; }
case EDGE_SOUTH -> { System.arraycopy(heights, 0, e, 0, verts); }
case EDGE_EAST -> { for (int i = 0; i < verts; i++) e[i] = heights[i * verts + (verts - 1)]; }
case EDGE_WEST -> { for (int i = 0; i < verts; i++) e[i] = heights[i * verts]; }
}
return e;
}
/**
* Passt die Randwerte in {@code heights} (fineVerts × fineVerts) an die gröbere
* Nachbar-Kante an: nicht-ausgerichtete Zwischenvertices werden auf die lineare
* Interpolation der Anker-Vertices gesetzt → eliminiert Höhenrisse an LOD-Grenzen.
*
* @param heights height-Array des feinen Chunks (in-place modifiziert)
* @param fineVerts Vertices pro Kante (fein)
* @param coarseEdge Randwerte des groben Nachbarn (Länge coarseVerts)
* @param coarseVerts Länge von coarseEdge
* @param edge welche Kante dieses Chunks (EDGE_*)
*/
public static void stitchEdge(float[] heights, int fineVerts,
float[] coarseEdge, int coarseVerts, int edge) {
int ratio = (fineVerts - 1) / (coarseVerts - 1);
for (int i = 0; i < fineVerts; i++) {
if (i % ratio == 0) continue;
int k0 = i / ratio;
float t = (float) (i % ratio) / ratio;
float snapped = coarseEdge[k0] + (coarseEdge[k0 + 1] - coarseEdge[k0]) * t;
int idx = switch (edge) {
case EDGE_NORTH -> (fineVerts - 1) * fineVerts + i;
case EDGE_SOUTH -> i;
case EDGE_EAST -> i * fineVerts + (fineVerts - 1);
default -> i * fineVerts; // EDGE_WEST
};
heights[idx] = snapped;
}
}
// ── LOD-Hilfsmethoden ─────────────────────────────────────────────────────
/** Gibt den LOD-Level für eine gegebene Chebyshev-Distanz zurück. */
public static int lodForDistance(int chebyshev) {
if (chebyshev <= LOD0_RANGE) return 0;
if (chebyshev <= LOD1_RANGE) return 1;
return 2;
}
/** Chebyshev-Distanz (in Chunks) zwischen zwei Chunk-Koordinaten. */
public static int chebyshev(int ax, int az, int bx, int bz) {
return Math.max(Math.abs(ax - bx), Math.abs(az - bz));
}
/** Flacher Chunk-Index aus Chunk-Koordinaten. */
public static int chunkIndex(int cx, int cz) {
return cz * CHUNKS_PER_AXIS + cx;
}
}

View File

@@ -18,11 +18,17 @@ public record ModelMeta(
boolean castShadow, boolean castShadow,
boolean receiveShadow, boolean receiveShadow,
float randomScaleMin, float randomScaleMin,
float randomScaleMax float randomScaleMax,
String lod1Path, // relativer Asset-Pfad; "" = nicht gesetzt
String lod2Path,
float lod1Distance, // ab dieser Distanz LOD1 anzeigen
float lod2Distance, // ab dieser Distanz LOD2 anzeigen
float cullDistance // ab dieser Distanz ausblenden
) { ) {
public static ModelMeta defaults(String j3oFileName) { public static ModelMeta defaults(String j3oFileName) {
String name = j3oFileName.replaceFirst("\\.j3o$", ""); String name = j3oFileName.replaceFirst("\\.j3o$", "");
return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f, return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f,
false, true, true, 1f, 1f); false, true, true, 1f, 1f,
"", "", 30f, 80f, 120f);
} }
} }

View File

@@ -29,6 +29,11 @@ public final class ModelMetaIO {
p.setProperty("receiveShadow", String.valueOf(m.receiveShadow())); p.setProperty("receiveShadow", String.valueOf(m.receiveShadow()));
p.setProperty("randomScaleMin", String.valueOf(m.randomScaleMin())); p.setProperty("randomScaleMin", String.valueOf(m.randomScaleMin()));
p.setProperty("randomScaleMax", String.valueOf(m.randomScaleMax())); p.setProperty("randomScaleMax", String.valueOf(m.randomScaleMax()));
p.setProperty("lod1Path", m.lod1Path());
p.setProperty("lod2Path", m.lod2Path());
p.setProperty("lod1Distance", String.valueOf(m.lod1Distance()));
p.setProperty("lod2Distance", String.valueOf(m.lod2Distance()));
p.setProperty("cullDistance", String.valueOf(m.cullDistance()));
try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) { try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) {
p.store(w, null); p.store(w, null);
} }
@@ -57,7 +62,12 @@ public final class ModelMetaIO {
Boolean.parseBoolean(p.getProperty("castShadow", "true")), Boolean.parseBoolean(p.getProperty("castShadow", "true")),
Boolean.parseBoolean(p.getProperty("receiveShadow", "true")), Boolean.parseBoolean(p.getProperty("receiveShadow", "true")),
parseFloat(p, "randomScaleMin", 1f), parseFloat(p, "randomScaleMin", 1f),
parseFloat(p, "randomScaleMax", 1f) parseFloat(p, "randomScaleMax", 1f),
p.getProperty("lod1Path", ""),
p.getProperty("lod2Path", ""),
parseFloat(p, "lod1Distance", 30f),
parseFloat(p, "lod2Distance", 80f),
parseFloat(p, "cullDistance", 120f)
); );
} }

View File

@@ -0,0 +1,4 @@
package de.blight.common;
/** Ein Item-Pickup das auf der Spielwelt liegt (Kartendaten). */
public record PlacedItem(String itemId, float x, float y, float z) {}

View File

@@ -0,0 +1,51 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt auf der Karte liegende Items (Pickups).
* Datei: {@code blight_placed_items.bpi} neben {@code blight_map.blm}.
* Format: {@code itemId\tx\ty\tz}
*/
public final class PlacedItemIO {
private PlacedItemIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_placed_items.bpi");
}
public static void save(List<PlacedItem> items) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# itemId\tx\ty\tz");
w.newLine();
for (PlacedItem it : items) {
w.write(String.format(Locale.ROOT, "%s\t%.5f\t%.5f\t%.5f%n",
it.itemId(), it.x(), it.y(), it.z()));
}
}
}
public static List<PlacedItem> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedItem> list = new ArrayList<>();
for (String line : Files.readAllLines(p)) {
line = line.strip();
if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1);
if (f.length < 4) continue;
try {
list.add(new PlacedItem(f[0],
Float.parseFloat(f[1]),
Float.parseFloat(f[2]),
Float.parseFloat(f[3])));
} catch (NumberFormatException ignored) {}
}
return list;
}
}

View File

@@ -13,5 +13,10 @@ public record PlacedModel(
/** AnimationLibrary-Clip-Key (z. B. "walk/Run"); "" = keine Animation. */ /** AnimationLibrary-Clip-Key (z. B. "walk/Run"); "" = keine Animation. */
String animClip, String animClip,
boolean castShadow, boolean castShadow,
boolean receiveShadow boolean receiveShadow,
String lod1Path, // relativer Asset-Pfad; "" = nicht gesetzt
String lod2Path,
float lod1Distance, // ab dieser Distanz LOD1 anzeigen
float lod2Distance, // ab dieser Distanz LOD2 anzeigen
float cullDistance // ab dieser Distanz ausblenden
) {} ) {}

View File

@@ -8,10 +8,11 @@ import java.util.*;
* Liest und schreibt platzierte Modelle als tab-separierte Textdatei * Liest und schreibt platzierte Modelle als tab-separierte Textdatei
* ({@code blight_objects.blo}) neben der Kartendatei. * ({@code blight_objects.blo}) neben der Kartendatei.
* *
* Spalten (seit v3): * Spalten (seit v4):
* modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow * modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow
* lod1Path lod2Path lod1Distance lod2Distance cullDistance
* *
* Alte Dateien mit 6 Spalten (v1/v2) werden gelesen; fehlende Felder erhalten Standardwerte. * Alte Dateien mit 6 Spalten (v1/v2/v3) werden gelesen; fehlende Felder erhalten Standardwerte.
*/ */
public final class PlacedModelIO { public final class PlacedModelIO {
@@ -25,11 +26,11 @@ public final class PlacedModelIO {
Path p = getPath(); Path p = getPath();
Files.createDirectories(p.getParent()); Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) { try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip\tcastShadow\treceiveShadow"); w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip\tcastShadow\treceiveShadow\tlod1Path\tlod2Path\tlod1Distance\tlod2Distance\tcullDistance");
w.newLine(); w.newLine();
for (PlacedModel m : models) { for (PlacedModel m : models) {
w.write(String.format(Locale.ROOT, w.write(String.format(Locale.ROOT,
"%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s\t%b\t%b%n", "%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s\t%b\t%b\t%s\t%s\t%.5f\t%.5f\t%.5f%n",
m.modelPath(), m.modelPath(),
m.x(), m.y(), m.z(), m.x(), m.y(), m.z(),
m.rotY(), m.scale(), m.rotY(), m.scale(),
@@ -37,7 +38,9 @@ public final class PlacedModelIO {
m.solid(), m.solid(),
nvl(m.texturePath()), nvl(m.normalMapPath()), nvl(m.materialPath()), nvl(m.texturePath()), nvl(m.normalMapPath()), nvl(m.materialPath()),
nvl(m.meshFile()), nvl(m.animClip()), nvl(m.meshFile()), nvl(m.animClip()),
m.castShadow(), m.receiveShadow())); m.castShadow(), m.receiveShadow(),
nvl(m.lod1Path()), nvl(m.lod2Path()),
m.lod1Distance(), m.lod2Distance(), m.cullDistance()));
} }
} }
} }
@@ -68,14 +71,25 @@ public final class PlacedModelIO {
String animClip = f.length > 13 ? f[13] : ""; String animClip = f.length > 13 ? f[13] : "";
boolean castShadow = f.length > 14 ? Boolean.parseBoolean(f[14]) : true; boolean castShadow = f.length > 14 ? Boolean.parseBoolean(f[14]) : true;
boolean receiveShadow = f.length > 15 ? Boolean.parseBoolean(f[15]) : true; boolean receiveShadow = f.length > 15 ? Boolean.parseBoolean(f[15]) : true;
String lod1Path = f.length > 16 ? f[16] : "";
String lod2Path = f.length > 17 ? f[17] : "";
float lod1Distance = f.length > 18 ? parseFloat(f[18], 30f) : 30f;
float lod2Distance = f.length > 19 ? parseFloat(f[19], 80f) : 80f;
float cullDistance = f.length > 20 ? parseFloat(f[20], 120f) : 120f;
list.add(new PlacedModel(modelPath, x, y, z, list.add(new PlacedModel(modelPath, x, y, z,
rotY, rotX, rotZ, scale, solid, rotY, rotX, rotZ, scale, solid,
texPath, nmPath, matPath, meshFile, animClip, texPath, nmPath, matPath, meshFile, animClip,
castShadow, receiveShadow)); castShadow, receiveShadow,
lod1Path, lod2Path, lod1Distance, lod2Distance, cullDistance));
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
return list; return list;
} }
private static String nvl(String s) { return s != null ? s : ""; } private static String nvl(String s) { return s != null ? s : ""; }
private static float parseFloat(String s, float def) {
try { return Float.parseFloat(s); }
catch (NumberFormatException e) { return def; }
}
} }

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

View File

@@ -76,6 +76,10 @@ public class SharedInput {
public record GrassVertexEdit(float screenX, float screenY, int action) {} public record GrassVertexEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<GrassVertexEdit> grassVertexEditQueue = new ConcurrentLinkedQueue<>(); 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) ─────────────────────────────────── // ── Gras-Einstellungen (JavaFX → JME3) ───────────────────────────────────
/** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */ /** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */
public volatile String grassTexturePath = ""; public volatile String grassTexturePath = "";
@@ -582,4 +586,23 @@ public class SharedInput {
/** JFX → JME: Model-Editor schließen. */ /** JFX → JME: Model-Editor schließen. */
public volatile boolean modelEditorCloseRequest = false; 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 texturePath = "";
public String normalMapPath = ""; public String normalMapPath = "";
public String materialPath = ""; 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, public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
boolean solid) { 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 Logger log = LoggerFactory.getLogger(EzTreeState.class);
private static final int IMPOSTOR_SIZE = 512; 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 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 Path TOOLS_DIR = de.blight.editor.ProjectRoot.resolve("tools");
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
@@ -72,10 +75,14 @@ public class EzTreeState extends BaseAppState {
// ── Capture-Phase ──────────────────────────────────────────────────────── // ── Capture-Phase ────────────────────────────────────────────────────────
private SharedInput.EzTreeGenRequest pendingRequest = null; 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 ViewPort captureVP = null;
private FrameBuffer captureFB = null; private FrameBuffer captureFB = null;
private volatile boolean captureReady = false; private volatile boolean captureReady = false;
private int capturePass = 0;
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
public EzTreeState(SharedInput input) { this.input = input; } public EzTreeState(SharedInput input) { this.input = input; }
@@ -113,14 +120,15 @@ public class EzTreeState extends BaseAppState {
private void startGeneration(SharedInput.EzTreeGenRequest req) { private void startGeneration(SharedInput.EzTreeGenRequest req) {
cleanupCapture(); cleanupCapture();
Node treeNode = tryNodeJsGeneration(req); Node hdNode = tryNodeJsGeneration(req);
if (treeNode == null) { if (hdNode == null) hdNode = javaFallback(req);
treeNode = javaFallback(req); hdNode.setLocalScale(1f / 3f);
} hdNode.updateGeometricState();
final Node finalNode = treeNode;
finalNode.updateGeometricState();
BoundingBox bb = boundsOf(finalNode); Node ld1Node = buildLod1Node(req.options());
ld1Node.setLocalScale(1f / 3f);
BoundingBox bb = boundsOf(hdNode);
float camDist = bb != null float camDist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
: 20f; : 20f;
@@ -128,22 +136,45 @@ public class EzTreeState extends BaseAppState {
? new Vector3f(0f, bb.getCenter().y, 0f) ? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 5f, 0f); : new Vector3f(0f, 5f, 0f);
final Node finalHd = hdNode;
final Node finalLd1 = ld1Node;
final BoundingBox finalBb = bb;
final float dist = camDist; final float dist = camDist;
final Vector3f tgt = target; final Vector3f tgt = target;
app.enqueue(() -> { app.enqueue(() -> {
previewHost.setPreviewContent(finalNode, dist, tgt); previewHost.setPreviewContent(finalHd, dist, tgt);
if (req.exportAfter()) { 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()) { if (!req.exportAfter()) {
input.treeGenStatusMsg = "EZ-Tree Vorschau: " + resolveSubPath(req.presetName()); 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 ─────────────────────────────────────────────────── // ── Node.js-Generierung ───────────────────────────────────────────────────
private Node tryNodeJsGeneration(SharedInput.EzTreeGenRequest req) { private Node tryNodeJsGeneration(SharedInput.EzTreeGenRequest req) {
@@ -328,45 +359,71 @@ public class EzTreeState extends BaseAppState {
Tree tree = new Tree(req.options()); Tree tree = new Tree(req.options());
tree.generate(); tree.generate();
applyMaterials(tree, req.options()); 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); BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
captureFB.addColorTexture(capTex); captureFB.addColorTexture(capTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
float angle = pass * com.jme3.math.FastMath.HALF_PI;
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB); captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB, angle);
captureReady = false; captureReady = false;
pendingRequest = req;
pendingTreeNode = treeNode;
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
} }
private void finishCapture() { private void finishCapture() {
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
app.getRenderer().readFrameBuffer(captureFB, pixels); app.getRenderer().readFrameBuffer(captureFB, pixels);
capturePixels[capturePass] = pixels;
SharedInput.EzTreeGenRequest req = pendingRequest;
Node treeNode = pendingTreeNode;
cleanupCapture(); 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 subPath = resolveSubPath(req.presetName());
String namePart = req.presetName() != null String namePart = req.presetName() != null
? req.presetName().toLowerCase().replace(" ", "_") ? req.presetName().toLowerCase().replace(" ", "_")
: subPath; : subPath;
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
String exportName = namePart + "_" + timestamp; String exportName = namePart + "_" + timestamp;
saveImpostor(pixels, "ez_impostor_" + exportName);
exportTree(treeNode, exportName, subPath);
pendingRequest = null; ByteBuffer atlas = combineAtlas(savedPixels);
pendingTreeNode = null; 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 ─────────────────────────────────────────────────────── // ── Material-Aufbau ───────────────────────────────────────────────────────
@@ -440,15 +497,22 @@ public class EzTreeState extends BaseAppState {
// ── Offscreen-Viewport für Impostor ─────────────────────────────────────── // ── Offscreen-Viewport für Impostor ───────────────────────────────────────
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb) { private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb, float angle) {
Vector3f center = bb.getCenter().add(0f, 2f, 0f); Vector3f center = bb.getCenter();
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); float yExt = bb.getYExtent();
float dist = extent * 3f; 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); 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.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() ViewPort vp = app.getRenderManager()
.createPostView("ezCapture_" + System.nanoTime(), cam); .createPostView("ezCapture_" + System.nanoTime(), cam);
@@ -524,34 +588,123 @@ public class EzTreeState extends BaseAppState {
captureReady = false; captureReady = false;
} }
private void saveImpostor(ByteBuffer pixels, String name) { private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
try { try {
pixels.rewind(); pixels.rewind();
BufferedImage img = new BufferedImage( BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB); for (int y = 0; y < height; y++) {
for (int y = 0; y < IMPOSTOR_SIZE; y++) { for (int x = 0; x < width; x++) {
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
int r = pixels.get() & 0xFF, g = pixels.get() & 0xFF, int r = pixels.get() & 0xFF, g = pixels.get() & 0xFF,
b = pixels.get() & 0xFF, a = 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"); Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
Files.createDirectories(texDir); 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) { } catch (IOException e) {
log.error("[EzTree] Impostor-Fehler: {}", e.getMessage()); 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 { try {
treeNode.setLocalScale(0.33f);
treeNode.updateGeometricState();
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath); Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
Files.createDirectories(baseDir); Files.createDirectories(baseDir);
File out = baseDir.resolve(fileName + ".j3o").toFile(); 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()); log.info("[EZ-Tree] Gespeichert: {}", out.getAbsolutePath());
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + subPath + "/" + fileName + ".j3o"; input.treeGenStatusMsg = "Gespeichert: Models/trees/" + subPath + "/" + fileName + ".j3o";
input.refreshAssets = true; 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) { private static String resolveSubPath(String presetName) {
if (presetName == null) return "unknown"; if (presetName == null) return "unknown";
String lo = presetName.toLowerCase(); 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

@@ -47,8 +47,12 @@ public class GrassVertexState extends BaseAppState {
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 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); 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_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 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 ─────────────────────────────────────────────────────────────── // ── Zustand ───────────────────────────────────────────────────────────────
private final SharedInput input; private final SharedInput input;
@@ -156,11 +160,42 @@ public class GrassVertexState extends BaseAppState {
private final Random rng = new Random(); private final Random rng = new Random();
/** 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) { private void addBlades(Vector3f center) {
float radius = (float) input.grassVertexTool.brushRadius.getValue(); float radius = (float) input.grassVertexTool.brushRadius.getValue();
float height = (float) input.grassVertexTool.bladeHeight.getValue(); float height = (float) input.grassVertexTool.bladeHeight.getValue();
int density = (int) input.grassVertexTool.density.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++) { for (int i = 0; i < density; i++) {
float angle = rng.nextFloat() * (float) (Math.PI * 2); float angle = rng.nextFloat() * (float) (Math.PI * 2);
float r = rng.nextFloat() * radius; float r = rng.nextFloat() * radius;
@@ -168,8 +203,10 @@ public class GrassVertexState extends BaseAppState {
float bz = center.z + (float) Math.sin(angle) * r; float bz = center.z + (float) Math.sin(angle) * r;
float by = terrain.getHeight(new Vector2f(bx, bz)); float by = terrain.getHeight(new Vector2f(bx, bz));
if (Float.isNaN(by)) continue; if (Float.isNaN(by)) continue;
float h = height * (0.75f + rng.nextFloat() * 0.5f); float distRatio = r / radius;
float witherPct = (float) input.grassVertexTool.dryness.getValue() / 100f; float h = height * brushFalloff(distRatio)
* (1f + variation * (rng.nextFloat() * 2f - 1f));
h = Math.max(0.05f, h);
float bladeDryness = rng.nextFloat() < witherPct float bladeDryness = rng.nextFloat() < witherPct
? 0.5f + rng.nextFloat() * 0.5f : 0f; ? 0.5f + rng.nextFloat() * 0.5f : 0f;
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness); 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 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 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; float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
col[ci] = gr + (dr - gr) * dryness; float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
col[ci+1] = gg + (dg - gg) * dryness; float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
col[ci+2] = gb + (db - gb) * dryness; float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
col[ci+3] = 1f; // 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; int ti = vi * 2;
tex[ti] = wf; tex[ti+1] = 0f; 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

@@ -51,6 +51,11 @@ public class ModelEditorState extends BaseAppState {
private final SharedInput input; 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) // Mittelpunkt für Orbit (Bounding-Box-Zentrum des Modells)
private Vector3f orbitCenter = Vector3f.ZERO.clone(); private Vector3f orbitCenter = Vector3f.ZERO.clone();
@@ -91,9 +96,34 @@ public class ModelEditorState extends BaseAppState {
String openPath = input.modelEditorOpenPath; String openPath = input.modelEditorOpenPath;
if (openPath != null) { if (openPath != null) {
input.modelEditorOpenPath = null; input.modelEditorOpenPath = null;
mainModelPath = openPath;
input.modelEditorLodPreview = 0;
loadModel(openPath); 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; if (previewRoot == null) return;
// Schließen // Schließen
@@ -152,12 +182,31 @@ public class ModelEditorState extends BaseAppState {
currentPath = assetPath; currentPath = assetPath;
modelWrapper = new Node("model_wrapper"); modelWrapper = new Node("model_wrapper");
hasEmbeddedLods = false;
embeddedLodSpatials = null;
input.modelEditorHasEmbeddedLods = false;
try { try {
Spatial model = app.getAssetManager().loadModel(assetPath); Spatial model = app.getAssetManager().loadModel(assetPath);
stripControls(model); stripControls(model);
modelWrapper.attachChild(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) { } 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)); 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"); Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Red); mat.setColor("Color", ColorRGBA.Red);
@@ -243,6 +292,9 @@ public class ModelEditorState extends BaseAppState {
modelWrapper = null; modelWrapper = null;
gridGeo = null; gridGeo = null;
} }
hasEmbeddedLods = false;
embeddedLodSpatials = null;
input.modelEditorHasEmbeddedLods = false;
app.getRootNode().setCullHint(Spatial.CullHint.Inherit); app.getRootNode().setCullHint(Spatial.CullHint.Inherit);
if (savedCamPos != null) { if (savedCamPos != null) {

View File

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

View File

@@ -97,6 +97,7 @@ public class TerrainEditorState extends BaseAppState {
private PlacedObjectState placedObjectState; private PlacedObjectState placedObjectState;
private GrassVertexState grassVertexState; private GrassVertexState grassVertexState;
private SceneObjectState sceneObjState; private SceneObjectState sceneObjState;
private ItemPlacementState itemPlacementState;
private LightState lightState; private LightState lightState;
private EmitterState emitterState; private EmitterState emitterState;
private WaterBodyState waterBodyState; private WaterBodyState waterBodyState;
@@ -231,6 +232,14 @@ public class TerrainEditorState extends BaseAppState {
sceneObjState = app.getStateManager().getState(SceneObjectState.class); sceneObjState = app.getStateManager().getState(SceneObjectState.class);
if (sceneObjState != null) { if (sceneObjState != null) {
sceneObjState.setTerrain(terrain); sceneObjState.setTerrain(terrain);
}
itemPlacementState = app.getStateManager().getState(ItemPlacementState.class);
if (itemPlacementState != null) {
itemPlacementState.setTerrain(terrain);
}
if (sceneObjState != null) {
try { try {
var placed = PlacedModelIO.load(); var placed = PlacedModelIO.load();
if (!placed.isEmpty()) sceneObjState.loadPlacedModels(placed); 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); } catch (IOException e) { log.error("Vertex-Gras nicht speicherbar", e); }
} }
MapIO.save(data); 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 (models != null) PlacedModelIO.save(models);
if (lights != null) LightIO.save(lights); if (lights != null) LightIO.save(lights);
if (emitters != null) EmitterIO.save(emitters); if (emitters != null) EmitterIO.save(emitters);
@@ -1193,8 +1210,7 @@ public class TerrainEditorState extends BaseAppState {
private void updateCamera(float tpf) { private void updateCamera(float tpf) {
if (input.activeLayer == SharedInput.LAYER_MODEL_EDITOR) { if (input.activeLayer == SharedInput.LAYER_MODEL_EDITOR) {
input.consumeMouseDelta(); // konsumieren ohne zu verarbeiten return; // ModelEditorState konsumiert das Delta selbst
return;
} }
int[] delta = input.consumeMouseDelta(); int[] delta = input.consumeMouseDelta();
if (delta[0] != 0 || delta[1] != 0) { 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 Logger log = LoggerFactory.getLogger(TreeGeneratorState.class);
private static final int IMPOSTOR_SIZE = 512; 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 int PREVIEW_SIZE = 1024;
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
@@ -100,7 +103,9 @@ public class TreeGeneratorState extends BaseAppState {
private ViewPort captureVP = null; private ViewPort captureVP = null;
private FrameBuffer captureFB = null; private FrameBuffer captureFB = null;
private Texture2D captureTex = null; private Texture2D captureTex = null;
private volatile boolean captureReady = false; // vom SceneProcessor gesetzt private volatile boolean captureReady = false;
private int capturePass = 0;
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
public TreeGeneratorState(SharedInput input) { this.input = input; } public TreeGeneratorState(SharedInput input) { this.input = input; }
@@ -123,7 +128,7 @@ public class TreeGeneratorState extends BaseAppState {
previewVP = this.app.getRenderManager().createPostView("treePreview", previewCam); previewVP = this.app.getRenderManager().createPostView("treePreview", previewCam);
previewVP.setOutputFrameBuffer(previewFB); 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); previewVP.setClearFlags(true, true, true);
previewScene = new Node("previewScene"); 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))); previewScene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.60f, 1f)));
previewTreeHolder = new Node("treeHolder"); previewTreeHolder = new Node("treeHolder");
previewScene.attachChild(previewTreeHolder); previewScene.attachChild(previewTreeHolder);
previewScene.attachChild(buildPreviewGround()); previewScene.attachChild(buildPreviewGrid());
previewScene.attachChild(buildPreviewSky());
previewVP.attachScene(previewScene); previewVP.attachScene(previewScene);
DirectionalLightShadowRenderer shadowRenderer = DirectionalLightShadowRenderer shadowRenderer =
@@ -148,6 +152,19 @@ public class TreeGeneratorState extends BaseAppState {
shadowRenderer.setShadowIntensity(0.25f); shadowRenderer.setShadowIntensity(0.25f);
shadowRenderer.setShadowZExtend(80f); shadowRenderer.setShadowZExtend(80f);
previewVP.addProcessor(shadowRenderer); 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); previewTransfer = new FrameTransfer(input.treePreviewImage);
previewVP.addProcessor(previewTransfer); previewVP.addProcessor(previewTransfer);
} }
@@ -193,8 +210,8 @@ public class TreeGeneratorState extends BaseAppState {
resizePreviewViewport(reqW, reqH); resizePreviewViewport(reqW, reqH);
} }
// 3. Kamera-Orbit + updateGeometricState immer zuletzt // 3. Kamera-Orbit updateGeometricState wird jetzt per preFrame-SceneProcessor
// nach allen Szenenänderungen dieser Frame // direkt vor dem Rendern des previewVP aufgerufen (nach allen State-Updates).
if (previewVP != null) { if (previewVP != null) {
float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD; float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD;
float rotX = input.treePreviewRotX * 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, previewTarget.y + FastMath.sin(rotX) * dist,
FastMath.cos(rotY) * cosX * dist)); FastMath.cos(rotY) * cosX * dist));
c.lookAt(previewTarget, Vector3f.UNIT_Y); 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 hdNode = makeTreeNode(hd, barkMat, leafMat, "hd");
Node ldNode = makeTreeNode(ld, barkMat.clone(), leafMat.clone(), "ld"); 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; pendingRequest = req;
pendingHdNode = hdNode; pendingHdNode = hdNode;
pendingLdNode = ldNode; pendingLdNode = ldNode;
pendingHdResult = hd; pendingHdResult = hd;
pendingBarkMat = barkMat; pendingBarkMat = barkMat;
pendingLeafMat = leafMat; 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 ────────────────────────────────────────── // ── Phase 2: Capture abschließen ──────────────────────────────────────────
private void finishCapture() { private void finishCapture() {
// Pixel aus Framebuffer lesen
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
app.getRenderer().readFrameBuffer(captureFB, pixels); app.getRenderer().readFrameBuffer(captureFB, pixels);
capturePixels[capturePass] = pixels;
cleanupCapture(); 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 treeType = pendingRequest.treeType();
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); 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, Node previewTree = makeTreeNode(pendingHdResult,
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev"); pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
@@ -321,6 +342,7 @@ public class TreeGeneratorState extends BaseAppState {
pendingHdResult = null; pendingHdResult = null;
pendingBarkMat = null; pendingBarkMat = null;
pendingLeafMat = null; pendingLeafMat = null;
capturePixels = new ByteBuffer[ATLAS_DIRS];
} }
// ── LOD-Aufbau ──────────────────────────────────────────────────────────── // ── LOD-Aufbau ────────────────────────────────────────────────────────────
@@ -346,7 +368,7 @@ public class TreeGeneratorState extends BaseAppState {
float h = bb.getYExtent() * 2f; float h = bb.getYExtent() * 2f;
float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f; float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f;
float size = Math.max(h, w); 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"); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
if (tex != null) mat.setTexture("ColorMap", tex); if (tex != null) mat.setTexture("ColorMap", tex);
@@ -355,14 +377,18 @@ public class TreeGeneratorState extends BaseAppState {
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
Node n = new Node("lod2"); Node n = new Node("lod2");
n.attachChild(buildBillboardQuad("quad_a", 0f, yOff, size, mat)); for (int d = 0; d < ATLAS_DIRS; d++) {
n.attachChild(buildBillboardQuad("quad_b", FastMath.HALF_PI, yOff, size, mat.clone())); 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); n.setQueueBucket(RenderQueue.Bucket.Transparent);
return n; return n;
} }
private Geometry buildBillboardQuad(String name, float yRot, float yCent, 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 hw = size * 0.5f;
float hh = size * 0.5f; float hh = size * 0.5f;
float cos = FastMath.cos(yRot); float cos = FastMath.cos(yRot);
@@ -375,7 +401,9 @@ public class TreeGeneratorState extends BaseAppState {
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[]{ 0,0, 1,0, 1,1, 0,1 }); 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.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
mesh.updateBound(); mesh.updateBound();
@@ -384,6 +412,37 @@ public class TreeGeneratorState extends BaseAppState {
return g; 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 ──────────────────────────────────────────────────── // ── Material-Factories ────────────────────────────────────────────────────
private Material buildBarkMaterial(TreeParams p) { private Material buildBarkMaterial(TreeParams p) {
@@ -458,13 +517,15 @@ public class TreeGeneratorState extends BaseAppState {
// ── Offscreen-ViewPort ──────────────────────────────────────────────────── // ── 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); Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
Vector3f center = bb.getCenter().add(0f, 2f, 0f); Vector3f center = bb.getCenter().add(0f, 2f, 0f);
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
float dist = extent * 3.0f; 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.lookAt(center, Vector3f.UNIT_Y);
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f); cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
@@ -519,18 +580,17 @@ public class TreeGeneratorState extends BaseAppState {
// ── Impostor-PNG speichern ──────────────────────────────────────────────── // ── Impostor-PNG speichern ────────────────────────────────────────────────
private Texture2D saveImpostor(ByteBuffer pixels, String name) { private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
try { try {
pixels.rewind(); pixels.rewind();
BufferedImage img = new BufferedImage( BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB); for (int y = 0; y < height; y++) {
for (int y = 0; y < IMPOSTOR_SIZE; y++) { for (int x = 0; x < width; x++) {
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
int r = pixels.get() & 0xFF; int r = pixels.get() & 0xFF;
int g = pixels.get() & 0xFF; int g = pixels.get() & 0xFF;
int b = pixels.get() & 0xFF; int b = pixels.get() & 0xFF;
int a = 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"); 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"); return (Texture2D) assets.loadTexture("Textures/impostor/" + name + ".png");
} catch (Exception loadEx) { } catch (Exception loadEx) {
pixels.rewind(); 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); pixels, null, com.jme3.texture.image.ColorSpace.sRGB);
return new Texture2D(jmeImg); return new Texture2D(jmeImg);
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("[TreeGenerator] Impostor-Fehler: " + e.getMessage()); log.error("[Blight-Baum] Impostor-Fehler: {}", e.getMessage());
return null; return null;
} }
} }
@@ -615,45 +675,47 @@ public class TreeGeneratorState extends BaseAppState {
@Override protected void controlRender(RenderManager rm, ViewPort vp) {} @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() { private Geometry buildPreviewGrid() {
float size = 600f; float halfSize = 20f;
float tiles = 30f; // UV-Wiederholungen int n = (int)(halfSize * 2);
int lines = (n + 1) * 2;
// Eigenes Mesh mit gekachelten UVs (Quad unterstützt kein Tiling) java.nio.FloatBuffer pos = BufferUtils.createFloatBuffer(lines * 2 * 3);
com.jme3.scene.Mesh mesh = new com.jme3.scene.Mesh(); java.nio.FloatBuffer col = BufferUtils.createFloatBuffer(lines * 2 * 4);
float h = size * 0.5f;
mesh.setBuffer(VertexBuffer.Type.Position, 3, for (int i = 0; i <= n; i++) {
new float[]{ -h,0,-h, h,0,-h, h,0,h, -h,0,h }); float coord = -halfSize + i;
mesh.setBuffer(VertexBuffer.Type.Normal, 3, boolean major5 = (Math.abs(Math.round(coord)) % 5) == 0;
new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 }); boolean major10 = (Math.abs(Math.round(coord)) % 10) == 0;
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, float bright = major10 ? 0.70f : major5 ? 0.45f : 0.22f;
new float[]{ 0,0, tiles,0, tiles,tiles, 0,tiles });
mesh.setBuffer(VertexBuffer.Type.Index, 3, pos.put(coord).put(0).put(-halfSize);
new int[]{ 0,2,1, 0,3,2 }); 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(); 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"); Geometry geo = new Geometry("previewGrid", mesh);
mat.setBoolean("UseMaterialColors", true); geo.setMaterial(mat);
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.45f, 0.14f, 1f)); return geo;
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;
} }
// ── Skybox (Kuppel) ─────────────────────────────────────────────────────── // ── Skybox (Kuppel) ───────────────────────────────────────────────────────

View File

@@ -8,6 +8,8 @@ public class GrassVertexTool extends EditorTool {
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.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 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 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)"; } @Override public String getName() { return "Gras (Vertices)"; }
@@ -15,5 +17,5 @@ public class GrassVertexTool extends EditorTool {
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); } public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
@Override @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"/> <logger name="com.jme3" level="WARN"/>
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" bekanntes JME-Verhalten, kein Fehler --> <!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" bekanntes JME-Verhalten, kein Fehler -->
<logger name="com.jme3.scene.plugins.gltf" level="ERROR"/> <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"> <root level="INFO">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>

View File

@@ -12,7 +12,8 @@ public enum AnimationAction {
SPRINT, SPRINT,
JUMP, JUMP,
RUNNING_JUMP, RUNNING_JUMP,
DUCK; DUCK,
PICK_UP;
/** Lesbare Bezeichnung für UI-Anzeige. */ /** Lesbare Bezeichnung für UI-Anzeige. */
@@ -26,6 +27,7 @@ public enum AnimationAction {
case JUMP -> "Jump"; case JUMP -> "Jump";
case RUNNING_JUMP -> "Running Jump"; case RUNNING_JUMP -> "Running Jump";
case DUCK -> "Duck"; case DUCK -> "Duck";
case PICK_UP -> "Pick up";
}; };
} }
} }

View File

@@ -12,6 +12,7 @@ public class KeyBindings {
public int jump = KeyInput.KEY_SPACE; public int jump = KeyInput.KEY_SPACE;
public int sprint = KeyInput.KEY_LSHIFT; public int sprint = KeyInput.KEY_LSHIFT;
public int walk = KeyInput.KEY_LMENU; public int walk = KeyInput.KEY_LMENU;
public int interact = KeyInput.KEY_E;
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */ /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
public static final String[][] ENTRIES = { public static final String[][] ENTRIES = {
@@ -22,6 +23,7 @@ public class KeyBindings {
{"jump", "Springen"}, {"jump", "Springen"},
{"sprint", "Rennen"}, {"sprint", "Rennen"},
{"walk", "Gehen"}, {"walk", "Gehen"},
{"interact", "Interagieren"},
}; };
public int get(String fieldName) { public int get(String fieldName) {

View File

@@ -47,6 +47,9 @@ public class PlayerInputControl {
private String runningClip; private String runningClip;
/** Frames, für die JUMP erzwungen wird (überbrückt onGround()-Lag). */ /** Frames, für die JUMP erzwungen wird (überbrückt onGround()-Lag). */
private int jumpFrames = 0; private int jumpFrames = 0;
/** Läuft gerade eine Pickup-Animation? */
private boolean pickupActive = false;
private float pickupRemaining = 0f;
private final ActionListener actionListener = (name, isPressed, tpf) -> { private final ActionListener actionListener = (name, isPressed, tpf) -> {
if (paused) return; if (paused) return;
@@ -117,9 +120,33 @@ public class PlayerInputControl {
inputManager.addListener(actionListener, ACTION_NAMES); inputManager.addListener(actionListener, ACTION_NAMES);
} }
/**
* Spielt die PICK_UP-Animation einmalig ab und blockiert Bewegung für {@code duration} Sekunden.
* Wird von WorldItemsState aufgerufen, sobald der Spieler ein Item aufnimmt.
*/
public void requestPickup(float duration) {
pickupActive = true;
pickupRemaining = duration;
forward = backward = left = right = false;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
playAction(AnimationAction.PICK_UP);
currentAnim = AnimationAction.PICK_UP;
}
public void update(float tpf) { public void update(float tpf) {
if (physicsChar == null || paused) return; if (physicsChar == null || paused) return;
if (pickupActive) {
pickupRemaining -= tpf;
physicsChar.setWalkDirection(Vector3f.ZERO);
if (pickupRemaining <= 0f) {
pickupActive = false;
currentAnim = null; // erzwingt Neubewertung im nächsten Frame
} else {
return;
}
}
Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal(); Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal(); Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();

View File

@@ -6,7 +6,6 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.bullet.BulletAppState; import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.HeightfieldCollisionShape;
import com.jme3.bullet.control.CharacterControl; import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.bullet.util.CollisionShapeFactory;
@@ -41,8 +40,11 @@ import de.blight.game.state.GrassState;
import de.blight.game.state.GrassVertexRenderState; import de.blight.game.state.GrassVertexRenderState;
import de.blight.game.state.LocationState; import de.blight.game.state.LocationState;
import de.blight.game.state.RiverState; import de.blight.game.state.RiverState;
import de.blight.game.state.TerrainChunkState;
import de.blight.game.state.WaterBodyState; import de.blight.game.state.WaterBodyState;
import de.blight.game.state.WeatherState; import de.blight.game.state.WeatherState;
import de.blight.game.state.InteractionHudState;
import de.blight.game.state.WorldItemsState;
import de.blight.game.state.WorldObjectsState; import de.blight.game.state.WorldObjectsState;
import java.io.IOException; import java.io.IOException;
@@ -58,6 +60,7 @@ public class WorldScene extends BaseAppState {
private BulletAppState bulletAppState; private BulletAppState bulletAppState;
private MapData loadedMapData; private MapData loadedMapData;
private FilterPostProcessor sharedFPP; private FilterPostProcessor sharedFPP;
private TerrainChunkState terrainChunkState;
private final KeyBindings keyBindings; private final KeyBindings keyBindings;
private ThirdPersonCamera thirdPersonCam; private ThirdPersonCamera thirdPersonCam;
@@ -109,11 +112,12 @@ public class WorldScene extends BaseAppState {
buildLighting(); buildLighting();
BlightGame.status("Lade Terrain..."); BlightGame.status("Lade Terrain...");
TerrainQuad terrain = buildTerrain(); buildChunkTerrain();
BlightGame.status("Lade Vegetation..."); BlightGame.status("Lade Vegetation...");
app.getStateManager().attach(new GrassState(terrain)); app.getStateManager().attach(new GrassState(terrainChunkState));
app.getStateManager().attach(new GrassVertexRenderState()); GrassVertexRenderState gvrs = new GrassVertexRenderState(terrainChunkState);
app.getStateManager().attach(gvrs);
BlightGame.status("Lade Wasserflächen..."); BlightGame.status("Lade Wasserflächen...");
app.getStateManager().attach(new WaterBodyState(sharedFPP)); app.getStateManager().attach(new WaterBodyState(sharedFPP));
@@ -147,6 +151,9 @@ public class WorldScene extends BaseAppState {
MainCharacter mc = findMainCharacter(); MainCharacter mc = findMainCharacter();
if (mc != null) { if (mc != null) {
app.getStateManager().attach(new LocationState(mc, character)); app.getStateManager().attach(new LocationState(mc, character));
app.getStateManager().attach(
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
app.getStateManager().attach(new InteractionHudState());
} }
// Maus einfangen keine Klick-Pflicht für Kamerasteuerung // Maus einfangen keine Klick-Pflicht für Kamerasteuerung
@@ -322,117 +329,41 @@ public class WorldScene extends BaseAppState {
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Terrain // Terrain (Chunk-basiert)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
/** /**
* Baut das Terrain. Falls eine gespeicherte Karte vorhanden ist, wird diese * Erstellt den chunk-basierten TerrainChunkState.
* geladen; andernfalls wird ein prozedurales Demo-Terrain erzeugt. * Lädt MapData falls vorhanden, berechnet Spawn-Y aus Chunk-Höhen,
* Setzt außerdem {@link #spawnY}. * erstellt ein gemeinsames Terrain-Material und hängt den State ein.
*/ */
private TerrainQuad buildTerrain() { private void buildChunkTerrain() {
if (MapIO.exists()) { if (MapIO.exists()) {
try { try {
loadedMapData = MapIO.load(); loadedMapData = MapIO.load();
return buildTerrainFromMap(loadedMapData);
} catch (IOException e) { } catch (IOException e) {
System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage() System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage());
+ " — Fallback auf prozedurales Terrain.");
}
}
return buildProceduralTerrain();
}
/**
* Erstellt ein Terrain aus der gespeicherten {@link MapData}.
* Visuell: 4097×4097 Vertices (jeder 4. Editor-Vertex), Scale (1, 1, 1) = 1 m/Vertex.
* Physik: 513×513 Vertices (jeder 32. Editor-Vertex), Scale (8, 1, 8) unsichtbar,
* nur für Bullet-Kollision (33,5M Dreiecke bei 4097² wären zu viel für Bullet).
*/
private TerrainQuad buildTerrainFromMap(MapData map) {
final int SRC_VERTS = MapData.TERRAIN_VERTS; // 16385
// ── Visuelles Terrain (4097 Vertices, 1 m/Vertex) ────────────────────
final int GAME_VERTS = 4097;
final int STEP = (SRC_VERTS - 1) / (GAME_VERTS - 1); // 4
float[] heights = new float[GAME_VERTS * GAME_VERTS];
for (int gz = 0; gz < GAME_VERTS; gz++) {
int sz = gz * STEP;
for (int gx = 0; gx < GAME_VERTS; gx++) {
heights[gz * GAME_VERTS + gx] = map.terrainHeight[sz * SRC_VERTS + gx * STEP];
} }
} }
// Temp-Spawn aus Editor-Property überschreibt gespeicherten Karten-Spawn // Spawn aus Map oder Editor-Property
String propX = System.getProperty("blight.temp.spawn.x"); String propX = System.getProperty("blight.temp.spawn.x");
String propZ = System.getProperty("blight.temp.spawn.z"); String propZ = System.getProperty("blight.temp.spawn.z");
spawnX = propX != null ? Float.parseFloat(propX) : map.spawnX; if (loadedMapData != null) {
spawnZ = propZ != null ? Float.parseFloat(propZ) : map.spawnZ; spawnX = propX != null ? Float.parseFloat(propX) : loadedMapData.spawnX;
System.out.println("[WorldScene] SpawnXZ Quelle: " + (propX != null ? "Editor-Property" : "Karte") spawnZ = propZ != null ? Float.parseFloat(propZ) : loadedMapData.spawnZ;
+ " → X=" + spawnX + " Z=" + spawnZ);
TerrainQuad terrain = new TerrainQuad("terrain", 65, GAME_VERTS, heights);
terrain.setLocalScale(1f, 1f, 1f);
terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
applyTerrainMaterial(terrain, map);
rootNode.attachChild(terrain);
// Terrain-Höhe am Spawnpunkt (scale=1 → lokale Koordinaten = Weltkoordinaten)
float terrainH = terrain.getHeight(new Vector2f(spawnX, spawnZ));
if (Float.isNaN(terrainH)) {
float maxH = -Float.MAX_VALUE;
for (float h : heights) { if (h > maxH) maxH = h; }
terrainH = maxH;
} }
System.out.println("[WorldScene] SpawnXZ: X=" + spawnX + " Z=" + spawnZ);
Material mat = buildTerrainMaterial(loadedMapData);
terrainChunkState = new TerrainChunkState(bulletAppState, mat, loadedMapData);
app.getStateManager().attach(terrainChunkState);
// Spawn-Höhe aus Chunk-Daten
float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ);
spawnY = terrainH + 10f; spawnY = terrainH + 10f;
System.out.println("[WorldScene] SpawnXYZ=(" + spawnX + ", " + spawnY + ", " + spawnZ + ")");
// ── Physik-Terrain: HeightfieldCollisionShape mit identischem heights[]-Array ─
// Gleiche 4097×4097-Daten wie das visuelle Terrain → pixel-genaue Übereinstimmung.
// HeightfieldCollisionShape ist für Terrain optimiert (kein BVH, O(log n) Queries)
// und braucht keine separate TerrainQuad-Instanz.
RigidBodyControl terrainPhysics = new RigidBodyControl(
new HeightfieldCollisionShape(heights, terrain.getLocalScale()), 0f);
terrain.addControl(terrainPhysics);
bulletAppState.getPhysicsSpace().add(terrainPhysics);
System.out.println("[WorldScene] Karte geladen, SpawnXYZ=("
+ spawnX + ", " + spawnY + ", " + spawnZ + ")");
return terrain;
}
/** Prozedurales Demo-Terrain als Fallback (keine gespeicherte Karte). */
private TerrainQuad buildProceduralTerrain() {
int size = 257;
float[] heights = new float[size * size];
for (int z = 0; z < size; z++) {
for (int x = 0; x < size; x++) {
float nx = x / (float) size;
float nz = z / (float) size;
heights[z * size + x] =
FastMath.sin(nx * FastMath.TWO_PI * 2) * 2f
+ FastMath.sin(nz * FastMath.TWO_PI * 3) * 1.5f
+ FastMath.sin((nx + nz) * FastMath.TWO_PI * 1.5f) * 1f;
}
}
spawnY = 5f;
TerrainQuad terrain = new TerrainQuad("terrain", 65, size, heights);
terrain.setLocalTranslation(0, -5f, 0);
terrain.setLocalScale(0.5f, 0.5f, 0.5f);
terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
applyTerrainMaterial(terrain, null);
rootNode.attachChild(terrain);
RigidBodyControl terrainPhysics = new RigidBodyControl(
CollisionShapeFactory.createMeshShape(terrain), 0f);
terrain.addControl(terrainPhysics);
bulletAppState.getPhysicsSpace().add(terrainPhysics);
return terrain;
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -588,7 +519,7 @@ public class WorldScene extends BaseAppState {
new ColorRGBA(0.80f, 0.72f, 0.50f, 1f), new ColorRGBA(0.80f, 0.72f, 0.50f, 1f),
}; };
private void applyTerrainMaterial(TerrainQuad terrain, MapData map) { private Material buildTerrainMaterial(MapData map) {
if (map != null) { if (map != null) {
try { try {
Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md"); Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
@@ -630,9 +561,7 @@ public class WorldScene extends BaseAppState {
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
splatTex.setMagFilter(Texture.MagFilter.Bilinear); splatTex.setMagFilter(Texture.MagFilter.Bilinear);
mat.setTexture("AlphaMap", splatTex); mat.setTexture("AlphaMap", splatTex);
return mat;
terrain.setMaterial(mat);
return;
} catch (Exception e) { } catch (Exception e) {
System.err.println("[WorldScene] Splat-Material fehlgeschlagen: " + e.getMessage()); System.err.println("[WorldScene] Splat-Material fehlgeschlagen: " + e.getMessage());
} }
@@ -646,13 +575,13 @@ public class WorldScene extends BaseAppState {
mat.setTexture("DiffuseMap", grass); mat.setTexture("DiffuseMap", grass);
mat.setFloat("DiffuseMap_0_scale", 32f); mat.setFloat("DiffuseMap_0_scale", 32f);
mat.setBoolean("useTriPlanarMapping", false); mat.setBoolean("useTriPlanarMapping", false);
terrain.setMaterial(mat); return mat;
} catch (Exception e) { } catch (Exception e) {
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
mat.setBoolean("UseMaterialColors", true); mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f)); mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
mat.setColor("Ambient", new ColorRGBA(0.15f, 0.30f, 0.09f, 1f)); mat.setColor("Ambient", new ColorRGBA(0.15f, 0.30f, 0.09f, 1f));
terrain.setMaterial(mat); return mat;
} }
} }

View File

@@ -16,7 +16,6 @@ import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.GrassTuft; import de.blight.common.GrassTuft;
import de.blight.common.GrassTuftIO; import de.blight.common.GrassTuftIO;
@@ -44,7 +43,7 @@ public class GrassState extends BaseAppState {
private static final float FAR_DIST_SQ = FAR_DIST * FAR_DIST; private static final float FAR_DIST_SQ = FAR_DIST * FAR_DIST;
private static final int INIT_PER_FRAME = 4; private static final int INIT_PER_FRAME = 4;
private final TerrainQuad terrain; private final TerrainChunkState terrainChunkState;
private Camera cam; private Camera cam;
private Node grassNode; private Node grassNode;
@@ -54,8 +53,8 @@ public class GrassState extends BaseAppState {
private final Map<Integer, Material> slotMaterials = new LinkedHashMap<>(); private final Map<Integer, Material> slotMaterials = new LinkedHashMap<>();
private int nextChunk = 0; private int nextChunk = 0;
public GrassState(TerrainQuad terrain) { public GrassState(TerrainChunkState terrainChunkState) {
this.terrain = terrain; this.terrainChunkState = terrainChunkState;
} }
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -176,7 +175,7 @@ public class GrassState extends BaseAppState {
for (int b = 0; b < BLADES_PER_TUFT; b++) { for (int b = 0; b < BLADES_PER_TUFT; b++) {
float bx = t.x() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f; float bx = t.x() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f;
float bz = t.z() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f; float bz = t.z() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f;
float th = terrain.getHeight(new Vector2f(bx, bz)); float th = terrainChunkState.getHeightAt(bx, bz);
if (Float.isNaN(th)) continue; if (Float.isNaN(th)) continue;
float h = t.height() * (0.7f + rng.nextFloat() * 0.6f); float h = t.height() * (0.7f + rng.nextFloat() * 0.6f);
blades.add(new float[]{bx, th, bz, h}); blades.add(new float[]{bx, th, bz, h});

View File

@@ -8,15 +8,11 @@ import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh; import com.jme3.scene.Mesh;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexBlade;
import de.blight.common.GrassVertexIO; import de.blight.common.GrassVertexIO;
@@ -27,8 +23,10 @@ import java.util.List;
/** /**
* Rendert Vertex-Gras-Büschel im Spiel: 3 geneigte, verjüngte Halme pro Büschel, * Rendert Vertex-Gras-Büschel im Spiel: 3 geneigte, verjüngte Halme pro Büschel,
* beleuchtet über NormalBuffer + Custom-Shader, chunk-basiertes Lazy-Loading. * beleuchtet über NormalBuffer + Custom-Shader, chunk-basiertes Lazy-Loading.
* Implementiert {@link TerrainChunkState.ChunkListener}: Gras wird ab LOD 1 ausgeblendet.
*/ */
public class GrassVertexRenderState extends BaseAppState { public class GrassVertexRenderState extends BaseAppState
implements TerrainChunkState.ChunkListener {
// ── Chunks ──────────────────────────────────────────────────────────────── // ── Chunks ────────────────────────────────────────────────────────────────
private static final int TERRAIN_HALF = 2048; private static final int TERRAIN_HALF = 2048;
@@ -36,7 +34,6 @@ public class GrassVertexRenderState extends BaseAppState {
private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE; private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE;
private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS; private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS;
private static final int INIT_PER_FRAME = 4; private static final int INIT_PER_FRAME = 4;
private static final float FAR_DIST_SQ = 200f * 200f;
// ── Geometrie (identisch zu GrassVertexState) ───────────────────────────── // ── Geometrie (identisch zu GrassVertexState) ─────────────────────────────
private static final int BLADES_PER_TUFT = 3; private static final int BLADES_PER_TUFT = 3;
@@ -45,11 +42,15 @@ public class GrassVertexRenderState extends BaseAppState {
private static final float BEND_FACTOR = 0.15f; private static final float BEND_FACTOR = 0.15f;
private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f); private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f); private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
// 050 % Trockenheit: Grün → Goldgelb
private static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f); private static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f);
private static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f); private static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
// 50100 % Trockenheit: Goldgelb → Dunkelbraun
private static final ColorRGBA VERY_DRY_ROOT_COLOR = new ColorRGBA(0.18f, 0.09f, 0.02f, 1f);
private static final ColorRGBA VERY_DRY_TIP_COLOR = new ColorRGBA(0.38f, 0.20f, 0.05f, 1f);
// ── Zustand ─────────────────────────────────────────────────────────────── // ── Zustand ───────────────────────────────────────────────────────────────
private Camera cam; private final TerrainChunkState terrainChunkState;
private AssetManager assetManager; private AssetManager assetManager;
private Node grassNode; private Node grassNode;
private Material material; private Material material;
@@ -59,12 +60,16 @@ public class GrassVertexRenderState extends BaseAppState {
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT]; private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
private final Node[] chunkNodes = new Node[CHUNK_COUNT]; private final Node[] chunkNodes = new Node[CHUNK_COUNT];
public GrassVertexRenderState(TerrainChunkState terrainChunkState) {
this.terrainChunkState = terrainChunkState;
}
@Override @Override
protected void initialize(Application app) { protected void initialize(Application app) {
this.cam = app.getCamera();
this.assetManager = app.getAssetManager(); this.assetManager = app.getAssetManager();
grassNode = new Node("grassVertexNode"); grassNode = new Node("grassVertexNode");
((SimpleApplication) app).getRootNode().attachChild(grassNode); ((SimpleApplication) app).getRootNode().attachChild(grassNode);
terrainChunkState.addChunkListener(this);
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>(); for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
@@ -82,6 +87,7 @@ public class GrassVertexRenderState extends BaseAppState {
@Override @Override
protected void cleanup(Application app) { protected void cleanup(Application app) {
terrainChunkState.removeChunkListener(this);
((SimpleApplication) app).getRootNode().detachChild(grassNode); ((SimpleApplication) app).getRootNode().detachChild(grassNode);
} }
@@ -158,18 +164,35 @@ public class GrassVertexRenderState extends BaseAppState {
Geometry geo = new Geometry("gv_" + ci, mesh); Geometry geo = new Geometry("gv_" + ci, mesh);
geo.setMaterial(material); geo.setMaterial(material);
int cx = ci % CHUNKS_PER_AXIS;
int cz = ci / CHUNKS_PER_AXIS;
float chunkCX = -TERRAIN_HALF + cx * CHUNK_SIZE + CHUNK_SIZE * 0.5f;
float chunkCZ = -TERRAIN_HALF + cz * CHUNK_SIZE + CHUNK_SIZE * 0.5f;
Node node = new Node("gvc_" + ci); Node node = new Node("gvc_" + ci);
node.attachChild(geo); node.attachChild(geo);
node.addControl(new VisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ)));
chunkNodes[ci] = node; chunkNodes[ci] = node;
grassNode.attachChild(node); grassNode.attachChild(node);
} }
// ── ChunkListener: Gras ab LOD 1 ausblenden ───────────────────────────────
@Override
public void onChunkVisible(int cx, int cz, int lod) {
setChunkVisible(cx, cz, lod == 0);
}
@Override
public void onChunkHidden(int cx, int cz) {
setChunkVisible(cx, cz, false);
}
@Override
public void onChunkLodChanged(int cx, int cz, int oldLod, int newLod) {
setChunkVisible(cx, cz, newLod == 0);
}
private void setChunkVisible(int cx, int cz, boolean visible) {
int ci = cz * CHUNKS_PER_AXIS + cx;
if (ci < 0 || ci >= CHUNK_COUNT || chunkNodes[ci] == null) return;
chunkNodes[ci].setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
// ── Mesh-Logik (gespiegelt zu GrassVertexState) ─────────────────────────── // ── Mesh-Logik (gespiegelt zu GrassVertexState) ───────────────────────────
private static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx, private static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx,
@@ -272,34 +295,26 @@ public class GrassVertexRenderState extends BaseAppState {
float dr = DRY_ROOT_COLOR.r + (DRY_TIP_COLOR.r - DRY_ROOT_COLOR.r) * wf; 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 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; float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
col[ci] = gr + (dr - gr) * dryness; float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
col[ci+1] = gg + (dg - gg) * dryness; float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
col[ci+2] = gb + (db - gb) * dryness; float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
col[ci+3] = 1f; // 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; int ti = vi * 2;
tex[ti] = wf; tex[ti+1] = 0f; tex[ti] = wf; tex[ti+1] = 0f;
} }
// ── LOD-Culling ───────────────────────────────────────────────────────────
private static final class VisibilityControl extends AbstractControl {
private final Camera cam;
private final Vector3f center;
VisibilityControl(Camera cam, Vector3f center) {
this.cam = cam;
this.center = center;
}
@Override
protected void controlUpdate(float tpf) {
float dx = cam.getLocation().x - center.x;
float dz = cam.getLocation().z - center.z;
spatial.setCullHint(dx*dx + dz*dz <= FAR_DIST_SQ
? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
}
} }

View File

@@ -0,0 +1,135 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import de.blight.common.model.Item;
import de.blight.common.model.TextRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Zeigt oberhalb von nahegelegenen Item-Pickups den Namen an,
* wenn der Spieler darauf zielt.
*/
public class InteractionHudState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class);
private static final float SHOW_RANGE = 3.5f;
private static final float DOT_THRESH = 0.65f;
private static final float Y_OFFSET = 0.6f;
private Camera cam;
private Node guiNode;
private WorldItemsState worldItems;
private BitmapText labelText;
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
SimpleApplication sapp = (SimpleApplication) app;
this.cam = app.getCamera();
this.guiNode = sapp.getGuiNode();
BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
labelText = new BitmapText(font, false);
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f);
labelText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f));
labelText.setCullHint(Spatial.CullHint.Always);
guiNode.attachChild(labelText);
}
@Override
protected void onEnable() {
worldItems = getStateManager().getState(WorldItemsState.class);
if (worldItems == null)
log.warn("[InteractionHud] WorldItemsState nicht gefunden.");
}
@Override
protected void onDisable() {
labelText.setCullHint(Spatial.CullHint.Always);
}
@Override
protected void cleanup(Application app) {
guiNode.detachChild(labelText);
}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
if (worldItems == null) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
Node itemsRoot = worldItems.getItemsRoot();
if (itemsRoot == null || itemsRoot.getQuantity() == 0) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
Vector3f playerPos = worldItems.getPhysicsChar() != null
? worldItems.getPhysicsChar().getPhysicsLocation()
: cam.getLocation();
Vector3f camDir = cam.getDirection().normalizeLocal();
Spatial bestTarget = null;
float bestDot = -1f;
for (Spatial s : itemsRoot.getChildren()) {
Vector3f itemPos = s.getWorldTranslation();
float dx = itemPos.x - playerPos.x;
float dz = itemPos.z - playerPos.z;
float dist = (float) Math.sqrt(dx * dx + dz * dz);
if (dist > SHOW_RANGE) continue;
Vector3f toItem = itemPos.subtract(cam.getLocation()).normalizeLocal();
float dot = camDir.dot(toItem);
if (dot > DOT_THRESH && dot > bestDot) {
bestDot = dot;
bestTarget = s;
}
}
if (bestTarget == null) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
String label = resolveLabel(bestTarget);
labelText.setText(label);
Vector3f worldPos = bestTarget.getWorldTranslation().add(0f, Y_OFFSET, 0f);
Vector3f screenV3 = cam.getScreenCoordinates(worldPos);
Vector2f screen = new Vector2f(screenV3.x, screenV3.y);
float textW = labelText.getLineWidth();
labelText.setLocalTranslation(screen.x - textW * 0.5f, screen.y, 1f);
labelText.setCullHint(Spatial.CullHint.Inherit);
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private String resolveLabel(Spatial s) {
String itemId = s.getUserData("itemId");
if (itemId == null) return "?";
WorldItemsState state = worldItems;
// Resolve via TextRegistry if a full Item definition is available
// For now fall back to itemId
return itemId;
}
}

View File

@@ -0,0 +1,121 @@
package de.blight.game.state;
import com.jme3.asset.AssetManager;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
/**
* Wechselt zwischen Main-Model, LOD1, LOD2 und Ausblenden basierend auf Kameradistanz.
*
* Struktur des kontrollierten Node:
* [0] = Haupt-Spatial (LOD0)
* [1] = LOD1-Spatial (wird lazy geladen, wenn lod1Path gesetzt)
* [2] = LOD2-Spatial (wird lazy geladen, wenn lod2Path gesetzt)
*/
public class ModelLodControl extends AbstractControl {
private final AssetManager assets;
private final String lod1Path;
private final String lod2Path;
private final float lod1DistSq;
private final float lod2DistSq;
private final float cullDistSq;
private boolean lod1Loaded = false;
private boolean lod2Loaded = false;
private int currentSlot = 0; // 0=main, 1=lod1, 2=lod2, -1=culled
public ModelLodControl(AssetManager assets,
String lod1Path, String lod2Path,
float lod1Distance, float lod2Distance, float cullDistance) {
this.assets = assets;
this.lod1Path = (lod1Path != null && !lod1Path.isBlank()) ? lod1Path : null;
this.lod2Path = (lod2Path != null && !lod2Path.isBlank()) ? lod2Path : null;
this.lod1DistSq = lod1Distance * lod1Distance;
this.lod2DistSq = lod2Distance * lod2Distance;
this.cullDistSq = cullDistance * cullDistance;
}
@Override
protected void controlUpdate(float tpf) {
if (!(spatial instanceof Node node)) return;
Camera cam = null;
// walk up to get the app's camera via the scene
// We use the ViewPort supplied during render; cache camera via controlRender instead.
// Nothing to do here without camera ref — logic moved to controlRender.
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
if (!(spatial instanceof Node node)) return;
Camera cam = vp.getCamera();
float dx = cam.getLocation().x - spatial.getWorldTranslation().x;
float dy = cam.getLocation().y - spatial.getWorldTranslation().y;
float dz = cam.getLocation().z - spatial.getWorldTranslation().z;
float distSq = dx*dx + dy*dy + dz*dz;
int targetSlot;
if (distSq >= cullDistSq) {
targetSlot = -1;
} else if (lod2Path != null && distSq >= lod2DistSq) {
targetSlot = 2;
} else if (lod1Path != null && distSq >= lod1DistSq) {
targetSlot = 1;
} else {
targetSlot = 0;
}
if (targetSlot == currentSlot) return;
currentSlot = targetSlot;
// Ensure LOD spatials are loaded
if (targetSlot == 1 && !lod1Loaded) {
lod1Loaded = true;
try {
Spatial lod1 = assets.loadModel(lod1Path);
lod1.setName("lod1");
node.attachChildAt(lod1, 1);
} catch (Exception e) {
// LOD1 load failed — fall back to main
currentSlot = 0;
targetSlot = 0;
}
}
if (targetSlot == 2 && !lod2Loaded) {
lod2Loaded = true;
try {
Spatial lod2 = assets.loadModel(lod2Path);
lod2.setName("lod2");
// ensure index 2 exists
if (node.getChildren().size() < 2 && lod1Path != null && !lod1Loaded) {
// lod1 slot not yet loaded — add placeholder to maintain index
Node placeholder = new Node("lod1_placeholder");
node.attachChild(placeholder);
}
node.attachChild(lod2);
} catch (Exception e) {
currentSlot = lod1Path != null ? 1 : 0;
targetSlot = currentSlot;
}
}
// Apply visibility: cull hint on node itself for -1, else show correct child
if (targetSlot == -1) {
spatial.setCullHint(Spatial.CullHint.Always);
return;
}
spatial.setCullHint(Spatial.CullHint.Dynamic);
for (int i = 0; i < node.getChildren().size(); i++) {
Spatial child = node.getChildren().get(i);
child.setCullHint(i == targetSlot
? Spatial.CullHint.Dynamic
: Spatial.CullHint.Always);
}
}
}

View File

@@ -0,0 +1,381 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.HeightfieldCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import de.blight.common.ChunkTerrainIO;
import de.blight.common.MapData;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Verwaltet das chunk-basierte LOD-Terrain im Spiel.
*
* 1024 Chunks à 128×128 m, jeweils 3 LOD-Stufen (1/4/16 m pro Vertex).
* LOD richtet sich nach Chebyshev-Distanz zum Spieler-Chunk.
* Physik-Collider (HeightfieldCollisionShape, native 129²-Auflösung) werden
* nur für Chunks innerhalb PHYSICS_RANGE gehalten.
*
* Registrierte {@link ChunkListener} werden über LOD-Wechsel informiert
* (z. B. GrassVertexRenderState versteckt Gras ab LOD 1).
*/
public class TerrainChunkState extends BaseAppState {
// ── Listener-Interface ────────────────────────────────────────────────────
public interface ChunkListener {
void onChunkVisible(int cx, int cz, int lod);
void onChunkHidden(int cx, int cz);
void onChunkLodChanged(int cx, int cz, int oldLod, int newLod);
}
// ── Konstanten ────────────────────────────────────────────────────────────
private static final int N = ChunkTerrainIO.CHUNKS_PER_AXIS; // 32
private static final int TOTAL = ChunkTerrainIO.CHUNK_COUNT; // 1024
// ── Eingabe ───────────────────────────────────────────────────────────────
private final BulletAppState bulletAppState;
private final Material terrainMaterial;
private final MapData mapData;
// ── Laufzeitstatus ────────────────────────────────────────────────────────
private SimpleApplication app;
private Camera cam;
private Node terrainRoot;
private final float[][] chunkHeights = new float[TOTAL][];
private final int[] chunkLod = new int[TOTAL];
private final Node[] chunkNodes = new Node[TOTAL];
private final RigidBodyControl[] physics = new RigidBodyControl[TOTAL];
private final List<ChunkListener> listeners = new ArrayList<>();
private int lastPlayerCx = Integer.MIN_VALUE;
private int lastPlayerCz = Integer.MIN_VALUE;
// ── Konstruktor ───────────────────────────────────────────────────────────
public TerrainChunkState(BulletAppState bulletAppState, Material terrainMaterial, MapData mapData) {
this.bulletAppState = bulletAppState;
this.terrainMaterial = terrainMaterial;
this.mapData = mapData;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.cam = app.getCamera();
Arrays.fill(chunkLod, -1);
terrainRoot = new Node("terrainChunks");
this.app.getRootNode().attachChild(terrainRoot);
if (!ChunkTerrainIO.allChunksExist()) {
try {
if (mapData != null)
ChunkTerrainIO.exportFromMapData(mapData);
else
ChunkTerrainIO.exportBlankChunks();
System.out.println("[TerrainChunkState] Chunk-Dateien erzeugt.");
} catch (IOException e) {
System.err.println("[TerrainChunkState] Chunk-Export fehlgeschlagen: " + e);
}
}
for (int cz = 0; cz < N; cz++) {
for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
try {
chunkHeights[ci] = ChunkTerrainIO.loadChunk(cx, cz);
} catch (IOException e) {
chunkHeights[ci] = flatChunk();
System.err.println("[TerrainChunkState] Chunk " + cx + "," + cz + " nicht ladbar: " + e.getMessage());
}
}
}
System.out.println("[TerrainChunkState] " + TOTAL + " Chunks geladen.");
}
@Override
protected void cleanup(Application app) {
for (int ci = 0; ci < TOTAL; ci++) removePhysics(ci);
((SimpleApplication) app).getRootNode().detachChild(terrainRoot);
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
// ── Update: LOD + Physik ──────────────────────────────────────────────────
@Override
public void update(float tpf) {
Vector3f camPos = cam.getLocation();
int pcx = worldToChunk(camPos.x);
int pcz = worldToChunk(camPos.z);
if (pcx == lastPlayerCx && pcz == lastPlayerCz) return;
lastPlayerCx = pcx;
lastPlayerCz = pcz;
// Ziel-LOD für alle Chunks berechnen
int[] targetLod = new int[TOTAL];
boolean[] dirty = new boolean[TOTAL];
for (int cz = 0; cz < N; cz++) {
for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz);
targetLod[ci] = ChunkTerrainIO.lodForDistance(dist);
dirty[ci] = targetLod[ci] != chunkLod[ci];
}
}
// Schmutzige Chunks + ihre Nachbarn neu bauen (Seam-Korrektheit)
boolean[] rebuildSet = dirty.clone();
for (int cz = 0; cz < N; cz++) {
for (int cx = 0; cx < N; cx++) {
if (!dirty[ChunkTerrainIO.chunkIndex(cx, cz)]) continue;
if (cx > 0) rebuildSet[ChunkTerrainIO.chunkIndex(cx-1, cz)] = true;
if (cx < N-1) rebuildSet[ChunkTerrainIO.chunkIndex(cx+1, cz)] = true;
if (cz > 0) rebuildSet[ChunkTerrainIO.chunkIndex(cx, cz-1)] = true;
if (cz < N-1) rebuildSet[ChunkTerrainIO.chunkIndex(cx, cz+1)] = true;
}
}
for (int cz = 0; cz < N; cz++) {
for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
if (!rebuildSet[ci]) continue;
int oldLod = chunkLod[ci];
int newLod = targetLod[ci];
rebuildChunkMesh(cx, cz, newLod, targetLod);
// Physik: nur für nahe Chunks halten
int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz);
boolean wantsPhysics = dist <= ChunkTerrainIO.PHYSICS_RANGE;
if (wantsPhysics && physics[ci] == null) addPhysics(ci);
if (!wantsPhysics && physics[ci] != null) removePhysics(ci);
// Listener benachrichtigen
if (oldLod < 0) {
notifyVisible(cx, cz, newLod);
} else if (newLod != oldLod) {
notifyLodChanged(cx, cz, oldLod, newLod);
}
}
}
}
// ── Öffentliche API ───────────────────────────────────────────────────────
/** Bilinear interpolierte Terrain-Höhe an einer Welt-XZ-Position. */
public float getHeightAt(float worldX, float worldZ) {
int cx = Math.max(0, Math.min(N-1, worldToChunk(worldX)));
int cz = Math.max(0, Math.min(N-1, worldToChunk(worldZ)));
float[] h = chunkHeights[ChunkTerrainIO.chunkIndex(cx, cz)];
if (h == null) return 1f;
float originX = -2048f + cx * ChunkTerrainIO.CHUNK_SIZE;
float originZ = -2048f + cz * ChunkTerrainIO.CHUNK_SIZE;
float fx = Math.max(0, Math.min(ChunkTerrainIO.CHUNK_SIZE, worldX - originX));
float fz = Math.max(0, Math.min(ChunkTerrainIO.CHUNK_SIZE, worldZ - originZ));
int V = ChunkTerrainIO.CHUNK_VERTS; // 129
int col0 = Math.min((int) fx, V - 2);
int row0 = Math.min((int) fz, V - 2);
float tx = fx - col0;
float tz = fz - row0;
float h00 = h[row0 * V + col0];
float h10 = h[row0 * V + col0 + 1];
float h01 = h[(row0 + 1) * V + col0];
float h11 = h[(row0 + 1) * V + col0 + 1];
return h00 * (1-tx)*(1-tz) + h10 * tx*(1-tz) + h01 * (1-tx)*tz + h11 * tx*tz;
}
public void addChunkListener(ChunkListener l) { listeners.add(l); }
public void removeChunkListener(ChunkListener l) { listeners.remove(l); }
// ── Chunk-Mesh-Aufbau ─────────────────────────────────────────────────────
private void rebuildChunkMesh(int cx, int cz, int lod, int[] targetLod) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
// Knoten anlegen oder altes Mesh entfernen
if (chunkNodes[ci] == null) {
float nx = -2048f + cx * ChunkTerrainIO.CHUNK_SIZE + 64f;
float nz = -2048f + cz * ChunkTerrainIO.CHUNK_SIZE + 64f;
Node node = new Node("tn_" + cx + "_" + cz);
node.setLocalTranslation(nx, 0f, nz);
chunkNodes[ci] = node;
terrainRoot.attachChild(node);
} else {
chunkNodes[ci].detachAllChildren();
}
int verts = ChunkTerrainIO.LOD_VERTS[lod];
float spacing = ChunkTerrainIO.LOD_SPACING[lod];
float[] lodH = ChunkTerrainIO.downsample(chunkHeights[ci], ChunkTerrainIO.CHUNK_VERTS, verts);
applySeams(cx, cz, lod, lodH, verts, targetLod);
Mesh mesh = buildMesh(cx, cz, lodH, verts, spacing);
Geometry geom = new Geometry("tc_" + cx + "_" + cz, mesh);
geom.setMaterial(terrainMaterial);
geom.setShadowMode(RenderQueue.ShadowMode.Receive);
chunkNodes[ci].attachChild(geom);
chunkLod[ci] = lod;
}
private void applySeams(int cx, int cz, int lod, float[] lodH, int verts, int[] targetLod) {
int[] dxArr = { 0, 0, 1, -1 };
int[] dzArr = { 1, -1, 0, 0 };
int[] thisEdges = { ChunkTerrainIO.EDGE_NORTH, ChunkTerrainIO.EDGE_SOUTH,
ChunkTerrainIO.EDGE_EAST, ChunkTerrainIO.EDGE_WEST };
int[] neighborEdge = { ChunkTerrainIO.EDGE_SOUTH, ChunkTerrainIO.EDGE_NORTH,
ChunkTerrainIO.EDGE_WEST, ChunkTerrainIO.EDGE_EAST };
for (int e = 0; e < 4; e++) {
int ncx = cx + dxArr[e];
int ncz = cz + dzArr[e];
if (ncx < 0 || ncx >= N || ncz < 0 || ncz >= N) continue;
int nci = ChunkTerrainIO.chunkIndex(ncx, ncz);
int neighborLod = targetLod[nci] < 0 ? 0 : targetLod[nci];
if (neighborLod <= lod || chunkHeights[nci] == null) continue;
int nVerts = ChunkTerrainIO.LOD_VERTS[neighborLod];
float[] nLodH = ChunkTerrainIO.downsample(chunkHeights[nci], ChunkTerrainIO.CHUNK_VERTS, nVerts);
float[] nEdgeVals = ChunkTerrainIO.extractEdge(nLodH, nVerts, neighborEdge[e]);
ChunkTerrainIO.stitchEdge(lodH, verts, nEdgeVals, nVerts, thisEdges[e]);
}
}
private static Mesh buildMesh(int cx, int cz, float[] lodH, int verts, float spacing) {
float chunkCX = -2048f + cx * ChunkTerrainIO.CHUNK_SIZE + 64f;
float chunkCZ = -2048f + cz * ChunkTerrainIO.CHUNK_SIZE + 64f;
float half = (verts - 1) * spacing * 0.5f; // immer 64 m
int vertCount = verts * verts;
int indexCount = (verts - 1) * (verts - 1) * 6;
float[] positions = new float[vertCount * 3];
float[] normals = new float[vertCount * 3];
float[] texCoords = new float[vertCount * 2];
int[] indices = new int[indexCount];
for (int row = 0; row < verts; row++) {
for (int col = 0; col < verts; col++) {
int vi = row * verts + col;
float lx = col * spacing - half;
float lz = row * spacing - half;
float h = lodH[vi];
int pi = vi * 3;
positions[pi] = lx; positions[pi+1] = h; positions[pi+2] = lz;
// Normale per zentrale Differenzen
float hL = lodH[row * verts + Math.max(0, col-1)];
float hR = lodH[row * verts + Math.min(verts-1, col+1)];
float hD = lodH[Math.max(0, row-1) * verts + col];
float hU = lodH[Math.min(verts-1, row+1) * verts + col];
float nx = -(hR - hL);
float ny = 2f * spacing;
float nz = -(hU - hD);
float nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
if (nLen > 1e-6f) { nx /= nLen; ny /= nLen; nz /= nLen; }
normals[pi] = nx; normals[pi+1] = ny; normals[pi+2] = nz;
// Welt-Raum UV (01 über 4096 m) für globale Splat-Map
float worldX = chunkCX + lx;
float worldZ = chunkCZ + lz;
int ti = vi * 2;
texCoords[ti] = (worldX + 2048f) / 4096f;
texCoords[ti+1] = (worldZ + 2048f) / 4096f;
}
}
int ii = 0;
for (int row = 0; row < verts - 1; row++) {
for (int col = 0; col < verts - 1; col++) {
int v00 = row * verts + col;
int v10 = v00 + 1;
int v01 = v00 + verts;
int v11 = v01 + 1;
indices[ii++] = v00; indices[ii++] = v01; indices[ii++] = v10;
indices[ii++] = v10; indices[ii++] = v01; indices[ii++] = v11;
}
}
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(positions));
mesh.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(normals));
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoords));
mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indices));
mesh.updateBound();
mesh.updateCounts();
return mesh;
}
// ── Physik ────────────────────────────────────────────────────────────────
private void addPhysics(int ci) {
if (chunkNodes[ci] == null || chunkHeights[ci] == null) return;
HeightfieldCollisionShape shape = new HeightfieldCollisionShape(
chunkHeights[ci], new Vector3f(1f, 1f, 1f));
RigidBodyControl rbc = new RigidBodyControl(shape, 0f);
chunkNodes[ci].addControl(rbc);
bulletAppState.getPhysicsSpace().add(rbc);
physics[ci] = rbc;
}
private void removePhysics(int ci) {
if (physics[ci] == null) return;
bulletAppState.getPhysicsSpace().remove(physics[ci]);
if (chunkNodes[ci] != null) chunkNodes[ci].removeControl(physics[ci]);
physics[ci] = null;
}
// ── Listener ─────────────────────────────────────────────────────────────
private void notifyVisible(int cx, int cz, int lod) {
for (ChunkListener l : listeners) l.onChunkVisible(cx, cz, lod);
}
private void notifyLodChanged(int cx, int cz, int oldLod, int newLod) {
for (ChunkListener l : listeners) l.onChunkLodChanged(cx, cz, oldLod, newLod);
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private static int worldToChunk(float world) {
return Math.max(0, Math.min(N - 1, (int) ((world + 2048f) / ChunkTerrainIO.CHUNK_SIZE)));
}
private static float[] flatChunk() {
float[] h = new float[ChunkTerrainIO.CHUNK_VERTS * ChunkTerrainIO.CHUNK_VERTS];
Arrays.fill(h, 1f);
return h;
}
}

View File

@@ -0,0 +1,230 @@
package de.blight.game.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.asset.plugins.FileLocator;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import de.blight.common.PlacedItem;
import de.blight.common.PlacedItemIO;
import de.blight.common.model.Inventar;
import de.blight.common.model.Item;
import de.blight.common.model.ItemIO;
import de.blight.common.model.MainCharacter;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.KeyBindings;
import de.blight.game.control.PlayerInputControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Lädt alle auf der Karte platzierten Items, stellt sie als 3D-Objekte dar und
* verarbeitet das Aufheben (E-Taste) mit PICK_UP-Animation.
*/
public class WorldItemsState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WorldItemsState.class);
private static final float PICKUP_RANGE = 2.5f;
private static final float PICKUP_ANIM_DURATION = 0.8f;
private static final String INTERACT_ACTION = "Interact";
private final KeyBindings keyBindings;
private final CharacterControl physicsChar;
private final MainCharacter mainCharacter;
private final PlayerInputControl playerInput;
private SimpleApplication app;
private AssetManager assets;
private InputManager inputManager;
private Node rootNode;
private Node itemsRoot;
private final List<PlacedItem> items = new ArrayList<>();
private final List<Spatial> visuals = new ArrayList<>();
private final Map<String, Item> itemDefs = new HashMap<>();
private final Quaternion rotQuat = new Quaternion();
private float rotAccum = 0f;
public WorldItemsState(KeyBindings keyBindings, CharacterControl physicsChar,
MainCharacter mainCharacter, PlayerInputControl playerInput) {
this.keyBindings = keyBindings;
this.physicsChar = physicsChar;
this.mainCharacter = mainCharacter;
this.playerInput = playerInput;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assets = app.getAssetManager();
this.inputManager = app.getInputManager();
this.rootNode = this.app.getRootNode();
this.itemsRoot = new Node("worldItemsRoot");
try {
assets.registerLocator(
AnimationLibrary.findAssetRoot().toAbsolutePath().toString(),
FileLocator.class);
} catch (Exception ignored) {}
Path itemDir = AnimationLibrary.findAssetRoot().resolve("items");
for (Item it : ItemIO.loadAll(itemDir)) {
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
}
log.info("[WorldItems] {} Item-Definitionen geladen.", itemDefs.size());
}
@Override
protected void onEnable() {
items.clear();
visuals.clear();
try {
items.addAll(PlacedItemIO.load());
} catch (Exception e) {
log.warn("[WorldItems] Laden fehlgeschlagen: {}", e.getMessage());
}
for (PlacedItem pi : items) {
Spatial s = buildVisual(pi);
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
s.setUserData("itemId", pi.itemId());
itemsRoot.attachChild(s);
visuals.add(s);
}
rootNode.attachChild(itemsRoot);
log.info("[WorldItems] {} Item-Pickups geladen.", items.size());
inputManager.addMapping(INTERACT_ACTION, new KeyTrigger(keyBindings.interact));
inputManager.addListener(interactListener, INTERACT_ACTION);
}
@Override
protected void onDisable() {
itemsRoot.detachAllChildren();
itemsRoot.removeFromParent();
visuals.clear();
items.clear();
inputManager.removeListener(interactListener);
try { inputManager.deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {}
}
@Override
protected void cleanup(Application app) {}
@Override
public void update(float tpf) {
if (visuals.isEmpty()) return;
rotAccum += tpf * 60f;
rotQuat.fromAngles(0f, rotAccum * FastMath.DEG_TO_RAD, 0f);
for (Spatial s : visuals) s.setLocalRotation(rotQuat);
}
// ── Interaktion ───────────────────────────────────────────────────────────
private final ActionListener interactListener = (name, isPressed, tpf) -> {
if (isPressed) tryPickup();
};
private void tryPickup() {
if (physicsChar == null || items.isEmpty()) return;
Vector3f playerPos = physicsChar.getPhysicsLocation();
int nearest = -1;
float bestDist = Float.MAX_VALUE;
for (int i = 0; i < items.size(); i++) {
PlacedItem pi = items.get(i);
float dx = pi.x() - playerPos.x;
float dz = pi.z() - playerPos.z;
float dist = (float) Math.sqrt(dx * dx + dz * dz);
if (dist < PICKUP_RANGE && dist < bestDist) {
bestDist = dist;
nearest = i;
}
}
if (nearest < 0) return;
PlacedItem picked = items.get(nearest);
Spatial pickedSpatial = visuals.get(nearest);
pickedSpatial.removeFromParent();
items.remove(nearest);
visuals.remove(nearest);
// Kartendatei sofort aktualisieren
try {
PlacedItemIO.save(items);
} catch (IOException e) {
log.warn("[WorldItems] Speichern fehlgeschlagen: {}", e.getMessage());
}
// Ins Inventar legen
Item def = itemDefs.get(picked.itemId());
if (mainCharacter != null) {
if (mainCharacter.getInventar() == null) {
mainCharacter.setInventar(new Inventar());
}
if (def != null) {
mainCharacter.getInventar().collect(def);
log.info("[WorldItems] '{}' aufgehoben.", picked.itemId());
} else {
log.warn("[WorldItems] Keine Item-Definition für '{}'.", picked.itemId());
}
}
// PICK_UP-Animation spielen und Bewegung sperren
if (playerInput != null) {
playerInput.requestPickup(PICKUP_ANIM_DURATION);
}
}
// ── Zugriff ───────────────────────────────────────────────────────────────
public Node getItemsRoot() { return itemsRoot; }
public CharacterControl getPhysicsChar() { return physicsChar; }
// ── Visuelles ─────────────────────────────────────────────────────────────
private Spatial buildVisual(PlacedItem pi) {
Item def = itemDefs.get(pi.itemId());
if (def != null && def.getModelRef() != null
&& def.getModelRef().getPath() != null
&& !def.getModelRef().getPath().isBlank()) {
try {
Spatial model = assets.loadModel(def.getModelRef().getPath());
model.setName("item_" + pi.itemId());
return model;
} catch (Exception e) {
log.warn("[WorldItems] Modell für '{}' nicht ladbar: {}", pi.itemId(), e.getMessage());
}
}
// Fallback: goldener Würfel
Geometry g = new Geometry("item_" + pi.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);
return g;
}
}

View File

@@ -115,6 +115,19 @@ public class WorldObjectsState extends BaseAppState {
spatial = assets.loadModel(path); spatial = assets.loadModel(path);
} }
spatial.setName("obj_" + path); spatial.setName("obj_" + path);
// LOD / Distance-Culling
if (m.cullDistance() > 0f) {
Node lodRoot = new Node("lodRoot_" + path);
lodRoot.attachChild(spatial);
ModelLodControl ctrl = new ModelLodControl(
assets,
m.lod1Path(), m.lod2Path(),
m.lod1Distance(), m.lod2Distance(), m.cullDistance());
lodRoot.addControl(ctrl);
return lodRoot;
}
return spatial; return spatial;
} }

Binary file not shown.

View File

@@ -1,31 +1,45 @@
# modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow # modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow lod1Path lod2Path lod1Distance lod2Distance cullDistance
Models/Palm_Palme1_20260524_153405.j3o -74.09265 8.19780 -18.47723 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260524_153405.j3o -74.09265 8.19780 -18.47723 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260524_153421.j3o -59.20779 13.84631 -17.90553 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260524_153421.j3o -59.20779 13.84631 -17.90553 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260524_153421.j3o -37.63680 32.63404 -18.65228 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260524_153421.j3o -37.63680 32.63404 -18.65228 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260524_153421.j3o -15.66568 10.46379 -25.60722 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260524_153421.j3o -15.66568 10.46379 -25.60722 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
@box -471.37857 22.27976 -91.47378 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_0.j3o true true @box -471.37857 22.27976 -91.47378 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_0.j3o true true 30.00000 80.00000 120.00000
@box -469.23279 22.49469 -91.51467 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_1.j3o true true @box -469.23279 22.49469 -91.51467 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_1.j3o true true 30.00000 80.00000 120.00000
@group -462.16046 3.59008 -122.32437 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_2.j3o true true @group -462.16046 3.59008 -122.32437 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_2.j3o true true 30.00000 80.00000 120.00000
Models/Palm_Palme1.j3o -245.90610 165.20883 99.17912 -6.43500 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1.j3o -245.90610 165.20883 99.17912 -6.43500 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075053.j3o -246.13976 166.24338 89.64748 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260522_075053.j3o -246.13976 166.24338 89.64748 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075102.j3o -240.90959 166.65724 85.45869 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260522_075102.j3o -240.90959 166.65724 85.45869 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075134.j3o -254.69368 165.64214 91.69685 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260522_075134.j3o -254.69368 165.64214 91.69685 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075137.j3o -248.68674 167.64050 76.46214 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260522_075137.j3o -248.68674 167.64050 76.46214 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1.j3o -205.20802 165.35493 88.46772 -18.77974 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1.j3o -205.20802 165.35493 88.46772 -18.77974 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075102.j3o -135.84702 158.69884 84.12026 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260522_075102.j3o -135.84702 158.69884 84.12026 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
@plane -224.78107 164.15782 108.96712 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_3.j3o true true @plane -224.78107 164.15782 108.96712 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_3.j3o true true 30.00000 80.00000 120.00000
@plane -220.53658 165.75740 108.73174 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_4.j3o true true @plane -220.53658 165.75740 108.73174 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_4.j3o true true 30.00000 80.00000 120.00000
@plane -237.96001 165.08000 113.44000 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_5.j3o true true @plane -237.96001 165.08000 113.44000 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_5.j3o true true 30.00000 80.00000 120.00000
@plane -235.81349 164.75165 114.21590 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_6.j3o true true @plane -235.81349 164.75165 114.21590 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_6.j3o true true 30.00000 80.00000 120.00000
Models/Tree/Tree.mesh.j3o -246.86934 164.83850 107.50585 0.00000 1.00000 0.00000 0.00000 false true true Models/Tree/Tree.mesh.j3o -246.86934 164.83850 107.50585 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Boat/boat.j3o -261.74731 164.72397 120.62883 0.00000 1.00000 0.00000 0.00000 false true true Models/Boat/boat.j3o -261.74731 164.72397 120.62883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Campfire.j3o -350.02148 164.72334 72.42865 0.00000 1.00000 0.00000 0.00000 false true true Models/Campfire.j3o -350.02148 164.72334 72.42865 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Campfire.j3o -67.93591 1.71510 -36.36593 0.00000 1.00000 0.00000 0.00000 false true true Models/Campfire.j3o -67.93591 1.71510 -36.36593 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/tree.j3o -28.09666 16.39296 -34.64375 0.00000 1.00000 0.00000 0.00000 false true true Models/tree.j3o -28.09666 16.39296 -34.64375 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/tree.j3o -83.04567 -2.69246 -39.34207 0.00000 1.00000 0.00000 0.00000 false true true Models/tree.j3o -83.04567 -2.69246 -39.34207 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Tree/Tree.mesh.j3o 295.18826 1.00000 189.92809 0.00000 1.00000 0.00000 0.00000 false true true Models/Tree/Tree.mesh.j3o 295.18826 1.00000 189.92809 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/gltf/duck/Duck.gltf 300.54111 1.00000 198.35368 0.00000 1.00000 0.00000 0.00000 false true true Models/gltf/duck/Duck.gltf 300.54111 1.00000 198.35368 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Buggy/Buggy.j3o 296.60590 1.00000 201.24153 0.00000 1.00000 0.00000 0.00000 false true true Models/Buggy/Buggy.j3o 296.60590 1.00000 201.24153 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Jaime/Jaime.j3o 304.02374 1.00000 199.25398 0.00000 1.00000 0.00000 0.00000 false true true Models/Jaime/Jaime.j3o 304.02374 1.00000 199.25398 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/tree.j3o 221.98000 1.00000 130.25000 0.00000 1.00000 0.00000 0.00000 true true true Models/tree.j3o 221.98000 1.00000 130.25000 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075134.j3o 240.61151 1.00000 97.48973 0.00000 1.00000 0.00000 0.00000 false true true Models/Palm_Palme1_20260522_075134.j3o 240.61151 1.00000 97.48973 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/Palm_Palme1_20260522_075134.j3o 150.18289 0.96490 16.69994 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/FernPlantV2.j3o 336.18240 1.00000 -164.72426 0.00000 0.00200 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165631.j3o 158.98721 0.96388 25.31449 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 157.71002 0.96395 20.47514 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 159.09314 0.95784 17.07811 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 160.70380 0.93929 19.18060 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 161.39258 0.90349 21.80369 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 163.20784 0.61551 16.58227 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 164.25797 0.72795 11.98990 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 164.15082 0.42406 17.46363 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 163.74956 0.53243 21.45433 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/fern/fern_20260608_165628.j3o 162.27063 0.88450 24.03092 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/misc/heliconia+plant+3d+model.j3o 152.94803 0.96488 8.99865 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/misc/heliconia+plant+3d+model.j3o 155.57500 0.96457 10.04861 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More