From 51b1b53a274a24bcff11fb7099376b0cf8ea48e9 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 21 Jul 2026 22:26:06 +0200 Subject: [PATCH] Dialog Camera und DoF bearbeitet --- .../game/control/ThirdPersonCamera.java | 3 +- .../java/de/blight/game/scene/WorldScene.java | 10 +- .../de/blight/game/state/DialogHudState.java | 22 +++- .../de/blight/game/state/WorldNpcsState.java | 104 ++++++++++++------ 4 files changed, 97 insertions(+), 42 deletions(-) diff --git a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java index e0645de..3ad48a1 100644 --- a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java +++ b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java @@ -38,6 +38,7 @@ public class ThirdPersonCamera { private float distance = BASE_DISTANCE; private boolean paused = false; + public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) { this.cam = cam; this.inputManager = inputManager; @@ -100,6 +101,6 @@ public class ThirdPersonCamera { 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; } } diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 739e12d..ceeac0f 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -503,11 +503,15 @@ public class WorldScene extends BaseAppState { log.info("[Debug] Nebel {}", nowEnabled ? "AN" : "AUS"); } - /** Tiefenschärfe für Dialogszenen aktivieren. focusDistance in Welt-Einheiten ab Kamera. */ - public void focusDof(float focusDistance, float blurScale) { + /** + * 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(focusDistance * 0.4f); + dofFilter.setFocusRange(focusRange); dofFilter.setBlurScale(blurScale); dofFilter.setEnabled(true); } diff --git a/blight-game/src/main/java/de/blight/game/state/DialogHudState.java b/blight-game/src/main/java/de/blight/game/state/DialogHudState.java index 6d8c734..55881e6 100644 --- a/blight-game/src/main/java/de/blight/game/state/DialogHudState.java +++ b/blight-game/src/main/java/de/blight/game/state/DialogHudState.java @@ -35,6 +35,7 @@ import de.blight.common.model.trigger.ChangeRoutineTrigger; import de.blight.common.model.trigger.NpcStatusTrigger; import de.blight.common.model.trigger.Trigger; import de.blight.game.audio.DialogTtsService; +import de.blight.game.scene.WorldScene; import de.blight.game.config.MenuCanvas; import de.blight.game.config.NinePatch; import de.blight.lang.TextResolver; @@ -224,7 +225,7 @@ public class DialogHudState extends BaseAppState { int optIdx = scrollOffset + slot; if (optIdx >= total) break; 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) { selectedOpt = optIdx; renderOptions(); @@ -320,6 +321,10 @@ public class DialogHudState extends BaseAppState { this.pageIdx = 0; renderCurrentTextPage(speaker); 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) { @@ -358,6 +363,10 @@ public class DialogHudState extends BaseAppState { 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) { // Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten) String label = ""; @@ -480,7 +489,7 @@ public class DialogHudState extends BaseAppState { // Klick auf Scroll-Pfeil ▲ if (scrollOffset > 0) { 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); renderOptions(); return; @@ -490,7 +499,7 @@ public class DialogHudState extends BaseAppState { // Klick auf Scroll-Pfeil ▼ if (scrollOffset + MAX_OPTIONS < total) { 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); renderOptions(); return; @@ -502,8 +511,8 @@ public class DialogHudState extends BaseAppState { int optIdx = scrollOffset + slot; if (optIdx >= total) break; float ty = baseY - slot * LINE_H_OPT; - float by = ty - LINE_H_OPT + 4f; - float byEnd = ty + 4f; + float by = ty - LINE_H_OPT; + float byEnd = ty + FONT_OPT; if (vy >= by && vy <= byEnd) { log.debug("[DialogHud] Treffer: Option {} (slot {} ty={})", optIdx, slot, ty); selectedOpt = optIdx; @@ -716,6 +725,9 @@ public class DialogHudState extends BaseAppState { autoAdvanceTimer = -1f; if (tts != null) tts.stop(); setHudsVisible(true); + + WorldScene ws = getStateManager().getState(WorldScene.class); + if (ws != null) ws.clearDof(); unregisterInput(); if (canvasNode != null) { canvasNode.setCullHint(Spatial.CullHint.Always); diff --git a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java index f2e2f5d..ffd2fad 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java @@ -35,6 +35,7 @@ import de.blight.game.animation.AnimationLibrary; import de.blight.game.config.KeyBindings; import de.blight.game.control.PlayerInputControl; import de.blight.game.control.ThirdPersonCamera; +import de.blight.game.scene.WorldScene; import de.blight.game.state.TerrainChunkState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -497,40 +498,72 @@ public class WorldNpcsState extends BaseAppState { } 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); } + // 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.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). */ @@ -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() { if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;