Weiter am Editor gearbeitet, unter anderem LOD System, Items, Trees, Modelle
79
blight-assets/src/main/resources/MatDefs/Fern.j3md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
15
blight-assets/src/main/resources/Models/FernPlantV2.j3o.meta
Normal 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
|
||||
@@ -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
|
||||
42
blight-assets/src/main/resources/Shaders/Fern.frag
Normal 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);
|
||||
}
|
||||
34
blight-assets/src/main/resources/Shaders/Fern.vert
Normal 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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.0 MiB |
BIN
blight-assets/src/main/resources/Textures/fern/Fern02_Normal.tga
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 506 KiB |
|
After Width: | Height: | Size: 695 KiB |
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 178 KiB |
BIN
blight-assets/src/main/resources/animations/pickup.glb
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"itemId": "neues_item_1780949443027",
|
||||
"category": "CONSUMABLES",
|
||||
"name": {
|
||||
"id": "bloddagave.name"
|
||||
},
|
||||
"description": {
|
||||
"id": "bloddagave.description"
|
||||
},
|
||||
"worthGold": 50
|
||||
}
|
||||
254
blight-common/src/main/java/de/blight/common/ChunkTerrainIO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,17 @@ public record ModelMeta(
|
||||
boolean castShadow,
|
||||
boolean receiveShadow,
|
||||
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) {
|
||||
String name = j3oFileName.replaceFirst("\\.j3o$", "");
|
||||
return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f,
|
||||
false, true, true, 1f, 1f);
|
||||
false, true, true, 1f, 1f,
|
||||
"", "", 30f, 80f, 120f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ public final class ModelMetaIO {
|
||||
p.setProperty("receiveShadow", String.valueOf(m.receiveShadow()));
|
||||
p.setProperty("randomScaleMin", String.valueOf(m.randomScaleMin()));
|
||||
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))) {
|
||||
p.store(w, null);
|
||||
}
|
||||
@@ -57,7 +62,12 @@ public final class ModelMetaIO {
|
||||
Boolean.parseBoolean(p.getProperty("castShadow", "true")),
|
||||
Boolean.parseBoolean(p.getProperty("receiveShadow", "true")),
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,10 @@ public record PlacedModel(
|
||||
/** AnimationLibrary-Clip-Key (z. B. "walk/Run"); "" = keine Animation. */
|
||||
String animClip,
|
||||
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
|
||||
) {}
|
||||
|
||||
@@ -8,10 +8,11 @@ import java.util.*;
|
||||
* Liest und schreibt platzierte Modelle als tab-separierte Textdatei
|
||||
* ({@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
|
||||
* 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 {
|
||||
|
||||
@@ -25,11 +26,11 @@ public final class PlacedModelIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
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();
|
||||
for (PlacedModel m : models) {
|
||||
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.x(), m.y(), m.z(),
|
||||
m.rotY(), m.scale(),
|
||||
@@ -37,7 +38,9 @@ public final class PlacedModelIO {
|
||||
m.solid(),
|
||||
nvl(m.texturePath()), nvl(m.normalMapPath()), nvl(m.materialPath()),
|
||||
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] : "";
|
||||
boolean castShadow = f.length > 14 ? Boolean.parseBoolean(f[14]) : 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,
|
||||
rotY, rotX, rotZ, scale, solid,
|
||||
texPath, nmPath, matPath, meshFile, animClip,
|
||||
castShadow, receiveShadow));
|
||||
castShadow, receiveShadow,
|
||||
lod1Path, lod2Path, lod1Distance, lod2Distance, cullDistance));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import de.blight.editor.state.AnimPreviewState;
|
||||
import de.blight.editor.state.ItemPlacementState;
|
||||
import de.blight.editor.state.AreaState;
|
||||
import de.blight.editor.state.ModelEditorState;
|
||||
import de.blight.editor.state.EmitterState;
|
||||
@@ -18,6 +19,7 @@ import de.blight.editor.state.SoundAreaState;
|
||||
import de.blight.editor.state.WaterBodyState;
|
||||
import de.blight.editor.state.EzTreeState;
|
||||
import de.blight.editor.state.LightState;
|
||||
import de.blight.editor.state.FernGeneratorState;
|
||||
import de.blight.editor.state.PalmGeneratorState;
|
||||
import de.blight.editor.state.SceneObjectState;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
@@ -91,6 +93,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new TreeGeneratorState(input));
|
||||
stateManager.attach(new EzTreeState(input));
|
||||
stateManager.attach(new PalmGeneratorState(input));
|
||||
stateManager.attach(new FernGeneratorState(input));
|
||||
stateManager.attach(new LightState(input));
|
||||
stateManager.attach(new EmitterState(input));
|
||||
stateManager.attach(new WaterBodyState(input));
|
||||
@@ -101,6 +104,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new PlayToolState(input));
|
||||
stateManager.attach(new AnimPreviewState(input));
|
||||
stateManager.attach(new ModelEditorState(input));
|
||||
stateManager.attach(new ItemPlacementState(input));
|
||||
|
||||
input.loadingStatus = "Initialisiere Konsole...";
|
||||
jmeConsole = new JmeConsole(false);
|
||||
|
||||
@@ -76,6 +76,10 @@ public class SharedInput {
|
||||
public record GrassVertexEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<GrassVertexEdit> grassVertexEditQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Farn-Generator ────────────────────────────────────────────────────────
|
||||
public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {}
|
||||
public final ConcurrentLinkedQueue<FernGenRequest> fernGenQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Gras-Einstellungen (JavaFX → JME3) ───────────────────────────────────
|
||||
/** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */
|
||||
public volatile String grassTexturePath = "";
|
||||
@@ -582,4 +586,23 @@ public class SharedInput {
|
||||
|
||||
/** JFX → JME: Model-Editor schließen. */
|
||||
public volatile boolean modelEditorCloseRequest = false;
|
||||
|
||||
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
|
||||
public volatile boolean modelEditorHasEmbeddedLods = false;
|
||||
|
||||
/** JFX → JME: welche LOD-Stufe anzeigen (0=LOD0, 1=LOD1, 2=LOD2). */
|
||||
public volatile int modelEditorLodPreview = 0;
|
||||
public volatile String modelEditorLod1Path = "";
|
||||
public volatile String modelEditorLod2Path = "";
|
||||
public volatile boolean modelEditorLodChanged = false;
|
||||
|
||||
// ── Item-Platzierung ──────────────────────────────────────────────────────
|
||||
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
|
||||
public static final int LAYER_ITEMS = 21;
|
||||
|
||||
/** JFX → JME: itemId des zu platzierenden Items (aus .item-Datei). */
|
||||
public volatile String pendingItemId = null;
|
||||
|
||||
/** Klick-Events für Item-Platzierung (analoges Format zu objectClickQueue). */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> itemClickQueue = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ public class SceneObject extends PlacedObject {
|
||||
public String texturePath = "";
|
||||
public String normalMapPath = "";
|
||||
public String materialPath = "";
|
||||
public String lod1Path = "";
|
||||
public String lod2Path = "";
|
||||
public float lod1Distance = 30f;
|
||||
public float lod2Distance = 80f;
|
||||
public float cullDistance = 120f;
|
||||
|
||||
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
||||
boolean solid) {
|
||||
|
||||
@@ -61,6 +61,9 @@ public class EzTreeState extends BaseAppState {
|
||||
private static final Logger log = LoggerFactory.getLogger(EzTreeState.class);
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
private static final int ATLAS_DIRS = 4;
|
||||
private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS;
|
||||
private static final int ATLAS_H = IMPOSTOR_SIZE;
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
private static final Path TOOLS_DIR = de.blight.editor.ProjectRoot.resolve("tools");
|
||||
private static final Gson GSON = new Gson();
|
||||
@@ -72,10 +75,14 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
// ── Capture-Phase ────────────────────────────────────────────────────────
|
||||
private SharedInput.EzTreeGenRequest pendingRequest = null;
|
||||
private Node pendingTreeNode = null;
|
||||
private Node pendingHdNode = null;
|
||||
private Node pendingLd1Node = null;
|
||||
private BoundingBox pendingBb = null;
|
||||
private ViewPort captureVP = null;
|
||||
private FrameBuffer captureFB = null;
|
||||
private volatile boolean captureReady = false;
|
||||
private int capturePass = 0;
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
|
||||
public EzTreeState(SharedInput input) { this.input = input; }
|
||||
|
||||
@@ -113,14 +120,15 @@ public class EzTreeState extends BaseAppState {
|
||||
private void startGeneration(SharedInput.EzTreeGenRequest req) {
|
||||
cleanupCapture();
|
||||
|
||||
Node treeNode = tryNodeJsGeneration(req);
|
||||
if (treeNode == null) {
|
||||
treeNode = javaFallback(req);
|
||||
}
|
||||
final Node finalNode = treeNode;
|
||||
finalNode.updateGeometricState();
|
||||
Node hdNode = tryNodeJsGeneration(req);
|
||||
if (hdNode == null) hdNode = javaFallback(req);
|
||||
hdNode.setLocalScale(1f / 3f);
|
||||
hdNode.updateGeometricState();
|
||||
|
||||
BoundingBox bb = boundsOf(finalNode);
|
||||
Node ld1Node = buildLod1Node(req.options());
|
||||
ld1Node.setLocalScale(1f / 3f);
|
||||
|
||||
BoundingBox bb = boundsOf(hdNode);
|
||||
float camDist = bb != null
|
||||
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
||||
: 20f;
|
||||
@@ -128,22 +136,45 @@ public class EzTreeState extends BaseAppState {
|
||||
? new Vector3f(0f, bb.getCenter().y, 0f)
|
||||
: new Vector3f(0f, 5f, 0f);
|
||||
|
||||
final Node finalHd = hdNode;
|
||||
final Node finalLd1 = ld1Node;
|
||||
final BoundingBox finalBb = bb;
|
||||
final float dist = camDist;
|
||||
final Vector3f tgt = target;
|
||||
app.enqueue(() -> {
|
||||
previewHost.setPreviewContent(finalNode, dist, tgt);
|
||||
previewHost.setPreviewContent(finalHd, dist, tgt);
|
||||
if (req.exportAfter()) {
|
||||
setupCapture(finalNode, boundsOf(finalNode), req);
|
||||
pendingRequest = req;
|
||||
pendingHdNode = finalHd;
|
||||
pendingLd1Node = finalLd1;
|
||||
pendingBb = finalBb;
|
||||
capturePass = 0;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
startCapturePass(0, finalHd, finalBb);
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (1/" + ATLAS_DIRS + ")…";
|
||||
}
|
||||
});
|
||||
|
||||
if (!req.exportAfter()) {
|
||||
input.treeGenStatusMsg = "EZ-Tree Vorschau: " + resolveSubPath(req.presetName());
|
||||
} else {
|
||||
input.treeGenStatusMsg = "EZ-Tree: generiere…";
|
||||
}
|
||||
}
|
||||
|
||||
private Node buildLod1Node(TreeOptions opts) {
|
||||
TreeOptions ld = opts.copy();
|
||||
// Levels NICHT reduzieren – gleiche Baumform und -größe wie LOD0 behalten.
|
||||
// Nur Polygon-Anzahl pro Ast halbieren (sections/segments) und Blattanzahl reduzieren.
|
||||
for (int lv = 0; lv <= ld.branch.levels; lv++) {
|
||||
ld.branch.sections.put(lv, Math.max(3, opts.branch.getSections(lv) / 2));
|
||||
ld.branch.segments.put(lv, Math.max(3, opts.branch.getSegments(lv) / 2));
|
||||
}
|
||||
ld.leaves.count = Math.max(0, opts.leaves.count / 2);
|
||||
Tree tree = new Tree(ld);
|
||||
tree.generate();
|
||||
applyMaterials(tree, opts);
|
||||
return treeToNode(tree, "EzTree_ld1");
|
||||
}
|
||||
|
||||
// ── Node.js-Generierung ───────────────────────────────────────────────────
|
||||
|
||||
private Node tryNodeJsGeneration(SharedInput.EzTreeGenRequest req) {
|
||||
@@ -328,45 +359,71 @@ public class EzTreeState extends BaseAppState {
|
||||
Tree tree = new Tree(req.options());
|
||||
tree.generate();
|
||||
applyMaterials(tree, req.options());
|
||||
return tree;
|
||||
return treeToNode(tree, "EzTree");
|
||||
}
|
||||
|
||||
// ── Phase 2: Impostor-Capture ─────────────────────────────────────────────
|
||||
/** Überträgt alle Kinder eines Tree in einen plain Node, damit kein Tree-Objekt im j3o landet. */
|
||||
private static Node treeToNode(Tree tree, String name) {
|
||||
Node n = new Node(name);
|
||||
n.setLocalScale(tree.getLocalScale());
|
||||
n.setLocalTranslation(tree.getLocalTranslation());
|
||||
for (Spatial child : new java.util.ArrayList<>(tree.getChildren())) {
|
||||
tree.detachChild(child);
|
||||
n.attachChild(child);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private void setupCapture(Node treeNode, BoundingBox bb, SharedInput.EzTreeGenRequest req) {
|
||||
// ── Phase 2: Impostor-Capture (4-Richtungen) ─────────────────────────────
|
||||
|
||||
private void startCapturePass(int pass, Node treeNode, BoundingBox bb) {
|
||||
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
||||
|
||||
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
|
||||
captureFB.addColorTexture(capTex);
|
||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||
|
||||
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB);
|
||||
float angle = pass * com.jme3.math.FastMath.HALF_PI;
|
||||
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB, angle);
|
||||
captureReady = false;
|
||||
pendingRequest = req;
|
||||
pendingTreeNode = treeNode;
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
|
||||
}
|
||||
|
||||
private void finishCapture() {
|
||||
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
|
||||
SharedInput.EzTreeGenRequest req = pendingRequest;
|
||||
Node treeNode = pendingTreeNode;
|
||||
capturePixels[capturePass] = pixels;
|
||||
cleanupCapture();
|
||||
|
||||
if (capturePass < ATLAS_DIRS - 1) {
|
||||
capturePass++;
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…";
|
||||
startCapturePass(capturePass, pendingHdNode, pendingBb);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alle Richtungen erfasst
|
||||
SharedInput.EzTreeGenRequest req = pendingRequest;
|
||||
Node hdNode = pendingHdNode;
|
||||
Node ld1Node = pendingLd1Node;
|
||||
BoundingBox bb = pendingBb;
|
||||
ByteBuffer[] savedPixels = capturePixels;
|
||||
pendingRequest = null;
|
||||
pendingHdNode = null;
|
||||
pendingLd1Node = null;
|
||||
pendingBb = null;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
|
||||
String subPath = resolveSubPath(req.presetName());
|
||||
String namePart = req.presetName() != null
|
||||
? req.presetName().toLowerCase().replace(" ", "_")
|
||||
: subPath;
|
||||
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
String exportName = namePart + "_" + timestamp;
|
||||
saveImpostor(pixels, "ez_impostor_" + exportName);
|
||||
exportTree(treeNode, exportName, subPath);
|
||||
|
||||
pendingRequest = null;
|
||||
pendingTreeNode = null;
|
||||
ByteBuffer atlas = combineAtlas(savedPixels);
|
||||
Texture2D atlasT2d = saveImpostor(atlas, "ez_impostor_" + exportName, ATLAS_W, ATLAS_H);
|
||||
|
||||
Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d);
|
||||
exportTree(lodRoot, exportName, subPath);
|
||||
}
|
||||
|
||||
// ── Material-Aufbau ───────────────────────────────────────────────────────
|
||||
@@ -440,15 +497,22 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
// ── Offscreen-Viewport für Impostor ───────────────────────────────────────
|
||||
|
||||
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb) {
|
||||
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
|
||||
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||
float dist = extent * 3f;
|
||||
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb, float angle) {
|
||||
Vector3f center = bb.getCenter();
|
||||
float yExt = bb.getYExtent();
|
||||
float hExt = Math.max(bb.getXExtent(), bb.getZExtent());
|
||||
float extent = Math.max(yExt, hExt);
|
||||
float dist = extent * 3.5f;
|
||||
|
||||
float camX = com.jme3.math.FastMath.sin(angle) * dist;
|
||||
float camZ = com.jme3.math.FastMath.cos(angle) * dist;
|
||||
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
|
||||
cam.setLocation(center.add(0f, 0f, dist));
|
||||
cam.setLocation(center.add(camX, 0f, camZ));
|
||||
cam.lookAt(center, Vector3f.UNIT_Y);
|
||||
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
|
||||
// FOV groß genug, um den vollen Baum (+ 15 % Rand) zu erfassen
|
||||
float fovY = (float) Math.toDegrees(2.0 * Math.atan2(yExt * 1.15, dist));
|
||||
float fovH = (float) Math.toDegrees(2.0 * Math.atan2(hExt * 1.15, dist));
|
||||
cam.setFrustumPerspective(Math.max(fovY, fovH), 1f, 0.1f, dist * 4f);
|
||||
|
||||
ViewPort vp = app.getRenderManager()
|
||||
.createPostView("ezCapture_" + System.nanoTime(), cam);
|
||||
@@ -524,34 +588,123 @@ public class EzTreeState extends BaseAppState {
|
||||
captureReady = false;
|
||||
}
|
||||
|
||||
private void saveImpostor(ByteBuffer pixels, String name) {
|
||||
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
|
||||
try {
|
||||
pixels.rewind();
|
||||
BufferedImage img = new BufferedImage(
|
||||
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
|
||||
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
|
||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int r = pixels.get() & 0xFF, g = pixels.get() & 0xFF,
|
||||
b = pixels.get() & 0xFF, a = pixels.get() & 0xFF;
|
||||
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
|
||||
img.setRGB(x, height - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
|
||||
}
|
||||
}
|
||||
Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
|
||||
Files.createDirectories(texDir);
|
||||
ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
|
||||
File pngFile = texDir.resolve(name + ".png").toFile();
|
||||
ImageIO.write(img, "PNG", pngFile);
|
||||
try {
|
||||
return (Texture2D) assets.loadTexture("Textures/impostor/" + name + ".png");
|
||||
} catch (Exception ignored) {
|
||||
pixels.rewind();
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, width, height, pixels, null,
|
||||
com.jme3.texture.image.ColorSpace.sRGB);
|
||||
return new Texture2D(jmeImg);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("[EzTree] Impostor-Fehler: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void exportTree(Node treeNode, String fileName, String subPath) {
|
||||
private ByteBuffer combineAtlas(ByteBuffer[] passes) {
|
||||
ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4);
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
ByteBuffer src = passes[d];
|
||||
src.rewind();
|
||||
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
|
||||
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
|
||||
int srcOff = (y * IMPOSTOR_SIZE + x) * 4;
|
||||
int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4;
|
||||
atlas.put(dstOff, src.get(srcOff));
|
||||
atlas.put(dstOff + 1, src.get(srcOff + 1));
|
||||
atlas.put(dstOff + 2, src.get(srcOff + 2));
|
||||
atlas.put(dstOff + 3, src.get(srcOff + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
return atlas;
|
||||
}
|
||||
|
||||
private Node assembleLodNode(String name, Node hd, Node ld1, BoundingBox bb, Texture2D atlasTex) {
|
||||
Node root = new Node(name != null ? name : "ez_tree");
|
||||
root.attachChild(hd);
|
||||
root.attachChild(ld1);
|
||||
Node lod2 = makeImpostorNode(bb, atlasTex);
|
||||
root.attachChild(lod2);
|
||||
|
||||
hd.setCullHint(Spatial.CullHint.Inherit);
|
||||
ld1.setCullHint(Spatial.CullHint.Always);
|
||||
lod2.setCullHint(Spatial.CullHint.Always);
|
||||
|
||||
root.addControl(new EzTreeLodControl(app.getCamera(), hd, ld1, lod2, 40f, 120f));
|
||||
return root;
|
||||
}
|
||||
|
||||
private Node makeImpostorNode(BoundingBox bb, Texture2D tex) {
|
||||
float h = bb != null ? bb.getYExtent() * 2f : 10f;
|
||||
float w = bb != null ? Math.max(bb.getXExtent(), bb.getZExtent()) * 2f : 4f;
|
||||
float size = Math.max(h, w);
|
||||
float yOff = bb != null ? bb.getCenter().y : 5f;
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
if (tex != null) mat.setTexture("ColorMap", tex);
|
||||
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
|
||||
mat.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(com.jme3.material.RenderState.FaceCullMode.Off);
|
||||
|
||||
Node n = new Node("lod2");
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
float angle = d * com.jme3.math.FastMath.HALF_PI;
|
||||
float uMin = (float) d / ATLAS_DIRS;
|
||||
float uMax = (float)(d + 1) / ATLAS_DIRS;
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, angle, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
return n;
|
||||
}
|
||||
|
||||
private Geometry buildBillboardQuad(String name, float yRot, float yCent,
|
||||
float size, Material mat, float uMin, float uMax) {
|
||||
float hw = size * 0.5f;
|
||||
float hh = size * 0.5f;
|
||||
float cos = com.jme3.math.FastMath.cos(yRot);
|
||||
float sin = com.jme3.math.FastMath.sin(yRot);
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{
|
||||
-hw*cos, yCent-hh, -hw*sin,
|
||||
hw*cos, yCent-hh, hw*sin,
|
||||
hw*cos, yCent+hh, hw*sin,
|
||||
-hw*cos, yCent+hh, -hw*sin
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{
|
||||
uMin, 0f, uMax, 0f, uMax, 1f, uMin, 1f
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
|
||||
mesh.updateBound();
|
||||
Geometry g = new Geometry(name, mesh);
|
||||
g.setMaterial(mat);
|
||||
return g;
|
||||
}
|
||||
|
||||
private void exportTree(Node lodRoot, String fileName, String subPath) {
|
||||
try {
|
||||
treeNode.setLocalScale(0.33f);
|
||||
treeNode.updateGeometricState();
|
||||
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
|
||||
Files.createDirectories(baseDir);
|
||||
File out = baseDir.resolve(fileName + ".j3o").toFile();
|
||||
BinaryExporter.getInstance().save(treeNode, out);
|
||||
// Controls vor Export entfernen (nicht serialisierbar über BinaryExporter)
|
||||
while (lodRoot.getNumControls() > 0) lodRoot.removeControl(lodRoot.getControl(0));
|
||||
BinaryExporter.getInstance().save(lodRoot, out);
|
||||
log.info("[EZ-Tree] Gespeichert: {}", out.getAbsolutePath());
|
||||
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + subPath + "/" + fileName + ".j3o";
|
||||
input.refreshAssets = true;
|
||||
@@ -562,6 +715,32 @@ public class EzTreeState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── LOD-Control ───────────────────────────────────────────────────────────
|
||||
|
||||
private static final class EzTreeLodControl extends com.jme3.scene.control.AbstractControl {
|
||||
private final Camera cam;
|
||||
private final Node lod0, lod1, lod2;
|
||||
private final float d01sq, d12sq;
|
||||
|
||||
EzTreeLodControl(Camera cam, Node l0, Node l1, Node l2, float d01, float d12) {
|
||||
this.cam = cam;
|
||||
this.lod0 = l0; this.lod1 = l1; this.lod2 = l2;
|
||||
this.d01sq = d01 * d01;
|
||||
this.d12sq = d12 * d12;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
|
||||
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
lod1.setCullHint(dSq >= d01sq && dSq < d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
@Override protected void controlRender(com.jme3.renderer.RenderManager rm,
|
||||
com.jme3.renderer.ViewPort vp) {}
|
||||
}
|
||||
|
||||
private static String resolveSubPath(String presetName) {
|
||||
if (presetName == null) return "unknown";
|
||||
String lo = presetName.toLowerCase();
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.export.binary.BinaryExporter;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.texture.Texture;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.tree.FernMeshBuilder;
|
||||
import de.blight.editor.tree.FernOptions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class FernGeneratorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FernGeneratorState.class);
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve(
|
||||
"blight-assets", "src", "main", "resources");
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private TreeGeneratorState previewHost;
|
||||
|
||||
public FernGeneratorState(SharedInput input) { this.input = input; }
|
||||
|
||||
@Override protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assets = app.getAssetManager();
|
||||
}
|
||||
@Override protected void cleanup(Application app) {}
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (previewHost == null) {
|
||||
previewHost = getStateManager().getState(TreeGeneratorState.class);
|
||||
if (previewHost == null) return;
|
||||
}
|
||||
|
||||
SharedInput.FernGenRequest req = input.fernGenQueue.poll();
|
||||
if (req == null) return;
|
||||
|
||||
FernOptions opts = req.options();
|
||||
Node fern = FernMeshBuilder.build(opts);
|
||||
applyMaterial(fern, opts);
|
||||
fern.updateGeometricState();
|
||||
|
||||
BoundingBox bb = fern.getWorldBound() instanceof BoundingBox b ? b : null;
|
||||
float dist = bb != null
|
||||
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 4f
|
||||
: 3f;
|
||||
Vector3f target = bb != null ? new Vector3f(0f, bb.getCenter().y, 0f)
|
||||
: new Vector3f(0f, 0.4f, 0f);
|
||||
|
||||
final Node finalFern = fern;
|
||||
final float finalDist = dist;
|
||||
final Vector3f finalTarget = target;
|
||||
final boolean doExport = req.exportAfter();
|
||||
|
||||
app.enqueue(() -> {
|
||||
previewHost.setPreviewContent(finalFern, finalDist, finalTarget);
|
||||
if (doExport) exportFern(finalFern);
|
||||
});
|
||||
input.treeGenStatusMsg = doExport ? "Farn: exportiere…" : "Farn: Vorschau";
|
||||
}
|
||||
|
||||
private void applyMaterial(Node fern, FernOptions opts) {
|
||||
Material mat;
|
||||
try {
|
||||
mat = new Material(assets, "MatDefs/Fern.j3md");
|
||||
mat.setFloat("WindStrength", opts.windStrength);
|
||||
mat.setFloat("WindSpeed", opts.windSpeed);
|
||||
try {
|
||||
Texture diff = assets.loadTexture("Textures/fern/Fern02_Diffuse.tga");
|
||||
mat.setTexture("DiffuseMap", diff);
|
||||
mat.setBoolean("HasDiffuseMap", true);
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Texture norm = assets.loadTexture("Textures/fern/Fern02_Normal.tga");
|
||||
mat.setTexture("NormalMap", norm);
|
||||
mat.setBoolean("HasNormalMap", true);
|
||||
} catch (Exception ignored) {}
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
} catch (Exception e) {
|
||||
mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new com.jme3.math.ColorRGBA(0.1f, 0.55f, 0.1f, 1f));
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
}
|
||||
for (Spatial child : fern.getChildren()) {
|
||||
if (child instanceof Geometry g) {
|
||||
g.setMaterial(mat.clone());
|
||||
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void exportFern(Node fern) {
|
||||
try {
|
||||
Path dir = ASSET_ROOT.resolve("Models").resolve("plants").resolve("fern");
|
||||
Files.createDirectories(dir);
|
||||
String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
Path out = dir.resolve("fern_" + ts + ".j3o");
|
||||
BinaryExporter.getInstance().save(fern, out.toFile());
|
||||
log.info("[Farn] Gespeichert: {}", out);
|
||||
input.treeGenStatusMsg = "Gespeichert: Models/plants/fern/fern_" + ts + ".j3o";
|
||||
input.refreshAssets = true;
|
||||
} catch (IOException e) {
|
||||
log.error("[Farn] Export-Fehler: {}", e.getMessage());
|
||||
input.treeGenStatusMsg = "Farn Export-Fehler: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,14 @@ public class GrassVertexState extends BaseAppState {
|
||||
static final float WIDTH_FACTOR = 0.05f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
|
||||
static final float BEND_FACTOR = 0.15f; // max. Krümmungsversatz an der Spitze
|
||||
|
||||
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
||||
static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
||||
static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f);
|
||||
static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
|
||||
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
||||
static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
||||
// 0–50 % Trockenheit: Grün → Goldgelb
|
||||
static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f);
|
||||
static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
|
||||
// 50–100 % Trockenheit: Goldgelb → Dunkelbraun
|
||||
static final ColorRGBA VERY_DRY_ROOT_COLOR = new ColorRGBA(0.18f, 0.09f, 0.02f, 1f);
|
||||
static final ColorRGBA VERY_DRY_TIP_COLOR = new ColorRGBA(0.38f, 0.20f, 0.05f, 1f);
|
||||
|
||||
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||
private final SharedInput input;
|
||||
@@ -156,20 +160,53 @@ public class GrassVertexState extends BaseAppState {
|
||||
|
||||
private final Random rng = new Random();
|
||||
|
||||
private void addBlades(Vector3f center) {
|
||||
float radius = (float) input.grassVertexTool.brushRadius.getValue();
|
||||
float height = (float) input.grassVertexTool.bladeHeight.getValue();
|
||||
int density = (int) input.grassVertexTool.density.getValue();
|
||||
/** Quadratischer Falloff: 1.0 im Zentrum, 0.5 am Rand. */
|
||||
private static float brushFalloff(float distRatio) {
|
||||
return 1f - 0.5f * distRatio * distRatio;
|
||||
}
|
||||
|
||||
private void addBlades(Vector3f center) {
|
||||
float radius = (float) input.grassVertexTool.brushRadius.getValue();
|
||||
float height = (float) input.grassVertexTool.bladeHeight.getValue();
|
||||
int density = (int) input.grassVertexTool.density.getValue();
|
||||
float witherPct = (float) input.grassVertexTool.dryness.getValue() / 100f;
|
||||
float uniformity = (float) input.grassVertexTool.uniformity.getValue();
|
||||
float variation = (1f - uniformity) * 0.25f; // max ±25 % bei uniformity=0
|
||||
float radSq = radius * radius;
|
||||
|
||||
// Kleinere Rand-Halme im Pinselbereich durch größere ersetzen
|
||||
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
|
||||
List<GrassVertexBlade> list = chunkBlades[ci];
|
||||
boolean changed = false;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
GrassVertexBlade b = list.get(i);
|
||||
float dx = b.x() - center.x, dz = b.z() - center.z;
|
||||
float distSq = dx*dx + dz*dz;
|
||||
if (distSq > radSq) continue;
|
||||
float distRatio = (float) Math.sqrt(distSq) / radius;
|
||||
float idealH = height * brushFalloff(distRatio);
|
||||
// Halm ist deutlich kleiner als die aktuelle Pinselposition erlaubt → ersetzen
|
||||
if (b.height() < idealH * 0.88f) {
|
||||
float newH = idealH * (1f + variation * (rng.nextFloat() * 2f - 1f));
|
||||
list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), b.dryness()));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) dirtyChunks[ci] = true;
|
||||
}
|
||||
|
||||
// Neue Halme mit Falloff und Gleichmäßigkeits-Variation setzen
|
||||
for (int i = 0; i < density; i++) {
|
||||
float angle = rng.nextFloat() * (float) (Math.PI * 2);
|
||||
float r = rng.nextFloat() * radius;
|
||||
float bx = center.x + (float) Math.cos(angle) * r;
|
||||
float bz = center.z + (float) Math.sin(angle) * r;
|
||||
float by = terrain.getHeight(new Vector2f(bx, bz));
|
||||
float angle = rng.nextFloat() * (float) (Math.PI * 2);
|
||||
float r = rng.nextFloat() * radius;
|
||||
float bx = center.x + (float) Math.cos(angle) * r;
|
||||
float bz = center.z + (float) Math.sin(angle) * r;
|
||||
float by = terrain.getHeight(new Vector2f(bx, bz));
|
||||
if (Float.isNaN(by)) continue;
|
||||
float h = height * (0.75f + rng.nextFloat() * 0.5f);
|
||||
float witherPct = (float) input.grassVertexTool.dryness.getValue() / 100f;
|
||||
float distRatio = r / radius;
|
||||
float h = height * brushFalloff(distRatio)
|
||||
* (1f + variation * (rng.nextFloat() * 2f - 1f));
|
||||
h = Math.max(0.05f, h);
|
||||
float bladeDryness = rng.nextFloat() < witherPct
|
||||
? 0.5f + rng.nextFloat() * 0.5f : 0f;
|
||||
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness);
|
||||
@@ -381,10 +418,23 @@ public class GrassVertexState extends BaseAppState {
|
||||
float dr = DRY_ROOT_COLOR.r + (DRY_TIP_COLOR.r - DRY_ROOT_COLOR.r) * wf;
|
||||
float dg = DRY_ROOT_COLOR.g + (DRY_TIP_COLOR.g - DRY_ROOT_COLOR.g) * wf;
|
||||
float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
|
||||
col[ci] = gr + (dr - gr) * dryness;
|
||||
col[ci+1] = gg + (dg - gg) * dryness;
|
||||
col[ci+2] = gb + (db - gb) * dryness;
|
||||
col[ci+3] = 1f;
|
||||
float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
|
||||
float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
|
||||
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
||||
// Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun
|
||||
float fr, fg, fb;
|
||||
if (dryness <= 0.5f) {
|
||||
float t = dryness * 2f;
|
||||
fr = gr + (dr - gr) * t;
|
||||
fg = gg + (dg - gg) * t;
|
||||
fb = gb + (db - gb) * t;
|
||||
} else {
|
||||
float t = (dryness - 0.5f) * 2f;
|
||||
fr = dr + (vr - dr) * t;
|
||||
fg = dg + (vg - dg) * t;
|
||||
fb = db + (vb - db) * t;
|
||||
}
|
||||
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
|
||||
|
||||
int ti = vi * 2;
|
||||
tex[ti] = wf; tex[ti+1] = 0f;
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Box;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedItem;
|
||||
import de.blight.common.PlacedItemIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor.
|
||||
* Aktiv wenn {@code input.activeLayer == LAYER_ITEMS}.
|
||||
*/
|
||||
public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ItemPlacementState.class);
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
private final List<PlacedItem> items = new ArrayList<>();
|
||||
private final List<Node> nodes = new ArrayList<>();
|
||||
private Node itemRoot;
|
||||
private Node previewNode;
|
||||
|
||||
public ItemPlacementState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) {
|
||||
this.terrain = terrain;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.cam = app.getCamera();
|
||||
this.assets = app.getAssetManager();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
|
||||
itemRoot = new Node("itemRoot");
|
||||
previewNode = new Node("itemPreview");
|
||||
previewNode.setCullHint(Spatial.CullHint.Always);
|
||||
Geometry prev = new Geometry("prev", new Box(0.15f, 0.15f, 0.15f));
|
||||
Material pm = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
pm.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 0.6f));
|
||||
prev.setMaterial(pm);
|
||||
prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines);
|
||||
previewNode.attachChild(prev);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
items.clear();
|
||||
nodes.clear();
|
||||
itemRoot.detachAllChildren();
|
||||
|
||||
try {
|
||||
items.addAll(PlacedItemIO.load());
|
||||
} catch (Exception e) {
|
||||
log.warn("[ItemPlacement] Laden fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
for (PlacedItem pi : items) {
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
}
|
||||
|
||||
rootNode.attachChild(itemRoot);
|
||||
rootNode.attachChild(previewNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
itemRoot.removeFromParent();
|
||||
previewNode.removeFromParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
updatePreview();
|
||||
|
||||
SharedInput.ObjectClick click;
|
||||
while ((click = input.itemClickQueue.poll()) != null) {
|
||||
if (click.rightButton()) {
|
||||
input.pendingItemId = null;
|
||||
input.activeLayer = 0;
|
||||
} else {
|
||||
handlePlace(click);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePreview() {
|
||||
if (input.activeLayer != SharedInput.LAYER_ITEMS
|
||||
|| input.pendingItemId == null
|
||||
|| terrain == null
|
||||
|| input.mouseScreenX < 0) {
|
||||
previewNode.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
float jx = input.mouseScreenX * (float) input.viewportScaleX;
|
||||
float jy = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
|
||||
Ray ray = screenToRay(jx, jy);
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) {
|
||||
previewNode.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
previewNode.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
|
||||
previewNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
private void handlePlace(SharedInput.ObjectClick click) {
|
||||
if (terrain == null || input.pendingItemId == null) return;
|
||||
float jx = click.screenX() * (float) input.viewportScaleX;
|
||||
float jy = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Ray ray = screenToRay(jx, jy);
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
PlacedItem pi = new PlacedItem(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
items.add(pi);
|
||||
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})",
|
||||
pi.itemId(), pt.x, pt.y, pt.z);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private Node buildItemNode(String itemId) {
|
||||
Node n = new Node("item_" + itemId);
|
||||
Geometry g = new Geometry("itemGeo_" + itemId, new Box(0.15f, 0.15f, 0.15f));
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 1f));
|
||||
g.setMaterial(mat);
|
||||
n.attachChild(g);
|
||||
n.setUserData("itemId", itemId);
|
||||
return n;
|
||||
}
|
||||
|
||||
private Ray screenToRay(float jmeX, float jmeY) {
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
return new Ray(near, far.subtract(near).normalizeLocal());
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,12 @@ public class ModelEditorState extends BaseAppState {
|
||||
private Quaternion savedCamRot;
|
||||
|
||||
private final SharedInput input;
|
||||
private String currentPath = null;
|
||||
private String currentPath = null;
|
||||
private String mainModelPath = null;
|
||||
|
||||
// Eingebettete LOD-Stufen (falls j3o ein LOD-Node-Baum ist)
|
||||
private boolean hasEmbeddedLods = false;
|
||||
private Spatial[] embeddedLodSpatials = null;
|
||||
|
||||
// Mittelpunkt für Orbit (Bounding-Box-Zentrum des Modells)
|
||||
private Vector3f orbitCenter = Vector3f.ZERO.clone();
|
||||
@@ -91,9 +96,34 @@ public class ModelEditorState extends BaseAppState {
|
||||
String openPath = input.modelEditorOpenPath;
|
||||
if (openPath != null) {
|
||||
input.modelEditorOpenPath = null;
|
||||
mainModelPath = openPath;
|
||||
input.modelEditorLodPreview = 0;
|
||||
loadModel(openPath);
|
||||
}
|
||||
|
||||
if (input.modelEditorLodChanged) {
|
||||
input.modelEditorLodChanged = false;
|
||||
if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||
// Eingebettete LOD-Kinder direkt ein-/ausblenden
|
||||
int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1);
|
||||
for (int i = 0; i < embeddedLodSpatials.length; i++) {
|
||||
embeddedLodSpatials[i].setCullHint(
|
||||
i == lodIdx ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
} else {
|
||||
String path = switch (input.modelEditorLodPreview) {
|
||||
case 1 -> input.modelEditorLod1Path;
|
||||
case 2 -> input.modelEditorLod2Path;
|
||||
default -> mainModelPath;
|
||||
};
|
||||
if (path == null || path.isBlank()) {
|
||||
path = mainModelPath;
|
||||
input.modelEditorLodPreview = 0;
|
||||
}
|
||||
if (path != null) loadModel(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (previewRoot == null) return;
|
||||
|
||||
// Schließen
|
||||
@@ -152,12 +182,31 @@ public class ModelEditorState extends BaseAppState {
|
||||
currentPath = assetPath;
|
||||
|
||||
modelWrapper = new Node("model_wrapper");
|
||||
hasEmbeddedLods = false;
|
||||
embeddedLodSpatials = null;
|
||||
input.modelEditorHasEmbeddedLods = false;
|
||||
try {
|
||||
Spatial model = app.getAssetManager().loadModel(assetPath);
|
||||
stripControls(model);
|
||||
modelWrapper.attachChild(model);
|
||||
|
||||
// Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei Kind 0 sichtbar
|
||||
// und alle weiteren mit CullHint.Always gesetzt sind (EZ-Tree / TreeGenerator-Muster).
|
||||
if (model instanceof Node rootNode && rootNode.getChildren().size() >= 2) {
|
||||
var children = rootNode.getChildren();
|
||||
boolean lodPattern = children.stream().skip(1)
|
||||
.allMatch(s -> s.getCullHint() == Spatial.CullHint.Always);
|
||||
if (lodPattern) {
|
||||
embeddedLodSpatials = children.toArray(new Spatial[0]);
|
||||
hasEmbeddedLods = true;
|
||||
input.modelEditorHasEmbeddedLods = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Fallback: roter Quader
|
||||
System.err.println("[ModelEditor] Fehler beim Laden von '" + assetPath + "': " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
input.modelEditorBoundsReady = false;
|
||||
// Fallback: roter Quader als visuelles Signal
|
||||
Geometry box = new Geometry("error_box", new Box(0.5f, 0.5f, 0.5f));
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", ColorRGBA.Red);
|
||||
@@ -243,6 +292,9 @@ public class ModelEditorState extends BaseAppState {
|
||||
modelWrapper = null;
|
||||
gridGeo = null;
|
||||
}
|
||||
hasEmbeddedLods = false;
|
||||
embeddedLodSpatials = null;
|
||||
input.modelEditorHasEmbeddedLods = false;
|
||||
app.getRootNode().setCullHint(Spatial.CullHint.Inherit);
|
||||
|
||||
if (savedCamPos != null) {
|
||||
|
||||
@@ -144,7 +144,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
so.getScale(), so.solid,
|
||||
so.getTexturePath(), so.getNormalMapPath(), so.getMaterialPath(),
|
||||
meshFile, animClips.get(i),
|
||||
so.castShadow, so.receiveShadow));
|
||||
so.castShadow, so.receiveShadow,
|
||||
so.lod1Path, so.lod2Path,
|
||||
so.lod1Distance, so.lod2Distance, so.cullDistance));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -168,6 +170,11 @@ public class SceneObjectState extends BaseAppState {
|
||||
so.setMaterialPath(pm.materialPath());
|
||||
so.castShadow = pm.castShadow();
|
||||
so.receiveShadow = pm.receiveShadow();
|
||||
so.lod1Path = pm.lod1Path() != null ? pm.lod1Path() : "";
|
||||
so.lod2Path = pm.lod2Path() != null ? pm.lod2Path() : "";
|
||||
so.lod1Distance = pm.lod1Distance();
|
||||
so.lod2Distance = pm.lod2Distance();
|
||||
so.cullDistance = pm.cullDistance();
|
||||
objects.add(so);
|
||||
animClips.add(pm.animClip() != null ? pm.animClip() : "");
|
||||
|
||||
@@ -566,6 +573,13 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
|
||||
SceneObject so = new SceneObject(modelPath, wx, wz, wy + placementOffY, defaultSolid);
|
||||
if (meta != null) {
|
||||
so.lod1Path = meta.lod1Path() != null ? meta.lod1Path() : "";
|
||||
so.lod2Path = meta.lod2Path() != null ? meta.lod2Path() : "";
|
||||
so.lod1Distance = meta.lod1Distance();
|
||||
so.lod2Distance = meta.lod2Distance();
|
||||
so.cullDistance = meta.cullDistance();
|
||||
}
|
||||
so.setRotation(0f, rotY, 0f);
|
||||
so.setScale(defaultScale);
|
||||
so.castShadow = defaultCast;
|
||||
@@ -1217,6 +1231,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
try {
|
||||
Spatial model = assets.loadModel(req.assetPath());
|
||||
if (!req.keepControls()) stripControlsRecursive(model);
|
||||
com.jme3.util.TangentBinormalGenerator.generate(model);
|
||||
|
||||
if (req.centerOrigin()) {
|
||||
model.setLocalTranslation(0f, 0f, 0f);
|
||||
|
||||
@@ -97,6 +97,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private PlacedObjectState placedObjectState;
|
||||
private GrassVertexState grassVertexState;
|
||||
private SceneObjectState sceneObjState;
|
||||
private ItemPlacementState itemPlacementState;
|
||||
private LightState lightState;
|
||||
private EmitterState emitterState;
|
||||
private WaterBodyState waterBodyState;
|
||||
@@ -231,6 +232,14 @@ public class TerrainEditorState extends BaseAppState {
|
||||
sceneObjState = app.getStateManager().getState(SceneObjectState.class);
|
||||
if (sceneObjState != null) {
|
||||
sceneObjState.setTerrain(terrain);
|
||||
}
|
||||
|
||||
itemPlacementState = app.getStateManager().getState(ItemPlacementState.class);
|
||||
if (itemPlacementState != null) {
|
||||
itemPlacementState.setTerrain(terrain);
|
||||
}
|
||||
|
||||
if (sceneObjState != null) {
|
||||
try {
|
||||
var placed = PlacedModelIO.load();
|
||||
if (!placed.isEmpty()) sceneObjState.loadPlacedModels(placed);
|
||||
@@ -880,6 +889,14 @@ public class TerrainEditorState extends BaseAppState {
|
||||
catch (IOException e) { log.error("Vertex-Gras nicht speicherbar", e); }
|
||||
}
|
||||
MapIO.save(data);
|
||||
if (heightSnap != null) {
|
||||
try {
|
||||
de.blight.common.ChunkTerrainIO.exportFromEditorHeightMap(heightSnap, TOTAL_SIZE);
|
||||
log.info("Chunk-Dateien exportiert.");
|
||||
} catch (IOException e) {
|
||||
log.error("Chunk-Export fehlgeschlagen", e);
|
||||
}
|
||||
}
|
||||
if (models != null) PlacedModelIO.save(models);
|
||||
if (lights != null) LightIO.save(lights);
|
||||
if (emitters != null) EmitterIO.save(emitters);
|
||||
@@ -1193,8 +1210,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
private void updateCamera(float tpf) {
|
||||
if (input.activeLayer == SharedInput.LAYER_MODEL_EDITOR) {
|
||||
input.consumeMouseDelta(); // konsumieren ohne zu verarbeiten
|
||||
return;
|
||||
return; // ModelEditorState konsumiert das Delta selbst
|
||||
}
|
||||
int[] delta = input.consumeMouseDelta();
|
||||
if (delta[0] != 0 || delta[1] != 0) {
|
||||
|
||||
@@ -69,6 +69,9 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
private static final Logger log = LoggerFactory.getLogger(TreeGeneratorState.class);
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
private static final int ATLAS_DIRS = 4;
|
||||
private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS; // 2048
|
||||
private static final int ATLAS_H = IMPOSTOR_SIZE; // 512
|
||||
private static final int PREVIEW_SIZE = 1024;
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
|
||||
@@ -97,10 +100,12 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
private TreeMeshBuilder.MeshResult pendingHdResult = null;
|
||||
private Material pendingBarkMat = null;
|
||||
private Material pendingLeafMat = null;
|
||||
private ViewPort captureVP = null;
|
||||
private FrameBuffer captureFB = null;
|
||||
private Texture2D captureTex = null;
|
||||
private volatile boolean captureReady = false; // vom SceneProcessor gesetzt
|
||||
private ViewPort captureVP = null;
|
||||
private FrameBuffer captureFB = null;
|
||||
private Texture2D captureTex = null;
|
||||
private volatile boolean captureReady = false;
|
||||
private int capturePass = 0;
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
|
||||
public TreeGeneratorState(SharedInput input) { this.input = input; }
|
||||
|
||||
@@ -123,7 +128,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
previewVP = this.app.getRenderManager().createPostView("treePreview", previewCam);
|
||||
previewVP.setOutputFrameBuffer(previewFB);
|
||||
previewVP.setBackgroundColor(new ColorRGBA(0.50f, 0.72f, 0.95f, 1f));
|
||||
previewVP.setBackgroundColor(new ColorRGBA(0.15f, 0.15f, 0.18f, 1f));
|
||||
previewVP.setClearFlags(true, true, true);
|
||||
|
||||
previewScene = new Node("previewScene");
|
||||
@@ -137,8 +142,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
previewScene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.60f, 1f)));
|
||||
previewTreeHolder = new Node("treeHolder");
|
||||
previewScene.attachChild(previewTreeHolder);
|
||||
previewScene.attachChild(buildPreviewGround());
|
||||
previewScene.attachChild(buildPreviewSky());
|
||||
previewScene.attachChild(buildPreviewGrid());
|
||||
previewVP.attachScene(previewScene);
|
||||
|
||||
DirectionalLightShadowRenderer shadowRenderer =
|
||||
@@ -148,6 +152,19 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
shadowRenderer.setShadowIntensity(0.25f);
|
||||
shadowRenderer.setShadowZExtend(80f);
|
||||
previewVP.addProcessor(shadowRenderer);
|
||||
// Stellt sicher, dass previewScene direkt vor dem Rendern immer aktuell ist –
|
||||
// unabhängig davon, welche AppStates nach TreeGeneratorState die Szene noch ändern.
|
||||
previewVP.addProcessor(new SceneProcessor() {
|
||||
private boolean inited = false;
|
||||
@Override public void initialize(RenderManager rm, ViewPort v) { inited = true; }
|
||||
@Override public void reshape(ViewPort v, int w, int h) {}
|
||||
@Override public boolean isInitialized() { return inited; }
|
||||
@Override public void preFrame(float tpf) { previewScene.updateGeometricState(); }
|
||||
@Override public void postQueue(RenderQueue rq) {}
|
||||
@Override public void postFrame(FrameBuffer out) {}
|
||||
@Override public void cleanup() {}
|
||||
@Override public void setProfiler(AppProfiler profiler) {}
|
||||
});
|
||||
previewTransfer = new FrameTransfer(input.treePreviewImage);
|
||||
previewVP.addProcessor(previewTransfer);
|
||||
}
|
||||
@@ -193,8 +210,8 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
resizePreviewViewport(reqW, reqH);
|
||||
}
|
||||
|
||||
// 3. Kamera-Orbit + updateGeometricState immer zuletzt –
|
||||
// nach allen Szenenänderungen dieser Frame
|
||||
// 3. Kamera-Orbit – updateGeometricState wird jetzt per preFrame-SceneProcessor
|
||||
// direkt vor dem Rendern des previewVP aufgerufen (nach allen State-Updates).
|
||||
if (previewVP != null) {
|
||||
float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD;
|
||||
float rotX = input.treePreviewRotX * FastMath.DEG_TO_RAD;
|
||||
@@ -206,7 +223,6 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
previewTarget.y + FastMath.sin(rotX) * dist,
|
||||
FastMath.cos(rotY) * cosX * dist));
|
||||
c.lookAt(previewTarget, Vector3f.UNIT_Y);
|
||||
previewScene.updateGeometricState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,36 +282,41 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
Node hdNode = makeTreeNode(hd, barkMat, leafMat, "hd");
|
||||
Node ldNode = makeTreeNode(ld, barkMat.clone(), leafMat.clone(), "ld");
|
||||
|
||||
// Capture-Viewport aufbauen
|
||||
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
|
||||
captureFB.addColorTexture(captureTex);
|
||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||
|
||||
captureVP = buildCaptureViewPort(hdNode, hd.bounds(), captureFB);
|
||||
captureReady = false;
|
||||
pendingRequest = req;
|
||||
pendingHdNode = hdNode;
|
||||
pendingLdNode = ldNode;
|
||||
pendingRequest = req;
|
||||
pendingHdNode = hdNode;
|
||||
pendingLdNode = ldNode;
|
||||
pendingHdResult = hd;
|
||||
pendingBarkMat = barkMat;
|
||||
pendingLeafMat = leafMat;
|
||||
pendingBarkMat = barkMat;
|
||||
pendingLeafMat = leafMat;
|
||||
capturePass = 0;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
|
||||
input.treeGenStatusMsg = "Rendere Impostor…";
|
||||
startCapturePass(0);
|
||||
input.treeGenStatusMsg = "Rendere Impostor (1/" + ATLAS_DIRS + ")…";
|
||||
}
|
||||
|
||||
// ── Phase 2: Capture abschließen ──────────────────────────────────────────
|
||||
|
||||
private void finishCapture() {
|
||||
// Pixel aus Framebuffer lesen
|
||||
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
capturePixels[capturePass] = pixels;
|
||||
cleanupCapture();
|
||||
|
||||
if (capturePass < ATLAS_DIRS - 1) {
|
||||
capturePass++;
|
||||
input.treeGenStatusMsg = "Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…";
|
||||
startCapturePass(capturePass);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alle Richtungen erfasst – Atlas zusammensetzen
|
||||
String treeType = pendingRequest.treeType();
|
||||
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
|
||||
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + treeType + "_" + timestamp);
|
||||
ByteBuffer atlas = combineAtlas(capturePixels);
|
||||
Texture2D impostorTex = saveImpostor(atlas, "impostor_" + treeType + "_" + timestamp,
|
||||
ATLAS_W, ATLAS_H);
|
||||
|
||||
Node previewTree = makeTreeNode(pendingHdResult,
|
||||
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
|
||||
@@ -321,6 +342,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
pendingHdResult = null;
|
||||
pendingBarkMat = null;
|
||||
pendingLeafMat = null;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
}
|
||||
|
||||
// ── LOD-Aufbau ────────────────────────────────────────────────────────────
|
||||
@@ -346,7 +368,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
float h = bb.getYExtent() * 2f;
|
||||
float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f;
|
||||
float size = Math.max(h, w);
|
||||
float yOff = bb.getCenter().y + 2f; // passt zur Baum-Offset-Y
|
||||
float yOff = bb.getCenter().y + 2f;
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
if (tex != null) mat.setTexture("ColorMap", tex);
|
||||
@@ -355,14 +377,18 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
|
||||
Node n = new Node("lod2");
|
||||
n.attachChild(buildBillboardQuad("quad_a", 0f, yOff, size, mat));
|
||||
n.attachChild(buildBillboardQuad("quad_b", FastMath.HALF_PI, yOff, size, mat.clone()));
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
float angle = d * FastMath.HALF_PI;
|
||||
float uMin = (float) d / ATLAS_DIRS;
|
||||
float uMax = (float)(d + 1) / ATLAS_DIRS;
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, angle, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
return n;
|
||||
}
|
||||
|
||||
private Geometry buildBillboardQuad(String name, float yRot, float yCent,
|
||||
float size, Material mat) {
|
||||
float size, Material mat, float uMin, float uMax) {
|
||||
float hw = size * 0.5f;
|
||||
float hh = size * 0.5f;
|
||||
float cos = FastMath.cos(yRot);
|
||||
@@ -375,8 +401,10 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
hw*cos, yCent+hh, hw*sin,
|
||||
-hw*cos, yCent+hh, -hw*sin
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ 0,0, 1,0, 1,1, 0,1 });
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{
|
||||
uMin, 0, uMax, 0, uMax, 1, uMin, 1
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry g = new Geometry(name, mesh);
|
||||
@@ -384,6 +412,37 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
return g;
|
||||
}
|
||||
|
||||
/** Kombiniert 4 einzelne 512×512-Puffer zu einem horizontalen 2048×512-Atlas. */
|
||||
private ByteBuffer combineAtlas(ByteBuffer[] passes) {
|
||||
ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4);
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
ByteBuffer src = passes[d];
|
||||
src.rewind();
|
||||
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
|
||||
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
|
||||
int srcOff = (y * IMPOSTOR_SIZE + x) * 4;
|
||||
int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4;
|
||||
atlas.put(dstOff, src.get(srcOff));
|
||||
atlas.put(dstOff + 1, src.get(srcOff + 1));
|
||||
atlas.put(dstOff + 2, src.get(srcOff + 2));
|
||||
atlas.put(dstOff + 3, src.get(srcOff + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
return atlas;
|
||||
}
|
||||
|
||||
/** Startet einen einzelnen Capture-Durchlauf für die gegebene Richtung (Pass 0..3). */
|
||||
private void startCapturePass(int pass) {
|
||||
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
|
||||
captureFB.addColorTexture(captureTex);
|
||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||
float angle = pass * FastMath.HALF_PI;
|
||||
captureVP = buildCaptureViewPort(pendingHdNode, pendingHdResult.bounds(), captureFB, angle);
|
||||
captureReady = false;
|
||||
}
|
||||
|
||||
// ── Material-Factories ────────────────────────────────────────────────────
|
||||
|
||||
private Material buildBarkMaterial(TreeParams p) {
|
||||
@@ -458,13 +517,15 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
// ── Offscreen-ViewPort ────────────────────────────────────────────────────
|
||||
|
||||
private ViewPort buildCaptureViewPort(Node treeNode, BoundingBox bb, FrameBuffer fb) {
|
||||
private ViewPort buildCaptureViewPort(Node treeNode, BoundingBox bb, FrameBuffer fb, float angle) {
|
||||
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
|
||||
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
|
||||
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||
float dist = extent * 3.0f;
|
||||
|
||||
cam.setLocation(center.add(0f, 0f, dist));
|
||||
float camX = FastMath.sin(angle) * dist;
|
||||
float camZ = FastMath.cos(angle) * dist;
|
||||
cam.setLocation(center.add(camX, 0f, camZ));
|
||||
cam.lookAt(center, Vector3f.UNIT_Y);
|
||||
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
|
||||
|
||||
@@ -519,18 +580,17 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
// ── Impostor-PNG speichern ────────────────────────────────────────────────
|
||||
|
||||
private Texture2D saveImpostor(ByteBuffer pixels, String name) {
|
||||
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
|
||||
try {
|
||||
pixels.rewind();
|
||||
BufferedImage img = new BufferedImage(
|
||||
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
|
||||
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
|
||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int r = pixels.get() & 0xFF;
|
||||
int g = pixels.get() & 0xFF;
|
||||
int b = pixels.get() & 0xFF;
|
||||
int a = pixels.get() & 0xFF;
|
||||
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
|
||||
img.setRGB(x, height - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
|
||||
}
|
||||
}
|
||||
Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
|
||||
@@ -543,12 +603,12 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
return (Texture2D) assets.loadTexture("Textures/impostor/" + name + ".png");
|
||||
} catch (Exception loadEx) {
|
||||
pixels.rewind();
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, IMPOSTOR_SIZE, IMPOSTOR_SIZE,
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, width, height,
|
||||
pixels, null, com.jme3.texture.image.ColorSpace.sRGB);
|
||||
return new Texture2D(jmeImg);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("[TreeGenerator] Impostor-Fehler: " + e.getMessage());
|
||||
log.error("[Blight-Baum] Impostor-Fehler: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -615,45 +675,47 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
}
|
||||
|
||||
// ── Vorschau-Boden (groß, Gras-Textur) ──────────────────────────────────
|
||||
// ── Vorschau-Raster (Offscreen-VP, analog zum Objekt-Editor) ────────────
|
||||
// Rendert in previewFB — völlig isoliert vom Haupt-Viewport des Welt-Editors.
|
||||
|
||||
private Geometry buildPreviewGround() {
|
||||
float size = 600f;
|
||||
float tiles = 30f; // UV-Wiederholungen
|
||||
private Geometry buildPreviewGrid() {
|
||||
float halfSize = 20f;
|
||||
int n = (int)(halfSize * 2);
|
||||
int lines = (n + 1) * 2;
|
||||
|
||||
// Eigenes Mesh mit gekachelten UVs (Quad unterstützt kein Tiling)
|
||||
com.jme3.scene.Mesh mesh = new com.jme3.scene.Mesh();
|
||||
float h = size * 0.5f;
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3,
|
||||
new float[]{ -h,0,-h, h,0,-h, h,0,h, -h,0,h });
|
||||
mesh.setBuffer(VertexBuffer.Type.Normal, 3,
|
||||
new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 });
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
|
||||
new float[]{ 0,0, tiles,0, tiles,tiles, 0,tiles });
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3,
|
||||
new int[]{ 0,2,1, 0,3,2 });
|
||||
java.nio.FloatBuffer pos = BufferUtils.createFloatBuffer(lines * 2 * 3);
|
||||
java.nio.FloatBuffer col = BufferUtils.createFloatBuffer(lines * 2 * 4);
|
||||
|
||||
for (int i = 0; i <= n; i++) {
|
||||
float coord = -halfSize + i;
|
||||
boolean major5 = (Math.abs(Math.round(coord)) % 5) == 0;
|
||||
boolean major10 = (Math.abs(Math.round(coord)) % 10) == 0;
|
||||
float bright = major10 ? 0.70f : major5 ? 0.45f : 0.22f;
|
||||
|
||||
pos.put(coord).put(0).put(-halfSize);
|
||||
pos.put(coord).put(0).put( halfSize);
|
||||
col.put(bright).put(bright).put(bright).put(1f);
|
||||
col.put(bright).put(bright).put(bright).put(1f);
|
||||
|
||||
pos.put(-halfSize).put(0).put(coord);
|
||||
pos.put( halfSize).put(0).put(coord);
|
||||
col.put(bright).put(bright).put(bright).put(1f);
|
||||
col.put(bright).put(bright).put(bright).put(1f);
|
||||
}
|
||||
pos.flip(); col.flip();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setMode(Mesh.Mode.Lines);
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
|
||||
mesh.setBuffer(VertexBuffer.Type.Color, 4, col);
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry ground = new Geometry("previewGround", mesh);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setBoolean("VertexColor", true);
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
|
||||
mat.setBoolean("UseMaterialColors", true);
|
||||
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.45f, 0.14f, 1f));
|
||||
mat.setColor("Ambient", new ColorRGBA(0.11f, 0.18f, 0.06f, 1f));
|
||||
mat.setColor("Specular", ColorRGBA.Black);
|
||||
mat.setFloat("Shininess", 0f);
|
||||
|
||||
try {
|
||||
Texture grassTex = assets.loadTexture("Textures/gras.png");
|
||||
grassTex.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture("DiffuseMap", grassTex);
|
||||
} catch (Exception ignored) {
|
||||
// Fallback auf Farbe, wenn Textur fehlt
|
||||
}
|
||||
|
||||
ground.setMaterial(mat);
|
||||
ground.setShadowMode(RenderQueue.ShadowMode.Receive);
|
||||
return ground;
|
||||
Geometry geo = new Geometry("previewGrid", mesh);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
// ── Skybox (Kuppel) ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -4,10 +4,12 @@ import java.util.List;
|
||||
|
||||
public class GrassVertexTool extends EditorTool {
|
||||
|
||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
|
||||
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0);
|
||||
public final ToolParameter density = new ToolParameter("Dichte", 5.0, 1.0, 100.0);
|
||||
public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0);
|
||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
|
||||
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0);
|
||||
public final ToolParameter density = new ToolParameter("Dichte", 5.0, 1.0, 100.0);
|
||||
public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0);
|
||||
/** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */
|
||||
public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0);
|
||||
|
||||
@Override public String getName() { return "Gras (Vertices)"; }
|
||||
|
||||
@@ -15,5 +17,5 @@ public class GrassVertexTool extends EditorTool {
|
||||
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
|
||||
|
||||
@Override
|
||||
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness); }
|
||||
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness, uniformity); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package de.blight.editor.tree;
|
||||
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.util.BufferUtils;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Baut einen prozeduralen Farn als Rosette von Band-Fächern.
|
||||
*
|
||||
* UV-Atlas (aus FernPlantV2.obj abgeleitet):
|
||||
* 5 Fächer-Varianten nebeneinander in U-Richtung:
|
||||
* Spalte 0: U ∈ [0.081, 0.249]
|
||||
* Spalte 1: U ∈ [0.249, 0.416]
|
||||
* Spalte 2: U ∈ [0.416, 0.585]
|
||||
* Spalte 3: U ∈ [0.585, 0.750]
|
||||
* Spalte 4: U ∈ [0.750, 0.914]
|
||||
* V-Richtung: V=0 = Wurzel, V=1 = Spitze
|
||||
*
|
||||
* Pro Scheitelpunktpaar:
|
||||
* linke Kante = (U_spalteLinks, t)
|
||||
* rechte Kante = (U_spalteRechts, t)
|
||||
*
|
||||
* Breiten-Richtung: senkrecht zur Wirbelsäule, in der vertikalen Fächer-Ebene
|
||||
* W = normalize(−dly·cosAz, 1, −dly·sinAz) → Fächer stehen aufrecht
|
||||
*
|
||||
* Vertex-Farbe R = Wind-Gewicht (0 = Wurzel → 1 = Spitze).
|
||||
*/
|
||||
public class FernMeshBuilder {
|
||||
|
||||
// UV-Atlas: 2 Fächer-Varianten (Spalten 0+1 und Spalten 3+4).
|
||||
// Spalte 2 [0.416..0.585] enthält in der Textur kein Blatt → nur 2 Varianten.
|
||||
private static final float[] FROND_UL = { 0.081f, 0.585f }; // linke Spitze
|
||||
private static final float[] FROND_SPINE = { 0.249f, 0.750f }; // Mittelrippe
|
||||
private static final float[] FROND_UR = { 0.416f, 0.914f }; // rechte Spitze
|
||||
|
||||
public static Node build(FernOptions opts) {
|
||||
Random rng = new Random(opts.seed);
|
||||
Node fern = new Node("fern");
|
||||
float baseAng = rng.nextFloat() * FastMath.TWO_PI;
|
||||
|
||||
for (int i = 0; i < opts.frondCount; i++) {
|
||||
float az = baseAng + i * (FastMath.TWO_PI / opts.frondCount)
|
||||
+ (rng.nextFloat() - 0.5f) * 0.55f;
|
||||
float lScale = 0.75f + rng.nextFloat() * 0.50f;
|
||||
float drScale = 0.80f + rng.nextFloat() * 0.40f;
|
||||
float til = opts.tiltMin + rng.nextFloat() * (opts.tiltMax - opts.tiltMin);
|
||||
int variant = rng.nextInt(FROND_UL.length);
|
||||
Geometry g = buildFrond(opts, az, lScale, drScale, til, variant);
|
||||
g.setName("frond_" + i);
|
||||
fern.attachChild(g);
|
||||
}
|
||||
return fern;
|
||||
}
|
||||
|
||||
private static Geometry buildFrond(FernOptions opts, float az,
|
||||
float lScale, float drScale, float til, int variant) {
|
||||
int S = Math.max(2, opts.frondSegments);
|
||||
float len = opts.frondLength * lScale;
|
||||
float drp = opts.droop * drScale;
|
||||
float tilRad = til * FastMath.DEG_TO_RAD;
|
||||
|
||||
float cosA = FastMath.cos(az);
|
||||
float sinA = FastMath.sin(az);
|
||||
|
||||
float cosTil = FastMath.cos(tilRad);
|
||||
float sinTil = FastMath.sin(tilRad);
|
||||
|
||||
float uL = FROND_UL[variant];
|
||||
float uSp = FROND_SPINE[variant];
|
||||
float uR = FROND_UR[variant];
|
||||
|
||||
// Breiten-Richtung: horizontal, senkrecht zur Azimut-Richtung
|
||||
float wx = sinA, wz = -cosA; // wy = 0
|
||||
|
||||
// 3 Vertex-Spalten pro Zeile: linke Spitze, Mittelrippe, rechte Spitze
|
||||
int vCount = (S + 1) * 3;
|
||||
int iCount = S * 12; // 4 Dreiecke × 3 Indices
|
||||
|
||||
FloatBuffer pos = BufferUtils.createFloatBuffer(vCount * 3);
|
||||
FloatBuffer norm = BufferUtils.createFloatBuffer(vCount * 3);
|
||||
FloatBuffer uv = BufferUtils.createFloatBuffer(vCount * 2);
|
||||
FloatBuffer tan = BufferUtils.createFloatBuffer(vCount * 4);
|
||||
FloatBuffer col4 = BufferUtils.createFloatBuffer(vCount * 4);
|
||||
IntBuffer idx = BufferUtils.createIntBuffer(iCount);
|
||||
|
||||
// Breite aus UV-Aspektverhältnis (quadratische Textur vorausgesetzt)
|
||||
float hw = len * (uR - uL) * 0.5f;
|
||||
|
||||
for (int j = 0; j <= S; j++) {
|
||||
float t = (float) j / S;
|
||||
// s = Bogenlänge entlang der Wirbelsäule (korrekte Winkel für alle Neigungen)
|
||||
float s = t * len;
|
||||
// eff = vertikale Ableitung d(cy)/d(s) inkl. Droop
|
||||
float eff = sinTil - 2f * drp * s / len;
|
||||
|
||||
float cx = cosA * cosTil * s;
|
||||
float cy = sinTil * s - drp * s * s / len;
|
||||
float cz = sinA * cosTil * s;
|
||||
|
||||
// Normale: N = T × W (T = Tangente, W = Breitenrichtung)
|
||||
// ergibt (-eff·cosA, cosTil, -eff·sinA), dann normieren
|
||||
float nLen = FastMath.sqrt(eff * eff + cosTil * cosTil);
|
||||
float nx = -eff * cosA / nLen, ny = cosTil / nLen, nz = -eff * sinA / nLen;
|
||||
|
||||
// Tangente entlang der Wirbelsäule (normiert durch nLen = |T|)
|
||||
float tx = cosA * cosTil / nLen, ty = eff / nLen, tz = sinA * cosTil / nLen;
|
||||
|
||||
int vi = j * 3; // linke Spitze=vi, Mittelrippe=vi+1, rechte Spitze=vi+2
|
||||
|
||||
// linke Spitze (+W)
|
||||
putVec3(pos, vi*3, cx + hw*wx, cy, cz + hw*wz);
|
||||
putVec3(norm, vi*3, nx, ny, nz);
|
||||
putVec2(uv, vi*2, uL, t);
|
||||
putVec4(tan, vi*4, tx, ty, tz, 1f);
|
||||
putVec4(col4, vi*4, t, 0f, 0f, 1f);
|
||||
|
||||
// Mittelrippe
|
||||
putVec3(pos, vi*3+3, cx, cy, cz);
|
||||
putVec3(norm, vi*3+3, nx, ny, nz);
|
||||
putVec2(uv, vi*2+2, uSp, t);
|
||||
putVec4(tan, vi*4+4, tx, ty, tz, 1f);
|
||||
putVec4(col4, vi*4+4, t, 0f, 0f, 1f);
|
||||
|
||||
// rechte Spitze (−W)
|
||||
putVec3(pos, vi*3+6, cx - hw*wx, cy, cz - hw*wz);
|
||||
putVec3(norm, vi*3+6, nx, ny, nz);
|
||||
putVec2(uv, vi*2+4, uR, t);
|
||||
putVec4(tan, vi*4+8, tx, ty, tz, 1f);
|
||||
putVec4(col4, vi*4+8, t, 0f, 0f, 1f);
|
||||
|
||||
if (j < S) {
|
||||
int base = j * 12;
|
||||
int vj = j * 3, vj1 = (j+1) * 3;
|
||||
|
||||
// Linkes Panel: L(j), C(j), L(j+1) und L(j+1), C(j), C(j+1)
|
||||
idx.put(base, vj); idx.put(base+1, vj+1); idx.put(base+2, vj1);
|
||||
idx.put(base+3, vj1); idx.put(base+4, vj+1); idx.put(base+5, vj1+1);
|
||||
|
||||
// Rechtes Panel: C(j), R(j), C(j+1) und C(j+1), R(j), R(j+1)
|
||||
idx.put(base+6, vj+1); idx.put(base+7, vj+2); idx.put(base+8, vj1+1);
|
||||
idx.put(base+9, vj1+1); idx.put(base+10, vj+2); idx.put(base+11, vj1+2);
|
||||
}
|
||||
}
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
pos.rewind(); norm.rewind(); uv.rewind(); tan.rewind(); col4.rewind(); idx.rewind();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
|
||||
mesh.setBuffer(VertexBuffer.Type.Normal, 3, norm);
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv);
|
||||
mesh.setBuffer(VertexBuffer.Type.Tangent, 4, tan);
|
||||
mesh.setBuffer(VertexBuffer.Type.Color, 4, col4);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
|
||||
mesh.updateBound();
|
||||
mesh.updateCounts();
|
||||
return new Geometry("frond", mesh);
|
||||
}
|
||||
|
||||
private static void putVec2(FloatBuffer b, int pos, float x, float y) {
|
||||
b.put(pos, x); b.put(pos+1, y);
|
||||
}
|
||||
private static void putVec3(FloatBuffer b, int pos, float x, float y, float z) {
|
||||
b.put(pos, x); b.put(pos+1, y); b.put(pos+2, z);
|
||||
}
|
||||
private static void putVec4(FloatBuffer b, int pos, float x, float y, float z, float w) {
|
||||
b.put(pos, x); b.put(pos+1, y); b.put(pos+2, z); b.put(pos+3, w);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.blight.editor.tree;
|
||||
|
||||
public class FernOptions {
|
||||
public int seed = 42;
|
||||
public int frondCount = 15;
|
||||
public float frondLength = 0.90f;
|
||||
public float droop = 0.60f;
|
||||
public float tiltMin = 30f; // Grad vom Boden (0 = flach, 90 = senkrecht)
|
||||
public float tiltMax = 60f;
|
||||
public int frondSegments = 6;
|
||||
public float heightVariation = 0.4f;
|
||||
public float windStrength = 0.15f;
|
||||
public float windSpeed = 0.60f;
|
||||
|
||||
public FernOptions copy() {
|
||||
FernOptions c = new FernOptions();
|
||||
c.seed = seed; c.frondCount = frondCount; c.frondLength = frondLength;
|
||||
c.droop = droop;
|
||||
c.tiltMin = tiltMin; c.tiltMax = tiltMax;
|
||||
c.frondSegments = frondSegments; c.heightVariation = heightVariation;
|
||||
c.windStrength = windStrength; c.windSpeed = windSpeed;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
<logger name="com.jme3" level="WARN"/>
|
||||
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||
<logger name="com.jme3.scene.plugins.gltf" level="ERROR"/>
|
||||
<!-- TangentBinormalGenerator warnt bei UV-Nähten und harten Kanten – erwartet, kein Fehler -->
|
||||
<logger name="com.jme3.util.TangentBinormalGenerator" level="ERROR"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
|
||||
@@ -12,7 +12,8 @@ public enum AnimationAction {
|
||||
SPRINT,
|
||||
JUMP,
|
||||
RUNNING_JUMP,
|
||||
DUCK;
|
||||
DUCK,
|
||||
PICK_UP;
|
||||
|
||||
|
||||
/** Lesbare Bezeichnung für UI-Anzeige. */
|
||||
@@ -26,6 +27,7 @@ public enum AnimationAction {
|
||||
case JUMP -> "Jump";
|
||||
case RUNNING_JUMP -> "Running Jump";
|
||||
case DUCK -> "Duck";
|
||||
case PICK_UP -> "Pick up";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,23 +5,25 @@ import com.jme3.input.KeyInput;
|
||||
/** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */
|
||||
public class KeyBindings {
|
||||
|
||||
public int forward = KeyInput.KEY_W;
|
||||
public int backward = KeyInput.KEY_S;
|
||||
public int left = KeyInput.KEY_A;
|
||||
public int right = KeyInput.KEY_D;
|
||||
public int jump = KeyInput.KEY_SPACE;
|
||||
public int sprint = KeyInput.KEY_LSHIFT;
|
||||
public int walk = KeyInput.KEY_LMENU;
|
||||
public int forward = KeyInput.KEY_W;
|
||||
public int backward = KeyInput.KEY_S;
|
||||
public int left = KeyInput.KEY_A;
|
||||
public int right = KeyInput.KEY_D;
|
||||
public int jump = KeyInput.KEY_SPACE;
|
||||
public int sprint = KeyInput.KEY_LSHIFT;
|
||||
public int walk = KeyInput.KEY_LMENU;
|
||||
public int interact = KeyInput.KEY_E;
|
||||
|
||||
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
||||
public static final String[][] ENTRIES = {
|
||||
{"forward", "Vorwärts"},
|
||||
{"backward", "Rückwärts"},
|
||||
{"left", "Links"},
|
||||
{"right", "Rechts"},
|
||||
{"jump", "Springen"},
|
||||
{"sprint", "Rennen"},
|
||||
{"walk", "Gehen"},
|
||||
{"forward", "Vorwärts"},
|
||||
{"backward", "Rückwärts"},
|
||||
{"left", "Links"},
|
||||
{"right", "Rechts"},
|
||||
{"jump", "Springen"},
|
||||
{"sprint", "Rennen"},
|
||||
{"walk", "Gehen"},
|
||||
{"interact", "Interagieren"},
|
||||
};
|
||||
|
||||
public int get(String fieldName) {
|
||||
|
||||
@@ -47,6 +47,9 @@ public class PlayerInputControl {
|
||||
private String runningClip;
|
||||
/** Frames, für die JUMP erzwungen wird (überbrückt onGround()-Lag). */
|
||||
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) -> {
|
||||
if (paused) return;
|
||||
@@ -117,9 +120,33 @@ public class PlayerInputControl {
|
||||
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) {
|
||||
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 camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
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.RigidBodyControl;
|
||||
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.LocationState;
|
||||
import de.blight.game.state.RiverState;
|
||||
import de.blight.game.state.TerrainChunkState;
|
||||
import de.blight.game.state.WaterBodyState;
|
||||
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 java.io.IOException;
|
||||
@@ -58,6 +60,7 @@ public class WorldScene extends BaseAppState {
|
||||
private BulletAppState bulletAppState;
|
||||
private MapData loadedMapData;
|
||||
private FilterPostProcessor sharedFPP;
|
||||
private TerrainChunkState terrainChunkState;
|
||||
|
||||
private final KeyBindings keyBindings;
|
||||
private ThirdPersonCamera thirdPersonCam;
|
||||
@@ -109,11 +112,12 @@ public class WorldScene extends BaseAppState {
|
||||
buildLighting();
|
||||
|
||||
BlightGame.status("Lade Terrain...");
|
||||
TerrainQuad terrain = buildTerrain();
|
||||
buildChunkTerrain();
|
||||
|
||||
BlightGame.status("Lade Vegetation...");
|
||||
app.getStateManager().attach(new GrassState(terrain));
|
||||
app.getStateManager().attach(new GrassVertexRenderState());
|
||||
app.getStateManager().attach(new GrassState(terrainChunkState));
|
||||
GrassVertexRenderState gvrs = new GrassVertexRenderState(terrainChunkState);
|
||||
app.getStateManager().attach(gvrs);
|
||||
|
||||
BlightGame.status("Lade Wasserflächen...");
|
||||
app.getStateManager().attach(new WaterBodyState(sharedFPP));
|
||||
@@ -147,6 +151,9 @@ public class WorldScene extends BaseAppState {
|
||||
MainCharacter mc = findMainCharacter();
|
||||
if (mc != null) {
|
||||
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
|
||||
@@ -322,117 +329,41 @@ public class WorldScene extends BaseAppState {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Terrain
|
||||
// Terrain (Chunk-basiert)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Baut das Terrain. Falls eine gespeicherte Karte vorhanden ist, wird diese
|
||||
* geladen; andernfalls wird ein prozedurales Demo-Terrain erzeugt.
|
||||
* Setzt außerdem {@link #spawnY}.
|
||||
* Erstellt den chunk-basierten TerrainChunkState.
|
||||
* Lädt MapData falls vorhanden, berechnet Spawn-Y aus Chunk-Höhen,
|
||||
* erstellt ein gemeinsames Terrain-Material und hängt den State ein.
|
||||
*/
|
||||
private TerrainQuad buildTerrain() {
|
||||
private void buildChunkTerrain() {
|
||||
if (MapIO.exists()) {
|
||||
try {
|
||||
loadedMapData = MapIO.load();
|
||||
return buildTerrainFromMap(loadedMapData);
|
||||
} catch (IOException e) {
|
||||
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];
|
||||
System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Temp-Spawn aus Editor-Property überschreibt gespeicherten Karten-Spawn
|
||||
// Spawn aus Map oder Editor-Property
|
||||
String propX = System.getProperty("blight.temp.spawn.x");
|
||||
String propZ = System.getProperty("blight.temp.spawn.z");
|
||||
spawnX = propX != null ? Float.parseFloat(propX) : map.spawnX;
|
||||
spawnZ = propZ != null ? Float.parseFloat(propZ) : map.spawnZ;
|
||||
System.out.println("[WorldScene] SpawnXZ Quelle: " + (propX != null ? "Editor-Property" : "Karte")
|
||||
+ " → 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;
|
||||
if (loadedMapData != null) {
|
||||
spawnX = propX != null ? Float.parseFloat(propX) : loadedMapData.spawnX;
|
||||
spawnZ = propZ != null ? Float.parseFloat(propZ) : loadedMapData.spawnZ;
|
||||
}
|
||||
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;
|
||||
|
||||
// ── 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;
|
||||
System.out.println("[WorldScene] SpawnXYZ=(" + spawnX + ", " + spawnY + ", " + spawnZ + ")");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -588,7 +519,7 @@ public class WorldScene extends BaseAppState {
|
||||
new ColorRGBA(0.80f, 0.72f, 0.50f, 1f),
|
||||
};
|
||||
|
||||
private void applyTerrainMaterial(TerrainQuad terrain, MapData map) {
|
||||
private Material buildTerrainMaterial(MapData map) {
|
||||
if (map != null) {
|
||||
try {
|
||||
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.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
mat.setTexture("AlphaMap", splatTex);
|
||||
|
||||
terrain.setMaterial(mat);
|
||||
return;
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WorldScene] Splat-Material fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
@@ -646,13 +575,13 @@ public class WorldScene extends BaseAppState {
|
||||
mat.setTexture("DiffuseMap", grass);
|
||||
mat.setFloat("DiffuseMap_0_scale", 32f);
|
||||
mat.setBoolean("useTriPlanarMapping", false);
|
||||
terrain.setMaterial(mat);
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
|
||||
mat.setBoolean("UseMaterialColors", true);
|
||||
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
|
||||
mat.setColor("Ambient", new ColorRGBA(0.15f, 0.30f, 0.09f, 1f));
|
||||
terrain.setMaterial(mat);
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassTuft;
|
||||
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 int INIT_PER_FRAME = 4;
|
||||
|
||||
private final TerrainQuad terrain;
|
||||
private final TerrainChunkState terrainChunkState;
|
||||
|
||||
private Camera cam;
|
||||
private Node grassNode;
|
||||
@@ -54,8 +53,8 @@ public class GrassState extends BaseAppState {
|
||||
private final Map<Integer, Material> slotMaterials = new LinkedHashMap<>();
|
||||
private int nextChunk = 0;
|
||||
|
||||
public GrassState(TerrainQuad terrain) {
|
||||
this.terrain = terrain;
|
||||
public GrassState(TerrainChunkState terrainChunkState) {
|
||||
this.terrainChunkState = terrainChunkState;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
@@ -176,7 +175,7 @@ public class GrassState extends BaseAppState {
|
||||
for (int b = 0; b < BLADES_PER_TUFT; b++) {
|
||||
float bx = t.x() + (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;
|
||||
float h = t.height() * (0.7f + rng.nextFloat() * 0.6f);
|
||||
blades.add(new float[]{bx, th, bz, h});
|
||||
|
||||
@@ -8,15 +8,11 @@ import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
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.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassVertexBlade;
|
||||
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,
|
||||
* 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 ────────────────────────────────────────────────────────────────
|
||||
private static final int TERRAIN_HALF = 2048;
|
||||
@@ -36,20 +34,23 @@ public class GrassVertexRenderState extends BaseAppState {
|
||||
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 INIT_PER_FRAME = 4;
|
||||
private static final float FAR_DIST_SQ = 200f * 200f;
|
||||
|
||||
// ── Geometrie (identisch zu GrassVertexState) ─────────────────────────────
|
||||
private static final int BLADES_PER_TUFT = 3;
|
||||
private static final int SEGMENTS = 5;
|
||||
private static final float WIDTH_FACTOR = 0.05f;
|
||||
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 TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 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 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);
|
||||
// 0–50 % 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_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
|
||||
// 50–100 % 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 ───────────────────────────────────────────────────────────────
|
||||
private Camera cam;
|
||||
private final TerrainChunkState terrainChunkState;
|
||||
private AssetManager assetManager;
|
||||
private Node grassNode;
|
||||
private Material material;
|
||||
@@ -59,12 +60,16 @@ public class GrassVertexRenderState extends BaseAppState {
|
||||
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
|
||||
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
|
||||
|
||||
public GrassVertexRenderState(TerrainChunkState terrainChunkState) {
|
||||
this.terrainChunkState = terrainChunkState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.cam = app.getCamera();
|
||||
this.assetManager = app.getAssetManager();
|
||||
grassNode = new Node("grassVertexNode");
|
||||
((SimpleApplication) app).getRootNode().attachChild(grassNode);
|
||||
terrainChunkState.addChunkListener(this);
|
||||
|
||||
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
|
||||
|
||||
@@ -82,6 +87,7 @@ public class GrassVertexRenderState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
terrainChunkState.removeChunkListener(this);
|
||||
((SimpleApplication) app).getRootNode().detachChild(grassNode);
|
||||
}
|
||||
|
||||
@@ -158,18 +164,35 @@ public class GrassVertexRenderState extends BaseAppState {
|
||||
Geometry geo = new Geometry("gv_" + ci, mesh);
|
||||
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.attachChild(geo);
|
||||
node.addControl(new VisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ)));
|
||||
chunkNodes[ci] = 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) ───────────────────────────
|
||||
|
||||
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 dg = DRY_ROOT_COLOR.g + (DRY_TIP_COLOR.g - DRY_ROOT_COLOR.g) * wf;
|
||||
float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
|
||||
col[ci] = gr + (dr - gr) * dryness;
|
||||
col[ci+1] = gg + (dg - gg) * dryness;
|
||||
col[ci+2] = gb + (db - gb) * dryness;
|
||||
col[ci+3] = 1f;
|
||||
float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
|
||||
float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
|
||||
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
||||
// Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun
|
||||
float fr, fg, fb;
|
||||
if (dryness <= 0.5f) {
|
||||
float t = dryness * 2f;
|
||||
fr = gr + (dr - gr) * t;
|
||||
fg = gg + (dg - gg) * t;
|
||||
fb = gb + (db - gb) * t;
|
||||
} else {
|
||||
float t = (dryness - 0.5f) * 2f;
|
||||
fr = dr + (vr - dr) * t;
|
||||
fg = dg + (vg - dg) * t;
|
||||
fb = db + (vb - db) * t;
|
||||
}
|
||||
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
|
||||
|
||||
int ti = vi * 2;
|
||||
tex[ti] = wf; tex[ti+1] = 0f;
|
||||
}
|
||||
|
||||
// ── 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) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (0–1 ü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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,19 @@ public class WorldObjectsState extends BaseAppState {
|
||||
spatial = assets.loadModel(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;
|
||||
}
|
||||
|
||||
|
||||
BIN
blight-map/src/main/map/blight_ferns.blf
Normal file
@@ -1,31 +1,45 @@
|
||||
# modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow
|
||||
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_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 -37.63680 32.63404 -18.65228 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
|
||||
@box -471.37857 22.27976 -91.47378 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_0.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
|
||||
@group -462.16046 3.59008 -122.32437 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_2.j3o true true
|
||||
Models/Palm_Palme1.j3o -245.90610 165.20883 99.17912 -6.43500 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
|
||||
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_075134.j3o -254.69368 165.64214 91.69685 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
|
||||
Models/Palm_Palme1.j3o -205.20802 165.35493 88.46772 -18.77974 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
|
||||
@plane -224.78107 164.15782 108.96712 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_3.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
|
||||
@plane -237.96001 165.08000 113.44000 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_5.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
|
||||
Models/Tree/Tree.mesh.j3o -246.86934 164.83850 107.50585 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
|
||||
Models/Campfire.j3o -350.02148 164.72334 72.42865 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
|
||||
Models/tree.j3o -28.09666 16.39296 -34.64375 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
|
||||
Models/Tree/Tree.mesh.j3o 295.18826 1.00000 189.92809 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
|
||||
Models/Buggy/Buggy.j3o 296.60590 1.00000 201.24153 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
|
||||
Models/tree.j3o 221.98000 1.00000 130.25000 0.00000 1.00000 0.00000 0.00000 true 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
|
||||
# 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
|
||||
|
||||