Objekte können jetzt Sounds haben (z.B. Lagerfeuer). Sound System optimiert
This commit is contained in:
179
blight-game/src/main/java/de/blight/game/audio/StereoSource.java
Normal file
179
blight-game/src/main/java/de/blight/game/audio/StereoSource.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package de.blight.game.audio;
|
||||
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.audio.AudioBuffer;
|
||||
import com.jme3.audio.AudioData;
|
||||
import com.jme3.audio.AudioKey;
|
||||
import com.jme3.audio.AudioNode;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
/**
|
||||
* Zentrale Abstraktion für positionalen Stereo-Klang (L/R-Panning).
|
||||
*
|
||||
* Strategie (identisch zu OceanSoundState):
|
||||
* Zwei positionale AudioNodes (L + R) bleiben immer PAN_DIST Meter links/rechts
|
||||
* der Kamera. Das Lautstärkeverhältnis (constant-power Panning) simuliert die
|
||||
* wahrgenommene Richtung. Distanz-Fading wird vom Aufrufer übergeben.
|
||||
*
|
||||
* Stereo-Dateien werden beim Laden automatisch auf Mono downgemixet (L+R average),
|
||||
* damit setPositional(true) immer funktioniert – transparent für den Aufrufer.
|
||||
*
|
||||
* Typische Verwendung:
|
||||
* <pre>
|
||||
* StereoSource src = StereoSource.tryLoad(assets, "audio/birds.ogg");
|
||||
* // pro Frame:
|
||||
* if (inRange && !src.isAttached()) src.attachAndPlay(rootNode);
|
||||
* if (src.isAttached()) src.update(cam, soundWorldPos, playerPos, volume);
|
||||
* if (!inRange && src.isAttached()) src.stop(rootNode);
|
||||
* </pre>
|
||||
*/
|
||||
public final class StereoSource {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(StereoSource.class);
|
||||
/** L/R-Quellen PAN_DIST Meter neben der Kamera — muss < refDistance (10 m JME3-Default) sein. */
|
||||
public static final float PAN_DIST = 2f;
|
||||
|
||||
private final AudioNode nodeL;
|
||||
private final AudioNode nodeR;
|
||||
private boolean attached = false;
|
||||
|
||||
// ── Erzeugung ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lädt die Audiodatei, downmixed Stereo automatisch zu Mono und erstellt
|
||||
* ein L/R-Paar. Gibt {@code null} zurück wenn die Datei nicht ladbar ist.
|
||||
*/
|
||||
public static StereoSource tryLoad(AssetManager assets, String path) {
|
||||
try {
|
||||
AudioNode l = buildPositionalNode(assets, path);
|
||||
AudioNode r = buildPositionalNode(assets, path);
|
||||
return new StereoSource(l, r);
|
||||
} catch (Exception e) {
|
||||
log.warn("[StereoSource] Nicht ladbar '{}': {}", path, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private StereoSource(AudioNode nodeL, AudioNode nodeR) {
|
||||
this.nodeL = nodeL;
|
||||
this.nodeR = nodeR;
|
||||
}
|
||||
|
||||
private static AudioNode buildPositionalNode(AssetManager assets, String path) {
|
||||
AudioKey key = new AudioKey(path, false, true); // Standard-Key (stream=false, streamCache=true)
|
||||
AudioData ad = (AudioData) assets.loadAsset(key);
|
||||
|
||||
if (ad instanceof AudioBuffer ab && ab.getChannels() == 2) {
|
||||
ad = downmixToMono(ab);
|
||||
log.debug("[StereoSource] '{}' downgemixed Stereo→Mono für Positional-Audio.", path);
|
||||
}
|
||||
|
||||
AudioNode n = new AudioNode(ad, key);
|
||||
n.setLooping(true);
|
||||
n.setVolume(0f);
|
||||
n.setPositional(true);
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
public void attachAndPlay(Node rootNode) {
|
||||
if (attached) return;
|
||||
rootNode.attachChild(nodeL);
|
||||
rootNode.attachChild(nodeR);
|
||||
nodeL.play();
|
||||
nodeR.play();
|
||||
attached = true;
|
||||
}
|
||||
|
||||
public void stop(Node rootNode) {
|
||||
if (!attached) return;
|
||||
nodeL.stop();
|
||||
nodeR.stop();
|
||||
if (nodeL.getParent() != null) rootNode.detachChild(nodeL);
|
||||
if (nodeR.getParent() != null) rootNode.detachChild(nodeR);
|
||||
attached = false;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aktualisiert Position und Lautstärke der L/R-Nodes.
|
||||
*
|
||||
* @param cam aktuelle Kamera
|
||||
* @param soundPos Weltposition der Sound-Quelle
|
||||
* @param playerPos Weltposition des Spielers
|
||||
* @param volume Endlautstärke (distanceFactor * baseVol * ambientVol bereits eingerechnet)
|
||||
*/
|
||||
public void update(Camera cam, Vector3f soundPos, Vector3f playerPos, float volume) {
|
||||
Vector3f camLoc = cam.getLocation();
|
||||
Vector3f camLeft = cam.getLeft();
|
||||
|
||||
// L/R-Quellen PAN_DIST Meter neben der Kamera halten
|
||||
float lx = camLoc.x + camLeft.x * PAN_DIST;
|
||||
float lz = camLoc.z + camLeft.z * PAN_DIST;
|
||||
float rx = camLoc.x - camLeft.x * PAN_DIST;
|
||||
float rz = camLoc.z - camLeft.z * PAN_DIST;
|
||||
nodeL.setLocalTranslation(lx, camLoc.y, lz);
|
||||
nodeR.setLocalTranslation(rx, camLoc.y, rz);
|
||||
|
||||
// Pan = Dot(Richtung Spieler→Quelle, Kamera-Rechts) = −Dot mit camLeft
|
||||
float dx = soundPos.x - playerPos.x;
|
||||
float dz = soundPos.z - playerPos.z;
|
||||
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
float pan = 0f;
|
||||
if (len > 0.1f) {
|
||||
dx /= len; dz /= len;
|
||||
pan = Math.max(-1f, Math.min(1f, -(dx * camLeft.x + dz * camLeft.z)));
|
||||
}
|
||||
|
||||
// Constant-power Panning
|
||||
float panL = (float) Math.sqrt((1f - pan) / 2f);
|
||||
float panR = (float) Math.sqrt((1f + pan) / 2f);
|
||||
nodeL.setVolume(volume * panL);
|
||||
nodeR.setVolume(volume * panR);
|
||||
}
|
||||
|
||||
public boolean isAttached() { return attached; }
|
||||
|
||||
// ── Stereo → Mono Downmix ────────────────────────────────────────────────
|
||||
|
||||
private static AudioBuffer downmixToMono(AudioBuffer stereo) {
|
||||
ByteBuffer src = stereo.getData().duplicate().order(ByteOrder.LITTLE_ENDIAN);
|
||||
src.rewind();
|
||||
int bps = stereo.getBitsPerSample();
|
||||
int rate = stereo.getSampleRate();
|
||||
|
||||
ByteBuffer dst;
|
||||
if (bps == 16) {
|
||||
ShortBuffer inS = src.asShortBuffer();
|
||||
int monoSamples = inS.limit() / 2;
|
||||
dst = BufferUtils.createByteBuffer(monoSamples * 2);
|
||||
ShortBuffer outS = dst.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
|
||||
for (int i = 0; i < monoSamples; i++) {
|
||||
outS.put((short) ((inS.get() + inS.get()) >> 1));
|
||||
}
|
||||
} else {
|
||||
int monoSamples = src.limit() / 2;
|
||||
dst = BufferUtils.createByteBuffer(monoSamples);
|
||||
for (int i = 0; i < monoSamples; i++) {
|
||||
dst.put((byte) (((src.get() & 0xFF) + (src.get() & 0xFF)) / 2));
|
||||
}
|
||||
}
|
||||
dst.rewind();
|
||||
|
||||
AudioBuffer mono = new AudioBuffer();
|
||||
mono.setupFormat(1, bps, rate);
|
||||
mono.updateData(dst);
|
||||
return mono;
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,8 @@ public class WorldScene extends BaseAppState {
|
||||
private de.blight.game.state.AmbientSoundSystem ambientSounds;
|
||||
private de.blight.game.state.MusicSystem musicSystem;
|
||||
private de.blight.game.state.EmitterSystem emitterSystem;
|
||||
private de.blight.game.state.WaterfallState waterfallState;
|
||||
private de.blight.game.state.PlacedSoundSystem placedSoundSystem;
|
||||
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||
private de.blight.game.state.DrownState drownState;
|
||||
private WaterInteractionState waterInteractionState;
|
||||
@@ -277,9 +279,12 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
BlightGame.status("Lade Welt-Objekte...");
|
||||
app.getStateManager().attach(new RiverState());
|
||||
app.getStateManager().attach(new de.blight.game.state.WaterfallState());
|
||||
waterfallState = new de.blight.game.state.WaterfallState();
|
||||
app.getStateManager().attach(waterfallState);
|
||||
emitterSystem = new de.blight.game.state.EmitterSystem();
|
||||
app.getStateManager().attach(emitterSystem);
|
||||
placedSoundSystem = new de.blight.game.state.PlacedSoundSystem();
|
||||
app.getStateManager().attach(placedSoundSystem);
|
||||
WorldObjectsState worldObjects = new WorldObjectsState();
|
||||
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
|
||||
app.getStateManager().attach(worldObjects);
|
||||
@@ -470,7 +475,9 @@ public class WorldScene extends BaseAppState {
|
||||
if (riverSound != null) riverSound.setPlayerPosition(pos);
|
||||
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
|
||||
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
|
||||
if (emitterSystem != null) emitterSystem.setPlayerPosition(pos);
|
||||
if (emitterSystem != null) emitterSystem.setPlayerPosition(pos);
|
||||
if (placedSoundSystem != null) placedSoundSystem.setPlayerPosition(pos);
|
||||
if (waterfallState != null) waterfallState.setPlayerPosition(pos);
|
||||
}
|
||||
|
||||
if (musicSystem != null && dayNight != null)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.common.PlacedSound;
|
||||
import de.blight.common.SoundIO;
|
||||
import de.blight.game.audio.StereoSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Lädt platzierte Sound-Quellen (blight_sounds.bls) und spielt sie mit
|
||||
* korrektem L/R-Panning und Distanz-Fading ab.
|
||||
* Panning-Logik: {@link StereoSource}.
|
||||
*/
|
||||
public class PlacedSoundSystem extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PlacedSoundSystem.class);
|
||||
private static final float CHECK_INTERVAL = 0.1f;
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node rootNode;
|
||||
private AudioSettingsState audioSettings;
|
||||
|
||||
private final List<PlacedSound> data = new ArrayList<>();
|
||||
private final List<StereoSource> sources = new ArrayList<>();
|
||||
|
||||
private final Vector3f playerPos = new Vector3f();
|
||||
private float checkTimer = 0f;
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
rootNode = ((SimpleApplication) application).getRootNode();
|
||||
audioSettings = getStateManager().getState(AudioSettingsState.class);
|
||||
|
||||
try {
|
||||
for (PlacedSound ps : SoundIO.load()) {
|
||||
if (ps.soundPath() == null || ps.soundPath().isEmpty()) continue;
|
||||
StereoSource src = StereoSource.tryLoad(app.getAssetManager(), ps.soundPath());
|
||||
if (src != null) {
|
||||
data.add(ps);
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
if (!data.isEmpty()) log.info("[PlacedSoundSystem] {} Sound-Quellen geladen.", data.size());
|
||||
} catch (IOException e) {
|
||||
log.warn("[PlacedSoundSystem] blight_sounds.bls nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
for (StereoSource src : sources) src.stop(rootNode);
|
||||
data.clear();
|
||||
sources.clear();
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setPlayerPosition(Vector3f pos) { playerPos.set(pos); }
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (data.isEmpty()) return;
|
||||
|
||||
checkTimer += tpf;
|
||||
if (checkTimer < CHECK_INTERVAL) return;
|
||||
checkTimer = 0f;
|
||||
|
||||
float ambientVol = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f;
|
||||
Vector3f soundPos = new Vector3f();
|
||||
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
PlacedSound ps = data.get(i);
|
||||
StereoSource src = sources.get(i);
|
||||
|
||||
float dx = ps.x() - playerPos.x;
|
||||
float dz = ps.z() - playerPos.z;
|
||||
float dist = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
float innerR = ps.radius() * 0.33f;
|
||||
float distFactor;
|
||||
if (dist <= innerR) distFactor = 1f;
|
||||
else if (dist >= ps.radius()) distFactor = 0f;
|
||||
else distFactor = 1f - (dist - innerR) / (ps.radius() - innerR);
|
||||
|
||||
float volume = ps.volume() * ambientVol * distFactor;
|
||||
|
||||
if (volume > 0f && !src.isAttached()) {
|
||||
src.attachAndPlay(rootNode);
|
||||
}
|
||||
|
||||
if (src.isAttached()) {
|
||||
if (volume <= 0f) {
|
||||
src.stop(rootNode);
|
||||
} else {
|
||||
soundPos.set(ps.x(), ps.y(), ps.z());
|
||||
src.update(app.getCamera(), soundPos, playerPos, volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.jme3.texture.Texture2D;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.PlacedWaterfall;
|
||||
import de.blight.common.WaterfallIO;
|
||||
import de.blight.game.audio.StereoSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -38,11 +39,16 @@ public class WaterfallState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WaterfallState.class);
|
||||
|
||||
private static final int ROWS = 24; // Tessellierungs-Zeilen vertikal
|
||||
private static final int COLS = 6; // Tessellierungs-Spalten horizontal
|
||||
private static final float BULGE_MAX = 1.2f; // Maximale horizontale Vorwölbung am oberen Rand (m)
|
||||
private static final float V_RANGE = 0.30f; // Anteil der Wasserfallhöhe für den Bogen (0..1)
|
||||
private static final float SPLASH_HEIGHT = 0.9f; // Höhe des vertikalen Schaum-Vorhangs (m)
|
||||
private static final int ROWS = 24;
|
||||
private static final int COLS = 6;
|
||||
private static final float BULGE_MAX = 1.2f;
|
||||
private static final float V_RANGE = 0.30f;
|
||||
private static final float SPLASH_HEIGHT = 0.9f;
|
||||
|
||||
private static final String AUDIO_PATH = "audio/ambient/water/waterfall.ogg";
|
||||
private static final float AUDIO_RADIUS = 25f;
|
||||
private static final float AUDIO_INNER_R = 8f;
|
||||
private static final float AUDIO_VOLUME = 0.8f;
|
||||
|
||||
private Node rootNode;
|
||||
private final List<Geometry> geos = new ArrayList<>();
|
||||
@@ -51,12 +57,18 @@ public class WaterfallState extends BaseAppState {
|
||||
private final List<Material> foamMaterials = new ArrayList<>();
|
||||
private final List<ParticleEmitter> mists = new ArrayList<>();
|
||||
private float time = 0f;
|
||||
private Texture2D softCircleTex = null; // lazily built, shared by alle Emitter
|
||||
private Texture2D softCircleTex = null;
|
||||
|
||||
private final List<StereoSource> audioSources = new ArrayList<>();
|
||||
private final List<Vector3f> audioPositions = new ArrayList<>();
|
||||
private AudioSettingsState audioSettings;
|
||||
private final Vector3f playerPos = new Vector3f();
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
rootNode = ((SimpleApplication) app).getRootNode();
|
||||
AssetManager assets = app.getAssetManager();
|
||||
protected void initialize(Application application) {
|
||||
rootNode = ((SimpleApplication) application).getRootNode();
|
||||
AssetManager assets = application.getAssetManager();
|
||||
audioSettings = getStateManager().getState(AudioSettingsState.class);
|
||||
|
||||
List<PlacedWaterfall> list;
|
||||
try {
|
||||
@@ -70,6 +82,7 @@ public class WaterfallState extends BaseAppState {
|
||||
for (PlacedWaterfall wf : list) {
|
||||
try {
|
||||
buildWaterfall(wf, assets);
|
||||
buildAudioSource(wf, assets);
|
||||
} catch (Exception e) {
|
||||
log.error("Fehler beim Aufbauen eines Wasserfalls", e);
|
||||
}
|
||||
@@ -77,6 +90,28 @@ public class WaterfallState extends BaseAppState {
|
||||
log.info("{} Wasserfall-Quad(s) geladen.", list.size());
|
||||
}
|
||||
|
||||
private void buildAudioSource(PlacedWaterfall wf, AssetManager assets) {
|
||||
Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az());
|
||||
Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz());
|
||||
Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz());
|
||||
Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz());
|
||||
Vector3f outward = computeOutward(a, b, c, d);
|
||||
float waterY = (wf.cy() + wf.dy()) * 0.5f;
|
||||
Vector3f audioPos = c.add(d).multLocal(0.5f).addLocal(outward.mult(BULGE_MAX));
|
||||
audioPos.y = waterY;
|
||||
|
||||
StereoSource src = StereoSource.tryLoad(assets, AUDIO_PATH);
|
||||
if (src != null) {
|
||||
// Sofort attachen und bei Vol=0 starten — kein späteres Start/Stop.
|
||||
// Identisches Muster wie OceanSoundState: immer spielend, nur Lautstärke variiert.
|
||||
src.attachAndPlay(rootNode);
|
||||
audioSources.add(src);
|
||||
audioPositions.add(audioPos);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPlayerPosition(Vector3f pos) { playerPos.set(pos); }
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (!materials.isEmpty()) {
|
||||
@@ -89,10 +124,36 @@ public class WaterfallState extends BaseAppState {
|
||||
foamMaterials.get(i).setColor("Color", new ColorRGBA(1f, 1f, 1f, alpha));
|
||||
}
|
||||
}
|
||||
|
||||
if (audioSources.isEmpty()) return;
|
||||
|
||||
float ambientVol = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f;
|
||||
|
||||
for (int i = 0; i < audioSources.size(); i++) {
|
||||
StereoSource src = audioSources.get(i);
|
||||
Vector3f apos = audioPositions.get(i);
|
||||
|
||||
float dx = apos.x - playerPos.x;
|
||||
float dz = apos.z - playerPos.z;
|
||||
float dist = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
float distFactor;
|
||||
if (dist <= AUDIO_INNER_R) distFactor = 1f;
|
||||
else if (dist >= AUDIO_RADIUS) distFactor = 0f;
|
||||
else distFactor = 1f - (dist - AUDIO_INNER_R) / (AUDIO_RADIUS - AUDIO_INNER_R);
|
||||
|
||||
float volume = AUDIO_VOLUME * ambientVol * distFactor;
|
||||
|
||||
// Panning + Lautstärke jeden Frame — kein Start/Stop, nur Vol=0 außerhalb
|
||||
src.update(getApplication().getCamera(), apos, playerPos, volume);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
protected void cleanup(Application application) {
|
||||
for (StereoSource src : audioSources) src.stop(rootNode);
|
||||
audioSources.clear();
|
||||
audioPositions.clear();
|
||||
for (Geometry g : geos) g.removeFromParent();
|
||||
for (Geometry g : foams) g.removeFromParent();
|
||||
for (ParticleEmitter e : mists) e.removeFromParent();
|
||||
|
||||
Reference in New Issue
Block a user