Weiter am Editor gearbeitet, unter anderem LOD System, Items, Trees, Modelle
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user