OceanSound: L/R-Panning fix via Dual-Source + constant-power panning
Zwei AudioNodes pro Sound (L + R) fest auf ±90° der Kamera positioniert; Lautstärkeverhältnis (constant-power) bestimmt die wahrgenommene Richtung. Behebt HRTF-0°-Stille wenn Kamera aufs Wasser zeigt. Außerdem weitere Soundsystem- und Editor-Korrekturen aus dieser Session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package de.blight.game.audio;
|
||||
|
||||
public enum SurfaceType {
|
||||
GRASS, DIRT, SAND, ROCK, GRAVEL, LEAVES, PAVEMENT, WOOD, UNKNOWN;
|
||||
GRASS, DIRT, SAND, ROCK, GRAVEL, LEAVES, PAVEMENT, WOOD, WATER, UNKNOWN;
|
||||
|
||||
public static SurfaceType fromTexturePath(String path) {
|
||||
if (path == null || path.isEmpty()) return UNKNOWN;
|
||||
|
||||
@@ -64,9 +64,15 @@ public class PlayerInputControl {
|
||||
private static final String BONE_RIGHT_FOOT = "mixamorig:RightFoot";
|
||||
// Y-Schwelle im Model-Space: Fuß gilt als am Boden wenn Y < FOOT_GROUND_Y
|
||||
private static final float FOOT_GROUND_Y = 0.05f;
|
||||
/** Wassertiefe (m) ab der Laufen/Sprinten gesperrt ist und Watgeräusch spielt. */
|
||||
public static final float WATER_WADING_DEPTH = 0.5f;
|
||||
|
||||
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||
private java.util.function.BiFunction<Float, Float, float[]> surfaceQuery;
|
||||
private java.util.function.BiFunction<Float, Float, Float> waterDepthQuery;
|
||||
private com.jme3.audio.AudioNode wadingNode;
|
||||
/** Aktuell berechnete Wassertiefe – einmal pro Update() gesetzt, in pollFootsteps() gelesen. */
|
||||
private float currentWaterDepth = 0f;
|
||||
private float lastLeftFootY = Float.MAX_VALUE;
|
||||
private float lastRightFootY = Float.MAX_VALUE;
|
||||
|
||||
@@ -191,6 +197,7 @@ public class PlayerInputControl {
|
||||
forward = backward = left = right = sprint = walk = false;
|
||||
autopilotDir = null;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
stopWading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +363,18 @@ public class PlayerInputControl {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sperrt alle Tastatureingaben, lässt aber die aktuelle Laufrichtung des
|
||||
* Physik-Charakters bestehen (Charakter bewegt sich weiter). Wird für den
|
||||
* Ertrink-Fade-out verwendet.
|
||||
*/
|
||||
public void blockInputsKeepMoving() {
|
||||
inputsBlocked = true;
|
||||
forward = backward = left = right = sprint = walk = false;
|
||||
autopilotDir = null;
|
||||
stopWading();
|
||||
}
|
||||
|
||||
/** Hebt die Input-Blockade auf und gibt Bewegungseingaben wieder frei. */
|
||||
public void unblockInputs() {
|
||||
inputsBlocked = false;
|
||||
@@ -554,11 +573,16 @@ public class PlayerInputControl {
|
||||
|
||||
boolean moving = moveDir.lengthSquared() > 0.001f;
|
||||
|
||||
// Wassertiefe bestimmen – einmal pro Frame, auch in pollFootsteps() genutzt
|
||||
Vector3f charPos = physicsChar.getPhysicsLocation();
|
||||
currentWaterDepth = (waterDepthQuery != null) ? waterDepthQuery.apply(charPos.x, charPos.z) : 0f;
|
||||
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
|
||||
|
||||
if (moving) {
|
||||
moveDir.normalizeLocal();
|
||||
float speed = walk ? MOVE_SPEED * WALK_MULT
|
||||
: sprint ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||
: sprint ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
physicsChar.setWalkDirection(moveDir.mult(speed));
|
||||
|
||||
if (visual != null) {
|
||||
@@ -580,9 +604,9 @@ public class PlayerInputControl {
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
} else if (moving) {
|
||||
target = walk ? AnimationAction.WALK
|
||||
: sprint ? AnimationAction.SPRINT
|
||||
: AnimationAction.RUN;
|
||||
target = (inDeepWater || walk) ? AnimationAction.WALK
|
||||
: sprint ? AnimationAction.SPRINT
|
||||
: AnimationAction.RUN;
|
||||
} else {
|
||||
target = AnimationAction.IDLE;
|
||||
}
|
||||
@@ -618,6 +642,14 @@ public class PlayerInputControl {
|
||||
this.surfaceQuery = query;
|
||||
}
|
||||
|
||||
public void setWaterDepthQuery(java.util.function.BiFunction<Float, Float, Float> query) {
|
||||
this.waterDepthQuery = query;
|
||||
}
|
||||
|
||||
public void setWadingSound(com.jme3.audio.AudioNode node) {
|
||||
this.wadingNode = node;
|
||||
}
|
||||
|
||||
private void logJointNames(com.jme3.anim.Armature arm) {
|
||||
if (arm == null) return;
|
||||
StringBuilder sb = new StringBuilder("[Footstep] Joint-Namen (").append(arm.getJointCount()).append("):");
|
||||
@@ -628,22 +660,34 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void pollFootsteps() {
|
||||
if (armature == null || footstepSystem == null || surfaceQuery == null) return;
|
||||
if (!physicsChar.onGround() || blockingAnimActive || lockedInPlace) {
|
||||
stopWading();
|
||||
lastLeftFootY = Float.MAX_VALUE;
|
||||
lastRightFootY = Float.MAX_VALUE;
|
||||
return;
|
||||
}
|
||||
float speed = physicsChar.getWalkDirection().length();
|
||||
if (speed < 0.001f) {
|
||||
stopWading();
|
||||
lastLeftFootY = Float.MAX_VALUE;
|
||||
lastRightFootY = Float.MAX_VALUE;
|
||||
return;
|
||||
}
|
||||
String gait = currentAnimToGait(currentAnim);
|
||||
if (gait == null) {
|
||||
|
||||
if (currentWaterDepth > WATER_WADING_DEPTH) {
|
||||
// Im Wasser: Watgeräusch abspielen, Fußgeräusche unterdrücken
|
||||
startWading();
|
||||
lastLeftFootY = Float.MAX_VALUE;
|
||||
lastRightFootY = Float.MAX_VALUE;
|
||||
return;
|
||||
}
|
||||
|
||||
stopWading();
|
||||
|
||||
if (armature == null || footstepSystem == null || surfaceQuery == null) return;
|
||||
String gait = currentAnimToGait(currentAnim);
|
||||
if (gait == null) return;
|
||||
|
||||
com.jme3.anim.Joint lf = armature.getJoint(BONE_LEFT_FOOT);
|
||||
com.jme3.anim.Joint rf = armature.getJoint(BONE_RIGHT_FOOT);
|
||||
if (lf != null) {
|
||||
@@ -662,6 +706,20 @@ public class PlayerInputControl {
|
||||
}
|
||||
}
|
||||
|
||||
private void startWading() {
|
||||
if (wadingNode == null) return;
|
||||
if (wadingNode.getStatus() != com.jme3.audio.AudioSource.Status.Playing) {
|
||||
wadingNode.play();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopWading() {
|
||||
if (wadingNode == null) return;
|
||||
if (wadingNode.getStatus() == com.jme3.audio.AudioSource.Status.Playing) {
|
||||
wadingNode.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private String currentAnimToGait(AnimationAction anim) {
|
||||
if (anim == AnimationAction.WALK) return "walking";
|
||||
if (anim == AnimationAction.RUN) return "running";
|
||||
|
||||
@@ -92,6 +92,7 @@ public class WorldScene extends BaseAppState {
|
||||
private de.blight.game.state.OceanSoundState oceanSound;
|
||||
private de.blight.game.state.AmbientSoundSystem ambientSounds;
|
||||
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||
private de.blight.game.state.DrownState drownState;
|
||||
|
||||
public WorldScene(KeyBindings keyBindings) {
|
||||
this.keyBindings = keyBindings;
|
||||
@@ -107,6 +108,11 @@ public class WorldScene extends BaseAppState {
|
||||
*/
|
||||
public float[] querySurfaceWeights(float worldX, float worldZ) {
|
||||
float[] result = new float[de.blight.game.audio.SurfaceType.values().length];
|
||||
// Wasser hat Vorrang vor Splatmap: Terrainoberfläche unter Wasserstand → WATER
|
||||
if (terrainChunkState != null && terrainChunkState.getHeightAt(worldX, worldZ) < 0f) {
|
||||
result[de.blight.game.audio.SurfaceType.WATER.ordinal()] = 1.0f;
|
||||
return result;
|
||||
}
|
||||
if (loadedMapData == null) return result;
|
||||
|
||||
int size = de.blight.common.MapData.SPLAT_SIZE;
|
||||
@@ -169,6 +175,12 @@ public class WorldScene extends BaseAppState {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Liefert die Wassertiefe an (worldX, worldZ) in Metern; 0 wenn an Land. */
|
||||
public float queryWaterDepth(float worldX, float worldZ) {
|
||||
if (terrainChunkState == null) return 0f;
|
||||
return Math.max(0f, -terrainChunkState.getHeightAt(worldX, worldZ));
|
||||
}
|
||||
|
||||
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
|
||||
public void reloadBindings(KeyBindings kb) {
|
||||
if (playerInput != null) playerInput.reloadBindings(kb);
|
||||
@@ -254,6 +266,20 @@ public class WorldScene extends BaseAppState {
|
||||
footstepSystem = new de.blight.game.audio.FootstepSystem(assetManager, rootNode, audioSettings,
|
||||
AnimationLibrary.findAssetRoot());
|
||||
playerInput.setFootstepSystem(footstepSystem, this::querySurfaceWeights);
|
||||
playerInput.setWaterDepthQuery(this::queryWaterDepth);
|
||||
try {
|
||||
com.jme3.audio.AudioNode wadingNode = new com.jme3.audio.AudioNode(
|
||||
assetManager, "audio/footsteps/water/wading.ogg", com.jme3.audio.AudioData.DataType.Buffer);
|
||||
wadingNode.setLooping(true);
|
||||
wadingNode.setPositional(false);
|
||||
de.blight.game.state.AudioSettingsState _as =
|
||||
app.getStateManager().getState(de.blight.game.state.AudioSettingsState.class);
|
||||
wadingNode.setVolume(_as != null ? _as.effectiveEffects() : 0.7f);
|
||||
rootNode.attachChild(wadingNode);
|
||||
playerInput.setWadingSound(wadingNode);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WorldScene] wading.ogg nicht ladbar – Watgeräusch deaktiviert: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Navigation: PathFinder + Terrain bereitstellen (Navigator wird in setAnimationContext erstellt)
|
||||
try {
|
||||
@@ -286,8 +312,14 @@ public class WorldScene extends BaseAppState {
|
||||
inventoryState = new InventoryState(mc, keyBindings);
|
||||
inventoryState.setEnabled(false);
|
||||
app.getStateManager().attach(inventoryState);
|
||||
app.getStateManager().attach(new de.blight.game.state.HudState(mc));
|
||||
}
|
||||
|
||||
// Ertrinken-System (Wassertiefe > 1,8 m → Teleport zum Strand)
|
||||
MainCharacter drownMc = findMainCharacter();
|
||||
drownState = new de.blight.game.state.DrownState(terrainChunkState, physicsChar, playerInput, drownMc);
|
||||
app.getStateManager().attach(drownState);
|
||||
|
||||
// Maus einfangen – keine Klick-Pflicht für Kamerasteuerung
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
}
|
||||
@@ -421,10 +453,16 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
playerInput.setInitialFacing(spawnYaw);
|
||||
|
||||
String reviveClip = de.blight.game.animation.AnimationLibrary.getClipForAction(
|
||||
AnimationLibrary.findAssetRoot(), setName, de.blight.game.animation.AnimationAction.REVIVE);
|
||||
float reviveLength = playerInput.getReviveClipLength();
|
||||
|
||||
// REVIVE-Info immer an DrownState weitergeben (für Ertrinken-Sequenz)
|
||||
if (drownState != null) {
|
||||
drownState.setReviveInfo(reviveClip, reviveLength);
|
||||
}
|
||||
|
||||
if ("true".equals(System.getProperty("blight.new.game"))) {
|
||||
String reviveClip = de.blight.game.animation.AnimationLibrary.getClipForAction(
|
||||
AnimationLibrary.findAssetRoot(), setName, de.blight.game.animation.AnimationAction.REVIVE);
|
||||
float reviveLength = playerInput.getReviveClipLength();
|
||||
app.getStateManager().attach(
|
||||
new de.blight.game.state.NewGameIntroState(playerInput, reviveClip, reviveLength));
|
||||
}
|
||||
|
||||
@@ -10,24 +10,29 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Spielt Wellensounds positional ab. Die AudioNodes werden EINMALIG nach dem
|
||||
* ersten gültigen Scan positioniert (vermeidet den initialen Sprung von (0,0,0)
|
||||
* zur Ozean-Kante, der OpenAL-Knistern verursacht). Danach wird die Position
|
||||
* nur noch alle SCAN_INTERVAL Sekunden aktualisiert, wenn sich der Ozean
|
||||
* signifikant verschoben hat.
|
||||
* Spielt Wellensounds mit korrektem L/R-Panning ab.
|
||||
*
|
||||
* Strategie: Je Sound zwei Quellen (L + R), die immer genau PAN_DIST Meter
|
||||
* links/rechts der Kamera positioniert sind (±90° Azimuth).
|
||||
* Das Lautstärkeverhältnis (constant-power Panning) bestimmt die wahrgenommene
|
||||
* Richtung. So entsteht kein HRTF-0°-Problem: Wasser direkt vorne → beide
|
||||
* Lautsprecher gleich laut (Phantommitte); Wasser links → links lauter.
|
||||
*/
|
||||
public class OceanSoundState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OceanSoundState.class);
|
||||
|
||||
private static final float MAX_DIST = 60f;
|
||||
private static final float REF_DIST = 8f;
|
||||
private static final float WIND_THRESHOLD = 20f;
|
||||
private static final float FADE_RATE = 1f / 3f;
|
||||
private static final float SCAN_INTERVAL = 0.25f;
|
||||
private static final float SCAN_STEP = 5f;
|
||||
/** Mindestverschiebung (m²) bevor die Node-Position aktualisiert wird. */
|
||||
private static final float POS_UPDATE_SQ = 4f * 4f; // 4 Meter
|
||||
private static final float MAX_DIST = 60f;
|
||||
private static final float REF_DIST = 8f;
|
||||
private static final float WIND_THRESHOLD = 20f;
|
||||
private static final float FADE_RATE = 1f / 3f;
|
||||
private static final float SCAN_INTERVAL = 0.25f;
|
||||
private static final float SCAN_STEP = 5f;
|
||||
/**
|
||||
* Abstand Source↔Listener. Muss < refDistance (10 m JME3-Default) sein,
|
||||
* damit Clamped-Inverse-Formel Gain = 1.0 liefert.
|
||||
*/
|
||||
private static final float PAN_DIST = 2f;
|
||||
|
||||
private static final float[] DIR_X = { 0f, 0.707f, 1f, 0.707f, 0f, -0.707f, -1f, -0.707f };
|
||||
private static final float[] DIR_Z = { 1f, 0.707f, 0f, -0.707f, -1f, -0.707f, 0f, 0.707f };
|
||||
@@ -35,22 +40,22 @@ public class OceanSoundState extends BaseAppState {
|
||||
private final TerrainChunkState terrain;
|
||||
|
||||
private SimpleApplication app;
|
||||
private AudioNode nodeCalmSound;
|
||||
private AudioNode nodeStormySound;
|
||||
// L/R-Paare: beide spielen dieselbe Datei, immer auf ±90° der Kamera
|
||||
private AudioNode nodeCalmL, nodeCalmR;
|
||||
private AudioNode nodeStormyL, nodeStormyR;
|
||||
|
||||
private final Vector3f playerPos = new Vector3f();
|
||||
private final Vector3f targetPos = new Vector3f();
|
||||
private final Vector3f nodePos = new Vector3f(); // zuletzt gesetzte Node-Position
|
||||
private final Vector3f playerPos = new Vector3f();
|
||||
private final Vector3f oceanPos = new Vector3f();
|
||||
|
||||
/** true bis der erste gültige Scan die Nodes positioniert und abgespielt hat. */
|
||||
private boolean firstScan = true;
|
||||
private boolean playing = false;
|
||||
private boolean firstScan = true;
|
||||
private boolean playing = false;
|
||||
private boolean oceanFound = false;
|
||||
|
||||
private float calmVol = 0f;
|
||||
private float stormyVol = 0f;
|
||||
private float scanTimer = 0f;
|
||||
private float calmVol = 0f;
|
||||
private float stormyVol = 0f;
|
||||
private float scanTimer = 0f;
|
||||
private boolean oceanInRange = false;
|
||||
private float oceanDist = Float.MAX_VALUE;
|
||||
private float oceanDist = Float.MAX_VALUE;
|
||||
|
||||
public OceanSoundState(TerrainChunkState terrain) {
|
||||
this.terrain = terrain;
|
||||
@@ -59,15 +64,18 @@ public class OceanSoundState extends BaseAppState {
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
nodeCalmSound = loadLoop("audio/ambient/water/waves_calm.ogg", "waves_calm");
|
||||
nodeStormySound = loadLoop("audio/ambient/water/waves_stormy.ogg", "waves_stormy");
|
||||
// Noch NICHT abspielen – erst wenn wir eine gültige Position haben (firstScan).
|
||||
nodeCalmL = loadLoop("audio/ambient/water/waves_calm.ogg", "waves_calm_L");
|
||||
nodeCalmR = loadLoop("audio/ambient/water/waves_calm.ogg", "waves_calm_R");
|
||||
nodeStormyL = loadLoop("audio/ambient/water/waves_stormy.ogg", "waves_stormy_L");
|
||||
nodeStormyR = loadLoop("audio/ambient/water/waves_stormy.ogg", "waves_stormy_R");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
stop(nodeCalmSound);
|
||||
stop(nodeStormySound);
|
||||
stop(nodeCalmL);
|
||||
stop(nodeCalmR);
|
||||
stop(nodeStormyL);
|
||||
stop(nodeStormyR);
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@@ -79,16 +87,24 @@ public class OceanSoundState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (nodeCalmSound == null && nodeStormySound == null) return;
|
||||
if (nodeCalmL == null && nodeStormyL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Periodisch nächste Ozean-Position berechnen
|
||||
scanTimer -= tpf;
|
||||
if (scanTimer <= 0f) {
|
||||
scanTimer = SCAN_INTERVAL;
|
||||
scanOceanSource();
|
||||
}
|
||||
|
||||
// Lautstärke
|
||||
boolean playerInWater = terrain.getHeightAt(playerPos.x, playerPos.z) < 0f;
|
||||
if (playerInWater) {
|
||||
applyVolumes(0f, 0f, 0.707f, 0.707f);
|
||||
calmVol = 0f;
|
||||
stormyVol = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
WeatherState weather = getApplication().getStateManager().getState(WeatherState.class);
|
||||
float wind = weather != null ? weather.getWindSpeed() : 4f;
|
||||
|
||||
@@ -99,17 +115,31 @@ public class OceanSoundState extends BaseAppState {
|
||||
? Math.max(0f, 1f - Math.max(0f, oceanDist - REF_DIST) / (MAX_DIST - REF_DIST))
|
||||
: 0f;
|
||||
|
||||
float calmTarget = (oceanInRange && wind < WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||
float calmTarget = (oceanInRange && wind < WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||
float stormyTarget = (oceanInRange && wind >= WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||
|
||||
calmVol = approach(calmVol, calmTarget, FADE_RATE * tpf);
|
||||
stormyVol = approach(stormyVol, stormyTarget, FADE_RATE * tpf);
|
||||
|
||||
if (nodeCalmSound != null) nodeCalmSound.setVolume(calmVol);
|
||||
if (nodeStormySound != null) nodeStormySound.setVolume(stormyVol);
|
||||
float panL = 0.707f, panR = 0.707f; // default: center
|
||||
if (playing && oceanFound) {
|
||||
float panValue = computePan(); // -1 = links, 0 = Mitte, +1 = rechts
|
||||
panL = (float) Math.sqrt((1f - panValue) / 2f);
|
||||
panR = (float) Math.sqrt((1f + panValue) / 2f);
|
||||
updateSourcePositions();
|
||||
}
|
||||
|
||||
applyVolumes(calmVol, stormyVol, panL, panR);
|
||||
}
|
||||
|
||||
// ── Scan ────────────────────────────────────────────────────────────────
|
||||
private void applyVolumes(float calm, float stormy, float panL, float panR) {
|
||||
if (nodeCalmL != null) nodeCalmL.setVolume(calm * panL);
|
||||
if (nodeCalmR != null) nodeCalmR.setVolume(calm * panR);
|
||||
if (nodeStormyL != null) nodeStormyL.setVolume(stormy * panL);
|
||||
if (nodeStormyR != null) nodeStormyR.setVolume(stormy * panR);
|
||||
}
|
||||
|
||||
// ── Scan ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void scanOceanSource() {
|
||||
float px = playerPos.x;
|
||||
@@ -118,7 +148,6 @@ public class OceanSoundState extends BaseAppState {
|
||||
if (terrain.getHeightAt(px, pz) < 0f) {
|
||||
oceanInRange = true;
|
||||
oceanDist = 0f;
|
||||
applyTarget(px, pz);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,53 +180,78 @@ public class OceanSoundState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zielposition setzen. Beim ersten Scan: Nodes positionieren und Wiedergabe starten.
|
||||
* Danach: Node nur aktualisieren wenn Verschiebung > POS_UPDATE_SQ.
|
||||
*/
|
||||
private void applyTarget(float x, float z) {
|
||||
targetPos.set(x, 0f, z);
|
||||
oceanPos.set(x, playerPos.y, z);
|
||||
oceanFound = true;
|
||||
|
||||
if (firstScan) {
|
||||
// Beim ersten gültigen Scan: Nodes korrekt platzieren, dann erst abspielen.
|
||||
firstScan = false;
|
||||
nodePos.set(targetPos);
|
||||
if (nodeCalmSound != null) {
|
||||
app.getRootNode().attachChild(nodeCalmSound);
|
||||
nodeCalmSound.setLocalTranslation(nodePos);
|
||||
nodeCalmSound.play();
|
||||
}
|
||||
if (nodeStormySound != null) {
|
||||
app.getRootNode().attachChild(nodeStormySound);
|
||||
nodeStormySound.setLocalTranslation(nodePos);
|
||||
nodeStormySound.play();
|
||||
}
|
||||
attachAndPlay(nodeCalmL);
|
||||
attachAndPlay(nodeCalmR);
|
||||
attachAndPlay(nodeStormyL);
|
||||
attachAndPlay(nodeStormyR);
|
||||
playing = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playing) return;
|
||||
|
||||
// Nur aktualisieren wenn sich die Zielposition signifikant geändert hat
|
||||
float dxSq = (targetPos.x - nodePos.x);
|
||||
float dzSq = (targetPos.z - nodePos.z);
|
||||
if (dxSq * dxSq + dzSq * dzSq < POS_UPDATE_SQ) return;
|
||||
|
||||
nodePos.set(targetPos);
|
||||
if (nodeCalmSound != null) nodeCalmSound.setLocalTranslation(nodePos);
|
||||
if (nodeStormySound != null) nodeStormySound.setLocalTranslation(nodePos);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ───────────────────────────────────────────────────────
|
||||
private void attachAndPlay(AudioNode node) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
app.getRootNode().attachChild(node);
|
||||
node.play();
|
||||
}
|
||||
|
||||
// ── Panning ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pan-Wert: -1 = Ozean voll links der Kamera, 0 = Mitte, +1 = voll rechts.
|
||||
* Berechnet als Dot-Produkt der horizontalen Ozean-Richtung mit dem
|
||||
* Kamera-Rechtsvektor — unabhängig davon, ob Kamera zum Wasser zeigt.
|
||||
*/
|
||||
private float computePan() {
|
||||
float dx = oceanPos.x - playerPos.x;
|
||||
float dz = oceanPos.z - playerPos.z;
|
||||
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
if (len < 0.1f) {
|
||||
return 0f;
|
||||
}
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
Vector3f camLeft = app.getCamera().getLeft(); // camRight = -camLeft
|
||||
float rightComp = -(dx * camLeft.x + dz * camLeft.z);
|
||||
return Math.max(-1f, Math.min(1f, rightComp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hält L-Quelle PAN_DIST Meter links der Kamera, R-Quelle PAN_DIST Meter rechts.
|
||||
* → Immer ±90° Azimuth zum Listener, nie 0°/180° → kein HRTF-Nullpunkt.
|
||||
*/
|
||||
private void updateSourcePositions() {
|
||||
Vector3f cam = app.getCamera().getLocation();
|
||||
Vector3f camLeft = app.getCamera().getLeft();
|
||||
|
||||
float lx = cam.x + camLeft.x * PAN_DIST;
|
||||
float lz = cam.z + camLeft.z * PAN_DIST;
|
||||
float rx = cam.x - camLeft.x * PAN_DIST;
|
||||
float rz = cam.z - camLeft.z * PAN_DIST;
|
||||
float y = cam.y;
|
||||
|
||||
if (nodeCalmL != null) nodeCalmL.setLocalTranslation(lx, y, lz);
|
||||
if (nodeCalmR != null) nodeCalmR.setLocalTranslation(rx, y, rz);
|
||||
if (nodeStormyL != null) nodeStormyL.setLocalTranslation(lx, y, lz);
|
||||
if (nodeStormyR != null) nodeStormyR.setLocalTranslation(rx, y, rz);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private AudioNode loadLoop(String path, String label) {
|
||||
try {
|
||||
AudioNode n = new AudioNode(app.getAssetManager(), path, AudioData.DataType.Stream);
|
||||
AudioNode n = new AudioNode(app.getAssetManager(), path, AudioData.DataType.Buffer);
|
||||
n.setLooping(true);
|
||||
n.setVolume(0f);
|
||||
n.setPositional(true);
|
||||
n.setRefDistance(REF_DIST);
|
||||
n.setMaxDistance(MAX_DIST);
|
||||
// refDistance = 10 m (JME3-Default), Dist = PAN_DIST = 2 m → Gain = 1.0
|
||||
return n;
|
||||
} catch (Exception e) {
|
||||
log.warn("[OceanSound] {} nicht ladbar: {}", label, e.getMessage());
|
||||
@@ -208,7 +262,9 @@ public class OceanSoundState extends BaseAppState {
|
||||
private void stop(AudioNode node) {
|
||||
if (node != null) {
|
||||
node.stop();
|
||||
if (node.getParent() != null) app.getRootNode().detachChild(node);
|
||||
if (node.getParent() != null) {
|
||||
app.getRootNode().detachChild(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user