Compare commits
2 Commits
b960fb689a
...
51b1b53a27
| Author | SHA1 | Date | |
|---|---|---|---|
| 51b1b53a27 | |||
| ae244e3969 |
@@ -30,7 +30,8 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
private static final int ROW_AA = 3;
|
private static final int ROW_AA = 3;
|
||||||
private static final int ROW_VIEWDIST = 4;
|
private static final int ROW_VIEWDIST = 4;
|
||||||
private static final int ROW_SHADOW = 5;
|
private static final int ROW_SHADOW = 5;
|
||||||
private static final int ROW_COUNT = 6;
|
private static final int ROW_POSTFX = 6;
|
||||||
|
private static final int ROW_COUNT = 7;
|
||||||
|
|
||||||
private final GraphicsSettings live;
|
private final GraphicsSettings live;
|
||||||
private GraphicsSettings edit;
|
private GraphicsSettings edit;
|
||||||
@@ -63,6 +64,7 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
edit.samples = live.samples;
|
edit.samples = live.samples;
|
||||||
edit.viewDistance = live.viewDistance;
|
edit.viewDistance = live.viewDistance;
|
||||||
edit.shadowQuality = live.shadowQuality;
|
edit.shadowQuality = live.shadowQuality;
|
||||||
|
edit.postFxQuality = live.postFxQuality;
|
||||||
|
|
||||||
resIdx = 0;
|
resIdx = 0;
|
||||||
for (int i = 0; i < resolutions.length; i++) {
|
for (int i = 0; i < resolutions.length; i++) {
|
||||||
@@ -86,7 +88,8 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
"menu.graphics.row.vsync",
|
"menu.graphics.row.vsync",
|
||||||
"menu.graphics.row.aa",
|
"menu.graphics.row.aa",
|
||||||
"menu.graphics.row.viewdist",
|
"menu.graphics.row.viewdist",
|
||||||
"menu.graphics.row.shadow"
|
"menu.graphics.row.shadow",
|
||||||
|
"menu.graphics.row.postfx"
|
||||||
};
|
};
|
||||||
|
|
||||||
float lblX = CONT_X + 30f;
|
float lblX = CONT_X + 30f;
|
||||||
@@ -145,6 +148,11 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
case MEDIUM -> t("menu.graphics.val.shadow.medium");
|
case MEDIUM -> t("menu.graphics.val.shadow.medium");
|
||||||
case HIGH -> t("menu.graphics.val.shadow.high");
|
case HIGH -> t("menu.graphics.val.shadow.high");
|
||||||
};
|
};
|
||||||
|
case ROW_POSTFX -> switch (edit.postFxQuality) {
|
||||||
|
case OFF -> t("menu.graphics.val.off");
|
||||||
|
case MEDIUM -> t("menu.graphics.val.shadow.medium");
|
||||||
|
case HIGH -> t("menu.graphics.val.shadow.high");
|
||||||
|
};
|
||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
BitmapText vt = valTexts[row];
|
BitmapText vt = valTexts[row];
|
||||||
@@ -178,18 +186,24 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
GraphicsSettings.ShadowQuality[] sqs = GraphicsSettings.ShadowQuality.values();
|
GraphicsSettings.ShadowQuality[] sqs = GraphicsSettings.ShadowQuality.values();
|
||||||
edit.shadowQuality = sqs[(edit.shadowQuality.ordinal() + dir + sqs.length) % sqs.length];
|
edit.shadowQuality = sqs[(edit.shadowQuality.ordinal() + dir + sqs.length) % sqs.length];
|
||||||
break;
|
break;
|
||||||
|
case ROW_POSTFX:
|
||||||
|
GraphicsSettings.PostFxQuality[] pfqs = GraphicsSettings.PostFxQuality.values();
|
||||||
|
edit.postFxQuality = pfqs[(edit.postFxQuality.ordinal() + dir + pfqs.length) % pfqs.length];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
refreshText(row);
|
refreshText(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyAndSave() {
|
private void applyAndSave() {
|
||||||
boolean shadowChanged = live.shadowQuality != edit.shadowQuality;
|
boolean shadowChanged = live.shadowQuality != edit.shadowQuality;
|
||||||
|
boolean postFxChanged = live.postFxQuality != edit.postFxQuality;
|
||||||
boolean needsRestart = live.width != edit.width
|
boolean needsRestart = live.width != edit.width
|
||||||
|| live.height != edit.height
|
|| live.height != edit.height
|
||||||
|| live.fullscreen != edit.fullscreen
|
|| live.fullscreen != edit.fullscreen
|
||||||
|| live.vsync != edit.vsync
|
|| live.vsync != edit.vsync
|
||||||
|| live.samples != edit.samples
|
|| live.samples != edit.samples
|
||||||
|| shadowChanged;
|
|| shadowChanged
|
||||||
|
|| postFxChanged;
|
||||||
|
|
||||||
live.width = edit.width;
|
live.width = edit.width;
|
||||||
live.height = edit.height;
|
live.height = edit.height;
|
||||||
@@ -198,6 +212,7 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
live.samples = edit.samples;
|
live.samples = edit.samples;
|
||||||
live.viewDistance = edit.viewDistance;
|
live.viewDistance = edit.viewDistance;
|
||||||
live.shadowQuality = edit.shadowQuality;
|
live.shadowQuality = edit.shadowQuality;
|
||||||
|
live.postFxQuality = edit.postFxQuality;
|
||||||
GraphicsStore.save(live);
|
GraphicsStore.save(live);
|
||||||
|
|
||||||
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
|
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
|
||||||
|
|||||||
@@ -10,6 +10,29 @@ public class GraphicsSettings {
|
|||||||
public ShadowQuality shadowQuality = ShadowQuality.MEDIUM;
|
public ShadowQuality shadowQuality = ShadowQuality.MEDIUM;
|
||||||
/** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */
|
/** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */
|
||||||
public boolean debugLod2 = false;
|
public boolean debugLod2 = false;
|
||||||
|
public PostFxQuality postFxQuality = PostFxQuality.HIGH;
|
||||||
|
|
||||||
|
public enum PostFxQuality {
|
||||||
|
// fxaa toneMap bloomI bloomExp godRays
|
||||||
|
OFF ( false, false, 0f, 0f, false),
|
||||||
|
MEDIUM( true, true, 0.4f, 3.0f, false),
|
||||||
|
HIGH ( true, true, 0.6f, 3.5f, true );
|
||||||
|
|
||||||
|
public final boolean fxaa;
|
||||||
|
public final boolean toneMap;
|
||||||
|
public final float bloomIntensity;
|
||||||
|
public final float bloomExposurePower;
|
||||||
|
public final boolean godRays;
|
||||||
|
|
||||||
|
PostFxQuality(boolean fxaa, boolean toneMap,
|
||||||
|
float bloomIntensity, float bloomExposurePower, boolean godRays) {
|
||||||
|
this.fxaa = fxaa;
|
||||||
|
this.toneMap = toneMap;
|
||||||
|
this.bloomIntensity = bloomIntensity;
|
||||||
|
this.bloomExposurePower = bloomExposurePower;
|
||||||
|
this.godRays = godRays;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schattenqualitäts-Stufen.
|
* Schattenqualitäts-Stufen.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class ThirdPersonCamera {
|
|||||||
private static final float BASE_DISTANCE = 5f;
|
private static final float BASE_DISTANCE = 5f;
|
||||||
private static final float MIN_DISTANCE = BASE_DISTANCE - 3f;
|
private static final float MIN_DISTANCE = BASE_DISTANCE - 3f;
|
||||||
private static final float MAX_DISTANCE = BASE_DISTANCE + 3f;
|
private static final float MAX_DISTANCE = BASE_DISTANCE + 3f;
|
||||||
private static final float MIN_VERTICAL_ANGLE = 0.08f; // Kamera immer leicht über Schulter
|
private static final float MIN_VERTICAL_ANGLE = -0.12f; // Kamera kann bis Kniehöhe sinken
|
||||||
private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
|
private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
|
||||||
private static final float TARGET_HEIGHT = 1.0f;
|
private static final float TARGET_HEIGHT = 1.0f;
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ public class ThirdPersonCamera {
|
|||||||
private float distance = BASE_DISTANCE;
|
private float distance = BASE_DISTANCE;
|
||||||
private boolean paused = false;
|
private boolean paused = false;
|
||||||
|
|
||||||
|
|
||||||
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
|
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
|
||||||
this.cam = cam;
|
this.cam = cam;
|
||||||
this.inputManager = inputManager;
|
this.inputManager = inputManager;
|
||||||
@@ -100,6 +101,6 @@ public class ThirdPersonCamera {
|
|||||||
cam.lookAt(pivot, Vector3f.UNIT_Y);
|
cam.lookAt(pivot, Vector3f.UNIT_Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
|
/** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
|
||||||
public float getYaw() { return yaw; }
|
public float getYaw() { return yaw; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ public class WorldScene extends BaseAppState {
|
|||||||
private MapData loadedMapData;
|
private MapData loadedMapData;
|
||||||
private FilterPostProcessor sharedFPP;
|
private FilterPostProcessor sharedFPP;
|
||||||
private BlightFogFilter fogFilter;
|
private BlightFogFilter fogFilter;
|
||||||
|
private com.jme3.post.filters.LightScatteringFilter lightScatterFilter;
|
||||||
|
private com.jme3.post.filters.DepthOfFieldFilter dofFilter;
|
||||||
private TerrainChunkState terrainChunkState;
|
private TerrainChunkState terrainChunkState;
|
||||||
private Material terrainMaterial;
|
private Material terrainMaterial;
|
||||||
|
|
||||||
@@ -435,6 +437,15 @@ public class WorldScene extends BaseAppState {
|
|||||||
for (Material mat : characterPbrMaterials) {
|
for (Material mat : characterPbrMaterials) {
|
||||||
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lightScatterFilter != null) {
|
||||||
|
Vector3f sunDir = dayNight.getSunDirection();
|
||||||
|
// Weltposition der Sonne: von der Kamera aus weit entfernt entgegen der Lichtrichtung
|
||||||
|
Vector3f sunWorld = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
|
||||||
|
lightScatterFilter.setLightPosition(sunWorld);
|
||||||
|
// Deaktivieren wenn Sonne hinter der Kamera (gleiche Richtung wie Kamera schaut)
|
||||||
|
lightScatterFilter.setEnabled(sunDir.dot(app.getCamera().getDirection()) < 0.2f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -492,6 +503,25 @@ public class WorldScene extends BaseAppState {
|
|||||||
log.info("[Debug] Nebel {}", nowEnabled ? "AN" : "AUS");
|
log.info("[Debug] Nebel {}", nowEnabled ? "AN" : "AUS");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tiefenschärfe aktivieren. focusRange definiert den beidseitigen Schärfebereich
|
||||||
|
* um focusDistance — alles innerhalb [focusDistance-focusRange, focusDistance+focusRange]
|
||||||
|
* bleibt scharf, der Rest wird geblurt.
|
||||||
|
*/
|
||||||
|
public void focusDof(float focusDistance, float focusRange, float blurScale) {
|
||||||
|
if (dofFilter == null) return;
|
||||||
|
dofFilter.setFocusDistance(focusDistance);
|
||||||
|
dofFilter.setFocusRange(focusRange);
|
||||||
|
dofFilter.setBlurScale(blurScale);
|
||||||
|
dofFilter.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tiefenschärfe deaktivieren. */
|
||||||
|
public void clearDof() {
|
||||||
|
if (dofFilter == null) return;
|
||||||
|
dofFilter.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override protected void cleanup(Application app) {
|
@Override protected void cleanup(Application app) {
|
||||||
}
|
}
|
||||||
@Override protected void onDisable() {}
|
@Override protected void onDisable() {}
|
||||||
@@ -778,6 +808,43 @@ public class WorldScene extends BaseAppState {
|
|||||||
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());
|
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GraphicsSettings.PostFxQuality pfx = graphicsSettings.postFxQuality;
|
||||||
|
|
||||||
|
if (pfx.bloomIntensity > 0f) {
|
||||||
|
com.jme3.post.filters.BloomFilter bloom =
|
||||||
|
new com.jme3.post.filters.BloomFilter(com.jme3.post.filters.BloomFilter.GlowMode.Scene);
|
||||||
|
bloom.setBloomIntensity(pfx.bloomIntensity);
|
||||||
|
bloom.setExposurePower(pfx.bloomExposurePower);
|
||||||
|
bloom.setDownSamplingFactor(2f);
|
||||||
|
fpp.addFilter(bloom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pfx.godRays) {
|
||||||
|
// Initiale Sonnenposition: von Kamera aus weit entfernt entgegen Lichtrichtung
|
||||||
|
Vector3f initSunPos = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
|
||||||
|
lightScatterFilter = new com.jme3.post.filters.LightScatteringFilter(initSunPos);
|
||||||
|
lightScatterFilter.setLightDensity(0.8f);
|
||||||
|
lightScatterFilter.setBlurStart(0.3f);
|
||||||
|
lightScatterFilter.setBlurWidth(0.4f);
|
||||||
|
lightScatterFilter.setNbSamples(150);
|
||||||
|
fpp.addFilter(lightScatterFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pfx.toneMap) {
|
||||||
|
fpp.addFilter(new com.jme3.post.filters.ToneMapFilter());
|
||||||
|
}
|
||||||
|
|
||||||
|
dofFilter = new com.jme3.post.filters.DepthOfFieldFilter();
|
||||||
|
dofFilter.setFocusDistance(0f);
|
||||||
|
dofFilter.setFocusRange(0f);
|
||||||
|
dofFilter.setBlurScale(1.5f);
|
||||||
|
dofFilter.setEnabled(false);
|
||||||
|
fpp.addFilter(dofFilter);
|
||||||
|
|
||||||
|
if (pfx.fxaa) {
|
||||||
|
fpp.addFilter(new com.jme3.post.filters.FXAAFilter());
|
||||||
|
}
|
||||||
|
|
||||||
app.getViewPort().addProcessor(fpp);
|
app.getViewPort().addProcessor(fpp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import de.blight.common.model.trigger.ChangeRoutineTrigger;
|
|||||||
import de.blight.common.model.trigger.NpcStatusTrigger;
|
import de.blight.common.model.trigger.NpcStatusTrigger;
|
||||||
import de.blight.common.model.trigger.Trigger;
|
import de.blight.common.model.trigger.Trigger;
|
||||||
import de.blight.game.audio.DialogTtsService;
|
import de.blight.game.audio.DialogTtsService;
|
||||||
|
import de.blight.game.scene.WorldScene;
|
||||||
import de.blight.game.config.MenuCanvas;
|
import de.blight.game.config.MenuCanvas;
|
||||||
import de.blight.game.config.NinePatch;
|
import de.blight.game.config.NinePatch;
|
||||||
import de.blight.lang.TextResolver;
|
import de.blight.lang.TextResolver;
|
||||||
@@ -224,7 +225,7 @@ public class DialogHudState extends BaseAppState {
|
|||||||
int optIdx = scrollOffset + slot;
|
int optIdx = scrollOffset + slot;
|
||||||
if (optIdx >= total) break;
|
if (optIdx >= total) break;
|
||||||
float ty = baseY - slot * LINE_H_OPT;
|
float ty = baseY - slot * LINE_H_OPT;
|
||||||
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) {
|
if (vy >= ty - LINE_H_OPT && vy < ty + FONT_OPT) {
|
||||||
if (selectedOpt != optIdx) {
|
if (selectedOpt != optIdx) {
|
||||||
selectedOpt = optIdx;
|
selectedOpt = optIdx;
|
||||||
renderOptions();
|
renderOptions();
|
||||||
@@ -320,6 +321,10 @@ public class DialogHudState extends BaseAppState {
|
|||||||
this.pageIdx = 0;
|
this.pageIdx = 0;
|
||||||
renderCurrentTextPage(speaker);
|
renderCurrentTextPage(speaker);
|
||||||
if (tts != null) tts.speak(rawText);
|
if (tts != null) tts.speak(rawText);
|
||||||
|
|
||||||
|
// Shot-Reverse-Shot: NPC spricht → über Schulter des Spielers; Spieler spricht → über NPC-Schulter
|
||||||
|
WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class);
|
||||||
|
if (npcs != null) npcs.switchDialogCamera(textPhase == Phase.TEXT_NPC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderCurrentTextPage(String speaker) {
|
private void renderCurrentTextPage(String speaker) {
|
||||||
@@ -358,6 +363,10 @@ public class DialogHudState extends BaseAppState {
|
|||||||
onOptionsShown.run();
|
onOptionsShown.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optionen = Spieler ist am Zug → Kamera über Schulter des NPC (blickt auf Spieler)
|
||||||
|
WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class);
|
||||||
|
if (npcs != null) npcs.switchDialogCamera(false);
|
||||||
|
|
||||||
for (DialogOption opt : options) {
|
for (DialogOption opt : options) {
|
||||||
// Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten)
|
// Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten)
|
||||||
String label = "";
|
String label = "";
|
||||||
@@ -480,7 +489,7 @@ public class DialogHudState extends BaseAppState {
|
|||||||
// Klick auf Scroll-Pfeil ▲
|
// Klick auf Scroll-Pfeil ▲
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset > 0) {
|
||||||
float ty = baseY + LINE_H_OPT;
|
float ty = baseY + LINE_H_OPT;
|
||||||
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) {
|
if (vy >= ty - LINE_H_OPT && vy < ty + FONT_OPT) {
|
||||||
scrollOffset = Math.max(0, scrollOffset - 1);
|
scrollOffset = Math.max(0, scrollOffset - 1);
|
||||||
renderOptions();
|
renderOptions();
|
||||||
return;
|
return;
|
||||||
@@ -490,7 +499,7 @@ public class DialogHudState extends BaseAppState {
|
|||||||
// Klick auf Scroll-Pfeil ▼
|
// Klick auf Scroll-Pfeil ▼
|
||||||
if (scrollOffset + MAX_OPTIONS < total) {
|
if (scrollOffset + MAX_OPTIONS < total) {
|
||||||
float ty = baseY - MAX_OPTIONS * LINE_H_OPT;
|
float ty = baseY - MAX_OPTIONS * LINE_H_OPT;
|
||||||
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) {
|
if (vy >= ty - LINE_H_OPT && vy < ty + FONT_OPT) {
|
||||||
scrollOffset = Math.min(total - MAX_OPTIONS, scrollOffset + 1);
|
scrollOffset = Math.min(total - MAX_OPTIONS, scrollOffset + 1);
|
||||||
renderOptions();
|
renderOptions();
|
||||||
return;
|
return;
|
||||||
@@ -502,8 +511,8 @@ public class DialogHudState extends BaseAppState {
|
|||||||
int optIdx = scrollOffset + slot;
|
int optIdx = scrollOffset + slot;
|
||||||
if (optIdx >= total) break;
|
if (optIdx >= total) break;
|
||||||
float ty = baseY - slot * LINE_H_OPT;
|
float ty = baseY - slot * LINE_H_OPT;
|
||||||
float by = ty - LINE_H_OPT + 4f;
|
float by = ty - LINE_H_OPT;
|
||||||
float byEnd = ty + 4f;
|
float byEnd = ty + FONT_OPT;
|
||||||
if (vy >= by && vy <= byEnd) {
|
if (vy >= by && vy <= byEnd) {
|
||||||
log.debug("[DialogHud] Treffer: Option {} (slot {} ty={})", optIdx, slot, ty);
|
log.debug("[DialogHud] Treffer: Option {} (slot {} ty={})", optIdx, slot, ty);
|
||||||
selectedOpt = optIdx;
|
selectedOpt = optIdx;
|
||||||
@@ -716,6 +725,9 @@ public class DialogHudState extends BaseAppState {
|
|||||||
autoAdvanceTimer = -1f;
|
autoAdvanceTimer = -1f;
|
||||||
if (tts != null) tts.stop();
|
if (tts != null) tts.stop();
|
||||||
setHudsVisible(true);
|
setHudsVisible(true);
|
||||||
|
|
||||||
|
WorldScene ws = getStateManager().getState(WorldScene.class);
|
||||||
|
if (ws != null) ws.clearDof();
|
||||||
unregisterInput();
|
unregisterInput();
|
||||||
if (canvasNode != null) {
|
if (canvasNode != null) {
|
||||||
canvasNode.setCullHint(Spatial.CullHint.Always);
|
canvasNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import de.blight.game.animation.AnimationLibrary;
|
|||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
import de.blight.game.control.PlayerInputControl;
|
import de.blight.game.control.PlayerInputControl;
|
||||||
import de.blight.game.control.ThirdPersonCamera;
|
import de.blight.game.control.ThirdPersonCamera;
|
||||||
|
import de.blight.game.scene.WorldScene;
|
||||||
import de.blight.game.state.TerrainChunkState;
|
import de.blight.game.state.TerrainChunkState;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -497,40 +498,72 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) {
|
private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) {
|
||||||
// NPC-Spatial auf Bodenniveau; playerPos = Physik-Kapsel-Mitte → auf NPC-Boden projizieren
|
|
||||||
float groundY = npcPos.y;
|
|
||||||
Vector3f npcFeet = new Vector3f(npcPos.x, groundY, npcPos.z);
|
|
||||||
Vector3f playerFeet = new Vector3f(playerPos.x, groundY, playerPos.z);
|
|
||||||
float charDist = Math.max(1.0f, npcFeet.distance(playerFeet));
|
|
||||||
|
|
||||||
// Lookat: Brust-/Schulterhöhe zwischen beiden Figuren
|
|
||||||
Vector3f lookAt = npcFeet.add(playerFeet).multLocal(0.5f);
|
|
||||||
lookAt.y += 1.2f;
|
|
||||||
|
|
||||||
// Senkrechte Seite zur NPC↔Spieler-Achse
|
|
||||||
Vector3f axis = npcFeet.subtract(playerFeet);
|
|
||||||
if (axis.lengthSquared() < 0.001f) { axis.set(1f, 0f, 0f); }
|
|
||||||
axis.normalizeLocal();
|
|
||||||
Vector3f side = axis.cross(Vector3f.UNIT_Y).normalizeLocal();
|
|
||||||
|
|
||||||
// FOV 45° → tan(22.5°) ≈ 0.41 → für charDist/2 sichtbar: dist = (charDist/2)/0.41 × Puffer
|
|
||||||
// Faktor 2.5 gibt genug Rand dass Figuren + Umgebung im Bild sind; Minimum 5 m
|
|
||||||
float dist = Math.max(5.0f, charDist * 2.5f);
|
|
||||||
float camUp = 1.6f;
|
|
||||||
|
|
||||||
// Seite wählen: erhöhter Horizontalstrahl (2 m über lookAt), um Terrain-Clipping zu vermeiden.
|
|
||||||
// avoidClipping() wird NICHT für die Distanz genutzt, weil Bodenstrahlen bei großen
|
|
||||||
// Distanzen häufig auf Geländekanten treffen und die Kamera fälschlich nah platzieren.
|
|
||||||
Vector3f elevated = lookAt.add(0f, 2f, 0f);
|
|
||||||
float freeA = openDistance(elevated, side);
|
|
||||||
float freeB = openDistance(elevated, side.negate());
|
|
||||||
Vector3f chosen = freeA >= freeB ? side : side.negate();
|
|
||||||
|
|
||||||
Vector3f camPos = lookAt.add(chosen.mult(dist)).add(0f, camUp, 0f);
|
|
||||||
|
|
||||||
if (thirdPersonCam != null) { thirdPersonCam.setPaused(true); }
|
if (thirdPersonCam != null) { thirdPersonCam.setPaused(true); }
|
||||||
|
// NPC spricht zuerst → Kamera über Schulter des Spielers (Zuhörer)
|
||||||
|
switchDialogCamera(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kamera für Shot-Reverse-Shot umschalten.
|
||||||
|
* npcSpeaking=true → Kamera hinter Spieler, blickt auf NPC
|
||||||
|
* npcSpeaking=false → Kamera hinter NPC, blickt auf Spieler
|
||||||
|
*/
|
||||||
|
public void switchDialogCamera(boolean npcSpeaking) {
|
||||||
|
if (dialogNpc == null || physicsChar == null) return;
|
||||||
|
// physicsChar.getPhysicsLocation() = Kapsel-Mitte (≈ 0,9 m über Boden)
|
||||||
|
Vector3f playerCenter = physicsChar.getPhysicsLocation();
|
||||||
|
Vector3f npcFeet = dialogNpc.visual().getLocalTranslation();
|
||||||
|
|
||||||
|
float playerFaceY = playerCenter.y + 0.9f;
|
||||||
|
float npcFaceY = npcFeet.y + 1.6f;
|
||||||
|
// Kamera-Abstand über dem Gesicht aus dem NPC-Fall ableiten, damit beide Shots identisch wirken
|
||||||
|
float camAboveFace = playerFaceY - npcFaceY; // Spielerschulter minus NPC-Gesicht (≈ 0.2 m)
|
||||||
|
|
||||||
|
if (npcSpeaking) {
|
||||||
|
// Listener = Spieler, Speaker = NPC
|
||||||
|
placeCamera(
|
||||||
|
playerCenter.x, playerFaceY, playerCenter.z,
|
||||||
|
npcFeet.x, npcFaceY, npcFeet.z
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Listener = NPC, Speaker = Spieler — gleicher Winkel wie im NPC-Fall
|
||||||
|
placeCamera(
|
||||||
|
npcFeet.x, playerFaceY + camAboveFace, npcFeet.z,
|
||||||
|
playerCenter.x, playerFaceY, playerCenter.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeCamera(float lisX, float lisY, float lisZ,
|
||||||
|
float spkX, float spkY, float spkZ) {
|
||||||
|
Vector3f shoulder = new Vector3f(lisX, lisY, lisZ);
|
||||||
|
Vector3f speakerFace = new Vector3f(spkX, spkY, spkZ);
|
||||||
|
|
||||||
|
// Horizontale Richtung Listener → Sprecher
|
||||||
|
Vector3f toSpeaker = new Vector3f(spkX - lisX, 0f, spkZ - lisZ);
|
||||||
|
if (toSpeaker.lengthSquared() < 0.001f) { toSpeaker.set(0f, 0f, 1f); }
|
||||||
|
toSpeaker.normalizeLocal();
|
||||||
|
|
||||||
|
// Schulterseite: die Seite mit mehr Freiraum wählen
|
||||||
|
Vector3f rightA = toSpeaker.cross(Vector3f.UNIT_Y).normalizeLocal();
|
||||||
|
Vector3f elevated = shoulder.add(0f, 0.5f, 0f);
|
||||||
|
Vector3f right = openDistance(elevated, rightA) >= openDistance(elevated, rightA.negate())
|
||||||
|
? rightA : rightA.negate();
|
||||||
|
|
||||||
|
// Kamera: hinter Listener, leicht zur Seite versetzt, weiter zurück für steileren Winkel
|
||||||
|
Vector3f camPos = shoulder
|
||||||
|
.add(right.mult(0.45f))
|
||||||
|
.subtract(toSpeaker.mult(1.5f));
|
||||||
|
|
||||||
|
// Gesicht nahe oberem Bildrand: Kamera deutlich unterhalb des Gesichts anzielen
|
||||||
|
Vector3f lookTarget = new Vector3f(spkX, spkY - 0.9f, spkZ);
|
||||||
cam.setLocation(camPos);
|
cam.setLocation(camPos);
|
||||||
cam.lookAt(lookAt, Vector3f.UNIT_Y);
|
cam.lookAt(lookTarget, Vector3f.UNIT_Y);
|
||||||
|
|
||||||
|
// DOF-Fokuspunkt = Gesicht des Sprechers (dynamischer Abstand)
|
||||||
|
float focusDist = camPos.distance(speakerFace);
|
||||||
|
WorldScene ws = getStateManager().getState(WorldScene.class);
|
||||||
|
if (ws != null) { ws.focusDof(focusDist, 10f, 0.8f); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Freier Abstand in {@code dir} ab {@code origin} bis zur ersten Kollision (max Float.MAX_VALUE). */
|
/** Freier Abstand in {@code dir} ab {@code origin} bis zur ersten Kollision (max Float.MAX_VALUE). */
|
||||||
@@ -597,7 +630,12 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Accessor für InteractionHudState ──────────────────────────────────────
|
// ── Accessoren ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Liefert das Spatial des aktuellen Dialog-NPCs, oder null wenn kein Dialog aktiv. */
|
||||||
|
public com.jme3.scene.Spatial getDialogNpcSpatial() {
|
||||||
|
return dialogNpc != null ? dialogNpc.visual() : null;
|
||||||
|
}
|
||||||
|
|
||||||
public String getHoveredLabelKey() {
|
public String getHoveredLabelKey() {
|
||||||
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;
|
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ menu.graphics.row.shadow=Schattenqualität
|
|||||||
menu.graphics.val.shadow.low=Niedrig
|
menu.graphics.val.shadow.low=Niedrig
|
||||||
menu.graphics.val.shadow.medium=Mittel
|
menu.graphics.val.shadow.medium=Mittel
|
||||||
menu.graphics.val.shadow.high=Hoch
|
menu.graphics.val.shadow.high=Hoch
|
||||||
|
menu.graphics.row.postfx=Post-Effekte
|
||||||
menu.graphics.val.resolution.native=Nativ
|
menu.graphics.val.resolution.native=Nativ
|
||||||
menu.graphics.btn.apply=Übernehmen
|
menu.graphics.btn.apply=Übernehmen
|
||||||
menu.graphics.btn.cancel=Abbrechen
|
menu.graphics.btn.cancel=Abbrechen
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ menu.graphics.row.shadow=Shadow Quality
|
|||||||
menu.graphics.val.shadow.low=Low
|
menu.graphics.val.shadow.low=Low
|
||||||
menu.graphics.val.shadow.medium=Medium
|
menu.graphics.val.shadow.medium=Medium
|
||||||
menu.graphics.val.shadow.high=High
|
menu.graphics.val.shadow.high=High
|
||||||
|
menu.graphics.row.postfx=Post Effects
|
||||||
menu.graphics.val.resolution.native=Native
|
menu.graphics.val.resolution.native=Native
|
||||||
menu.graphics.btn.apply=Apply
|
menu.graphics.btn.apply=Apply
|
||||||
menu.graphics.btn.cancel=Cancel
|
menu.graphics.btn.cancel=Cancel
|
||||||
|
|||||||
Reference in New Issue
Block a user