Weiter gearbeitet
This commit is contained in:
@@ -13,6 +13,7 @@ import de.blight.game.state.SaveGameState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
import de.blight.game.console.JmeConsole;
|
||||
import de.blight.game.scene.WorldScene;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -31,6 +32,7 @@ public class BlightGame extends SimpleApplication {
|
||||
private KeyBindings keyBindings;
|
||||
private GraphicsSettings graphicsSettings;
|
||||
private ScreenshotAppState screenshotState;
|
||||
private Path screenshotDir;
|
||||
private WorldScene worldScene;
|
||||
private ConfigScreen configScreen;
|
||||
private GraphicsScreen graphicsScreen;
|
||||
@@ -180,9 +182,28 @@ public class BlightGame extends SimpleApplication {
|
||||
stateManager.attach(pauseMenu);
|
||||
pauseMenu.setEnabled(false);
|
||||
|
||||
// ── Konsole (^) ──────────────────────────────────────────────────────────
|
||||
JmeConsole console = new JmeConsole();
|
||||
registerGameCommands(console);
|
||||
console.setOnVisibilityChanged(open -> {
|
||||
if (open) {
|
||||
worldScene.setPaused(true);
|
||||
} else if (!pauseMenu.isEnabled()) {
|
||||
worldScene.setPaused(false);
|
||||
}
|
||||
});
|
||||
stateManager.attach(console);
|
||||
|
||||
// ── Debug: Lighting-Toggle (F8) ──────────────────────────────────────────
|
||||
inputManager.addMapping("DebugNoLight", new KeyTrigger(KeyInput.KEY_F8));
|
||||
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
|
||||
if (!isPressed) return;
|
||||
worldScene.toggleDebugNoLight();
|
||||
}, "DebugNoLight");
|
||||
|
||||
// ── Screenshot (F12) ─────────────────────────────────────────────────────
|
||||
try {
|
||||
Path screenshotDir = BlightHome.resolve("screenshots");
|
||||
screenshotDir = BlightHome.resolve("screenshots");
|
||||
Files.createDirectories(screenshotDir);
|
||||
screenshotState = new ScreenshotAppState(screenshotDir + File.separator, "screenshot");
|
||||
stateManager.attach(screenshotState);
|
||||
@@ -192,7 +213,10 @@ public class BlightGame extends SimpleApplication {
|
||||
}
|
||||
inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12));
|
||||
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
|
||||
if (isPressed && screenshotState != null) screenshotState.takeScreenshot();
|
||||
if (isPressed && screenshotState != null) {
|
||||
log.info("[Screenshot] Speichere in: {}", screenshotDir.toAbsolutePath());
|
||||
screenshotState.takeScreenshot();
|
||||
}
|
||||
}, "Screenshot");
|
||||
|
||||
// ── Schnellspeichern (F5, konfigurierbar) ────────────────────────────
|
||||
@@ -295,4 +319,68 @@ public class BlightGame extends SimpleApplication {
|
||||
status("Bereit");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Konsolen-Befehle ─────────────────────────────────────────────────────
|
||||
|
||||
private void registerGameCommands(JmeConsole console) {
|
||||
console.registerCommand("goto", args -> {
|
||||
WorldScene ws = stateManager.getState(WorldScene.class);
|
||||
if (ws == null || !ws.isEnabled()) return "Welt nicht geladen";
|
||||
try {
|
||||
if (args.length >= 4) {
|
||||
float x = Float.parseFloat(args[1]);
|
||||
float y = Float.parseFloat(args[2]);
|
||||
float z = Float.parseFloat(args[3]);
|
||||
return ws.teleportPlayer(x, y, z);
|
||||
} else if (args.length >= 3) {
|
||||
float x = Float.parseFloat(args[1]);
|
||||
float z = Float.parseFloat(args[2]);
|
||||
return ws.teleportPlayer(x, Float.NaN, z);
|
||||
}
|
||||
return "Syntax: goto <x> <z> oder goto <x> <y> <z>";
|
||||
} catch (NumberFormatException e) {
|
||||
return "Fehler: Koordinaten müssen Zahlen sein";
|
||||
}
|
||||
});
|
||||
|
||||
console.registerCommand("pos", args -> {
|
||||
WorldScene ws = stateManager.getState(WorldScene.class);
|
||||
if (ws == null || !ws.isEnabled()) return "Welt nicht geladen";
|
||||
com.jme3.math.Vector3f p = ws.getPlayerLocation();
|
||||
return String.format("Position: X=%.1f Y=%.1f Z=%.1f", p.x, p.y, p.z);
|
||||
});
|
||||
|
||||
console.registerCommand("time", args -> {
|
||||
if (args.length < 2) return "Syntax: time <0–24> (0=Mitternacht, 12=Mittag)";
|
||||
try {
|
||||
float hours = Float.parseFloat(args[1]);
|
||||
if (hours < 0 || hours > 24) return "Fehler: Wert zwischen 0 und 24";
|
||||
de.blight.game.state.DayNightState dns =
|
||||
stateManager.getState(de.blight.game.state.DayNightState.class);
|
||||
if (dns == null) return "Tag/Nacht-System nicht aktiv";
|
||||
dns.getDayTime().setTimeOfDay(hours / 24f);
|
||||
int h = (int) hours, m = (int)((hours - h) * 60f);
|
||||
return String.format("Zeit gesetzt: %02d:%02d Uhr", h, m);
|
||||
} catch (NumberFormatException e) {
|
||||
return "Fehler: Zahl erwartet";
|
||||
}
|
||||
});
|
||||
|
||||
console.registerCommand("weather", args -> {
|
||||
de.blight.game.state.WeatherState ws =
|
||||
stateManager.getState(de.blight.game.state.WeatherState.class);
|
||||
if (ws == null) return "Wettersystem nicht aktiv";
|
||||
if (args.length < 2)
|
||||
return "Aktuell: " + ws.getActiveWeather()
|
||||
+ " | Syntax: weather <sunny|cloudy|overcast|storm>";
|
||||
try {
|
||||
de.blight.game.state.WeatherState.Weather w =
|
||||
de.blight.game.state.WeatherState.Weather.valueOf(args[1].toUpperCase());
|
||||
ws.forceWeather(w);
|
||||
return "Wetter gesetzt: " + w;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return "Unbekanntes Wetter. Erlaubt: sunny, cloudy, overcast, storm";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package de.blight.game;
|
||||
|
||||
import de.blight.common.MapIO;
|
||||
import de.blight.common.BlightHome;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Schreibt Live-Daten (Spielerposition) in eine Temp-Datei neben der Karte,
|
||||
* Schreibt Live-Daten (Spielerposition) in eine Temp-Datei im BlightHome-Verzeichnis,
|
||||
* damit der Editor sie live anzeigen kann.
|
||||
*/
|
||||
public final class LiveBroadcast {
|
||||
|
||||
public static final Path POS_FILE =
|
||||
MapIO.getMapPath().resolveSibling("blight_live.pos");
|
||||
public static final Path POS_FILE = BlightHome.resolve("blight_live.pos");
|
||||
|
||||
private LiveBroadcast() {}
|
||||
|
||||
|
||||
@@ -4,11 +4,16 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import de.blight.common.BlightHome;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class GraphicsStore {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GraphicsStore.class);
|
||||
|
||||
private static final Path FILE = BlightHome.resolve("config", "graphics.json");
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
@@ -17,7 +22,7 @@ public class GraphicsStore {
|
||||
try (Reader r = Files.newBufferedReader(FILE)) {
|
||||
return GSON.fromJson(r, GraphicsSettings.class);
|
||||
} catch (IOException e) {
|
||||
System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage());
|
||||
log.warn("graphics.json konnte nicht geladen werden: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return new GraphicsSettings();
|
||||
@@ -30,7 +35,7 @@ public class GraphicsStore {
|
||||
GSON.toJson(gs, w);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage());
|
||||
log.error("graphics.json konnte nicht gespeichert werden: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,16 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import de.blight.common.BlightHome;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class KeyBindingStore {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyBindingStore.class);
|
||||
|
||||
private static final Path FILE = BlightHome.resolve("config", "keybindings.json");
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
@@ -17,7 +22,7 @@ public class KeyBindingStore {
|
||||
try (Reader r = Files.newBufferedReader(FILE)) {
|
||||
return GSON.fromJson(r, KeyBindings.class);
|
||||
} catch (IOException e) {
|
||||
System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage());
|
||||
log.warn("keybindings.json konnte nicht geladen werden: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return new KeyBindings();
|
||||
@@ -30,7 +35,7 @@ public class KeyBindingStore {
|
||||
GSON.toJson(kb, w);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage());
|
||||
log.error("keybindings.json konnte nicht gespeichert werden: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,12 @@ public class JmeConsole extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyInputEvent e) {
|
||||
if (!e.isPressed()) return;
|
||||
int key = e.getKeyCode();
|
||||
if (key == KEY_TOGGLE) { toggle(); return; }
|
||||
if (key == KEY_TOGGLE && e.isPressed()) { toggle(); e.setConsumed(); return; }
|
||||
if (!open) return;
|
||||
// Konsole ist offen: alle Events konsumieren damit keine Ingame-Bindings feuern
|
||||
e.setConsumed();
|
||||
if (!e.isPressed()) return;
|
||||
char c = e.getKeyChar();
|
||||
if (key == KeyInput.KEY_RETURN) feedEnter();
|
||||
else if (key == KeyInput.KEY_BACK) feedBackspace();
|
||||
|
||||
@@ -15,10 +15,15 @@ import de.blight.game.animation.AnimationLibrary;
|
||||
import de.blight.game.animation.RetargetingSystem;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class PlayerInputControl {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||
|
||||
private static final float MOVE_SPEED = 0.07f;
|
||||
private static final float SPRINT_MULT = 1.5f;
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
@@ -85,7 +90,7 @@ public class PlayerInputControl {
|
||||
this.currentAnim = null;
|
||||
this.runningClip = null;
|
||||
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
||||
System.out.println("[AnimCtx] AnimComposer gefunden: " + (animComposer != null));
|
||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||
if (animSetName != null) {
|
||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.IDLE);
|
||||
if (clip != null && tryPlay(clip)) {
|
||||
@@ -243,13 +248,12 @@ public class PlayerInputControl {
|
||||
if (animLib == null || visual == null || animSetName == null) {
|
||||
if (!animCtxLogged) {
|
||||
animCtxLogged = true;
|
||||
System.out.println("[Anim] Kein Animations-Kontext:"
|
||||
+ " animLib=" + animLib + " visual=" + visual + " setName=" + animSetName);
|
||||
log.info("[Anim] Kein Animations-Kontext: animLib={} visual={} setName={}", animLib, visual, animSetName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||
System.out.println("[Anim] " + action + " → clip='" + clip + "' (set=" + animSetName + ")");
|
||||
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
|
||||
if (clip != null && tryPlay(clip)) return;
|
||||
if (action != AnimationAction.DEFAULT) {
|
||||
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
|
||||
@@ -259,11 +263,11 @@ public class PlayerInputControl {
|
||||
|
||||
private boolean tryPlay(String clip) {
|
||||
if (animComposer == null || !animLib.ensureApplied(clip, visual)) {
|
||||
System.out.println("[Anim] tryPlay('" + clip + "') → ensureApplied FAILED");
|
||||
log.info("[Anim] tryPlay('{}') → ensureApplied FAILED", clip);
|
||||
return false;
|
||||
}
|
||||
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
|
||||
System.out.println("[Anim] setCurrentAction('" + clip + "') → " + (action != null ? "OK" : "FAILED"));
|
||||
log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED");
|
||||
if (action != null) {
|
||||
runningClip = clip;
|
||||
return true;
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.jme3.shadow.*;
|
||||
import com.jme3.terrain.geomipmap.*;
|
||||
import com.jme3.texture.*;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import com.jme3.util.SkyFactory;
|
||||
import java.nio.ByteBuffer;
|
||||
import de.blight.common.MapData;
|
||||
import de.blight.common.MapIO;
|
||||
@@ -41,13 +40,18 @@ 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.VoxelChunkState;
|
||||
import de.blight.game.state.WaterBodyState;
|
||||
import de.blight.game.state.DayNightState;
|
||||
import de.blight.game.state.WeatherState;
|
||||
import de.blight.game.state.InteractionHudState;
|
||||
import de.blight.game.state.InventoryState;
|
||||
import de.blight.game.state.WorldItemsState;
|
||||
import de.blight.game.state.WorldObjectsState;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.ArrayList;
|
||||
@@ -55,6 +59,8 @@ import java.util.List;
|
||||
|
||||
public class WorldScene extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WorldScene.class);
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node rootNode;
|
||||
private AssetManager assetManager;
|
||||
@@ -62,6 +68,7 @@ public class WorldScene extends BaseAppState {
|
||||
private MapData loadedMapData;
|
||||
private FilterPostProcessor sharedFPP;
|
||||
private TerrainChunkState terrainChunkState;
|
||||
private Material terrainMaterial;
|
||||
|
||||
private final KeyBindings keyBindings;
|
||||
private ThirdPersonCamera thirdPersonCam;
|
||||
@@ -71,7 +78,8 @@ public class WorldScene extends BaseAppState {
|
||||
private Node character;
|
||||
private Spatial characterVisual;
|
||||
private CharacterControl physicsChar;
|
||||
private boolean animContextReady = false;
|
||||
private boolean animContextReady = false;
|
||||
private boolean physicsCharPending = false;
|
||||
private float spawnX = 0f;
|
||||
private float spawnY = 5f;
|
||||
private float spawnZ = 0f;
|
||||
@@ -108,6 +116,10 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
animLib = new AnimationLibrary();
|
||||
app.getStateManager().attach(animLib);
|
||||
|
||||
// Früh starten damit DayNightState bis onEnable() bereits initialisiert ist
|
||||
dayNight = new DayNightState(false);
|
||||
app.getStateManager().attach(dayNight);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,8 +160,9 @@ public class WorldScene extends BaseAppState {
|
||||
physicsChar.setFallSpeed(35f);
|
||||
physicsChar.setGravity(35f);
|
||||
character.addControl(physicsChar);
|
||||
bulletAppState.getPhysicsSpace().add(physicsChar);
|
||||
physicsChar.setPhysicsLocation(new Vector3f(spawnX, spawnY, spawnZ));
|
||||
// Physik-Aktivierung wird in update() verzögert bis BulletAppState und
|
||||
// Terrain-Physics für den Spawn-Bereich bereit sind (verhindert Fall-durch-Terrain).
|
||||
physicsCharPending = true;
|
||||
|
||||
playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings);
|
||||
playerInput.setPhysicsCharacter(physicsChar);
|
||||
@@ -178,10 +191,23 @@ public class WorldScene extends BaseAppState {
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
}
|
||||
|
||||
private float livePosTimer = 0f;
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// Physik-Charakter erst aktivieren wenn BulletAppState bereit ist UND
|
||||
// TerrainChunkState bereits einen Physik-Collider für den Spawn-Chunk hat.
|
||||
if (physicsCharPending) {
|
||||
com.jme3.bullet.PhysicsSpace ps = bulletAppState.getPhysicsSpace();
|
||||
if (ps == null || !terrainChunkState.hasPhysicsAt(spawnX, spawnZ)) {
|
||||
return; // Noch nicht bereit – ohne Kamera/Eingabe warten
|
||||
}
|
||||
ps.add(physicsChar);
|
||||
physicsChar.setPhysicsLocation(new Vector3f(spawnX, spawnY, spawnZ));
|
||||
physicsCharPending = false;
|
||||
log.info("[WorldScene] Spieler-Physik aktiviert bei ({}, {}, {})", spawnX, spawnY, spawnZ);
|
||||
// Kein return – Kamera im selben Frame positionieren damit
|
||||
// TerrainChunkState.update() danach die korrekte Referenzposition sieht.
|
||||
}
|
||||
|
||||
if (!animContextReady && animLib != null && animLib.isInitialized()) {
|
||||
setupAnimationContext();
|
||||
animContextReady = true;
|
||||
@@ -189,16 +215,73 @@ public class WorldScene extends BaseAppState {
|
||||
playerInput.update(tpf);
|
||||
thirdPersonCam.update(tpf);
|
||||
|
||||
livePosTimer += tpf;
|
||||
if (livePosTimer >= 0.2f && physicsChar != null) {
|
||||
livePosTimer = 0f;
|
||||
com.jme3.math.Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
de.blight.game.LiveBroadcast.writePosition(pos.x, pos.y, pos.z);
|
||||
// Terrain-Shader mit DayNightState-Licht synchronisieren (Richtung + Farben)
|
||||
if (terrainMaterial != null && dayNight != null
|
||||
&& dayNight.getSunLight() != null) {
|
||||
terrainMaterial.setVector3("LightDir",
|
||||
dayNight.getSunDirection().negate());
|
||||
ColorRGBA sc = dayNight.getSunLight().getColor();
|
||||
ColorRGBA ac = dayNight.getAmbientLight().getColor();
|
||||
terrainMaterial.setVector3("SunColor",
|
||||
new Vector3f(sc.r, sc.g, sc.b));
|
||||
terrainMaterial.setVector3("AmbientColor",
|
||||
new Vector3f(ac.r, ac.g, ac.b));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* F8: Zyklus durch Debug-Modi
|
||||
* 0 = normal
|
||||
* 1 = kein Licht (raw texture, Terrain + Voxel)
|
||||
* 2 = nur Slot-0 des TextureArrays, kein Blending, kein Licht (nur Terrain)
|
||||
*/
|
||||
public void toggleDebugNoLight() {
|
||||
debugMode = (debugMode + 1) % 4;
|
||||
VoxelChunkState vcs = getApplication().getStateManager().getState(VoxelChunkState.class);
|
||||
switch (debugMode) {
|
||||
case 0 -> {
|
||||
if (terrainMaterial != null) {
|
||||
terrainMaterial.setBoolean("DebugNoLight", false);
|
||||
terrainMaterial.setBoolean("DebugSlot0Only", false);
|
||||
terrainMaterial.clearParam("DebugDirectTex");
|
||||
}
|
||||
if (vcs != null) vcs.setDebugNoLight(false);
|
||||
log.info("[Debug] Modus 0: normal");
|
||||
}
|
||||
case 1 -> {
|
||||
if (terrainMaterial != null) {
|
||||
terrainMaterial.setBoolean("DebugNoLight", true);
|
||||
terrainMaterial.setBoolean("DebugSlot0Only", false);
|
||||
}
|
||||
if (vcs != null) vcs.setDebugNoLight(true);
|
||||
log.info("[Debug] Modus 1: kein Licht (Terrain + Voxel)");
|
||||
}
|
||||
case 2 -> {
|
||||
if (terrainMaterial != null) {
|
||||
terrainMaterial.setBoolean("DebugNoLight", false);
|
||||
terrainMaterial.setBoolean("DebugSlot0Only", true);
|
||||
terrainMaterial.clearParam("DebugDirectTex");
|
||||
}
|
||||
if (vcs != null) vcs.setDebugNoLight(true);
|
||||
log.info("[Debug] Modus 2: nur Slot-0 (kein Blending, kein Licht)");
|
||||
}
|
||||
case 3 -> {
|
||||
if (terrainMaterial != null && !debugSlot0Path.isEmpty()) {
|
||||
terrainMaterial.setBoolean("DebugNoLight", false);
|
||||
terrainMaterial.setBoolean("DebugSlot0Only", false);
|
||||
com.jme3.texture.Texture t = getApplication().getAssetManager()
|
||||
.loadTexture(debugSlot0Path);
|
||||
t.setWrap(com.jme3.texture.Texture.WrapMode.Repeat);
|
||||
terrainMaterial.setTexture("DebugDirectTex", t);
|
||||
}
|
||||
if (vcs != null) vcs.setDebugNoLight(true);
|
||||
log.info("[Debug] Modus 3: direkte Texture2D '{}' (bypasses TextureArray)", debugSlot0Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) {
|
||||
de.blight.game.LiveBroadcast.clear();
|
||||
}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
@@ -206,18 +289,19 @@ public class WorldScene extends BaseAppState {
|
||||
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
|
||||
MainCharacter mc = findMainCharacter();
|
||||
String setName = (mc != null) ? mc.getAnimSetPath() : null;
|
||||
System.out.println("[AnimCtx] MainCharacter: " + (mc != null ? mc.getCharacterId() : "null")
|
||||
+ " animSetPath: " + setName
|
||||
+ " clipCount: " + animLib.getClipKeys().size()
|
||||
+ " clips: " + animLib.getClipKeys());
|
||||
log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
|
||||
mc != null ? mc.getCharacterId() : "null",
|
||||
setName,
|
||||
animLib.getClipKeys().size(),
|
||||
animLib.getClipKeys());
|
||||
// AnimSet-ActionMap ausgeben
|
||||
if (setName != null) {
|
||||
java.nio.file.Path setDir = AnimationLibrary.findAssetRoot().resolve("animations").resolve("sets");
|
||||
try {
|
||||
de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, setName);
|
||||
System.out.println("[AnimCtx] AnimSet '" + setName + "' actionMap: " + set.getActionMap());
|
||||
log.info("[AnimCtx] AnimSet '{}' actionMap: {}", setName, set.getActionMap());
|
||||
} catch (Exception e) {
|
||||
System.out.println("[AnimCtx] AnimSet '" + setName + "' nicht ladbar: " + e.getMessage());
|
||||
log.info("[AnimCtx] AnimSet '{}' nicht ladbar: {}", setName, e.getMessage());
|
||||
}
|
||||
}
|
||||
playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot());
|
||||
@@ -239,19 +323,17 @@ public class WorldScene extends BaseAppState {
|
||||
// bei Skinned-Meshes, die vor der ersten SkinningControl-Runde falsche Bounds liefern)
|
||||
float[] yRange = vertexYRange(loaded);
|
||||
float modelHeight = yRange[1] - yRange[0];
|
||||
System.out.println("[WorldScene] Vertex-Y-Range: min=" + yRange[0] + " max=" + yRange[1]
|
||||
+ " height=" + modelHeight);
|
||||
log.info("[WorldScene] Vertex-Y-Range: min={} max={} height={}", yRange[0], yRange[1], modelHeight);
|
||||
float offsetY;
|
||||
if (modelHeight > 0.1f) {
|
||||
float scale = 1.8f / modelHeight;
|
||||
loaded.setLocalScale(scale);
|
||||
// Füße des Modells (scale * minY in loaded-Local) auf Kapsel-Unterkante legen
|
||||
offsetY = -(0.9f + scale * yRange[0]);
|
||||
System.out.println("[WorldScene] Charakter skaliert: " + scale
|
||||
+ "x offsetY=" + offsetY);
|
||||
log.info("[WorldScene] Charakter skaliert: {}x offsetY={}", scale, offsetY);
|
||||
} else {
|
||||
offsetY = CAPSULE_VISUAL_OFFSET_Y;
|
||||
System.out.println("[WorldScene] Kein Scale möglich (height=" + modelHeight + "), Fallback-Offset");
|
||||
log.info("[WorldScene] Kein Scale möglich (height={}), Fallback-Offset", modelHeight);
|
||||
}
|
||||
|
||||
// rotationNode als Drehpunkt (CharacterControl überschreibt wrapper-Rotation jeden Frame)
|
||||
@@ -263,11 +345,11 @@ public class WorldScene extends BaseAppState {
|
||||
wrapper.attachChild(rotNode);
|
||||
|
||||
characterVisual = rotNode;
|
||||
System.out.println("[WorldScene] Hauptcharakter geladen: " + mc.getModelPath());
|
||||
log.info("[WorldScene] Hauptcharakter geladen: {}", mc.getModelPath());
|
||||
return wrapper;
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WorldScene] Modell nicht ladbar (" + mc.getModelPath()
|
||||
+ "): " + e.getMessage() + " – Fallback auf Platzhalter");
|
||||
log.error("[WorldScene] Modell nicht ladbar ({}): {} – Fallback auf Platzhalter",
|
||||
mc.getModelPath(), e.getMessage());
|
||||
}
|
||||
}
|
||||
characterVisual = null;
|
||||
@@ -287,35 +369,25 @@ public class WorldScene extends BaseAppState {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void buildLighting() {
|
||||
DirectionalLight sun = new DirectionalLight();
|
||||
sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
|
||||
sun.setColor(ColorRGBA.White.mult(1.4f));
|
||||
rootNode.addLight(sun);
|
||||
// Licht, Ambient und Sky kommen von DayNightState (der wurde in initialize() attached).
|
||||
// Wir erstellen hier nur den Shadow-Filter und übergeben ihn an DayNightState,
|
||||
// damit dieser Intensität und Lichtbindung dynamisch verwaltet.
|
||||
DirectionalLightShadowFilter shadowFilter =
|
||||
new DirectionalLightShadowFilter(assetManager, 2048, 3);
|
||||
shadowFilter.setShadowIntensity(0.4f);
|
||||
shadowFilter.setEnabled(true);
|
||||
dayNight.setShadowFilter(shadowFilter); // bindet sun + übernimmt Intensitäts-Updates
|
||||
|
||||
AmbientLight ambient = new AmbientLight();
|
||||
ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f));
|
||||
rootNode.addLight(ambient);
|
||||
|
||||
DirectionalLightShadowRenderer shadowRenderer =
|
||||
new DirectionalLightShadowRenderer(assetManager, 2048, 3);
|
||||
shadowRenderer.setLight(sun);
|
||||
shadowRenderer.setShadowIntensity(0.4f);
|
||||
app.getViewPort().addProcessor(shadowRenderer);
|
||||
|
||||
try {
|
||||
Spatial sky = SkyFactory.createSky(assetManager,
|
||||
"Textures/Sky/Bright/BrightSky.dds",
|
||||
SkyFactory.EnvMapType.CubeMap);
|
||||
rootNode.attachChild(sky);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
setupPostProcessing(sun.getDirection());
|
||||
setupPostProcessing(dayNight.getSunDirection(), shadowFilter);
|
||||
}
|
||||
|
||||
private void setupPostProcessing(Vector3f sunDir) {
|
||||
private void setupPostProcessing(Vector3f sunDir, DirectionalLightShadowFilter shadowFilter) {
|
||||
sharedFPP = new FilterPostProcessor(assetManager);
|
||||
FilterPostProcessor fpp = sharedFPP;
|
||||
|
||||
// Schatten als Filter – vermeidet das Setzen von ShadowMap-Texturen in Scene-Materialien
|
||||
fpp.addFilter(shadowFilter);
|
||||
|
||||
// Globales Wasser bei Y=0 (bedeckt die gesamte Karte unterhalb der Wasserlinie)
|
||||
try {
|
||||
WaterFilter waterFilter = new WaterFilter(rootNode, sunDir);
|
||||
@@ -340,7 +412,7 @@ public class WorldScene extends BaseAppState {
|
||||
weather.setFogFilter(fogFilter);
|
||||
app.getStateManager().attach(weather);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WorldScene] Post-Processing nicht verfügbar: " + e.getMessage());
|
||||
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());
|
||||
}
|
||||
|
||||
app.getViewPort().addProcessor(fpp);
|
||||
@@ -360,7 +432,7 @@ public class WorldScene extends BaseAppState {
|
||||
try {
|
||||
loadedMapData = MapIO.load();
|
||||
} catch (IOException e) {
|
||||
System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage());
|
||||
log.error("[WorldScene] Karte nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,23 +454,35 @@ public class WorldScene extends BaseAppState {
|
||||
spawnZ = loadedMapData.spawnZ;
|
||||
}
|
||||
}
|
||||
System.out.println("[WorldScene] SpawnXZ: X=" + spawnX + " Z=" + spawnZ);
|
||||
log.info("[WorldScene] SpawnXZ: X={} Z={}", spawnX, spawnZ);
|
||||
|
||||
Material mat = buildTerrainMaterial(loadedMapData);
|
||||
terrainMaterial = buildTerrainMaterial(loadedMapData);
|
||||
|
||||
terrainChunkState = new TerrainChunkState(bulletAppState, mat, loadedMapData);
|
||||
terrainChunkState = new TerrainChunkState(bulletAppState, terrainMaterial, loadedMapData);
|
||||
// Höhen vorab laden, damit getHeightAt() bereits hier (vor initialize()) korrekte Werte liefert
|
||||
terrainChunkState.loadChunkHeights();
|
||||
app.getStateManager().attach(terrainChunkState);
|
||||
|
||||
// Spawn-Höhe: aus gespeicherter Position oder aus Terrain berechnen
|
||||
// Spawn-Höhe: gespeicherte Y nur verwenden wenn sie über dem Terrain liegt
|
||||
float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ);
|
||||
de.blight.game.state.SaveGameState _sv =
|
||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||
boolean hasSavedY = _sv != null && _sv.getSave().character.positionSaved
|
||||
&& System.getProperty("blight.temp.spawn.x") == null;
|
||||
if (!hasSavedY) {
|
||||
float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ);
|
||||
if (hasSavedY) {
|
||||
float savedY = _sv.getSave().character.y;
|
||||
spawnY = Math.max(savedY, terrainH + 1f);
|
||||
} else {
|
||||
spawnY = terrainH + 10f;
|
||||
}
|
||||
System.out.println("[WorldScene] SpawnXYZ=(" + spawnX + ", " + spawnY + ", " + spawnZ + ")");
|
||||
log.info("[WorldScene] SpawnXYZ=({}, {}, {}) terrainH={}", spawnX, spawnY, spawnZ, terrainH);
|
||||
// setSpawnHint sorgt dafür, dass TerrainChunkState.update() beim ersten Frame
|
||||
// sofort die Physik für die Spawn-Umgebung aufbaut.
|
||||
terrainChunkState.setSpawnHint(spawnX, spawnZ);
|
||||
|
||||
VoxelChunkState voxelState = new VoxelChunkState(bulletAppState, loadedMapData);
|
||||
terrainChunkState.addChunkListener(voxelState);
|
||||
app.getStateManager().attach(voxelState);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -554,39 +638,152 @@ public class WorldScene extends BaseAppState {
|
||||
new ColorRGBA(0.80f, 0.72f, 0.50f, 1f),
|
||||
};
|
||||
|
||||
private static final int TERRAIN_ARRAY_SIZE = 1024;
|
||||
private DayNightState dayNight;
|
||||
private int debugMode = 0; // 0=normal, 1=kein Licht, 2=nur Slot0, 3=direkte Tex2D
|
||||
private String debugSlot0Path = "";
|
||||
|
||||
/** Baut ein TextureArray mit paths.length Layern. Leere/fehlende Pfade → fallback-Farbe. */
|
||||
private com.jme3.texture.TextureArray buildTextureArray(
|
||||
String[] paths, ColorRGBA[] fallbacks, AssetManager am) {
|
||||
int size = TERRAIN_ARRAY_SIZE;
|
||||
java.util.List<Image> images = new java.util.ArrayList<>(paths.length);
|
||||
for (int i = 0; i < paths.length; i++) {
|
||||
String path = paths[i];
|
||||
ColorRGBA fb = (fallbacks != null && i < fallbacks.length && fallbacks[i] != null)
|
||||
? fallbacks[i] : new ColorRGBA(0.5f, 0.5f, 0.5f, 1f);
|
||||
if (path != null && !path.isEmpty()) {
|
||||
try {
|
||||
ByteBuffer buf = loadTextureToRGBA8(path, size, am);
|
||||
images.add(new Image(Image.Format.RGBA8, size, size, buf));
|
||||
continue;
|
||||
} catch (Exception e) { log.warn("[WorldScene-Array] Slot {} nicht ladbar: {}", i, e.getMessage()); }
|
||||
}
|
||||
ByteBuffer buf = BufferUtils.createByteBuffer(size * size * 4);
|
||||
byte r = (byte)(fb.r * 255), g = (byte)(fb.g * 255),
|
||||
b = (byte)(fb.b * 255), a = (byte)(fb.a * 255);
|
||||
for (int p = 0; p < size * size; p++) buf.put(r).put(g).put(b).put(a);
|
||||
buf.flip();
|
||||
images.add(new Image(Image.Format.RGBA8, size, size, buf));
|
||||
}
|
||||
com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images);
|
||||
texArr.setWrap(Texture.WrapMode.Repeat);
|
||||
texArr.setMinFilter(Texture.MinFilter.Trilinear);
|
||||
texArr.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
return texArr;
|
||||
}
|
||||
|
||||
private ByteBuffer loadTextureToRGBA8(String path, int size, AssetManager am) throws Exception {
|
||||
Texture tex = am.loadTexture(path);
|
||||
Image src = tex.getImage();
|
||||
int sw = src.getWidth(), sh = src.getHeight();
|
||||
log.info("[TextureArray] '{}' → Format={}, Größe={}×{}, Zielgröße={}",
|
||||
path, src.getFormat(), sw, sh, size);
|
||||
java.awt.image.BufferedImage bimg = new java.awt.image.BufferedImage(sw, sh,
|
||||
java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
ByteBuffer data = src.getData(0).duplicate();
|
||||
data.rewind();
|
||||
switch (src.getFormat()) {
|
||||
case RGBA8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) {
|
||||
int r = data.get()&0xFF, g = data.get()&0xFF, b = data.get()&0xFF, a = data.get()&0xFF;
|
||||
bimg.setRGB(x,y,(a<<24)|(r<<16)|(g<<8)|b); } }
|
||||
case RGB8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) {
|
||||
int r = data.get()&0xFF, g = data.get()&0xFF, b = data.get()&0xFF;
|
||||
bimg.setRGB(x,y,(0xFF<<24)|(r<<16)|(g<<8)|b); } }
|
||||
case BGR8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) {
|
||||
int b = data.get()&0xFF, g = data.get()&0xFF, r = data.get()&0xFF;
|
||||
bimg.setRGB(x,y,(0xFF<<24)|(r<<16)|(g<<8)|b); } }
|
||||
case ABGR8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) {
|
||||
int a = data.get()&0xFF, b = data.get()&0xFF, g = data.get()&0xFF, r = data.get()&0xFF;
|
||||
bimg.setRGB(x,y,(a<<24)|(r<<16)|(g<<8)|b); } }
|
||||
default -> {
|
||||
com.jme3.texture.image.ImageRaster raster = com.jme3.texture.image.ImageRaster.create(src);
|
||||
ColorRGBA pixel = new ColorRGBA();
|
||||
for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) {
|
||||
raster.getPixel(x, y, pixel);
|
||||
bimg.setRGB(x,y,((int)(pixel.a*255)<<24)|((int)(pixel.r*255)<<16)
|
||||
|((int)(pixel.g*255)<<8)|(int)(pixel.b*255));
|
||||
}
|
||||
}
|
||||
}
|
||||
java.awt.image.BufferedImage scaled;
|
||||
if (sw == size && sh == size) { scaled = bimg; } else {
|
||||
scaled = new java.awt.image.BufferedImage(size, size, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
java.awt.Graphics2D g2 = scaled.createGraphics();
|
||||
g2.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION,
|
||||
java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
||||
g2.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING,
|
||||
java.awt.RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2.drawImage(bimg, 0, 0, size, size, null); g2.dispose();
|
||||
}
|
||||
ByteBuffer buf = BufferUtils.createByteBuffer(size * size * 4);
|
||||
for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) {
|
||||
int argb = scaled.getRGB(x, y);
|
||||
buf.put((byte)((argb>>16)&0xFF)).put((byte)((argb>>8)&0xFF))
|
||||
.put((byte)(argb&0xFF)).put((byte)((argb>>24)&0xFF));
|
||||
}
|
||||
buf.flip();
|
||||
return buf;
|
||||
}
|
||||
|
||||
private Material buildTerrainMaterial(MapData map) {
|
||||
if (map != null) {
|
||||
try {
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
|
||||
mat.setBoolean("useTriPlanarMapping", false);
|
||||
mat.setFloat("Shininess", 0f);
|
||||
Material mat = new Material(assetManager, "MatDefs/TerrainArray.j3md");
|
||||
|
||||
String[] mapTex = map.terrainTextures;
|
||||
String[] matParams = {"DiffuseMap","DiffuseMap_1","DiffuseMap_2","DiffuseMap_3"};
|
||||
String[] scaleP = {"DiffuseMap_0_scale","DiffuseMap_1_scale","DiffuseMap_2_scale","DiffuseMap_3_scale"};
|
||||
String[] nmParams = {"NormalMap","NormalMap_1","NormalMap_2","NormalMap_3"};
|
||||
boolean hasUpperTex = false;
|
||||
if (map.upperTextures != null)
|
||||
for (String s : map.upperTextures) if (s != null && !s.isEmpty()) { hasUpperTex = true; break; }
|
||||
boolean hasThirdTex = false;
|
||||
if (map.thirdTextures != null)
|
||||
for (String s : map.thirdTextures) if (s != null && !s.isEmpty()) { hasThirdTex = true; break; }
|
||||
|
||||
// ── Diffuse-Array ─────────────────────────────────────────────
|
||||
String[] diffPaths = new String[12];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
String path = (mapTex[i] != null && !mapTex[i].isEmpty()) ? mapTex[i] : DEF_TEX[i];
|
||||
if (path == null || path.isEmpty()) continue;
|
||||
Texture tex = loadTexOrFallback(path, DEF_COLOR[i]);
|
||||
tex.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture(matParams[i], tex);
|
||||
mat.setFloat(scaleP[i], 512f);
|
||||
String nmp = map.terrainNormalMaps[i];
|
||||
System.out.println("[WorldScene] Slot " + i + " NormalMap: '" + nmp + "'");
|
||||
if (nmp != null && !nmp.isEmpty()) {
|
||||
try {
|
||||
Texture nm = assetManager.loadTexture(nmp);
|
||||
nm.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture(nmParams[i], nm);
|
||||
System.out.println("[WorldScene] Slot " + i + " NormalMap geladen OK");
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WorldScene] NormalMap nicht ladbar: " + nmp + " – " + e.getMessage());
|
||||
}
|
||||
}
|
||||
String p = (map.terrainTextures != null) ? map.terrainTextures[i] : "";
|
||||
diffPaths[i] = (p != null && !p.isEmpty()) ? p
|
||||
: (i < DEF_TEX.length ? DEF_TEX[i] : "");
|
||||
}
|
||||
if (map.upperTextures != null)
|
||||
System.arraycopy(map.upperTextures, 0, diffPaths, 4, 4);
|
||||
if (map.thirdTextures != null)
|
||||
System.arraycopy(map.thirdTextures, 0, diffPaths, 8, 4);
|
||||
ColorRGBA[] diffFb = new ColorRGBA[12];
|
||||
for (int i = 0; i < 4; i++) diffFb[i] = DEF_COLOR[i];
|
||||
for (int i = 0; i < 4; i++) diffFb[4+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f);
|
||||
for (int i = 0; i < 4; i++) diffFb[8+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f);
|
||||
debugSlot0Path = diffPaths[0];
|
||||
log.info("[Terrain] Slot-0 Textur = '{}' Slot-1 = '{}'", diffPaths[0], diffPaths[1]);
|
||||
mat.setParam("DiffuseArray", com.jme3.shader.VarType.TextureArray,
|
||||
buildTextureArray(diffPaths, diffFb, assetManager));
|
||||
|
||||
float[] scales = (map.diffuseScales != null && map.diffuseScales.length == 12)
|
||||
? map.diffuseScales : new float[]{8,8,8,8, 8,8,8,8, 8,8,8,8};
|
||||
log.info("[Terrain] DiffuseScales[0..3] = {}, {}, {}, {} (Voxel TexScale = 8.0)",
|
||||
scales[0], scales[1], scales[2], scales[3]);
|
||||
mat.setParam("DiffuseScales", com.jme3.shader.VarType.FloatArray, scales);
|
||||
|
||||
// ── Normal-Array ──────────────────────────────────────────────
|
||||
String[] normPaths = new String[12];
|
||||
if (map.terrainNormalMaps != null)
|
||||
System.arraycopy(map.terrainNormalMaps, 0, normPaths, 0, 4);
|
||||
if (map.upperNormalMaps != null)
|
||||
System.arraycopy(map.upperNormalMaps, 0, normPaths, 4, 4);
|
||||
if (map.thirdNormalMaps != null)
|
||||
System.arraycopy(map.thirdNormalMaps, 0, normPaths, 8, 4);
|
||||
boolean hasNormal = false;
|
||||
for (String p : normPaths) if (p != null && !p.isEmpty()) { hasNormal = true; break; }
|
||||
if (hasNormal) {
|
||||
ColorRGBA[] flatNorm = new ColorRGBA[12];
|
||||
java.util.Arrays.fill(flatNorm, new ColorRGBA(0.5f, 0.5f, 1f, 1f));
|
||||
mat.setParam("NormalArray", com.jme3.shader.VarType.TextureArray,
|
||||
buildTextureArray(normPaths, flatNorm, assetManager));
|
||||
} else {
|
||||
mat.clearParam("NormalArray");
|
||||
}
|
||||
|
||||
// Ältere Maps haben splatR=0 → Gras (Slot 0) wäre unsichtbar; auf 255 setzen.
|
||||
// ── AlphaMap ──────────────────────────────────────────────────
|
||||
byte[] splatR = map.splatR;
|
||||
boolean rAllZero = true;
|
||||
for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } }
|
||||
@@ -594,14 +791,10 @@ public class WorldScene extends BaseAppState {
|
||||
splatR = new byte[splatR.length];
|
||||
java.util.Arrays.fill(splatR, (byte) 255);
|
||||
}
|
||||
|
||||
int sz = MapData.SPLAT_SIZE;
|
||||
ByteBuffer splatBuf = BufferUtils.createByteBuffer(sz * sz * 4);
|
||||
for (int i = 0; i < sz * sz; i++) {
|
||||
splatBuf.put(splatR[i]);
|
||||
splatBuf.put(map.splatG[i]);
|
||||
splatBuf.put(map.splatB[i]);
|
||||
splatBuf.put(map.splatA[i]);
|
||||
splatBuf.put(splatR[i]).put(map.splatG[i]).put(map.splatB[i]).put(map.splatA[i]);
|
||||
}
|
||||
splatBuf.flip();
|
||||
Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf));
|
||||
@@ -609,16 +802,48 @@ public class WorldScene extends BaseAppState {
|
||||
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
splatTex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
mat.setTexture("AlphaMap", splatTex);
|
||||
|
||||
if (hasUpperTex && map.upperSplatR != null) {
|
||||
ByteBuffer upperBuf = BufferUtils.createByteBuffer(sz * sz * 4);
|
||||
for (int i = 0; i < sz * sz; i++) {
|
||||
upperBuf.put(map.upperSplatR[i]).put(map.upperSplatG[i])
|
||||
.put(map.upperSplatB[i]).put(map.upperSplatA[i]);
|
||||
}
|
||||
upperBuf.flip();
|
||||
Texture2D upperSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, upperBuf));
|
||||
upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
upperSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
mat.setTexture("AlphaMap_1", upperSplatTex);
|
||||
}
|
||||
|
||||
// ── AlphaMap_2 (dritte Gruppe) ────────────────────────────────
|
||||
if (hasThirdTex && map.thirdSplatR != null) {
|
||||
ByteBuffer thirdBuf = BufferUtils.createByteBuffer(sz * sz * 4);
|
||||
for (int i = 0; i < sz * sz; i++) {
|
||||
thirdBuf.put(map.thirdSplatR[i]);
|
||||
thirdBuf.put(map.thirdSplatG[i]);
|
||||
thirdBuf.put(map.thirdSplatB[i]);
|
||||
thirdBuf.put(map.thirdSplatA[i]);
|
||||
}
|
||||
thirdBuf.flip();
|
||||
Texture2D thirdSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, thirdBuf));
|
||||
thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
mat.setTexture("AlphaMap_2", thirdSplatTex);
|
||||
}
|
||||
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WorldScene] Splat-Material fehlgeschlagen: " + e.getMessage());
|
||||
log.error("[WorldScene] Splat-Material fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: einfaches Gras-Material
|
||||
try {
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
|
||||
Texture grass = assetManager.loadTexture("Textures/gras.png");
|
||||
Texture grass = assetManager.loadTexture("Textures/internal/gras.png");
|
||||
grass.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture("DiffuseMap", grass);
|
||||
mat.setFloat("DiffuseMap_0_scale", 32f);
|
||||
@@ -808,4 +1033,21 @@ public class WorldScene extends BaseAppState {
|
||||
return limb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleportiert den Spieler zu (x, y, z). Wenn y == Float.NaN wird die
|
||||
* Terrainhöhe an (x, z) verwendet und ein kleiner Offset nach oben addiert.
|
||||
*/
|
||||
public String teleportPlayer(float x, float y, float z) {
|
||||
if (physicsChar == null) return "Spieler noch nicht initialisiert";
|
||||
if (Float.isNaN(y)) {
|
||||
y = (terrainChunkState != null) ? terrainChunkState.getHeightAt(x, z) + 2f : 5f;
|
||||
}
|
||||
physicsChar.warp(new Vector3f(x, y, z));
|
||||
return String.format("Teleportiert → X=%.1f Y=%.1f Z=%.1f", x, y, z);
|
||||
}
|
||||
|
||||
public Vector3f getPlayerLocation() {
|
||||
return physicsChar != null ? physicsChar.getPhysicsLocation() : Vector3f.ZERO;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.shadow.DirectionalLightShadowFilter;
|
||||
import com.jme3.shadow.EdgeFilteringMode;
|
||||
import com.jme3.util.SkyFactory;
|
||||
import de.blight.common.time.DayTime;
|
||||
import de.blight.common.time.TimeListener;
|
||||
import org.slf4j.Logger;
|
||||
@@ -30,7 +29,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
|
||||
private static final ColorRGBA SUN_DAY = new ColorRGBA(1.00f, 0.95f, 0.88f, 1f);
|
||||
private static final ColorRGBA SUN_DAWN = new ColorRGBA(1.00f, 0.55f, 0.20f, 1f);
|
||||
private static final ColorRGBA AMB_DAY = new ColorRGBA(0.35f, 0.38f, 0.46f, 1f);
|
||||
private static final ColorRGBA AMB_DAY = new ColorRGBA(0.08f, 0.10f, 0.16f, 1f);
|
||||
private static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.04f, 0.04f, 0.12f, 1f);
|
||||
private static final ColorRGBA BG_DAY = new ColorRGBA(0.35f, 0.55f, 0.85f, 1f);
|
||||
private static final ColorRGBA BG_NIGHT = new ColorRGBA(0.01f, 0.01f, 0.06f, 1f);
|
||||
@@ -72,8 +71,20 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
public void setPaused(boolean paused) { dayTime.setPaused(paused); }
|
||||
|
||||
/** Aktuelle Sonnenrichtung (normalisiert), oder (0,-1,0) falls noch nicht initialisiert. */
|
||||
public com.jme3.math.Vector3f getSunDirection() {
|
||||
return sun != null ? sun.getDirection() : new com.jme3.math.Vector3f(0f, -1f, 0f);
|
||||
public Vector3f getSunDirection() {
|
||||
return sun != null ? sun.getDirection() : new Vector3f(0f, -1f, 0f);
|
||||
}
|
||||
|
||||
public DirectionalLight getSunLight() { return sun; }
|
||||
public AmbientLight getAmbientLight() { return ambient; }
|
||||
|
||||
/**
|
||||
* Übergibt einen Shadow-Filter an DayNightState, damit dieser die Intensität
|
||||
* mit der Sonnenhöhe synchronisiert. Muss nach initialize() aufgerufen werden.
|
||||
*/
|
||||
public void setShadowFilter(DirectionalLightShadowFilter f) {
|
||||
shadowFilter = f;
|
||||
if (f != null && sun != null) f.setLight(sun);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
@@ -83,15 +94,16 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.rootNode = this.app.getRootNode();
|
||||
|
||||
// Himmel
|
||||
try {
|
||||
sky = SkyFactory.createSky(app.getAssetManager(),
|
||||
"Textures/Sky/Bright/BrightSky.dds",
|
||||
SkyFactory.EnvMapType.CubeMap);
|
||||
rootNode.attachChild(sky);
|
||||
} catch (Exception e) {
|
||||
log.warn("Sky-Textur nicht geladen – Viewport-Farbe als Fallback");
|
||||
}
|
||||
// Himmel-Sphere (prozedural, nach innen gerendert)
|
||||
Sphere skyMesh = new Sphere(32, 32, 450f, true, true);
|
||||
Geometry skyGeo = new Geometry("sky", skyMesh);
|
||||
Material skyMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
skyMat.setColor("Color", BG_DAY.clone());
|
||||
skyGeo.setMaterial(skyMat);
|
||||
skyGeo.setQueueBucket(RenderQueue.Bucket.Sky);
|
||||
skyGeo.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
rootNode.attachChild(skyGeo);
|
||||
sky = skyGeo;
|
||||
|
||||
// Sonnen-Sphere (sichtbare Sonne am Himmel)
|
||||
Sphere sphereMesh = new Sphere(16, 16, 18f);
|
||||
@@ -112,6 +124,10 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
ambient = new AmbientLight();
|
||||
rootNode.addLight(ambient);
|
||||
|
||||
// Falls WorldScene.buildLighting() schon einen Filter registriert hat (setShadowFilter
|
||||
// wurde vor initialize() aufgerufen, sun war damals null) — jetzt nachholen.
|
||||
if (shadowFilter != null) shadowFilter.setLight(sun);
|
||||
|
||||
// Schatten (nur im Game) – als Filter, damit WorldScene ihn in den FPP einhängen kann
|
||||
if (withShadows) {
|
||||
try {
|
||||
@@ -157,9 +173,14 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
if (sunSphere != null && sunSphere.getParent() == null)
|
||||
rootNode.attachChild(sunSphere);
|
||||
|
||||
Vector3f camPos = app.getCamera().getLocation();
|
||||
|
||||
// Himmel-Sphere der Kamera folgen lassen
|
||||
if (sky != null)
|
||||
sky.setLocalTranslation(camPos);
|
||||
|
||||
// Sonnen-Sphere immer relativ zur Kamera positionieren
|
||||
if (sunSphere != null && sunSphere.getCullHint() != Spatial.CullHint.Always) {
|
||||
Vector3f camPos = app.getCamera().getLocation();
|
||||
sunSphere.setLocalTranslation(camPos.add(sun.getDirection().negate().mult(480f)));
|
||||
}
|
||||
}
|
||||
@@ -184,7 +205,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) ──────────────
|
||||
float dawnFactor = 1f - FastMath.clamp(elev * 5f, 0f, 1f);
|
||||
ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor);
|
||||
sun.setColor(sunColor.multLocal(elevC * 1.35f));
|
||||
sun.setColor(sunColor.multLocal(elevC * 0.85f));
|
||||
|
||||
// ── Sonnen-Sphere ausblenden wenn unter Horizont ────────────────────
|
||||
if (sunSphere != null) {
|
||||
@@ -209,12 +230,10 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
|
||||
// ── Himmel & Hintergrundfarbe ────────────────────────────────────────
|
||||
float skyFactor = FastMath.clamp((elev + 0.05f) / 0.2f, 0f, 1f);
|
||||
if (sky != null)
|
||||
sky.setCullHint(skyFactor > 0.01f
|
||||
? Spatial.CullHint.Inherit
|
||||
: Spatial.CullHint.Always);
|
||||
app.getViewPort().setBackgroundColor(
|
||||
BG_NIGHT.clone().interpolateLocal(BG_DAY, skyFactor));
|
||||
ColorRGBA skyColor = BG_NIGHT.clone().interpolateLocal(BG_DAY, skyFactor);
|
||||
if (sky instanceof Geometry skyGeo)
|
||||
skyGeo.getMaterial().setColor("Color", skyColor);
|
||||
app.getViewPort().setBackgroundColor(skyColor);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -20,6 +20,9 @@ import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassTuft;
|
||||
import de.blight.common.GrassTuftIO;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
@@ -32,6 +35,8 @@ import java.util.*;
|
||||
*/
|
||||
public class GrassState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GrassState.class);
|
||||
|
||||
private static final int TERRAIN_HALF = 2048;
|
||||
private static final int CHUNK_SIZE = 128;
|
||||
private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE; // 32
|
||||
@@ -77,7 +82,7 @@ public class GrassState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("[GrassState] Gras nicht ladbar: " + e.getMessage());
|
||||
log.warn("[GrassState] Gras nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (slotMaterials.isEmpty()) {
|
||||
@@ -101,6 +106,16 @@ public class GrassState extends BaseAppState {
|
||||
nextChunk++;
|
||||
built++;
|
||||
}
|
||||
|
||||
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
|
||||
if (dns != null) {
|
||||
for (Material mat : slotMaterials.values()) {
|
||||
if ("Grass".equals(mat.getMaterialDef().getName())) {
|
||||
mat.setVector3("SunDir", dns.getSunDirection().negate());
|
||||
mat.setColor("SunColor", dns.getSunLight().getColor());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Material ──────────────────────────────────────────────────────────────
|
||||
@@ -119,13 +134,15 @@ public class GrassState extends BaseAppState {
|
||||
Material mat = new Material(assets, "MatDefs/Grass.j3md");
|
||||
mat.setFloat("WindSpeed", 0.5f);
|
||||
mat.setFloat("WindStrength", 0.14f);
|
||||
mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f));
|
||||
mat.setColor("SunColor", ColorRGBA.White);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
if (texPath != null && !texPath.isEmpty()) {
|
||||
try {
|
||||
mat.setTexture("ColorMap", assets.loadTexture(texPath));
|
||||
mat.setColor("Color", ColorRGBA.White);
|
||||
} catch (Exception te) {
|
||||
System.err.println("[GrassState] Gras-Textur nicht ladbar '" + texPath + "': " + te.getMessage());
|
||||
log.warn("[GrassState] Gras-Textur nicht ladbar '{}': {}", texPath, te.getMessage());
|
||||
mat.setColor("Color", new ColorRGBA(0.28f, 0.72f, 0.18f, 1f));
|
||||
}
|
||||
} else {
|
||||
@@ -133,7 +150,7 @@ public class GrassState extends BaseAppState {
|
||||
}
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
System.err.println("[GrassState] Grass.j3md nicht gefunden, Fallback: " + e.getMessage());
|
||||
log.warn("[GrassState] Grass.j3md nicht gefunden, Fallback: {}", e.getMessage());
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.25f, 0.65f, 0.15f, 1f));
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
|
||||
@@ -17,6 +17,9 @@ import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassVertexBlade;
|
||||
import de.blight.common.GrassVertexIO;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -28,6 +31,8 @@ import java.util.List;
|
||||
public class GrassVertexRenderState extends BaseAppState
|
||||
implements TerrainChunkState.ChunkListener {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GrassVertexRenderState.class);
|
||||
|
||||
// ── Chunks ────────────────────────────────────────────────────────────────
|
||||
private static final int TERRAIN_HALF = 2048;
|
||||
private static final int CHUNK_SIZE = 128;
|
||||
@@ -79,7 +84,7 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
if (ci >= 0) chunkBlades[ci].add(b);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[GrassVertexRenderState] Daten nicht ladbar: " + e.getMessage());
|
||||
log.warn("[GrassVertexRenderState] Daten nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
|
||||
material = buildMaterial();
|
||||
@@ -101,6 +106,15 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
buildChunk(nextChunk++);
|
||||
built++;
|
||||
}
|
||||
|
||||
if (material != null && material.getMaterialDef().getMaterialParam("SunColor") != null) {
|
||||
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
|
||||
if (dns != null && dns.getSunLight() != null) {
|
||||
material.setVector3("SunDir", dns.getSunDirection().negate());
|
||||
com.jme3.math.ColorRGBA sc = dns.getSunLight().getColor();
|
||||
material.setColor("SunColor", sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Material ──────────────────────────────────────────────────────────────
|
||||
@@ -115,7 +129,7 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
System.err.println("[GrassVertexRenderState] Material nicht ladbar: " + e.getMessage());
|
||||
log.warn("[GrassVertexRenderState] Material nicht ladbar: {}", e.getMessage());
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f));
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
|
||||
@@ -8,6 +8,7 @@ import de.blight.common.VoxelChunk;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -333,17 +334,17 @@ public final class MarchingCubes {
|
||||
/**
|
||||
* Erzeugt ein JME-Mesh aus den Voxel-Daten mit dem angegebenen LOD-Schritt.
|
||||
*
|
||||
* @param chunk Quelldaten
|
||||
* @param lodStep 1=LOD0 (voll), 4=LOD1, 16=LOD2
|
||||
* @param chunk Quelldaten
|
||||
* @param lodStep 1=LOD0 (voll), 4=LOD1, 16=LOD2
|
||||
* @param neighbors 6 Nachbar-Chunks [+X,-X,+Y,-Y,+Z,-Z] für nahtlose Grenzen (null-Einträge = Luft)
|
||||
* @return JME-Mesh oder null wenn keine Oberfläche vorhanden
|
||||
*/
|
||||
public static Mesh build(VoxelChunk chunk, int lodStep) {
|
||||
public static Mesh build(VoxelChunk chunk, int lodStep, VoxelChunk[] neighbors) {
|
||||
if (chunk.isEmpty()) return null;
|
||||
|
||||
// Flache Listen für Positions-, Normal- und Color-Daten
|
||||
// Flache Listen für Positions- und Normal-Daten
|
||||
List<Float> positions = new ArrayList<>(4096);
|
||||
List<Float> normals = new ArrayList<>(4096);
|
||||
List<Float> colors = new ArrayList<>(4096);
|
||||
|
||||
int cells = VoxelChunk.CELLS; // 128
|
||||
int size = VoxelChunk.SIZE; // 129
|
||||
@@ -386,22 +387,11 @@ public final class MarchingCubes {
|
||||
|
||||
if (EDGE_TABLE[cubeIndex] == 0) continue;
|
||||
|
||||
// Materialen der 8 Eckpunkte
|
||||
int m0 = chunk.getMaterial(x0, y0, z0) & 0xFF;
|
||||
int m1 = chunk.getMaterial(x1, y0, z0) & 0xFF;
|
||||
int m2 = chunk.getMaterial(x1, y0, z1) & 0xFF;
|
||||
int m3 = chunk.getMaterial(x0, y0, z1) & 0xFF;
|
||||
int m4 = chunk.getMaterial(x0, y1, z0) & 0xFF;
|
||||
int m5 = chunk.getMaterial(x1, y1, z0) & 0xFF;
|
||||
int m6 = chunk.getMaterial(x1, y1, z1) & 0xFF;
|
||||
int m7 = chunk.getMaterial(x0, y1, z1) & 0xFF;
|
||||
|
||||
// Positionen der 8 Eckpunkte als float[]
|
||||
float[] vx = { x0, x1, x1, x0, x0, x1, x1, x0 };
|
||||
float[] vy = { y0, y0, y0, y0, y1, y1, y1, y1 };
|
||||
float[] vz = { z0, z0, z1, z1, z0, z0, z1, z1 };
|
||||
float[] dd = { d0, d1, d2, d3, d4, d5, d6, d7 };
|
||||
int[] mm = { m0, m1, m2, m3, m4, m5, m6, m7 };
|
||||
|
||||
// 12 Kantenpunkte berechnen
|
||||
float[] epx = new float[12];
|
||||
@@ -410,10 +400,6 @@ public final class MarchingCubes {
|
||||
float[] enx = new float[12];
|
||||
float[] eny = new float[12];
|
||||
float[] enz = new float[12];
|
||||
float[] ecR = new float[12];
|
||||
float[] ecG = new float[12];
|
||||
float[] ecB = new float[12];
|
||||
float[] ecA = new float[12];
|
||||
|
||||
// Kante i verbindet Vertex A mit Vertex B
|
||||
int[][] edgeVerts = {
|
||||
@@ -441,21 +427,14 @@ public final class MarchingCubes {
|
||||
epz[e] = vz[a] + t * (vz[b] - vz[a]);
|
||||
|
||||
// Normalen via Gradient
|
||||
float[] gA = gradient(chunk, (int)vx[a], (int)vy[a], (int)vz[a]);
|
||||
float[] gB = gradient(chunk, (int)vx[b], (int)vy[b], (int)vz[b]);
|
||||
float[] gA = gradient(chunk, neighbors, (int)vx[a], (int)vy[a], (int)vz[a]);
|
||||
float[] gB = gradient(chunk, neighbors, (int)vx[b], (int)vy[b], (int)vz[b]);
|
||||
enx[e] = gA[0] + t * (gB[0] - gA[0]);
|
||||
eny[e] = gA[1] + t * (gB[1] - gA[1]);
|
||||
enz[e] = gA[2] + t * (gB[2] - gA[2]);
|
||||
float nlen = (float)Math.sqrt(enx[e]*enx[e] + eny[e]*eny[e] + enz[e]*enz[e]);
|
||||
if (nlen > 1e-6f) { enx[e] /= nlen; eny[e] /= nlen; enz[e] /= nlen; }
|
||||
|
||||
// Material-Blend
|
||||
float[] wA = matWeights(mm[a]);
|
||||
float[] wB = matWeights(mm[b]);
|
||||
ecR[e] = wA[0] + t * (wB[0] - wA[0]);
|
||||
ecG[e] = wA[1] + t * (wB[1] - wA[1]);
|
||||
ecB[e] = wA[2] + t * (wB[2] - wA[2]);
|
||||
ecA[e] = wA[3] + t * (wB[3] - wA[3]);
|
||||
else { enx[e] = 0f; eny[e] = 1f; enz[e] = 0f; }
|
||||
}
|
||||
|
||||
// Dreiecke ausgeben
|
||||
@@ -466,15 +445,12 @@ public final class MarchingCubes {
|
||||
// Vertex 0
|
||||
positions.add(epx[e0]); positions.add(epy[e0]); positions.add(epz[e0]);
|
||||
normals.add(enx[e0]); normals.add(eny[e0]); normals.add(enz[e0]);
|
||||
colors.add(ecR[e0]); colors.add(ecG[e0]); colors.add(ecB[e0]); colors.add(ecA[e0]);
|
||||
// Vertex 1
|
||||
positions.add(epx[e1]); positions.add(epy[e1]); positions.add(epz[e1]);
|
||||
normals.add(enx[e1]); normals.add(eny[e1]); normals.add(enz[e1]);
|
||||
colors.add(ecR[e1]); colors.add(ecG[e1]); colors.add(ecB[e1]); colors.add(ecA[e1]);
|
||||
// Vertex 2
|
||||
positions.add(epx[e2]); positions.add(epy[e2]); positions.add(epz[e2]);
|
||||
normals.add(enx[e2]); normals.add(eny[e2]); normals.add(enz[e2]);
|
||||
colors.add(ecR[e2]); colors.add(ecG[e2]); colors.add(ecB[e2]); colors.add(ecA[e2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,42 +462,202 @@ public final class MarchingCubes {
|
||||
|
||||
FloatBuffer posBuf = BufferUtils.createFloatBuffer(positions.size());
|
||||
FloatBuffer normBuf = BufferUtils.createFloatBuffer(normals.size());
|
||||
FloatBuffer colBuf = BufferUtils.createFloatBuffer(colors.size());
|
||||
IntBuffer idxBuf = BufferUtils.createIntBuffer(vertCount);
|
||||
|
||||
for (float v : positions) posBuf.put(v);
|
||||
for (float v : normals) normBuf.put(v);
|
||||
for (float v : colors) colBuf.put(v);
|
||||
for (int i = 0; i < vertCount; i++) idxBuf.put(i);
|
||||
|
||||
posBuf.rewind(); normBuf.rewind(); colBuf.rewind(); idxBuf.rewind();
|
||||
posBuf.rewind(); normBuf.rewind(); idxBuf.rewind();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Color, 4, colBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
|
||||
mesh.setStatic();
|
||||
mesh.updateBound();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/** Erzeugt Mesh ohne Nachbar-Chunks (Chunk-Grenzen werden als Luft behandelt). */
|
||||
public static Mesh build(VoxelChunk chunk, int lodStep) {
|
||||
return build(chunk, lodStep, null);
|
||||
}
|
||||
|
||||
// ── Laplacian-Glättung ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Laplacian-Glättung eines MC-Meshes (kein Vertex-Sharing).
|
||||
* Gruppiert Vertices per Position, mittelt Nachbar-Positionen,
|
||||
* berechnet danach Normalen aus der geglätteten Geometrie neu.
|
||||
*
|
||||
* @param iterations Anzahl Glättungsdurchläufe (3-5 empfohlen)
|
||||
* @param factor Stärke pro Durchlauf (0.3–0.5; größer = glatter, aber leichtes Schrumpfen)
|
||||
*/
|
||||
public static Mesh smooth(Mesh mesh, int iterations, float factor) {
|
||||
if (mesh == null) return null;
|
||||
FloatBuffer posF = mesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
if (posF == null) return mesh;
|
||||
int vertCount = posF.capacity() / 3;
|
||||
if (vertCount < 3) return mesh;
|
||||
int triCount = vertCount / 3;
|
||||
|
||||
float[] pos = new float[vertCount * 3];
|
||||
posF.rewind(); posF.get(pos);
|
||||
|
||||
// Vertices gleicher Position zu Gruppen zusammenfassen.
|
||||
// Schlüssel: quantisierte XYZ-Koordinaten (1/2048 Voxel-Auflösung).
|
||||
HashMap<Long, Integer> keyToGroup = new HashMap<>(vertCount * 2);
|
||||
int[] vertGroup = new int[vertCount];
|
||||
int[] groupFirst = new int[vertCount]; // ein Repräsentant je Gruppe
|
||||
int groupCount = 0;
|
||||
for (int v = 0; v < vertCount; v++) {
|
||||
long key = posKey(pos, v);
|
||||
Integer g = keyToGroup.get(key);
|
||||
if (g == null) {
|
||||
keyToGroup.put(key, groupCount);
|
||||
groupFirst[groupCount] = v;
|
||||
vertGroup[v] = groupCount++;
|
||||
} else {
|
||||
vertGroup[v] = g;
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppen-Positionen initialisieren
|
||||
float[] gx = new float[groupCount];
|
||||
float[] gy = new float[groupCount];
|
||||
float[] gz = new float[groupCount];
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
int v = groupFirst[g];
|
||||
gx[g] = pos[v*3]; gy[g] = pos[v*3+1]; gz[g] = pos[v*3+2];
|
||||
}
|
||||
|
||||
// Randvertices einfrieren: Vertices an den 6 Chunk-Grenzflächen (x/y/z = 0 oder CELLS)
|
||||
// dürfen sich nicht verschieben, damit benachbarte Chunk-Meshes nahtlos bleiben.
|
||||
float bound = VoxelChunk.CELLS; // = 128.0
|
||||
boolean[] pinned = new boolean[groupCount];
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
float x = gx[g], y = gy[g], z = gz[g];
|
||||
if (x < 0.01f || x > bound - 0.01f ||
|
||||
y < 0.01f || y > bound - 0.01f ||
|
||||
z < 0.01f || z > bound - 0.01f) {
|
||||
pinned[g] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float[] ax = new float[groupCount];
|
||||
float[] ay = new float[groupCount];
|
||||
float[] az = new float[groupCount];
|
||||
int[] ac = new int[groupCount];
|
||||
|
||||
for (int iter = 0; iter < iterations; iter++) {
|
||||
java.util.Arrays.fill(ax, 0f); java.util.Arrays.fill(ay, 0f);
|
||||
java.util.Arrays.fill(az, 0f); java.util.Arrays.fill(ac, 0);
|
||||
|
||||
for (int t = 0; t < triCount; t++) {
|
||||
int g0 = vertGroup[t*3], g1 = vertGroup[t*3+1], g2 = vertGroup[t*3+2];
|
||||
ax[g0]+=gx[g1]+gx[g2]; ay[g0]+=gy[g1]+gy[g2]; az[g0]+=gz[g1]+gz[g2]; ac[g0]+=2;
|
||||
ax[g1]+=gx[g0]+gx[g2]; ay[g1]+=gy[g0]+gy[g2]; az[g1]+=gz[g0]+gz[g2]; ac[g1]+=2;
|
||||
ax[g2]+=gx[g0]+gx[g1]; ay[g2]+=gy[g0]+gy[g1]; az[g2]+=gz[g0]+gz[g1]; ac[g2]+=2;
|
||||
}
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
if (!pinned[g] && ac[g] > 0) {
|
||||
float inv = 1f / ac[g];
|
||||
gx[g] += factor * (ax[g]*inv - gx[g]);
|
||||
gy[g] += factor * (ay[g]*inv - gy[g]);
|
||||
gz[g] += factor * (az[g]*inv - gz[g]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertex-Positionen aktualisieren
|
||||
for (int v = 0; v < vertCount; v++) {
|
||||
int g = vertGroup[v];
|
||||
pos[v*3] = gx[g]; pos[v*3+1] = gy[g]; pos[v*3+2] = gz[g];
|
||||
}
|
||||
posF.rewind(); posF.put(pos); posF.rewind();
|
||||
|
||||
// Normalen aus geglätteter Geometrie neu berechnen.
|
||||
// 1. Flächennormalen pro Dreieck
|
||||
float[] fn = new float[triCount * 3];
|
||||
for (int t = 0; t < triCount; t++) {
|
||||
float p0x=pos[t*9], p0y=pos[t*9+1], p0z=pos[t*9+2];
|
||||
float p1x=pos[t*9+3], p1y=pos[t*9+4], p1z=pos[t*9+5];
|
||||
float p2x=pos[t*9+6], p2y=pos[t*9+7], p2z=pos[t*9+8];
|
||||
float ex=p1x-p0x, ey=p1y-p0y, ez=p1z-p0z;
|
||||
float fx=p2x-p0x, fy=p2y-p0y, fz=p2z-p0z;
|
||||
float nx=ey*fz-ez*fy, ny=ez*fx-ex*fz, nz=ex*fy-ey*fx;
|
||||
float len=(float)Math.sqrt(nx*nx+ny*ny+nz*nz);
|
||||
fn[t*3] = len>1e-6f ? nx/len : 0f;
|
||||
fn[t*3+1] = len>1e-6f ? ny/len : 1f;
|
||||
fn[t*3+2] = len>1e-6f ? nz/len : 0f;
|
||||
}
|
||||
// 2. Gruppen-Normalen: Dreieck-Normalen aller anliegenden Dreiecke mitteln
|
||||
float[] gnx = new float[groupCount];
|
||||
float[] gny = new float[groupCount];
|
||||
float[] gnz = new float[groupCount];
|
||||
for (int t = 0; t < triCount; t++) {
|
||||
int g0=vertGroup[t*3], g1=vertGroup[t*3+1], g2=vertGroup[t*3+2];
|
||||
gnx[g0]+=fn[t*3]; gny[g0]+=fn[t*3+1]; gnz[g0]+=fn[t*3+2];
|
||||
gnx[g1]+=fn[t*3]; gny[g1]+=fn[t*3+1]; gnz[g1]+=fn[t*3+2];
|
||||
gnx[g2]+=fn[t*3]; gny[g2]+=fn[t*3+1]; gnz[g2]+=fn[t*3+2];
|
||||
}
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
float len=(float)Math.sqrt(gnx[g]*gnx[g]+gny[g]*gny[g]+gnz[g]*gnz[g]);
|
||||
if (len>1e-6f) { gnx[g]/=len; gny[g]/=len; gnz[g]/=len; }
|
||||
else { gny[g]=1f; }
|
||||
}
|
||||
// 3. Vertex-Normalen schreiben
|
||||
FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||
normF.rewind();
|
||||
for (int v = 0; v < vertCount; v++) {
|
||||
int g = vertGroup[v];
|
||||
normF.put(gnx[g]).put(gny[g]).put(gnz[g]);
|
||||
}
|
||||
normF.rewind();
|
||||
|
||||
mesh.updateBound();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
private static long posKey(float[] pos, int v) {
|
||||
// Quantisierung auf 1/2048 Voxel; Position [0,128] → max Wert 262144 < 2^19 → 21 Bits/Achse
|
||||
int ix = Math.round(pos[v*3] * 2048f) + 131072;
|
||||
int iy = Math.round(pos[v*3+1] * 2048f) + 131072;
|
||||
int iz = Math.round(pos[v*3+2] * 2048f) + 131072;
|
||||
return (long)ix | ((long)iy << 21) | ((long)iz << 42);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Berechnet den negierten Gradienten (zeigt von Solid weg) an Position (ix,iy,iz). */
|
||||
private static float[] gradient(VoxelChunk chunk, int ix, int iy, int iz) {
|
||||
float nx = getDensityClamped(chunk, ix+1, iy, iz )
|
||||
- getDensityClamped(chunk, ix-1, iy, iz );
|
||||
float ny = getDensityClamped(chunk, ix, iy+1, iz )
|
||||
- getDensityClamped(chunk, ix, iy-1, iz );
|
||||
float nz = getDensityClamped(chunk, ix, iy, iz+1)
|
||||
- getDensityClamped(chunk, ix, iy, iz-1);
|
||||
private static float[] gradient(VoxelChunk chunk, VoxelChunk[] nb, int ix, int iy, int iz) {
|
||||
float nx = getDensityWithNeighbors(chunk, nb, ix+1, iy, iz )
|
||||
- getDensityWithNeighbors(chunk, nb, ix-1, iy, iz );
|
||||
float ny = getDensityWithNeighbors(chunk, nb, ix, iy+1, iz )
|
||||
- getDensityWithNeighbors(chunk, nb, ix, iy-1, iz );
|
||||
float nz = getDensityWithNeighbors(chunk, nb, ix, iy, iz+1)
|
||||
- getDensityWithNeighbors(chunk, nb, ix, iy, iz-1);
|
||||
// negieren: Gradient zeigt vom Solid weg (nach außen)
|
||||
float len = (float)Math.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (len < 1e-6f) return new float[]{ 0f, 1f, 0f };
|
||||
return new float[]{ -nx/len, -ny/len, -nz/len };
|
||||
}
|
||||
|
||||
/** Liest Dichte mit Klemmen an Chunk-Grenzen. */
|
||||
/** Liest Dichte: innerhalb des Chunks direkt, außerhalb via Nachbar-Chunk oder Klemmen. */
|
||||
private static float getDensityWithNeighbors(VoxelChunk chunk, VoxelChunk[] nb, int x, int y, int z) {
|
||||
int cells = VoxelChunk.CELLS; // 128
|
||||
int size = VoxelChunk.SIZE; // 129
|
||||
if (x < 0) return (nb != null && nb[1] != null) ? nb[1].getDensity(cells + x, y, z) : getDensityClamped(chunk, 0, y, z);
|
||||
if (x >= size) return (nb != null && nb[0] != null) ? nb[0].getDensity(x - cells, y, z) : getDensityClamped(chunk, size-1, y, z);
|
||||
if (y < 0) return (nb != null && nb[3] != null) ? nb[3].getDensity(x, cells + y, z) : getDensityClamped(chunk, x, 0, z);
|
||||
if (y >= size) return (nb != null && nb[2] != null) ? nb[2].getDensity(x, y - cells, z) : getDensityClamped(chunk, x, size-1, z);
|
||||
if (z < 0) return (nb != null && nb[5] != null) ? nb[5].getDensity(x, y, cells + z) : getDensityClamped(chunk, x, y, 0);
|
||||
if (z >= size) return (nb != null && nb[4] != null) ? nb[4].getDensity(x, y, z - cells) : getDensityClamped(chunk, x, y, size-1);
|
||||
return chunk.getDensity(x, y, z);
|
||||
}
|
||||
|
||||
/** Liest Dichte mit Klemmen an Chunk-Grenzen (Fallback wenn kein Nachbar bekannt). */
|
||||
private static float getDensityClamped(VoxelChunk chunk, int x, int y, int z) {
|
||||
int s = VoxelChunk.SIZE - 1; // 128
|
||||
x = Math.max(0, Math.min(s, x));
|
||||
@@ -530,13 +666,4 @@ public final class MarchingCubes {
|
||||
return chunk.getDensity(x, y, z);
|
||||
}
|
||||
|
||||
/** Gibt vec4-Gewichte für ein Material zurück: 0→(1,0,0,0), 1→(0,1,0,0), usw. */
|
||||
private static float[] matWeights(int matId) {
|
||||
switch (matId & 3) {
|
||||
case 0: return new float[]{ 1f, 0f, 0f, 0f };
|
||||
case 1: return new float[]{ 0f, 1f, 0f, 0f };
|
||||
case 2: return new float[]{ 0f, 0f, 1f, 0f };
|
||||
default: return new float[]{ 0f, 0f, 0f, 1f };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,146 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.renderer.Camera;
|
||||
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.
|
||||
* Wechselt zwischen LOD-Stufen und blendet Objekte ab einer Sichtweite aus.
|
||||
*
|
||||
* 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)
|
||||
* Zwei Modi:
|
||||
*
|
||||
* (A) Embedded-LOD-Modus: Das j3o enthält bereits child-Nodes "lod0", "lod1", "lod2"
|
||||
* (erzeugt vom TreeGenerator). ModelLodControl ist direkt am j3o-Root befestigt
|
||||
* und steuert deren CullHint. Kein lodRoot-Wrapper nötig.
|
||||
*
|
||||
* (B) Externer-LOD-Modus: LOD-Stufen sind separate j3o-Dateien und werden lazy geladen.
|
||||
* ModelLodControl sitzt an einem lodRoot-Knoten der als [0]=Main, [1]=LOD1, [2]=LOD2
|
||||
* strukturiert ist.
|
||||
*
|
||||
* controlRender() ist bewusst leer: der Shadow-Renderer ruft controlRender mit seiner
|
||||
* eigenen Kamera auf — Kameradistanz wird daher nur in controlUpdate() berechnet.
|
||||
*/
|
||||
public class ModelLodControl extends AbstractControl {
|
||||
|
||||
private final Camera cam;
|
||||
private final float lod1DistSq;
|
||||
private final float lod2DistSq;
|
||||
private final float cullDistSq;
|
||||
private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=culled
|
||||
|
||||
// ── Embedded-LOD-Modus ────────────────────────────────────────────────────
|
||||
private final Spatial embLod0;
|
||||
private final Spatial embLod1;
|
||||
private final Spatial embLod2;
|
||||
|
||||
// ── Externer-LOD-Modus ────────────────────────────────────────────────────
|
||||
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 String lod1Path;
|
||||
private String lod2Path;
|
||||
private boolean lod1Loaded;
|
||||
private boolean lod2Loaded;
|
||||
private java.util.function.Consumer<Spatial> lodLoadCallback;
|
||||
|
||||
private boolean lod1Loaded = false;
|
||||
private boolean lod2Loaded = false;
|
||||
private int currentSlot = 0; // 0=main, 1=lod1, 2=lod2, -1=culled
|
||||
// ── Konstruktoren ─────────────────────────────────────────────────────────
|
||||
|
||||
public ModelLodControl(AssetManager assets,
|
||||
/**
|
||||
* Embedded-LOD-Modus: Die j3o-Nodes lod0/lod1/lod2 existieren bereits als
|
||||
* Child-Nodes im Spatial, an dem dieser Control befestigt wird.
|
||||
* lod1 und lod2 dürfen null sein (dann bleibt lod0 bis zur cullDistance sichtbar).
|
||||
*/
|
||||
public ModelLodControl(Camera cam,
|
||||
Spatial lod0, Spatial lod1, Spatial lod2,
|
||||
float lod1Distance, float lod2Distance, float cullDistance) {
|
||||
this.cam = cam;
|
||||
this.embLod0 = lod0;
|
||||
this.embLod1 = lod1;
|
||||
this.embLod2 = lod2;
|
||||
this.lod1DistSq = lod1 != null ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
||||
this.lod2DistSq = lod2 != null ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
||||
this.cullDistSq = cullDistance * cullDistance;
|
||||
this.assets = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Externer-LOD-Modus: LOD-Stufen werden lazy aus separaten j3o-Dateien geladen.
|
||||
* spatial muss ein lodRoot-Node sein: [0]=Main, [1]=LOD1 (lazy), [2]=LOD2 (lazy).
|
||||
*/
|
||||
public ModelLodControl(AssetManager assets, Camera cam,
|
||||
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;
|
||||
this.assets = assets;
|
||||
this.cam = cam;
|
||||
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;
|
||||
this.embLod0 = null;
|
||||
this.embLod1 = null;
|
||||
this.embLod2 = null;
|
||||
}
|
||||
|
||||
public void setLodLoadCallback(java.util.function.Consumer<Spatial> cb) {
|
||||
this.lodLoadCallback = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
|
||||
@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;
|
||||
|
||||
boolean embedded = embLod0 != null;
|
||||
|
||||
int targetSlot;
|
||||
if (distSq >= cullDistSq) {
|
||||
targetSlot = -1;
|
||||
} else if (lod2Path != null && distSq >= lod2DistSq) {
|
||||
targetSlot = 2;
|
||||
} else if (lod1Path != null && distSq >= lod1DistSq) {
|
||||
targetSlot = 1;
|
||||
} else if (embedded) {
|
||||
if (embLod2 != null && distSq >= lod2DistSq) targetSlot = 2;
|
||||
else if (embLod1 != null && distSq >= lod1DistSq) targetSlot = 1;
|
||||
else targetSlot = 0;
|
||||
} else {
|
||||
targetSlot = 0;
|
||||
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) {
|
||||
spatial.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
spatial.setCullHint(Spatial.CullHint.Dynamic);
|
||||
|
||||
if (embedded) {
|
||||
if (embLod0 != null) embLod0.setCullHint(targetSlot == 0 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
if (embLod1 != null) embLod1.setCullHint(targetSlot == 1 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
if (embLod2 != null) embLod2.setCullHint(targetSlot == 2 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
|
||||
// Externer LOD-Modus: lazy Laden + Sichtbarkeit
|
||||
if (!(spatial instanceof Node node)) return;
|
||||
|
||||
if (targetSlot == 1 && !lod1Loaded) {
|
||||
lod1Loaded = true;
|
||||
try {
|
||||
Spatial lod1 = assets.loadModel(lod1Path);
|
||||
lod1.setName("lod1");
|
||||
node.attachChildAt(lod1, 1);
|
||||
if (lodLoadCallback != null) lodLoadCallback.accept(lod1);
|
||||
} catch (Exception e) {
|
||||
// LOD1 load failed — fall back to main
|
||||
lod1Path = null;
|
||||
currentSlot = 0;
|
||||
targetSlot = 0;
|
||||
}
|
||||
@@ -91,31 +150,21 @@ public class ModelLodControl extends AbstractControl {
|
||||
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(new Node("lod1_placeholder"));
|
||||
}
|
||||
node.attachChild(lod2);
|
||||
if (lodLoadCallback != null) lodLoadCallback.accept(lod2);
|
||||
} catch (Exception e) {
|
||||
lod2Path = null;
|
||||
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);
|
||||
node.getChildren().get(i).setCullHint(
|
||||
i == targetSlot ? Spatial.CullHint.Dynamic : Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,8 +218,8 @@ public class RiverState extends BaseAppState {
|
||||
|
||||
// Normal-Map: erst eigene Texturen versuchen, dann JME-Fallback
|
||||
Texture nm = loadTextureOr(
|
||||
isWaterfall ? "Textures/water/waterfall_normal.png"
|
||||
: "Textures/water/river_normal.jpg",
|
||||
isWaterfall ? "Textures/internal/water/waterfall_normal.png"
|
||||
: "Textures/internal/water/river_normal.jpg",
|
||||
"Common/MatDefs/Water/Textures/water_normalmap.png");
|
||||
if (nm != null) {
|
||||
nm.setWrap(Texture.WrapMode.Repeat);
|
||||
@@ -233,8 +233,8 @@ public class RiverState extends BaseAppState {
|
||||
}
|
||||
|
||||
// Diffuse-Map: river.jpg für Fluss (Farbmodulation), waterfall_diffuse für Gischt
|
||||
String diffPath = isWaterfall ? "Textures/water/waterfall_diffuse.png"
|
||||
: "Textures/water/river.jpg";
|
||||
String diffPath = isWaterfall ? "Textures/internal/water/waterfall_diffuse.png"
|
||||
: "Textures/internal/water/river.jpg";
|
||||
Texture diff = loadTextureOr(diffPath, null);
|
||||
if (diff != null) {
|
||||
diff.setWrap(Texture.WrapMode.Repeat);
|
||||
@@ -327,7 +327,7 @@ public class RiverState extends BaseAppState {
|
||||
"waterfall_particles", ParticleMesh.Type.Triangle, 60);
|
||||
|
||||
Material pMat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md");
|
||||
Texture pTex = loadTextureOr("Textures/Water/spray.png", "Effects/Smoke/Smoke.png");
|
||||
Texture pTex = loadTextureOr("Textures/internal/Water/spray.png", "Effects/Smoke/Smoke.png");
|
||||
if (pTex != null) pMat.setTexture("Texture", pTex);
|
||||
pMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive);
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.ChunkTerrainIO;
|
||||
import de.blight.common.MapData;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -36,6 +39,8 @@ import java.util.List;
|
||||
*/
|
||||
public class TerrainChunkState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TerrainChunkState.class);
|
||||
|
||||
// ── Listener-Interface ────────────────────────────────────────────────────
|
||||
|
||||
public interface ChunkListener {
|
||||
@@ -70,6 +75,10 @@ public class TerrainChunkState extends BaseAppState {
|
||||
private int lastPlayerCx = Integer.MIN_VALUE;
|
||||
private int lastPlayerCz = Integer.MIN_VALUE;
|
||||
|
||||
/** Beim nächsten update() diese Position statt Kamera-Position verwenden (einmalig). */
|
||||
private float spawnHintX = Float.NaN;
|
||||
private float spawnHintZ = Float.NaN;
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public TerrainChunkState(BulletAppState bulletAppState, Material terrainMaterial, MapData mapData) {
|
||||
@@ -80,57 +89,89 @@ public class TerrainChunkState extends BaseAppState {
|
||||
|
||||
// ── 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);
|
||||
|
||||
public void loadChunkHeights() {
|
||||
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);
|
||||
log.error("[TerrainChunkState] Chunk-Export fehlgeschlagen: {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
for (int cz = 0; cz < N; cz++) {
|
||||
for (int cx = 0; cx < N; cx++) {
|
||||
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
|
||||
if (chunkHeights[ci] != null) continue;
|
||||
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.");
|
||||
}
|
||||
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.cam = app.getCamera();
|
||||
Arrays.fill(chunkLod, -1);
|
||||
|
||||
ensureTerrainRoot(this.app);
|
||||
|
||||
loadChunkHeights(); // idempotent – überspringt bereits geladene Chunks
|
||||
log.info("[TerrainChunkState] {} Chunks geladen.", TOTAL);
|
||||
}
|
||||
|
||||
private void ensureTerrainRoot(SimpleApplication appRef) {
|
||||
if (terrainRoot == null) {
|
||||
terrainRoot = new Node("terrainChunks");
|
||||
appRef.getRootNode().attachChild(terrainRoot);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
for (int ci = 0; ci < TOTAL; ci++) removePhysics(ci);
|
||||
// Beim Herunterfahren ist BulletAppState u.U. bereits aufgeräumt und hat die
|
||||
// PhysicsSpace geleert. removePhysics() würde dann Warnungen "does not exist" loggen.
|
||||
// Wir null-en nur die Referenzen — BulletAppState kümmert sich um die PhysicsSpace.
|
||||
java.util.Arrays.fill(physics, null);
|
||||
((SimpleApplication) app).getRootNode().detachChild(terrainRoot);
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
/** Erzwingt beim nächsten update() eine Physik-/LOD-Aktualisierung um die Spawn-Position. */
|
||||
public void setSpawnHint(float worldX, float worldZ) {
|
||||
spawnHintX = worldX;
|
||||
spawnHintZ = worldZ;
|
||||
lastPlayerCx = Integer.MIN_VALUE;
|
||||
lastPlayerCz = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
/** Gibt zurück ob für den Chunk an der Weltposition bereits ein Physics-Collider aktiv ist. */
|
||||
public boolean hasPhysicsAt(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)));
|
||||
return physics[ChunkTerrainIO.chunkIndex(cx, cz)] != null;
|
||||
}
|
||||
|
||||
// ── Update: LOD + Physik ──────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
Vector3f camPos = cam.getLocation();
|
||||
int pcx = worldToChunk(camPos.x);
|
||||
int pcz = worldToChunk(camPos.z);
|
||||
float refX, refZ;
|
||||
if (!Float.isNaN(spawnHintX)) {
|
||||
refX = spawnHintX; refZ = spawnHintZ;
|
||||
spawnHintX = Float.NaN; spawnHintZ = Float.NaN;
|
||||
} else {
|
||||
Vector3f camPos = cam.getLocation();
|
||||
refX = camPos.x; refZ = camPos.z;
|
||||
}
|
||||
int pcx = worldToChunk(refX);
|
||||
int pcz = worldToChunk(refZ);
|
||||
|
||||
if (pcx == lastPlayerCx && pcz == lastPlayerCz) return;
|
||||
lastPlayerCx = pcx;
|
||||
@@ -243,7 +284,7 @@ public class TerrainChunkState extends BaseAppState {
|
||||
|
||||
Geometry geom = new Geometry("tc_" + cx + "_" + cz, mesh);
|
||||
geom.setMaterial(terrainMaterial);
|
||||
geom.setShadowMode(RenderQueue.ShadowMode.Receive);
|
||||
geom.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
chunkNodes[ci].attachChild(geom);
|
||||
|
||||
chunkLod[ci] = lod;
|
||||
@@ -360,6 +401,10 @@ public class TerrainChunkState extends BaseAppState {
|
||||
RigidBodyControl rbc = new RigidBodyControl(shape, 0f);
|
||||
chunkNodes[ci].addControl(rbc);
|
||||
bulletAppState.getPhysicsSpace().add(rbc);
|
||||
// jme3-jbullet's HeightfieldCollisionShape macht die AABB symmetrisch um 0
|
||||
// (min = -max), nicht um (min+max)/2. Die Höhenwerte h[i] werden direkt als
|
||||
// lokale Y-Koordinaten verwendet. Der Body muss deshalb bei Y=0 liegen (Node-Y=0),
|
||||
// damit Kollisionsfläche = 0 + h[i] = h[i]. Kein setPhysicsLocation nötig.
|
||||
physics[ci] = rbc;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
|
||||
import com.jme3.bullet.control.RigidBodyControl;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
@@ -24,6 +25,7 @@ public class VoxelChunkNode extends Node {
|
||||
|
||||
private final Geometry[] lodGeos = new Geometry[3];
|
||||
private int currentLod = -1;
|
||||
private boolean patchMode = false;
|
||||
|
||||
private RigidBodyControl physics;
|
||||
private BulletAppState bulletState;
|
||||
@@ -38,17 +40,24 @@ public class VoxelChunkNode extends Node {
|
||||
float wy = chunk.cy * (float) VoxelChunk.CELLS;
|
||||
float wz = chunk.cz * VoxelChunk.CELLS - 2048f;
|
||||
setLocalTranslation(wx, wy, wz);
|
||||
setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
}
|
||||
|
||||
/** Baut das Mesh für den angegebenen LOD-Level neu. lod: 0/1/2. */
|
||||
public void rebuildMesh(int lod) {
|
||||
rebuildMesh(lod, null);
|
||||
}
|
||||
|
||||
/** Baut das Mesh mit Nachbar-Chunks für nahtlose Grenzen. neighbors: [+X,-X,+Y,-Y,+Z,-Z]. */
|
||||
public void rebuildMesh(int lod, VoxelChunk[] neighbors) {
|
||||
if (lod < 0 || lod > 2) return;
|
||||
Mesh mesh = MarchingCubes.build(chunk, LOD_STEPS[lod]);
|
||||
Mesh mesh = MarchingCubes.build(chunk, LOD_STEPS[lod], neighbors);
|
||||
if (mesh == null) {
|
||||
// Keine Oberfläche: evtl. vorherige Geo entfernen
|
||||
if (lodGeos[lod] != null) { detachChild(lodGeos[lod]); lodGeos[lod] = null; }
|
||||
return;
|
||||
}
|
||||
applyPatchMode(mesh);
|
||||
if (lodGeos[lod] == null) {
|
||||
lodGeos[lod] = new Geometry("lod" + lod, mesh);
|
||||
lodGeos[lod].setMaterial(material);
|
||||
@@ -65,6 +74,7 @@ public class VoxelChunkNode extends Node {
|
||||
*/
|
||||
public void setLodMesh(int lod, Mesh mesh) {
|
||||
if (lod < 0 || lod > 2 || mesh == null) return;
|
||||
applyPatchMode(mesh);
|
||||
if (lodGeos[lod] == null) {
|
||||
lodGeos[lod] = new Geometry("lod" + lod, mesh);
|
||||
lodGeos[lod].setMaterial(material);
|
||||
@@ -73,6 +83,23 @@ public class VoxelChunkNode extends Node {
|
||||
}
|
||||
}
|
||||
|
||||
/** Aktiviert / deaktiviert GL_PATCHES-Modus auf allen vorhandenen LOD-Meshes. */
|
||||
public void enablePatchMode(boolean enabled) {
|
||||
this.patchMode = enabled;
|
||||
for (Geometry geo : lodGeos) {
|
||||
if (geo != null && geo.getMesh() != null) applyPatchMode(geo.getMesh());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPatchMode(Mesh mesh) {
|
||||
if (patchMode) {
|
||||
mesh.setMode(Mesh.Mode.Patch);
|
||||
mesh.setPatchVertexCount(3);
|
||||
} else {
|
||||
mesh.setMode(Mesh.Mode.Triangles);
|
||||
}
|
||||
}
|
||||
|
||||
/** Schaltet auf den angegebenen LOD um (blendet andere aus). */
|
||||
public void setActiveLod(int lod) {
|
||||
if (lod == currentLod) return;
|
||||
|
||||
@@ -5,17 +5,22 @@ import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.export.binary.BinaryImporter;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.texture.TextureArray;
|
||||
import de.blight.common.MapData;
|
||||
import de.blight.common.VoxelChunk;
|
||||
import de.blight.common.VoxelChunkIO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -32,7 +37,7 @@ public class VoxelChunkState extends BaseAppState
|
||||
private static final Logger log = LoggerFactory.getLogger(VoxelChunkState.class);
|
||||
|
||||
private final BulletAppState bulletState;
|
||||
private final String[] texturePaths; // 4 Pfade der unteren Terrain-Texturen
|
||||
private final MapData mapData;
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
@@ -42,9 +47,9 @@ public class VoxelChunkState extends BaseAppState
|
||||
// key = cx | ((long)cy << 16) | ((long)cz << 32)
|
||||
private final Map<Long, VoxelChunkNode> nodes = new HashMap<>();
|
||||
|
||||
public VoxelChunkState(BulletAppState bulletState, String[] texturePaths) {
|
||||
this.bulletState = bulletState;
|
||||
this.texturePaths = texturePaths;
|
||||
public VoxelChunkState(BulletAppState bulletState, MapData mapData) {
|
||||
this.bulletState = bulletState;
|
||||
this.mapData = mapData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,7 +70,21 @@ public class VoxelChunkState extends BaseAppState
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
@Override public void update(float tpf) {}
|
||||
|
||||
public void setDebugNoLight(boolean enabled) {
|
||||
if (voxelMaterial != null) voxelMaterial.setBoolean("DebugNoLight", enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
|
||||
if (dns == null || voxelMaterial == null || dns.getSunLight() == null) return;
|
||||
voxelMaterial.setVector3("LightDir", dns.getSunDirection().negate());
|
||||
ColorRGBA sc = dns.getSunLight().getColor();
|
||||
ColorRGBA ac = dns.getAmbientLight().getColor();
|
||||
voxelMaterial.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b));
|
||||
voxelMaterial.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b));
|
||||
}
|
||||
|
||||
// ── ChunkListener ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -104,12 +123,24 @@ public class VoxelChunkState extends BaseAppState
|
||||
// ── Intern ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadLayersForXZ(int cx, int cz, int lod) {
|
||||
// Suche alle vorhandenen .blvc-Dateien für diesen cx/cz
|
||||
// Einfachste Lösung: bekannte cy-Range scannen (-8..+8)
|
||||
for (int cy = -8; cy <= 8; cy++) {
|
||||
if (!VoxelChunkIO.exists(cx, cy, cz)) continue;
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (nodes.containsKey(key)) continue; // bereits geladen
|
||||
if (nodes.containsKey(key)) continue;
|
||||
|
||||
// Gebackene J3O-Meshes bevorzugt laden — nur wenn auch .blvc-Quelldaten existieren
|
||||
// (sonst werden veraltete Meshes von gelöschten Voxeln als Geisterflächen angezeigt)
|
||||
if (VoxelChunkIO.bakedExists(cx, cy, cz) && VoxelChunkIO.exists(cx, cy, cz)) {
|
||||
try {
|
||||
addBakedNode(key, cx, cy, cz, lod);
|
||||
} catch (Exception e) {
|
||||
log.warn("Gebackenen Voxel-Chunk laden fehlgeschlagen ({},{},{}): {}",
|
||||
cx, cy, cz, e.getMessage());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback: .blvc laden + Marching Cubes zur Laufzeit
|
||||
if (!VoxelChunkIO.exists(cx, cy, cz)) continue;
|
||||
try {
|
||||
VoxelChunk chunk = VoxelChunkIO.load(cx, cy, cz);
|
||||
addNode(key, chunk, lod);
|
||||
@@ -119,6 +150,31 @@ public class VoxelChunkState extends BaseAppState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt vorgebackene LOD-Meshes aus den .j3o-Dateien und hängt sie als
|
||||
* VoxelChunkNode in die Szene ein (kein Marching Cubes zur Laufzeit).
|
||||
*/
|
||||
private void addBakedNode(long key, int cx, int cy, int cz, int lod) throws Exception {
|
||||
BinaryImporter importer = BinaryImporter.getInstance();
|
||||
importer.setAssetManager(assets);
|
||||
|
||||
VoxelChunk dummy = new VoxelChunk(cx, cy, cz);
|
||||
VoxelChunkNode node = new VoxelChunkNode(dummy, voxelMaterial);
|
||||
|
||||
for (int l = 0; l < 3; l++) {
|
||||
Path p = VoxelChunkIO.getBakedPath(cx, cy, cz, l);
|
||||
if (Files.exists(p)) {
|
||||
Mesh m = (Mesh) importer.load(p.toFile());
|
||||
node.setLodMesh(l, m);
|
||||
}
|
||||
}
|
||||
|
||||
node.setActiveLod(lod);
|
||||
if (lod == 0) node.updatePhysics(bulletState);
|
||||
voxelRoot.attachChild(node);
|
||||
nodes.put(key, node);
|
||||
}
|
||||
|
||||
private void addNode(long key, VoxelChunk chunk, int lod) {
|
||||
VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial);
|
||||
for (int l = 0; l < 3; l++) node.rebuildMesh(l);
|
||||
@@ -150,27 +206,59 @@ public class VoxelChunkState extends BaseAppState
|
||||
|
||||
private Material buildMaterial() {
|
||||
Material mat = new Material(assets, "MatDefs/Voxel.j3md");
|
||||
mat.setFloat("TexScale", 4f);
|
||||
|
||||
// Texture2DArray aus 4 Terrain-Textur-Pfaden aufbauen
|
||||
try {
|
||||
List<Image> images = new ArrayList<>();
|
||||
for (String path : texturePaths) {
|
||||
Texture t = (path != null && !path.isEmpty())
|
||||
? assets.loadTexture(path)
|
||||
: assets.loadTexture("Common/Textures/MissingTexture.png");
|
||||
images.add(t.getImage());
|
||||
mat.setFloat("TexScale", 8f);
|
||||
int[] slotIdxs = {
|
||||
mapData != null ? mapData.voxelFlatSlot : -1,
|
||||
mapData != null ? mapData.voxelSteepSlot : -1,
|
||||
mapData != null ? mapData.voxelCeilSlot : -1,
|
||||
};
|
||||
log.info("[Voxel] FlatSlot={} → '{}', SteepSlot={} → '{}', TexScale=8.0",
|
||||
slotIdxs[0], resolveSlotTex(slotIdxs[0]),
|
||||
slotIdxs[1], resolveSlotTex(slotIdxs[1]));
|
||||
String[] colSlots = { "TexFlat", "TexSteep", "TexCeil" };
|
||||
String[] normSlots = { "NormalMapFlat", "NormalMapSteep", "NormalMapCeil" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
String tex = resolveSlotTex(slotIdxs[i]);
|
||||
String norm = resolveSlotNorm(slotIdxs[i]);
|
||||
if (!tex.isEmpty()) {
|
||||
try {
|
||||
Texture t = assets.loadTexture(tex);
|
||||
t.getImage().setColorSpace(com.jme3.texture.image.ColorSpace.Linear);
|
||||
t.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture(colSlots[i], t);
|
||||
} catch (Exception e) {
|
||||
log.warn("Voxel-Textur {} nicht ladbar: {}", tex, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (!norm.isEmpty()) {
|
||||
try {
|
||||
Texture n = assets.loadTexture(norm);
|
||||
n.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture(normSlots[i], n);
|
||||
} catch (Exception e) {
|
||||
log.warn("Voxel-NormalMap {} nicht ladbar: {}", norm, e.getMessage());
|
||||
}
|
||||
}
|
||||
TextureArray texArray = new TextureArray(images);
|
||||
texArray.setMinFilter(Texture.MinFilter.Trilinear);
|
||||
texArray.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
mat.setParam("TexArray", com.jme3.shader.VarType.TextureArray, texArray);
|
||||
} catch (Exception e) {
|
||||
log.warn("Voxel Texture2DArray Aufbau fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
private String resolveSlotTex(int slot) {
|
||||
if (slot < 0 || mapData == null) return "";
|
||||
if (slot < 4) { String p = mapData.terrainTextures[slot]; return p != null ? p : ""; }
|
||||
if (slot < 8) { String p = mapData.upperTextures[slot - 4]; return p != null ? p : ""; }
|
||||
if (slot < 12) { String p = mapData.thirdTextures[slot - 8]; return p != null ? p : ""; }
|
||||
return "";
|
||||
}
|
||||
|
||||
private String resolveSlotNorm(int slot) {
|
||||
if (slot < 0 || mapData == null) return "";
|
||||
if (slot < 4) { String p = mapData.terrainNormalMaps[slot]; return p != null ? p : ""; }
|
||||
if (slot < 8) { String p = mapData.upperNormalMaps[slot - 4]; return p != null ? p : ""; }
|
||||
if (slot < 12) { String p = mapData.thirdNormalMaps[slot - 8]; return p != null ? p : ""; }
|
||||
return "";
|
||||
}
|
||||
|
||||
public static long chunkKey(int cx, int cy, int cz) {
|
||||
return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.bullet.control.RigidBodyControl;
|
||||
import com.jme3.bullet.util.CollisionShapeFactory;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.MatParam;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
@@ -20,6 +22,7 @@ import de.blight.game.animation.AnimationLibrary;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WorldObjectsState extends BaseAppState {
|
||||
@@ -29,6 +32,7 @@ public class WorldObjectsState extends BaseAppState {
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private BulletAppState bulletAppState;
|
||||
private final List<Material> sceneLitMaterials = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
@@ -102,6 +106,23 @@ public class WorldObjectsState extends BaseAppState {
|
||||
@Override protected void cleanup(Application app) {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (sceneLitMaterials.isEmpty()) return;
|
||||
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
|
||||
if (dns == null || dns.getSunLight() == null) return;
|
||||
Vector3f lightDir = dns.getSunDirection().negate();
|
||||
ColorRGBA sc = dns.getSunLight().getColor();
|
||||
ColorRGBA ac = dns.getAmbientLight().getColor();
|
||||
Vector3f sunVec = new Vector3f(sc.r, sc.g, sc.b);
|
||||
Vector3f ambVec = new Vector3f(ac.r, ac.g, ac.b);
|
||||
for (Material mat : sceneLitMaterials) {
|
||||
mat.setVector3("LightDir", lightDir);
|
||||
mat.setVector3("SunColor", sunVec);
|
||||
mat.setVector3("AmbientColor", ambVec);
|
||||
}
|
||||
}
|
||||
|
||||
private Spatial buildSpatial(PlacedModel m) {
|
||||
// Exportiertes Mesh hat Vorrang vor modelPath
|
||||
String path = (m.meshFile() != null && !m.meshFile().isBlank())
|
||||
@@ -113,17 +134,42 @@ public class WorldObjectsState extends BaseAppState {
|
||||
applyMaterial(spatial, m);
|
||||
} else {
|
||||
spatial = assets.loadModel(path);
|
||||
convertUnshadedToLighting(spatial);
|
||||
collectSceneLitMaterials(spatial);
|
||||
}
|
||||
spatial.setName("obj_" + path);
|
||||
|
||||
// LOD / Distance-Culling
|
||||
if (m.cullDistance() > 0f) {
|
||||
// Embedded-LOD: j3o enthält bereits lod0/lod1/lod2 Child-Nodes (TreeGenerator).
|
||||
// TreeLodControl im j3o ist editor-only und wird im Spiel nicht deserialisiert —
|
||||
// wir übernehmen die LOD-Steuerung selbst, direkt am j3o-Root-Node.
|
||||
if (spatial instanceof Node treeNode) {
|
||||
Spatial lod0 = treeNode.getChild("lod0");
|
||||
Spatial lod1 = treeNode.getChild("lod1");
|
||||
Spatial lod2 = treeNode.getChild("lod2");
|
||||
if (lod0 != null) {
|
||||
lod0.setCullHint(Spatial.CullHint.Inherit);
|
||||
if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always);
|
||||
if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always);
|
||||
treeNode.addControl(new ModelLodControl(
|
||||
app.getCamera(), lod0, lod1, lod2,
|
||||
m.lod1Distance(), m.lod2Distance(), m.cullDistance()));
|
||||
return treeNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: externe LOD-Dateien (lod1Path / lod2Path)
|
||||
Node lodRoot = new Node("lodRoot_" + path);
|
||||
lodRoot.attachChild(spatial);
|
||||
ModelLodControl ctrl = new ModelLodControl(
|
||||
assets,
|
||||
assets, app.getCamera(),
|
||||
m.lod1Path(), m.lod2Path(),
|
||||
m.lod1Distance(), m.lod2Distance(), m.cullDistance());
|
||||
ctrl.setLodLoadCallback(lod -> {
|
||||
convertUnshadedToLighting(lod);
|
||||
collectSceneLitMaterials(lod);
|
||||
});
|
||||
lodRoot.addControl(ctrl);
|
||||
return lodRoot;
|
||||
}
|
||||
@@ -145,6 +191,58 @@ public class WorldObjectsState extends BaseAppState {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ersetzt Unshaded-Materialien in geladenen j3o-Modellen durch Lighting.j3md,
|
||||
* damit diese auf den Tag/Nacht-Zyklus reagieren.
|
||||
*/
|
||||
private void convertUnshadedToLighting(Spatial spatial) {
|
||||
if (spatial instanceof Geometry geo) {
|
||||
Material mat = geo.getMaterial();
|
||||
if (mat == null) return;
|
||||
if (!"Unshaded".equals(mat.getMaterialDef().getName())) return;
|
||||
|
||||
MatParam colorMap = mat.getParam("ColorMap");
|
||||
MatParam color = mat.getParam("Color");
|
||||
RenderState origRS = mat.getAdditionalRenderState();
|
||||
|
||||
Material litMat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
|
||||
litMat.setBoolean("UseMaterialColors", false);
|
||||
|
||||
if (colorMap != null && colorMap.getValue() instanceof Texture t) {
|
||||
litMat.setTexture("DiffuseMap", t);
|
||||
} else if (color != null && color.getValue() instanceof ColorRGBA c) {
|
||||
litMat.setBoolean("UseMaterialColors", true);
|
||||
litMat.setColor("Diffuse", new ColorRGBA(c.r, c.g, c.b, 1f));
|
||||
litMat.setColor("Ambient", new ColorRGBA(c.r * 0.3f, c.g * 0.3f, c.b * 0.3f, 1f));
|
||||
}
|
||||
|
||||
RenderState rs = litMat.getAdditionalRenderState();
|
||||
rs.setBlendMode(origRS.getBlendMode());
|
||||
rs.setFaceCullMode(origRS.getFaceCullMode());
|
||||
|
||||
geo.setMaterial(litMat);
|
||||
} else if (spatial instanceof Node node) {
|
||||
for (Spatial child : new ArrayList<>(node.getChildren())) {
|
||||
convertUnshadedToLighting(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectSceneLitMaterials(Spatial spatial) {
|
||||
if (spatial instanceof Geometry geo) {
|
||||
Material mat = geo.getMaterial();
|
||||
if (mat == null) return;
|
||||
String name = mat.getMaterialDef().getName();
|
||||
if ("Tree".equals(name) || "TreeLeaf".equals(name)) {
|
||||
sceneLitMaterials.add(mat);
|
||||
}
|
||||
} else if (spatial instanceof Node node) {
|
||||
for (Spatial child : node.getChildren()) {
|
||||
collectSceneLitMaterials(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMaterial(Spatial s, PlacedModel m) {
|
||||
boolean hasTex = m.texturePath() != null && !m.texturePath().isBlank();
|
||||
boolean hasNmap = m.normalMapPath() != null && !m.normalMapPath().isBlank();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>ERROR</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||
</encoder>
|
||||
|
||||
Reference in New Issue
Block a user