Objekte können jetzt Sounds haben (z.B. Lagerfeuer). Sound System optimiert

This commit is contained in:
2026-09-14 16:09:21 +02:00
parent 08c76a798d
commit 45f7ee1045
42 changed files with 1721 additions and 109 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,6 +1,10 @@
#Tue Aug 25 19:02:59 CEST 2026 #Mon Sep 14 11:35:19 CEST 2026
attachedEmitters.count=0 attachedEmitter.0=0.00000|0.00000|0.00000|Effects/Explosion/flame.png|2|2|1.0000|0.7000|0.1000|1.0000|1.0000|0.1000|0.0000|0.0000|0.2739|0.6573|0.0000|0.7500|0.0000|0.5000|0.0000|-0.1000|0.0000|1.0000|3.0000|60|25.0000
attachedLights.count=0 attachedEmitters.count=1
attachedLight.0=0.00000|0.70000|0.00000|1.00000|0.60000|0.40000|2.00000|8.00000
attachedLights.count=1
attachedSound.0=0.00000|0.50000|0.00000|audio/ambient/fire/campfire.ogg|1.0000|20.0000
attachedSounds.count=1
castShadow=true castShadow=true
category= category=
cullDistance=120.0 cullDistance=120.0
@@ -14,6 +18,7 @@ lod1Distance=30.0
lod1Path= lod1Path=
lod2Distance=80.0 lod2Distance=80.0
lod2Path= lod2Path=
modelCategory=NONE
name=feuerstelle1 name=feuerstelle1
pivotOffsetY=0.0 pivotOffsetY=0.0
placementOffsetY=0.0 placementOffsetY=0.0

View File

@@ -28,6 +28,7 @@ public record ModelMeta(
float cullDistance, float cullDistance,
List<AttachedLight> attachedLights, List<AttachedLight> attachedLights,
List<AttachedEmitter> attachedEmitters, List<AttachedEmitter> attachedEmitters,
List<AttachedSound> attachedSounds,
de.blight.common.model.InteractableType interactableType, de.blight.common.model.InteractableType interactableType,
float interactableOffsetX, float interactableOffsetX,
float interactableOffsetY, float interactableOffsetY,
@@ -42,17 +43,51 @@ public record ModelMeta(
float r, float g, float b, float r, float g, float b,
float intensity, float radius) {} float intensity, float radius) {}
/** Partikel-Emitter relativ zum Modell-Ursprung. Preset: 0=Feuer, 1=Rauch, 2=Funken. */ /** Sound-Quelle relativ zum Modell-Ursprung. */
public record AttachedSound(
float offsetX, float offsetY, float offsetZ,
String soundPath,
float volume,
float radius) {}
/** Partikel-Emitter relativ zum Modell-Ursprung — vollständige Parameter wie PlacedEmitter. */
public record AttachedEmitter( public record AttachedEmitter(
float offsetX, float offsetY, float offsetZ, float offsetX, float offsetY, float offsetZ,
int preset) {} String texturePath, int imagesX, int imagesY,
float startR, float startG, float startB, float startA,
float endR, float endG, float endB, float endA,
float startSize, float endSize,
float velX, float velY, float velZ, float velocityVariation,
float gravX, float gravY, float gravZ,
float lowLife, float highLife,
int maxParticles, float emitRate) {
public static AttachedEmitter fire(float ox, float oy, float oz) {
return new AttachedEmitter(ox, oy, oz,
"Effects/Explosion/flame.png", 2, 2,
1f, 0.7f, 0.1f, 1f, 1f, 0.1f, 0f, 0f,
0.5f, 1.2f, 0f, 2.5f, 0f, 0.5f, 0f, -0.1f, 0f, 1f, 3f, 60, 25f);
}
public static AttachedEmitter smoke(float ox, float oy, float oz) {
return new AttachedEmitter(ox, oy, oz,
"Effects/Smoke/Smoke.png", 1, 1,
0.5f, 0.5f, 0.5f, 0.4f, 0.2f, 0.2f, 0.2f, 0f,
0.8f, 2.5f, 0f, 1.2f, 0f, 0.3f, 0f, 0.05f, 0f, 3f, 6f, 24, 6f);
}
public static AttachedEmitter sparks(float ox, float oy, float oz) {
return new AttachedEmitter(ox, oy, oz,
"Effects/Explosion/spark.png", 1, 1,
1f, 0.9f, 0.2f, 1f, 1f, 0.3f, 0f, 0f,
0.05f, 0.02f, 0f, 4f, 0f, 1.0f, 0f, -5f, 0f, 0.5f, 2f, 50, 20f);
}
}
public static ModelMeta defaults(String j3oFileName) { public static ModelMeta defaults(String j3oFileName) {
String name = j3oFileName.replaceFirst("\\.j3o$", ""); String name = j3oFileName.replaceFirst("\\.j3o$", "");
return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f, return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f,
false, true, true, 1f, 1f, false, true, true, 1f, 1f,
"", "", 30f, 80f, 120f, "", "", 30f, 80f, 120f,
List.of(), List.of(), List.of(), List.of(), List.of(),
de.blight.common.model.InteractableType.NONE, de.blight.common.model.InteractableType.NONE,
0f, 0.5f, 0f, 0f, "", 0f, 0.5f, 0f, 0f, "",
de.blight.common.ModelCategory.NONE); de.blight.common.ModelCategory.NONE);

View File

@@ -59,8 +59,27 @@ public final class ModelMetaIO {
for (int i = 0; i < emitters.size(); i++) { for (int i = 0; i < emitters.size(); i++) {
ModelMeta.AttachedEmitter e = emitters.get(i); ModelMeta.AttachedEmitter e = emitters.get(i);
p.setProperty("attachedEmitter." + i, String.format(Locale.ROOT, p.setProperty("attachedEmitter." + i, String.format(Locale.ROOT,
"%.5f|%.5f|%.5f|%d", "%.5f|%.5f|%.5f|%s|%d|%d|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%d|%.4f",
e.offsetX(), e.offsetY(), e.offsetZ(), e.preset())); e.offsetX(), e.offsetY(), e.offsetZ(),
e.texturePath(), e.imagesX(), e.imagesY(),
e.startR(), e.startG(), e.startB(), e.startA(),
e.endR(), e.endG(), e.endB(), e.endA(),
e.startSize(), e.endSize(),
e.velX(), e.velY(), e.velZ(), e.velocityVariation(),
e.gravX(), e.gravY(), e.gravZ(),
e.lowLife(), e.highLife(),
e.maxParticles(), e.emitRate()));
}
// Anhänge: Sounds
List<ModelMeta.AttachedSound> sounds = m.attachedSounds();
p.setProperty("attachedSounds.count", String.valueOf(sounds.size()));
for (int i = 0; i < sounds.size(); i++) {
ModelMeta.AttachedSound s = sounds.get(i);
p.setProperty("attachedSound." + i, String.format(Locale.ROOT,
"%.5f|%.5f|%.5f|%s|%.4f|%.4f",
s.offsetX(), s.offsetY(), s.offsetZ(),
s.soundPath(), s.volume(), s.radius()));
} }
try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) { try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) {
@@ -103,11 +122,48 @@ public final class ModelMetaIO {
String v = p.getProperty("attachedEmitter." + i, ""); String v = p.getProperty("attachedEmitter." + i, "");
if (!v.isEmpty()) { if (!v.isEmpty()) {
String[] f = v.split("\\|", -1); String[] f = v.split("\\|", -1);
if (f.length >= 4) { try {
try { if (f.length >= 27) {
// Neues Format mit vollständigen Parametern
emitters.add(new ModelMeta.AttachedEmitter( emitters.add(new ModelMeta.AttachedEmitter(
Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]),
Integer.parseInt(f[3]))); f[3], Integer.parseInt(f[4]), Integer.parseInt(f[5]),
Float.parseFloat(f[6]), Float.parseFloat(f[7]), Float.parseFloat(f[8]), Float.parseFloat(f[9]),
Float.parseFloat(f[10]), Float.parseFloat(f[11]), Float.parseFloat(f[12]), Float.parseFloat(f[13]),
Float.parseFloat(f[14]), Float.parseFloat(f[15]),
Float.parseFloat(f[16]), Float.parseFloat(f[17]), Float.parseFloat(f[18]), Float.parseFloat(f[19]),
Float.parseFloat(f[20]), Float.parseFloat(f[21]), Float.parseFloat(f[22]),
Float.parseFloat(f[23]), Float.parseFloat(f[24]),
Integer.parseInt(f[25]), Float.parseFloat(f[26])));
} else if (f.length >= 4) {
// Altes Format (offsetX|offsetY|offsetZ|preset) → Preset-Defaults übernehmen
float ox = Float.parseFloat(f[0]);
float oy = Float.parseFloat(f[1]);
float oz = Float.parseFloat(f[2]);
int preset = Integer.parseInt(f[3]);
emitters.add(switch (preset) {
case 1 -> ModelMeta.AttachedEmitter.smoke(ox, oy, oz);
case 2 -> ModelMeta.AttachedEmitter.sparks(ox, oy, oz);
default -> ModelMeta.AttachedEmitter.fire(ox, oy, oz);
});
}
} catch (NumberFormatException ignored) {}
}
}
// Anhänge: Sounds
int soundCount = parseInt(p, "attachedSounds.count", 0);
List<ModelMeta.AttachedSound> sounds = new ArrayList<>(soundCount);
for (int i = 0; i < soundCount; i++) {
String v = p.getProperty("attachedSound." + i, "");
if (!v.isEmpty()) {
String[] f = v.split("\\|", -1);
if (f.length >= 6) {
try {
sounds.add(new ModelMeta.AttachedSound(
Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]),
f[3],
Float.parseFloat(f[4]), Float.parseFloat(f[5])));
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
} }
@@ -135,6 +191,7 @@ public final class ModelMetaIO {
parseFloat(p, "cullDistance", 120f), parseFloat(p, "cullDistance", 120f),
Collections.unmodifiableList(lights), Collections.unmodifiableList(lights),
Collections.unmodifiableList(emitters), Collections.unmodifiableList(emitters),
Collections.unmodifiableList(sounds),
de.blight.common.model.InteractableType.fromString( de.blight.common.model.InteractableType.fromString(
p.getProperty("interactableType", "NONE")), p.getProperty("interactableType", "NONE")),
parseFloat(p, "interactableOffsetX", 0f), parseFloat(p, "interactableOffsetX", 0f),

View File

@@ -0,0 +1,8 @@
package de.blight.common;
public record PlacedSound(
float x, float y, float z,
String soundPath,
float volume,
float radius
) {}

View File

@@ -0,0 +1,58 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Sound-Quellen als tab-separierte Textdatei
* ({@code blight_sounds.bls}) neben der Kartendatei.
*
* Spalten: x y z soundPath volume radius
*/
public final class SoundIO {
private SoundIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_sounds.bls");
}
public static void save(List<PlacedSound> sounds) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# x\ty\tz\tsoundPath\tvolume\tradius");
w.newLine();
for (PlacedSound s : sounds) {
w.write(String.format(Locale.ROOT,
"%.5f\t%.5f\t%.5f\t%s\t%.4f\t%.4f%n",
s.x(), s.y(), s.z(),
s.soundPath(),
s.volume(), s.radius()));
}
}
}
public static List<PlacedSound> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedSound> list = new ArrayList<>();
for (String line : Files.readAllLines(p)) {
line = line.strip();
if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1);
if (f.length < 6) continue;
try {
list.add(new PlacedSound(
Float.parseFloat(f[0]),
Float.parseFloat(f[1]),
Float.parseFloat(f[2]),
f[3],
Float.parseFloat(f[4]),
Float.parseFloat(f[5])));
} catch (NumberFormatException ignored) {}
}
return list;
}
}

View File

@@ -165,7 +165,8 @@ public class EditorApp extends Application {
this::setStatus, this::setCenterView, this::setStatus, this::setCenterView,
root::setRight, node -> topBar.getChildren().set(1, node), root::setRight, node -> topBar.getChildren().set(1, node),
() -> worldViewport, ASSET_ROOT, this::switchToWorldEditor, registry, () -> worldViewport, ASSET_ROOT, this::switchToWorldEditor, registry,
root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool,
this::scheduleAutoSave
); );
vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx); vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx);
ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx); ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx);
@@ -447,6 +448,14 @@ public class EditorApp extends Application {
atmosphereController.updateEmitterPanel(input.selectedEmitterInfo); atmosphereController.updateEmitterPanel(input.selectedEmitterInfo);
} }
if (input.auswahlSoundSelected) {
input.auswahlSoundSelected = false;
topBarController.getAtmosphaereBtn().setSelected(true);
input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
root.setRight(atmosphereController.buildPanel());
atmosphereController.updateSoundPanel(input.selectedSoundInfo);
}
if (input.auswahlWaterSelected) { if (input.auswahlWaterSelected) {
input.auswahlWaterSelected = false; input.auswahlWaterSelected = false;
topBarController.getWasserMergedBtn().setSelected(true); topBarController.getWasserMergedBtn().setSelected(true);
@@ -548,6 +557,9 @@ public class EditorApp extends Application {
setStatus("Auto-Speicherung…"); setStatus("Auto-Speicherung…");
} }
}); });
input.onObjectDeleted = () ->
Platform.runLater(() -> topBarController.getAuswahlBtn().fire());
} }
// ── Modus-Wechsel ──────────────────────────────────────────────────────── // ── Modus-Wechsel ────────────────────────────────────────────────────────
@@ -930,6 +942,14 @@ public class EditorApp extends Application {
if (input.activeLayer == SharedInput.LAYER_OBJECTS if (input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT) || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
input.deleteSelectedRequested = true; input.deleteSelectedRequested = true;
else if (input.activeLayer == SharedInput.LAYER_LIGHTS && input.selectedLightInfo != null)
input.deleteLightRequested = true;
else if (input.activeLayer == SharedInput.LAYER_EMITTERS && input.selectedEmitterInfo != null)
input.deleteEmitterRequested = true;
else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null)
input.deleteSoundRequested = true;
else if (input.activeLayer == SharedInput.LAYER_ITEMS && input.selectedItemInfo != null)
input.deleteSelectedItemRequested = true;
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) { else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); } if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else { input.deleteSoundAreaRequested = true; scheduleAutoSave(); } else { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
@@ -965,8 +985,10 @@ public class EditorApp extends Application {
confirm.setHeaderText(null); confirm.setHeaderText(null);
confirm.initOwner(primaryStage); confirm.initOwner(primaryStage);
confirm.showAndWait().ifPresent(btn -> { confirm.showAndWait().ifPresent(btn -> {
if (btn == javafx.scene.control.ButtonType.YES) if (btn == javafx.scene.control.ButtonType.YES) {
input.deleteWaterfallRequested = true; input.deleteWaterfallRequested = true;
topBarController.getAuswahlBtn().fire();
}
}); });
}); });
} }

View File

@@ -26,6 +26,7 @@ import de.blight.editor.state.LocationZoneState;
import de.blight.editor.state.RiverEditorState; import de.blight.editor.state.RiverEditorState;
import de.blight.editor.state.WaterfallEditorState; import de.blight.editor.state.WaterfallEditorState;
import de.blight.editor.state.PlayToolState; import de.blight.editor.state.PlayToolState;
import de.blight.editor.state.PlacedSoundState;
import de.blight.editor.state.SoundAreaState; import de.blight.editor.state.SoundAreaState;
import de.blight.editor.state.WaterBodyState; import de.blight.editor.state.WaterBodyState;
import de.blight.editor.state.EzTreeState; import de.blight.editor.state.EzTreeState;
@@ -204,6 +205,7 @@ public class JmeEditorApp extends SimpleApplication {
stateManager.attach(new WillowGeneratorState(input)); stateManager.attach(new WillowGeneratorState(input));
stateManager.attach(new LightState(input)); stateManager.attach(new LightState(input));
stateManager.attach(new EmitterState(input)); stateManager.attach(new EmitterState(input));
stateManager.attach(new PlacedSoundState(input));
stateManager.attach(new WaterBodyState(input)); stateManager.attach(new WaterBodyState(input));
stateManager.attach(new SoundAreaState(input)); stateManager.attach(new SoundAreaState(input));
stateManager.attach(new AreaState(input)); stateManager.attach(new AreaState(input));

View File

@@ -445,6 +445,10 @@ public class SharedInput {
/** activeLayer==8 → Partikel-Emitter platzieren und bearbeiten */ /** activeLayer==8 → Partikel-Emitter platzieren und bearbeiten */
public static final int LAYER_EMITTERS = 8; public static final int LAYER_EMITTERS = 8;
// ── Platzierte Sound-Quellen ──────────────────────────────────────────────
/** activeLayer==35 → einzelne Sound-Quellen platzieren und bearbeiten */
public static final int LAYER_PLACED_SOUNDS = 35;
/** Klick im Viewport im Emitter-Modus: platzieren oder selektieren. */ /** Klick im Viewport im Emitter-Modus: platzieren oder selektieren. */
public record EmitterClick(float screenX, float screenY, boolean rightButton) {} public record EmitterClick(float screenX, float screenY, boolean rightButton) {}
public final ConcurrentLinkedQueue<EmitterClick> emitterClickQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<EmitterClick> emitterClickQueue = new ConcurrentLinkedQueue<>();
@@ -469,6 +473,35 @@ public class SharedInput {
/** JavaFX → JME: Preset für nächsten neu platzierten Emitter (0=Feuer,1=Rauch,2=Funken). */ /** JavaFX → JME: Preset für nächsten neu platzierten Emitter (0=Feuer,1=Rauch,2=Funken). */
public volatile int emitterPreset = 0; public volatile int emitterPreset = 0;
// ── Sound-Quellen-Werkzeug ────────────────────────────────────────────────
/** Klick im Viewport im Sound-Modus: platzieren oder selektieren. */
public record SoundClick(float screenX, float screenY, boolean rightButton) {}
public final ConcurrentLinkedQueue<SoundClick> soundClickQueue = new ConcurrentLinkedQueue<>();
/** JME → JavaFX: Info der selektierten Sound-Quelle. Format: "idx|x|y|z|soundPath|volume|radius" oder null. */
public volatile String selectedSoundInfo = null;
public volatile boolean soundSelectionChanged = false;
/** JavaFX → JME: aktualisierte Parameter der selektierten Sound-Quelle. */
public record SoundPropertyChange(String soundPath, float volume, float radius) {}
public final AtomicReference<SoundPropertyChange> pendingSoundProp = new AtomicReference<>();
/** JavaFX → JME: Selektierte Sound-Quelle löschen. */
public volatile boolean deleteSoundRequested = false;
/** JME3-intern: Auswahl-Klick für PlacedSoundState. */
public final ConcurrentLinkedQueue<ObjectClick> auswahlSoundClickQueue = new ConcurrentLinkedQueue<>();
/** JME3 → JavaFX: Sound-Quelle im Auswahl-Modus selektiert → Atmosphäre/Sound-Werkzeug aktivieren. */
public volatile boolean auswahlSoundSelected = false;
// ── Kaskaden-Löschung (SceneObject → Lichter/Emitter/Sounds) ─────────────
/** SceneObjectState → LightState: Weltpositionen von zu löschenden Lichtern. */
public final ConcurrentLinkedQueue<float[]> removeLightPositions = new ConcurrentLinkedQueue<>();
/** SceneObjectState → EmitterState: Weltpositionen von zu löschenden Emittern. */
public final ConcurrentLinkedQueue<float[]> removeEmitterPositions = new ConcurrentLinkedQueue<>();
/** SceneObjectState → PlacedSoundState: Weltpositionen von zu löschenden Sounds. */
public final ConcurrentLinkedQueue<float[]> removeSoundPositions = new ConcurrentLinkedQueue<>();
// ── Wasser-Werkzeug ─────────────────────────────────────────────────────── // ── Wasser-Werkzeug ───────────────────────────────────────────────────────
/** activeLayer==9 → Wasseroberflächen platzieren und bearbeiten */ /** activeLayer==9 → Wasseroberflächen platzieren und bearbeiten */
public static final int LAYER_WATER = 9; public static final int LAYER_WATER = 9;
@@ -794,6 +827,7 @@ public class SharedInput {
/** JFX → JME: aktualisierte Anhang-Listen; JME rendert Gizmos neu. */ /** JFX → JME: aktualisierte Anhang-Listen; JME rendert Gizmos neu. */
public volatile java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorAttachedLights = java.util.List.of(); public volatile java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorAttachedLights = java.util.List.of();
public volatile java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorAttachedEmitters = java.util.List.of(); public volatile java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorAttachedEmitters = java.util.List.of();
public volatile java.util.List<de.blight.common.ModelMeta.AttachedSound> modelEditorAttachedSounds = java.util.List.of();
public volatile boolean modelEditorAttachmentsChanged = false; public volatile boolean modelEditorAttachmentsChanged = false;
/** JFX → JME: Pfad des zu ladenden Modells (relativ zu blight-assets/src/main/resources/). */ /** JFX → JME: Pfad des zu ladenden Modells (relativ zu blight-assets/src/main/resources/). */
@@ -1223,6 +1257,7 @@ public class SharedInput {
public volatile boolean auswahlWaterSelected = false; public volatile boolean auswahlWaterSelected = false;
/** JME3 → JavaFX: Wasserfall im Auswahl-Modus selektiert → Wasserfall-Werkzeug aktivieren. */ /** JME3 → JavaFX: Wasserfall im Auswahl-Modus selektiert → Wasserfall-Werkzeug aktivieren. */
public volatile boolean auswahlWaterfallSelected = false; public volatile boolean auswahlWaterfallSelected = false;
// auswahlSoundSelected + auswahlSoundClickQueue → defined in Sound-Quellen-Werkzeug section above
// ── Voxel-Textur-Malen ──────────────────────────────────────────────────── // ── Voxel-Textur-Malen ────────────────────────────────────────────────────
/** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt, /** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt,
@@ -1241,4 +1276,11 @@ public class SharedInput {
public final ConcurrentLinkedQueue<PathRename> sceneObjectPathRenames = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<PathRename> sceneObjectPathRenames = new ConcurrentLinkedQueue<>();
/** EmitterState pollt diese Queue und ersetzt betroffene PlacedEmitter-Records. */ /** EmitterState pollt diese Queue und ersetzt betroffene PlacedEmitter-Records. */
public final ConcurrentLinkedQueue<PathRename> emitterPathRenames = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<PathRename> emitterPathRenames = new ConcurrentLinkedQueue<>();
/**
* JME3 → JavaFX: Callback nach Löschung eines ganzen Objekts (Zone, Knoten, Licht, …).
* States rufen dies nach der Löschung auf; EditorApp setzt den Callback auf den FX-Thread.
* Nie bei Vertex-Löschung aufrufen.
*/
public Runnable onObjectDeleted = () -> {};
} }

View File

@@ -27,6 +27,7 @@ import java.util.function.Supplier;
* @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane * @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane
* @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper * @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper
* @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing) * @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing)
* @param scheduleAutoSave Triggert den 8-Sekunden-Autosave-Timer neu (FX-Thread)
*/ */
public record ToolContext( public record ToolContext(
SharedInput input, SharedInput input,
@@ -43,5 +44,6 @@ public record ToolContext(
Consumer<Node> setLeftPanel, Consumer<Node> setLeftPanel,
Consumer<Node> setBottomPanel, Consumer<Node> setBottomPanel,
Supplier<StackPane> centerStackSupplier, Supplier<StackPane> centerStackSupplier,
Consumer<String> setCurrentTool Consumer<String> setCurrentTool,
Runnable scheduleAutoSave
) {} ) {}

View File

@@ -128,7 +128,7 @@ public class AreaState extends BaseAppState {
if (input.deleteAreaRequested) { if (input.deleteAreaRequested) {
input.deleteAreaRequested = false; input.deleteAreaRequested = false;
if (selectedIdx >= 0) removeArea(selectedIdx); if (selectedIdx >= 0) { removeArea(selectedIdx); input.onObjectDeleted.run(); }
} }
} }

View File

@@ -47,7 +47,8 @@ public class EmitterState extends BaseAppState {
private final List<Node> markers = new ArrayList<>(); private final List<Node> markers = new ArrayList<>();
private final List<ParticleEmitter> particles = new ArrayList<>(); private final List<ParticleEmitter> particles = new ArrayList<>();
private int selectedIdx = -1; private int selectedIdx = -1;
private int lastKnownLayer = -1;
private List<PlacedEmitter> pendingEmitters = null; private List<PlacedEmitter> pendingEmitters = null;
public EmitterState(SharedInput input) { public EmitterState(SharedInput input) {
@@ -80,8 +81,23 @@ public class EmitterState extends BaseAppState {
// ── Update ──────────────────────────────────────────────────────────────── // ── Update ────────────────────────────────────────────────────────────────
private static final float POS_EPSILON = 0.05f;
@Override @Override
public void update(float tpf) { public void update(float tpf) {
float[] rmPos;
while ((rmPos = input.removeEmitterPositions.poll()) != null) {
removeByPosition(rmPos[0], rmPos[1], rmPos[2]);
}
// Beim Verlassen des Emitter-Layers Selektion aufheben
if (lastKnownLayer == SharedInput.LAYER_EMITTERS
&& input.activeLayer != SharedInput.LAYER_EMITTERS
&& selectedIdx >= 0) {
deselect();
}
lastKnownLayer = input.activeLayer;
SharedInput.PathRename rename; SharedInput.PathRename rename;
while ((rename = input.emitterPathRenames.poll()) != null) { while ((rename = input.emitterPathRenames.poll()) != null) {
for (int i = 0; i < emitters.size(); i++) { for (int i = 0; i < emitters.size(); i++) {
@@ -121,7 +137,11 @@ public class EmitterState extends BaseAppState {
if (input.deleteEmitterRequested) { if (input.deleteEmitterRequested) {
input.deleteEmitterRequested = false; input.deleteEmitterRequested = false;
if (selectedIdx >= 0) removeEmitter(selectedIdx); if (selectedIdx >= 0) {
removeEmitter(selectedIdx);
try { de.blight.common.EmitterIO.save(new ArrayList<>(emitters)); } catch (java.io.IOException ignored) {}
input.onObjectDeleted.run();
}
} }
} }
@@ -188,6 +208,8 @@ public class EmitterState extends BaseAppState {
deselect(); deselect();
selectedIdx = idx; selectedIdx = idx;
setMarkerColor(idx, SEL_COLOR); setMarkerColor(idx, SEL_COLOR);
setMarkerVisible(idx, false);
setParticleVisible(idx, true);
publishSelection(idx); publishSelection(idx);
} }
@@ -195,12 +217,25 @@ public class EmitterState extends BaseAppState {
if (selectedIdx >= 0 && selectedIdx < emitters.size()) { if (selectedIdx >= 0 && selectedIdx < emitters.size()) {
PlacedEmitter e = emitters.get(selectedIdx); PlacedEmitter e = emitters.get(selectedIdx);
setMarkerColor(selectedIdx, new ColorRGBA(e.startR(), e.startG(), e.startB(), 1f)); setMarkerColor(selectedIdx, new ColorRGBA(e.startR(), e.startG(), e.startB(), 1f));
setMarkerVisible(selectedIdx, true);
setParticleVisible(selectedIdx, false);
} }
selectedIdx = -1; selectedIdx = -1;
input.selectedEmitterInfo = null; input.selectedEmitterInfo = null;
input.emitterSelectionChanged = true; input.emitterSelectionChanged = true;
} }
private void setMarkerVisible(int idx, boolean visible) {
markers.get(idx).setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
private void setParticleVisible(int idx, boolean visible) {
ParticleEmitter pe = particles.get(idx);
if (pe != null) {
pe.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
}
private void publishSelection(int idx) { private void publishSelection(int idx) {
PlacedEmitter e = emitters.get(idx); PlacedEmitter e = emitters.get(idx);
input.selectedEmitterInfo = buildInfoString(idx, e); input.selectedEmitterInfo = buildInfoString(idx, e);
@@ -234,7 +269,10 @@ public class EmitterState extends BaseAppState {
markers.add(marker); markers.add(marker);
ParticleEmitter particle = buildParticleEmitter(pe); ParticleEmitter particle = buildParticleEmitter(pe);
if (particle != null) rootNode.attachChild(particle); if (particle != null) {
particle.setCullHint(Spatial.CullHint.Always); // standardmäßig unsichtbar
rootNode.attachChild(particle);
}
particles.add(particle); particles.add(particle);
emitters.add(pe); emitters.add(pe);
@@ -252,6 +290,25 @@ public class EmitterState extends BaseAppState {
input.emitterSelectionChanged = true; input.emitterSelectionChanged = true;
} }
private void removeByPosition(float x, float y, float z) {
for (int i = emitters.size() - 1; i >= 0; i--) {
PlacedEmitter e = emitters.get(i);
if (Math.abs(e.x() - x) < POS_EPSILON
&& Math.abs(e.y() - y) < POS_EPSILON
&& Math.abs(e.z() - z) < POS_EPSILON) {
if (selectedIdx == i) {
selectedIdx = -1;
input.selectedEmitterInfo = null;
input.emitterSelectionChanged = true;
} else if (selectedIdx > i) {
selectedIdx--;
}
removeEmitter(i);
break;
}
}
}
private void clearAll() { private void clearAll() {
for (Node m : markers) rootNode.detachChild(m); for (Node m : markers) rootNode.detachChild(m);
for (ParticleEmitter pe : particles) if (pe != null) rootNode.detachChild(pe); for (ParticleEmitter pe : particles) if (pe != null) rootNode.detachChild(pe);
@@ -270,15 +327,18 @@ public class EmitterState extends BaseAppState {
Node newMarker = buildMarker(updated); Node newMarker = buildMarker(updated);
setMarkerColor(newMarker, SEL_COLOR); setMarkerColor(newMarker, SEL_COLOR);
newMarker.setCullHint(Spatial.CullHint.Always); // selektiert → Marker versteckt
rootNode.attachChild(newMarker); rootNode.attachChild(newMarker);
markers.set(idx, newMarker); markers.set(idx, newMarker);
ParticleEmitter newPe = buildParticleEmitter(updated); ParticleEmitter newPe = buildParticleEmitter(updated);
if (newPe != null) rootNode.attachChild(newPe); if (newPe != null) rootNode.attachChild(newPe); // selektiert → Partikel sichtbar
particles.set(idx, newPe); particles.set(idx, newPe);
emitters.set(idx, updated); emitters.set(idx, updated);
publishSelection(idx); // selectedEmitterInfo aktualisieren, aber kein Panel-Rebuild auslösen –
// sonst werden initStartSize/initVelY neu erfasst und Groesse-Buttons compoundieren.
input.selectedEmitterInfo = buildInfoString(idx, updated);
} }
// ── Marker visuals ──────────────────────────────────────────────────────── // ── Marker visuals ────────────────────────────────────────────────────────

View File

@@ -737,7 +737,7 @@ public class EzTreeState extends BaseAppState {
true, true, true, true,
1f, 1f, 1f, 1f,
"", "", 30f, 80f, 120f, "", "", 30f, 80f, 120f,
java.util.List.of(), java.util.List.of(), java.util.List.of(), java.util.List.of(), java.util.List.of(),
de.blight.common.model.InteractableType.NONE, de.blight.common.model.InteractableType.NONE,
0f, 0.5f, 0f, 0f, "bark", autoCategory); 0f, 0.5f, 0f, 0f, "bark", autoCategory);
de.blight.common.ModelMetaIO.save(meta, outPath); de.blight.common.ModelMetaIO.save(meta, outPath);

View File

@@ -152,6 +152,7 @@ public class ItemPlacementState extends BaseAppState {
if (selectedItemIdx >= 0) { if (selectedItemIdx >= 0) {
removeItem(selectedItemIdx); removeItem(selectedItemIdx);
deselectItem(); deselectItem();
input.onObjectDeleted.run();
} }
} }
} }

View File

@@ -84,8 +84,15 @@ public class LightState extends BaseAppState {
// ── Update ──────────────────────────────────────────────────────────────── // ── Update ────────────────────────────────────────────────────────────────
private static final float POS_EPSILON = 0.05f;
@Override @Override
public void update(float tpf) { public void update(float tpf) {
float[] rmPos;
while ((rmPos = input.removeLightPositions.poll()) != null) {
removeByPosition(rmPos[0], rmPos[1], rmPos[2]);
}
SharedInput.ObjectClick auswahlClick; SharedInput.ObjectClick auswahlClick;
while ((auswahlClick = input.auswahlLightClickQueue.poll()) != null) { while ((auswahlClick = input.auswahlLightClickQueue.poll()) != null) {
handleAuswahlClick(auswahlClick); handleAuswahlClick(auswahlClick);
@@ -105,7 +112,11 @@ public class LightState extends BaseAppState {
if (input.deleteLightRequested) { if (input.deleteLightRequested) {
input.deleteLightRequested = false; input.deleteLightRequested = false;
if (selectedIdx >= 0) removeLight(selectedIdx); if (selectedIdx >= 0) {
removeLight(selectedIdx);
try { de.blight.common.LightIO.save(new ArrayList<>(lights)); } catch (java.io.IOException ignored) {}
input.onObjectDeleted.run();
}
} }
} }
@@ -221,6 +232,25 @@ public class LightState extends BaseAppState {
input.lightSelectionChanged = true; input.lightSelectionChanged = true;
} }
private void removeByPosition(float x, float y, float z) {
for (int i = lights.size() - 1; i >= 0; i--) {
PlacedLight l = lights.get(i);
if (Math.abs(l.x() - x) < POS_EPSILON
&& Math.abs(l.y() - y) < POS_EPSILON
&& Math.abs(l.z() - z) < POS_EPSILON) {
if (selectedIdx == i) {
selectedIdx = -1;
input.selectedLightInfo = null;
input.lightSelectionChanged = true;
} else if (selectedIdx > i) {
selectedIdx--;
}
removeLight(i);
break;
}
}
}
private void clearAll() { private void clearAll() {
for (PointLight pl : pointLights) rootNode.removeLight(pl); for (PointLight pl : pointLights) rootNode.removeLight(pl);
for (Node m : markers) rootNode.detachChild(m); for (Node m : markers) rootNode.detachChild(m);

View File

@@ -127,7 +127,7 @@ public class LocationZoneState extends BaseAppState {
if (input.deleteLocationZoneRequested) { if (input.deleteLocationZoneRequested) {
input.deleteLocationZoneRequested = false; input.deleteLocationZoneRequested = false;
if (selectedIdx >= 0) removeZone(selectedIdx); if (selectedIdx >= 0) { removeZone(selectedIdx); input.onObjectDeleted.run(); }
} }
} }

View File

@@ -28,6 +28,9 @@ import com.jme3.scene.shape.Cylinder;
import com.jme3.scene.shape.Dome; import com.jme3.scene.shape.Dome;
import com.jme3.scene.shape.Sphere; import com.jme3.scene.shape.Sphere;
import com.jme3.anim.SkinningControl; import com.jme3.anim.SkinningControl;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.RenderState;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil; import de.blight.editor.util.ImpostorUtil;
@@ -333,7 +336,8 @@ public class ModelEditorState extends BaseAppState {
input.modelEditorAttachmentsChanged = false; input.modelEditorAttachmentsChanged = false;
rebuildAttachmentGizmos( rebuildAttachmentGizmos(
input.modelEditorAttachedLights, input.modelEditorAttachedLights,
input.modelEditorAttachedEmitters); input.modelEditorAttachedEmitters,
input.modelEditorAttachedSounds);
} }
// Thumbnail auf Anforderung generieren (Model-Editor) // Thumbnail auf Anforderung generieren (Model-Editor)
@@ -953,7 +957,8 @@ public class ModelEditorState extends BaseAppState {
private void rebuildAttachmentGizmos( private void rebuildAttachmentGizmos(
java.util.List<de.blight.common.ModelMeta.AttachedLight> lights, java.util.List<de.blight.common.ModelMeta.AttachedLight> lights,
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters) { java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
java.util.List<de.blight.common.ModelMeta.AttachedSound> sounds) {
if (previewRoot == null) return; if (previewRoot == null) return;
@@ -965,7 +970,7 @@ public class ModelEditorState extends BaseAppState {
attachmentGizmos = null; attachmentGizmos = null;
} }
if (lights.isEmpty() && emitters.isEmpty()) return; if (lights.isEmpty() && emitters.isEmpty() && sounds.isEmpty()) return;
attachmentGizmos = new Node("attachment_gizmos"); attachmentGizmos = new Node("attachment_gizmos");
@@ -991,21 +996,22 @@ public class ModelEditorState extends BaseAppState {
previewRoot.addLight(pl); previewRoot.addLight(pl);
} }
// Emitter-Gizmos: kleine Kugel in Preset-Farbe // Emitter: echte ParticleEmitter, keine Kugel-Gizmos
for (de.blight.common.ModelMeta.AttachedEmitter ae : emitters) { for (de.blight.common.ModelMeta.AttachedEmitter ae : emitters) {
ColorRGBA color = switch (ae.preset()) { ParticleEmitter pe = buildAttachedParticleEffect(ae);
case 1 -> new ColorRGBA(0.5f, 0.5f, 0.5f, 1f); // Rauch if (pe != null) attachmentGizmos.attachChild(pe);
case 2 -> new ColorRGBA(1.0f, 0.9f, 0.1f, 1f); // Funken }
default -> new ColorRGBA(1.0f, 0.4f, 0.0f, 1f); // Feuer
}; // Sound-Gizmos: kleine türkise Kugel
Geometry sphere = new Geometry("emitter_gizmo", for (de.blight.common.ModelMeta.AttachedSound as : sounds) {
new Sphere(8, 8, 0.10f)); Geometry soundSphere = new Geometry("sound_gizmo",
Material mat = new Material(app.getAssetManager(), new com.jme3.scene.shape.Sphere(8, 8, 0.10f));
Material sMat = new Material(app.getAssetManager(),
"Common/MatDefs/Misc/Unshaded.j3md"); "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color); sMat.setColor("Color", new ColorRGBA(0.0f, 0.85f, 0.85f, 1f));
sphere.setMaterial(mat); soundSphere.setMaterial(sMat);
sphere.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ()); soundSphere.setLocalTranslation(as.offsetX(), as.offsetY(), as.offsetZ());
attachmentGizmos.attachChild(sphere); attachmentGizmos.attachChild(soundSphere);
} }
previewRoot.attachChild(attachmentGizmos); previewRoot.attachChild(attachmentGizmos);
@@ -1019,6 +1025,34 @@ public class ModelEditorState extends BaseAppState {
} }
} }
private ParticleEmitter buildAttachedParticleEffect(de.blight.common.ModelMeta.AttachedEmitter ae) {
try {
ParticleEmitter effect = new ParticleEmitter(
"attached_particle", ParticleMesh.Type.Triangle, ae.maxParticles());
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
mat.setTexture("Texture", app.getAssetManager().loadTexture(ae.texturePath()));
effect.setMaterial(mat);
effect.setImagesX(ae.imagesX());
effect.setImagesY(ae.imagesY());
effect.setStartColor(new ColorRGBA(ae.startR(), ae.startG(), ae.startB(), ae.startA()));
effect.setEndColor( new ColorRGBA(ae.endR(), ae.endG(), ae.endB(), ae.endA()));
effect.setStartSize(ae.startSize());
effect.setEndSize(ae.endSize());
effect.getParticleInfluencer()
.setInitialVelocity(new Vector3f(ae.velX(), ae.velY(), ae.velZ()));
effect.getParticleInfluencer().setVelocityVariation(ae.velocityVariation());
effect.setGravity(ae.gravX(), ae.gravY(), ae.gravZ());
effect.setLowLife(ae.lowLife());
effect.setHighLife(ae.highLife());
effect.setParticlesPerSec(ae.emitRate());
effect.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ());
return effect;
} catch (Exception e) {
log.warn("[ModelEditorState] Emitter-Textur nicht ladbar: {}", e.getMessage());
return null;
}
}
// ── Hilfsmethoden ───────────────────────────────────────────────────────── // ── Hilfsmethoden ─────────────────────────────────────────────────────────
/** Markiert alle Vertex-Buffer als upload-needed – zwingt JME3 zu korrektem erstem Frame. */ /** Markiert alle Vertex-Buffer als upload-needed – zwingt JME3 zu korrektem erstem Frame. */

View File

@@ -424,7 +424,7 @@ public class ModelImportState extends BaseAppState {
def.randomScaleMin(), def.randomScaleMax(), def.randomScaleMin(), def.randomScaleMax(),
def.lod1Path(), def.lod2Path(), def.lod1Path(), def.lod2Path(),
def.lod1Distance(), def.lod2Distance(), def.cullDistance(), def.lod1Distance(), def.lod2Distance(), def.cullDistance(),
def.attachedLights(), def.attachedEmitters(), def.attachedLights(), def.attachedEmitters(), def.attachedSounds(),
def.interactableType(), def.interactableType(),
def.interactableOffsetX(), def.interactableOffsetY(), def.interactableOffsetX(), def.interactableOffsetY(),
def.interactableOffsetZ(), def.interactableRotY(), def.interactableOffsetZ(), def.interactableRotY(),

View File

@@ -138,7 +138,7 @@ public class PathNetworkEditorState extends BaseAppState {
if (input.deletePathNodeRequested) { if (input.deletePathNodeRequested) {
input.deletePathNodeRequested = false; input.deletePathNodeRequested = false;
deleteSelected(); if (selectedNode != null) { deleteSelected(); input.onObjectDeleted.run(); }
} }
} }

View File

@@ -0,0 +1,312 @@
package de.blight.editor.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*;
import com.jme3.scene.shape.Sphere;
import com.jme3.terrain.geomipmap.TerrainQuad;
import de.blight.common.PlacedSound;
import de.blight.editor.SharedInput;
import java.util.ArrayList;
import java.util.List;
public class PlacedSoundState extends BaseAppState {
private static final float MARKER_RADIUS = 0.30f;
private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f;
private static final float GLOW_ALPHA = 0.18f;
private static final ColorRGBA COL_NORMAL = new ColorRGBA(0.0f, 0.85f, 0.85f, 1f);
private static final ColorRGBA COL_SELECTED = new ColorRGBA(1.0f, 1.0f, 0.0f, 1f);
private static final ColorRGBA COL_GLOW = new ColorRGBA(0.0f, 0.85f, 0.85f, GLOW_ALPHA);
private static final float POS_EPSILON = 0.05f;
private final SharedInput input;
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private Node rootNode;
private TerrainQuad terrain;
// parallel lists: sounds[i] <-> markers[i]
private final List<PlacedSound> sounds = new ArrayList<>();
private final List<Node> markers = new ArrayList<>();
private int selectedIdx = -1;
private List<PlacedSound> pendingSounds = null;
public PlacedSoundState(SharedInput input) {
this.input = input;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
cam = app.getCamera();
assets = app.getAssetManager();
rootNode = app.getRootNode();
}
@Override
protected void cleanup(Application application) {
clearAll();
}
@Override
protected void onEnable() {
if (pendingSounds != null) {
loadPlacedSounds(pendingSounds);
pendingSounds = null;
}
}
@Override protected void onDisable() {}
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
float[] rmPos;
while ((rmPos = input.removeSoundPositions.poll()) != null) {
removeByPosition(rmPos[0], rmPos[1], rmPos[2]);
}
SharedInput.ObjectClick auswahlClick;
while ((auswahlClick = input.auswahlSoundClickQueue.poll()) != null) {
handleAuswahlClick(auswahlClick);
}
if (input.activeLayer != SharedInput.LAYER_PLACED_SOUNDS) return;
SharedInput.SoundClick click;
while ((click = input.soundClickQueue.poll()) != null) {
handleClick(click);
}
SharedInput.SoundPropertyChange prop = input.pendingSoundProp.getAndSet(null);
if (prop != null && selectedIdx >= 0) {
applyProperty(selectedIdx, prop);
}
if (input.deleteSoundRequested) {
input.deleteSoundRequested = false;
if (selectedIdx >= 0) {
removeSound(selectedIdx);
try { de.blight.common.SoundIO.save(new ArrayList<>(sounds)); } catch (java.io.IOException ignored) {}
input.onObjectDeleted.run();
}
}
}
// ── Click handling ────────────────────────────────────────────────────────
private void handleClick(SharedInput.SoundClick click) {
float jmeX = click.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
int hit = pickMarker(ray);
if (hit >= 0) {
if (click.rightButton()) {
deselect();
} else {
selectSound(hit);
}
return;
}
if (click.rightButton()) {
deselect();
return;
}
Vector3f pt = raycastTerrain(ray);
if (pt == null) return;
PlacedSound ps = new PlacedSound(pt.x, pt.y + 0.5f, pt.z, "", 1.0f, 20.0f);
addSound(ps);
selectSound(sounds.size() - 1);
}
private void handleAuswahlClick(SharedInput.ObjectClick click) {
float jmeX = click.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
int hit = pickMarker(ray);
if (hit >= 0) {
selectSound(hit);
input.auswahlSoundSelected = true;
}
}
private int pickMarker(Ray ray) {
for (int i = 0; i < markers.size(); i++) {
CollisionResults res = new CollisionResults();
markers.get(i).collideWith(ray, res);
if (res.size() > 0) return i;
}
return -1;
}
// ── Selection ─────────────────────────────────────────────────────────────
private void selectSound(int idx) {
deselect();
selectedIdx = idx;
setMarkerColor(idx, COL_SELECTED);
publishSelection(idx);
}
private void deselect() {
if (selectedIdx >= 0 && selectedIdx < sounds.size()) {
setMarkerColor(selectedIdx, COL_NORMAL);
}
selectedIdx = -1;
input.selectedSoundInfo = null;
input.soundSelectionChanged = true;
}
private void publishSelection(int idx) {
PlacedSound s = sounds.get(idx);
input.selectedSoundInfo = String.format(
java.util.Locale.ROOT,
"%d|%.3f|%.3f|%.3f|%s|%.3f|%.3f",
idx, s.x(), s.y(), s.z(), s.soundPath(), s.volume(), s.radius());
input.soundSelectionChanged = true;
}
// ── Add / Remove ──────────────────────────────────────────────────────────
private void addSound(PlacedSound ps) {
Node marker = buildMarker(ps);
rootNode.attachChild(marker);
sounds.add(ps);
markers.add(marker);
}
private void removeSound(int idx) {
rootNode.detachChild(markers.get(idx));
sounds.remove(idx);
markers.remove(idx);
selectedIdx = -1;
input.selectedSoundInfo = null;
input.soundSelectionChanged = true;
}
private void removeByPosition(float x, float y, float z) {
for (int i = sounds.size() - 1; i >= 0; i--) {
PlacedSound s = sounds.get(i);
if (Math.abs(s.x() - x) < POS_EPSILON
&& Math.abs(s.y() - y) < POS_EPSILON
&& Math.abs(s.z() - z) < POS_EPSILON) {
if (selectedIdx == i) {
selectedIdx = -1;
input.selectedSoundInfo = null;
input.soundSelectionChanged = true;
} else if (selectedIdx > i) {
selectedIdx--;
}
rootNode.detachChild(markers.get(i));
sounds.remove(i);
markers.remove(i);
break;
}
}
}
private void clearAll() {
for (Node m : markers) rootNode.detachChild(m);
sounds.clear();
markers.clear();
selectedIdx = -1;
}
// ── Property application ──────────────────────────────────────────────────
private void applyProperty(int idx, SharedInput.SoundPropertyChange prop) {
PlacedSound old = sounds.get(idx);
PlacedSound updated = new PlacedSound(
old.x(), old.y(), old.z(),
prop.soundPath(), prop.volume(), prop.radius());
sounds.set(idx, updated);
publishSelection(idx);
}
// ── Marker visuals ────────────────────────────────────────────────────────
private Node buildMarker(PlacedSound ps) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", COL_NORMAL);
Geometry sphere = new Geometry("sound_sphere", new Sphere(10, 10, MARKER_RADIUS));
sphere.setMaterial(mat);
Material glowMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
glowMat.setColor("Color", COL_GLOW);
glowMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
glowMat.getAdditionalRenderState().setDepthWrite(false);
glowMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Front);
Geometry glow = new Geometry("sound_glow", new Sphere(8, 8, GLOW_RADIUS));
glow.setMaterial(glowMat);
glow.setQueueBucket(RenderQueue.Bucket.Transparent);
Node node = new Node("sound_node");
node.attachChild(glow);
node.attachChild(sphere);
node.setLocalTranslation(ps.x(), ps.y(), ps.z());
return node;
}
private void setMarkerColor(int idx, ColorRGBA color) {
Node marker = markers.get(idx);
for (Spatial child : marker.getChildren()) {
if (child instanceof Geometry geo && "sound_sphere".equals(geo.getName())) {
geo.getMaterial().setColor("Color", color);
return;
}
}
}
// ── Save / Load ───────────────────────────────────────────────────────────
public List<PlacedSound> getPlacedSounds() {
return new ArrayList<>(sounds);
}
public void loadPlacedSounds(List<PlacedSound> loaded) {
if (rootNode == null) {
pendingSounds = new ArrayList<>(loaded);
return;
}
clearAll();
for (PlacedSound ps : loaded) {
addSound(ps);
}
}
private Vector3f raycastTerrain(Ray ray) {
if (terrain == null) return null;
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) return hits.getClosestCollision().getContactPoint();
return null;
}
}

View File

@@ -85,9 +85,13 @@ public class SceneObjectState extends BaseAppState {
private Node rootNode; private Node rootNode;
private TerrainQuad terrain; private TerrainQuad terrain;
private final List<SceneObject> objects = new ArrayList<>(); private final List<SceneObject> objects = new ArrayList<>();
private final List<Node> objNodes = new ArrayList<>(); private final List<Node> objNodes = new ArrayList<>();
private final List<String> animClips = new ArrayList<>(); // parallel zu objects private final List<String> animClips = new ArrayList<>(); // parallel zu objects
// Kaskaden-Löschung: Welt-Positionen der mitplatzierten Lichter/Emitter/Sounds pro Objekt
private final List<java.util.List<float[]>> placedLightPositions = new ArrayList<>();
private final List<java.util.List<float[]>> placedEmitterPositions = new ArrayList<>();
private final List<java.util.List<float[]>> placedSoundPositions = new ArrayList<>();
private Node objectRoot; private Node objectRoot;
private Node gizmoNode; // Wurzel aller Gizmo-Geometrien (direktes Kind von rootNode) private Node gizmoNode; // Wurzel aller Gizmo-Geometrien (direktes Kind von rootNode)
@@ -205,6 +209,9 @@ public class SceneObjectState extends BaseAppState {
objects.clear(); objects.clear();
objNodes.clear(); objNodes.clear();
animClips.clear(); animClips.clear();
placedLightPositions.clear();
placedEmitterPositions.clear();
placedSoundPositions.clear();
sceneLitMaterials.clear(); sceneLitMaterials.clear();
windMaterials.clear(); windMaterials.clear();
pbrMaterials.clear(); pbrMaterials.clear();
@@ -231,6 +238,9 @@ public class SceneObjectState extends BaseAppState {
so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE; so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE;
objects.add(so); objects.add(so);
animClips.add(pm.animClip() != null ? pm.animClip() : ""); animClips.add(pm.animClip() != null ? pm.animClip() : "");
placedLightPositions.add(new ArrayList<>());
placedEmitterPositions.add(new ArrayList<>());
placedSoundPositions.add(new ArrayList<>());
// meshFile hat Vorrang: enthält exportierte Geometrie inkl. Material // meshFile hat Vorrang: enthält exportierte Geometrie inkl. Material
String loadPath = !pm.meshFile().isEmpty() ? pm.meshFile() : pm.modelPath(); String loadPath = !pm.meshFile().isEmpty() ? pm.meshFile() : pm.modelPath();
@@ -465,7 +475,7 @@ public class SceneObjectState extends BaseAppState {
// Löschen // Löschen
if (input.deleteSelectedRequested) { if (input.deleteSelectedRequested) {
input.deleteSelectedRequested = false; input.deleteSelectedRequested = false;
deleteSelected(); if (!selectedIndices.isEmpty()) { deleteSelected(); input.onObjectDeleted.run(); }
} }
// Zusammenfassen // Zusammenfassen
@@ -731,6 +741,7 @@ public class SceneObjectState extends BaseAppState {
input.auswahlClickQueue.offer(click); input.auswahlClickQueue.offer(click);
input.auswahlLightClickQueue.offer(click); input.auswahlLightClickQueue.offer(click);
input.auswahlEmitterClickQueue.offer(click); input.auswahlEmitterClickQueue.offer(click);
input.auswahlSoundClickQueue.offer(click);
input.auswahlWaterClickQueue.offer(click); input.auswahlWaterClickQueue.offer(click);
input.auswahlWaterfallClickQueue.offer(click); input.auswahlWaterfallClickQueue.offer(click);
return; return;
@@ -879,6 +890,9 @@ public class SceneObjectState extends BaseAppState {
so.receiveShadow = defaultReceive; so.receiveShadow = defaultReceive;
objects.add(so); objects.add(so);
animClips.add(""); animClips.add("");
placedLightPositions.add(new ArrayList<>());
placedEmitterPositions.add(new ArrayList<>());
placedSoundPositions.add(new ArrayList<>());
Node node = loadModelNode(modelPath, wx, wy + placementOffY, wz); Node node = loadModelNode(modelPath, wx, wy + placementOffY, wz);
node.setLocalScale(defaultScale); node.setLocalScale(defaultScale);
@@ -891,10 +905,11 @@ public class SceneObjectState extends BaseAppState {
input.objectJustPlaced = true; input.objectJustPlaced = true;
setStatus("Platziert: " + modelPath); setStatus("Platziert: " + modelPath);
// Anhänge aus ModelMeta: Lichter + Emitter mitplatzieren // Anhänge aus ModelMeta: Lichter + Emitter + Sounds mitplatzieren
if (meta != null) { if (meta != null) {
placeAttachedLights(meta, wx, wy + placementOffY, wz); placeAttachedLights(meta, wx, wy + placementOffY, wz);
placeAttachedEmitters(meta, wx, wy + placementOffY, wz); placeAttachedEmitters(meta, wx, wy + placementOffY, wz);
placeAttachedSounds(meta, wx, wy + placementOffY, wz);
} }
} }
@@ -904,10 +919,12 @@ public class SceneObjectState extends BaseAppState {
try { try {
java.util.List<de.blight.common.PlacedLight> list = java.util.List<de.blight.common.PlacedLight> list =
new java.util.ArrayList<>(de.blight.common.LightIO.load()); new java.util.ArrayList<>(de.blight.common.LightIO.load());
java.util.List<float[]> track = placedLightPositions.get(objects.size() - 1);
for (de.blight.common.ModelMeta.AttachedLight al : meta.attachedLights()) { for (de.blight.common.ModelMeta.AttachedLight al : meta.attachedLights()) {
list.add(new de.blight.common.PlacedLight( float lx = wx + al.offsetX(), ly = wy + al.offsetY(), lz = wz + al.offsetZ();
wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ(), list.add(new de.blight.common.PlacedLight(lx, ly, lz,
al.r(), al.g(), al.b(), al.intensity(), al.radius())); al.r(), al.g(), al.b(), al.intensity(), al.radius()));
track.add(new float[]{lx, ly, lz});
} }
de.blight.common.LightIO.save(list); de.blight.common.LightIO.save(list);
input.reloadLightsEmitters = true; input.reloadLightsEmitters = true;
@@ -922,16 +939,22 @@ public class SceneObjectState extends BaseAppState {
try { try {
java.util.List<de.blight.common.PlacedEmitter> list = java.util.List<de.blight.common.PlacedEmitter> list =
new java.util.ArrayList<>(de.blight.common.EmitterIO.load()); new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
java.util.List<float[]> track = placedEmitterPositions.get(objects.size() - 1);
for (de.blight.common.ModelMeta.AttachedEmitter ae : meta.attachedEmitters()) { for (de.blight.common.ModelMeta.AttachedEmitter ae : meta.attachedEmitters()) {
float ax = wx + ae.offsetX(); float ax = wx + ae.offsetX();
float ay = wy + ae.offsetY(); float ay = wy + ae.offsetY();
float az = wz + ae.offsetZ(); float az = wz + ae.offsetZ();
de.blight.common.PlacedEmitter pe = switch (ae.preset()) { list.add(new de.blight.common.PlacedEmitter(
case 1 -> de.blight.common.PlacedEmitter.smoke(ax, ay, az); ax, ay, az, 20f,
case 2 -> de.blight.common.PlacedEmitter.sparks(ax, ay, az); ae.texturePath(), ae.imagesX(), ae.imagesY(),
default -> de.blight.common.PlacedEmitter.fire(ax, ay, az); ae.startR(), ae.startG(), ae.startB(), ae.startA(),
}; ae.endR(), ae.endG(), ae.endB(), ae.endA(),
list.add(pe); ae.startSize(), ae.endSize(),
ae.velX(), ae.velY(), ae.velZ(), ae.velocityVariation(),
ae.gravX(), ae.gravY(), ae.gravZ(),
ae.lowLife(), ae.highLife(),
ae.maxParticles(), ae.emitRate()));
track.add(new float[]{ax, ay, az});
} }
de.blight.common.EmitterIO.save(list); de.blight.common.EmitterIO.save(list);
input.reloadLightsEmitters = true; input.reloadLightsEmitters = true;
@@ -940,6 +963,28 @@ public class SceneObjectState extends BaseAppState {
} }
} }
private void placeAttachedSounds(de.blight.common.ModelMeta meta,
float wx, float wy, float wz) {
if (meta.attachedSounds().isEmpty()) return;
try {
java.util.List<de.blight.common.PlacedSound> list =
new java.util.ArrayList<>(de.blight.common.SoundIO.load());
java.util.List<float[]> track = placedSoundPositions.get(objects.size() - 1);
for (de.blight.common.ModelMeta.AttachedSound as : meta.attachedSounds()) {
float sx = wx + as.offsetX();
float sy = wy + as.offsetY();
float sz = wz + as.offsetZ();
list.add(new de.blight.common.PlacedSound(sx, sy, sz,
as.soundPath(), as.volume(), as.radius()));
track.add(new float[]{sx, sy, sz});
}
de.blight.common.SoundIO.save(list);
input.reloadLightsEmitters = true;
} catch (java.io.IOException e) {
log.error("[SceneObject] Anhang-Sound speichern fehlgeschlagen: {}", e.getMessage());
}
}
private void convertUnshadedToPbr(Spatial spatial) { private void convertUnshadedToPbr(Spatial spatial) {
if (spatial instanceof Geometry geo) { if (spatial instanceof Geometry geo) {
Material mat = geo.getMaterial(); Material mat = geo.getMaterial();
@@ -1898,16 +1943,48 @@ public class SceneObjectState extends BaseAppState {
List<Integer> sortedDesc = new ArrayList<>(selectedIndices); List<Integer> sortedDesc = new ArrayList<>(selectedIndices);
sortedDesc.sort(Comparator.reverseOrder()); sortedDesc.sort(Comparator.reverseOrder());
// Positionen aller Kaskaden-Anhänge sammeln (aus Meta – funktioniert auch nach Neustart)
List<float[]> cascadeLights = new ArrayList<>();
List<float[]> cascadeEmitters = new ArrayList<>();
List<float[]> cascadeSounds = new ArrayList<>();
for (int idx : sortedDesc) { for (int idx : sortedDesc) {
SceneObject so = objects.get(idx); SceneObject so = objects.get(idx);
de.blight.common.ModelMeta meta = loadMetaCached(so.modelPath);
if (meta != null) {
float wx = so.getWorldX(), wy = so.getGroundY(), wz = so.getWorldZ();
for (var al : meta.attachedLights()) {
float[] p = {wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ()};
input.removeLightPositions.offer(p);
cascadeLights.add(p);
}
for (var ae : meta.attachedEmitters()) {
float[] p = {wx + ae.offsetX(), wy + ae.offsetY(), wz + ae.offsetZ()};
input.removeEmitterPositions.offer(p);
cascadeEmitters.add(p);
}
for (var as : meta.attachedSounds()) {
float[] p = {wx + as.offsetX(), wy + as.offsetY(), wz + as.offsetZ()};
input.removeSoundPositions.offer(p);
cascadeSounds.add(p);
}
}
deleteInteractableFile(so); deleteInteractableFile(so);
objectRoot.detachChild(objNodes.get(idx)); objectRoot.detachChild(objNodes.get(idx));
cachedGizmoRadii.remove(objNodes.get(idx)); cachedGizmoRadii.remove(objNodes.get(idx));
objects.remove(idx); objects.remove(idx);
objNodes.remove(idx); objNodes.remove(idx);
animClips.remove(idx); animClips.remove(idx);
placedLightPositions.remove(idx);
placedEmitterPositions.remove(idx);
placedSoundPositions.remove(idx);
} }
// Dateien sofort bereinigen, damit reloadLightsEmitters keine alten Daten zurückbringt
if (!cascadeLights.isEmpty()) cascadeRemoveFromFile(cascadeLights, "lights");
if (!cascadeEmitters.isEmpty()) cascadeRemoveFromFile(cascadeEmitters, "emitters");
if (!cascadeSounds.isEmpty()) cascadeRemoveFromFile(cascadeSounds, "sounds");
selectedIndices.clear(); selectedIndices.clear();
clearSubSelection(); clearSubSelection();
activeGizmo = -1; activeGizmo = -1;
@@ -1920,6 +1997,42 @@ public class SceneObjectState extends BaseAppState {
setStatus("Objekt(e) gelöscht"); setStatus("Objekt(e) gelöscht");
} }
private void cascadeRemoveFromFile(List<float[]> positions, String type) {
try {
switch (type) {
case "lights" -> {
List<de.blight.common.PlacedLight> list = new java.util.ArrayList<>(de.blight.common.LightIO.load());
for (float[] p : positions) {
list.removeIf(l -> Math.abs(l.x()-p[0]) < 0.05f
&& Math.abs(l.y()-p[1]) < 0.05f
&& Math.abs(l.z()-p[2]) < 0.05f);
}
de.blight.common.LightIO.save(list);
}
case "emitters" -> {
List<de.blight.common.PlacedEmitter> list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
for (float[] p : positions) {
list.removeIf(e -> Math.abs(e.x()-p[0]) < 0.05f
&& Math.abs(e.y()-p[1]) < 0.05f
&& Math.abs(e.z()-p[2]) < 0.05f);
}
de.blight.common.EmitterIO.save(list);
}
case "sounds" -> {
List<de.blight.common.PlacedSound> list = new java.util.ArrayList<>(de.blight.common.SoundIO.load());
for (float[] p : positions) {
list.removeIf(s -> Math.abs(s.x()-p[0]) < 0.05f
&& Math.abs(s.y()-p[1]) < 0.05f
&& Math.abs(s.z()-p[2]) < 0.05f);
}
de.blight.common.SoundIO.save(list);
}
}
} catch (java.io.IOException e) {
log.warn("[SceneObject] Kaskade {}-Datei bereinigen fehlgeschlagen: {}", type, e.getMessage());
}
}
private void deleteInteractableFile(SceneObject so) { private void deleteInteractableFile(SceneObject so) {
if (so.interactableId == null || so.interactableId.isEmpty()) return; if (so.interactableId == null || so.interactableId.isEmpty()) return;
try { try {

View File

@@ -131,7 +131,7 @@ public class SoundAreaState extends BaseAppState {
if (input.deleteSoundAreaRequested) { if (input.deleteSoundAreaRequested) {
input.deleteSoundAreaRequested = false; input.deleteSoundAreaRequested = false;
if (selectedIdx >= 0) removeArea(selectedIdx); if (selectedIdx >= 0) { removeArea(selectedIdx); input.onObjectDeleted.run(); }
} }
} }

View File

@@ -47,6 +47,7 @@ import de.blight.common.MapIO;
import de.blight.common.PlacedModelIO; import de.blight.common.PlacedModelIO;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.state.PathNetworkEditorState; import de.blight.editor.state.PathNetworkEditorState;
import de.blight.editor.state.PlacedSoundState;
import de.blight.editor.state.RoutineMapState; import de.blight.editor.state.RoutineMapState;
import de.blight.editor.tool.HeightTool; import de.blight.editor.tool.HeightTool;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -105,6 +106,7 @@ public class TerrainEditorState extends BaseAppState {
private ItemPlacementState itemPlacementState; private ItemPlacementState itemPlacementState;
private LightState lightState; private LightState lightState;
private EmitterState emitterState; private EmitterState emitterState;
private PlacedSoundState placedSoundState;
private WaterBodyState waterBodyState; private WaterBodyState waterBodyState;
private SoundAreaState soundAreaState; private SoundAreaState soundAreaState;
private AreaState areaState; private AreaState areaState;
@@ -326,6 +328,18 @@ public class TerrainEditorState extends BaseAppState {
} }
} }
input.loadingStatus = "Lade Sound-Quellen...";
placedSoundState = app.getStateManager().getState(PlacedSoundState.class);
if (placedSoundState != null) {
placedSoundState.setTerrain(terrain);
try {
var sounds = de.blight.common.SoundIO.load();
if (!sounds.isEmpty()) placedSoundState.loadPlacedSounds(sounds);
} catch (IOException e) {
log.error("Sound-Quellen nicht ladbar", e);
}
}
input.loadingStatus = "Lade Wasserflächen..."; input.loadingStatus = "Lade Wasserflächen...";
waterBodyState = app.getStateManager().getState(WaterBodyState.class); waterBodyState = app.getStateManager().getState(WaterBodyState.class);
if (waterBodyState != null) { if (waterBodyState != null) {
@@ -1303,14 +1317,16 @@ public class TerrainEditorState extends BaseAppState {
if (input.reloadLightsEmitters) { if (input.reloadLightsEmitters) {
input.reloadLightsEmitters = false; input.reloadLightsEmitters = false;
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {} try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
try { if (placedSoundState != null) placedSoundState.loadPlacedSounds(de.blight.common.SoundIO.load()); } catch (Exception ignored) {}
} }
if (input.reloadPlacedOther) { if (input.reloadPlacedOther) {
input.reloadPlacedOther = false; input.reloadPlacedOther = false;
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {} try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
try { if (placedSoundState != null) placedSoundState.loadPlacedSounds(de.blight.common.SoundIO.load()); } catch (Exception ignored) {}
try { if (waterBodyState != null) waterBodyState.loadPlacedBodies(de.blight.common.WaterBodyIO.load()); } catch (Exception ignored) {} try { if (waterBodyState != null) waterBodyState.loadPlacedBodies(de.blight.common.WaterBodyIO.load()); } catch (Exception ignored) {}
try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {} try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {}
try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {} try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {}
@@ -1558,6 +1574,7 @@ public class TerrainEditorState extends BaseAppState {
final List<PlacedModel> models = sceneObjState != null ? sceneObjState.getPlacedModels() : null; final List<PlacedModel> models = sceneObjState != null ? sceneObjState.getPlacedModels() : null;
final List<PlacedLight> lights = lightState != null ? lightState.getPlacedLights() : null; final List<PlacedLight> lights = lightState != null ? lightState.getPlacedLights() : null;
final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null; final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null;
final List<de.blight.common.PlacedSound> placedSounds = placedSoundState != null ? placedSoundState.getPlacedSounds() : null;
final List<PlacedWater> waters = waterBodyState != null ? waterBodyState.getPlacedBodies() : null; final List<PlacedWater> waters = waterBodyState != null ? waterBodyState.getPlacedBodies() : null;
final List<List<RiverPoint>> rivers = riverEditorState != null ? riverEditorState.getPlacedRivers() : null; final List<List<RiverPoint>> rivers = riverEditorState != null ? riverEditorState.getPlacedRivers() : null;
final List<PlacedSoundArea> soundAreas = soundAreaState != null ? soundAreaState.getPlacedAreas() : null; final List<PlacedSoundArea> soundAreas = soundAreaState != null ? soundAreaState.getPlacedAreas() : null;
@@ -1573,6 +1590,7 @@ public class TerrainEditorState extends BaseAppState {
try { if (models != null) PlacedModelIO.save(models); } catch (IOException e) { log.error("Modelle speichern", e); } try { if (models != null) PlacedModelIO.save(models); } catch (IOException e) { log.error("Modelle speichern", e); }
try { if (lights != null) LightIO.save(lights); } catch (IOException e) { log.error("Lichter speichern", e); } try { if (lights != null) LightIO.save(lights); } catch (IOException e) { log.error("Lichter speichern", e); }
try { if (emitters != null) EmitterIO.save(emitters); } catch (IOException e) { log.error("Emitter speichern", e); } try { if (emitters != null) EmitterIO.save(emitters); } catch (IOException e) { log.error("Emitter speichern", e); }
try { if (placedSounds != null) de.blight.common.SoundIO.save(placedSounds); } catch (IOException e) { log.error("Sound-Quellen speichern", e); }
try { if (waters != null) WaterBodyIO.save(waters); } catch (IOException e) { log.error("Wasser speichern", e); } try { if (waters != null) WaterBodyIO.save(waters); } catch (IOException e) { log.error("Wasser speichern", e); }
try { if (rivers != null) de.blight.common.RiverIO.save(rivers); } catch (IOException e) { log.error("Flüsse speichern", e); } try { if (rivers != null) de.blight.common.RiverIO.save(rivers); } catch (IOException e) { log.error("Flüsse speichern", e); }
try { if (soundAreas != null) SoundAreaIO.save(soundAreas); } catch (IOException e) { log.error("Soundbereiche speichern", e); } try { if (soundAreas != null) SoundAreaIO.save(soundAreas); } catch (IOException e) { log.error("Soundbereiche speichern", e); }

View File

@@ -489,7 +489,7 @@ public class TreeGeneratorState extends BaseAppState {
de.blight.common.ModelMeta autoMeta = new de.blight.common.ModelMeta( de.blight.common.ModelMeta autoMeta = new de.blight.common.ModelMeta(
fileName, "trees", "", 1f, 1f, 1f, true, 0f, 0f, fileName, "trees", "", 1f, 1f, 1f, true, 0f, 0f,
true, true, true, 1f, 1f, "", "", 30f, 80f, 120f, true, true, true, 1f, 1f, "", "", 30f, 80f, 120f,
java.util.List.of(), java.util.List.of(), java.util.List.of(), java.util.List.of(), java.util.List.of(),
de.blight.common.model.InteractableType.NONE, de.blight.common.model.InteractableType.NONE,
0f, 0.5f, 0f, 0f, "bark", autoCategory); 0f, 0.5f, 0f, 0f, "bark", autoCategory);
de.blight.common.ModelMetaIO.save(autoMeta, out.toPath()); de.blight.common.ModelMetaIO.save(autoMeta, out.toPath());

View File

@@ -109,9 +109,7 @@ public class VoxelCliffEditorState extends BaseAppState {
if (input.deleteVoxelCliffZoneRequested) { if (input.deleteVoxelCliffZoneRequested) {
input.deleteVoxelCliffZoneRequested = false; input.deleteVoxelCliffZoneRequested = false;
if (selectedIdx >= 0) { if (selectedIdx >= 0) { removeZone(selectedIdx); input.onObjectDeleted.run(); }
removeZone(selectedIdx);
}
} }
if (input.generateCliffVoxelsRequested) { if (input.generateCliffVoxelsRequested) {

View File

@@ -191,8 +191,10 @@ public class WaterBodyState extends BaseAppState {
input.deleteWaterRequested = false; input.deleteWaterRequested = false;
if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) { if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) {
deleteCornerPoint(selectedIdx, selectedHandle); deleteCornerPoint(selectedIdx, selectedHandle);
input.onObjectDeleted.run();
} else if (selectedIdx >= 0) { } else if (selectedIdx >= 0) {
removeBody(selectedIdx); removeBody(selectedIdx);
input.onObjectDeleted.run();
} }
} }

View File

@@ -4,6 +4,8 @@ import de.blight.common.PlacedEmitter;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext; import de.blight.editor.ToolContext;
import de.blight.editor.toolset.UiHelper; import de.blight.editor.toolset.UiHelper;
import de.blight.editor.ui.SoundChooser;
import javafx.application.Platform;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
@@ -14,6 +16,7 @@ public class AtmosphereController {
private VBox lightDynamicContent; private VBox lightDynamicContent;
private VBox emitterDynamicContent; private VBox emitterDynamicContent;
private VBox soundDynamicContent;
public AtmosphereController(ToolContext ctx) { public AtmosphereController(ToolContext ctx) {
this.ctx = ctx; this.ctx = ctx;
@@ -28,6 +31,10 @@ public class AtmosphereController {
ctx.input().emitterSelectionChanged = false; ctx.input().emitterSelectionChanged = false;
updateEmitterPanel(ctx.input().selectedEmitterInfo); updateEmitterPanel(ctx.input().selectedEmitterInfo);
} }
if (ctx.input().soundSelectionChanged) {
ctx.input().soundSelectionChanged = false;
updateSoundPanel(ctx.input().selectedSoundInfo);
}
} }
public VBox buildPanel() { public VBox buildPanel() {
@@ -41,15 +48,19 @@ public class AtmosphereController {
ToggleButton subLichtBtn = new ToggleButton("* Licht"); ToggleButton subLichtBtn = new ToggleButton("* Licht");
ToggleButton subEmitterBtn = new ToggleButton("~ Emitter"); ToggleButton subEmitterBtn = new ToggleButton("~ Emitter");
ToggleButton subSoundBtn = new ToggleButton("♪ Sound");
subLichtBtn.setMaxWidth(Double.MAX_VALUE); subLichtBtn.setMaxWidth(Double.MAX_VALUE);
subEmitterBtn.setMaxWidth(Double.MAX_VALUE); subEmitterBtn.setMaxWidth(Double.MAX_VALUE);
subSoundBtn.setMaxWidth(Double.MAX_VALUE);
ToggleGroup subGroup = new ToggleGroup(); ToggleGroup subGroup = new ToggleGroup();
subLichtBtn.setToggleGroup(subGroup); subLichtBtn.setToggleGroup(subGroup);
subEmitterBtn.setToggleGroup(subGroup); subEmitterBtn.setToggleGroup(subGroup);
subSoundBtn.setToggleGroup(subGroup);
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn); HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn, subSoundBtn);
HBox.setHgrow(subLichtBtn, Priority.ALWAYS); HBox.setHgrow(subLichtBtn, Priority.ALWAYS);
HBox.setHgrow(subEmitterBtn, Priority.ALWAYS); HBox.setHgrow(subEmitterBtn, Priority.ALWAYS);
HBox.setHgrow(subSoundBtn, Priority.ALWAYS);
panel.getChildren().add(subRow); panel.getChildren().add(subRow);
VBox contentBox = new VBox(0); VBox contentBox = new VBox(0);
@@ -63,13 +74,21 @@ public class AtmosphereController {
ctx.input().activeLayer = SharedInput.LAYER_EMITTERS; ctx.input().activeLayer = SharedInput.LAYER_EMITTERS;
contentBox.getChildren().setAll(buildEmitterPanel()); contentBox.getChildren().setAll(buildEmitterPanel());
}; };
Runnable activateSound = () -> {
ctx.input().activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
contentBox.getChildren().setAll(buildSoundPanel());
};
subLichtBtn.setOnAction(e -> activateLicht.run()); subLichtBtn.setOnAction(e -> activateLicht.run());
subEmitterBtn.setOnAction(e -> activateEmitter.run()); subEmitterBtn.setOnAction(e -> activateEmitter.run());
subSoundBtn.setOnAction(e -> activateSound.run());
if (ctx.input().activeLayer == SharedInput.LAYER_EMITTERS) { if (ctx.input().activeLayer == SharedInput.LAYER_EMITTERS) {
subEmitterBtn.setSelected(true); subEmitterBtn.setSelected(true);
activateEmitter.run(); activateEmitter.run();
} else if (ctx.input().activeLayer == SharedInput.LAYER_PLACED_SOUNDS) {
subSoundBtn.setSelected(true);
activateSound.run();
} else { } else {
subLichtBtn.setSelected(true); subLichtBtn.setSelected(true);
activateLicht.run(); activateLicht.run();
@@ -282,6 +301,7 @@ public class AtmosphereController {
vals[17], vals[18], vals[17], vals[18],
mxPart[0], emit[0]); mxPart[0], emit[0]);
ctx.input().pendingEmitter.set(pe); ctx.input().pendingEmitter.set(pe);
ctx.scheduleAutoSave().run();
}; };
emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius")); emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius"));
@@ -296,14 +316,50 @@ public class AtmosphereController {
} }
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße")); emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße"));
final float initStartSize = vals[8], initEndSize = vals[9];
final float initVelX = vals[10], initVelY = vals[11], initVelZ = vals[12];
Slider groesseSl = new Slider(0.05, 10.0, 1.0);
groesseSl.setBlockIncrement(0.05);
Label groesseValLbl = new Label("1.00");
groesseValLbl.setStyle("-fx-font-size: 10;");
groesseValLbl.setMinWidth(38);
groesseSl.valueProperty().addListener((o, ov, nv) -> {
float f = nv.floatValue();
float sqrtF = (float) Math.sqrt(f);
groesseValLbl.setText(String.format("%.2f", f));
vals[8] = Math.max(0.01f, initStartSize * sqrtF);
vals[9] = Math.max(0.01f, initEndSize * sqrtF);
vals[10] = initVelX * f;
vals[11] = initVelY * f;
vals[12] = initVelZ * f;
publish.run();
});
Label groesseNameLbl = new Label("Groesse");
groesseNameLbl.setStyle("-fx-font-size: 10;");
groesseNameLbl.setMinWidth(120);
HBox groesseRow = new HBox(4, groesseNameLbl, groesseSl, groesseValLbl);
HBox.setHgrow(groesseSl, Priority.ALWAYS);
emitterDynamicContent.getChildren().add(groesseRow);
double[] sizeFactors = {0.1, 0.3, 1.0, 3.0, 8.0};
String[] sizeLabels = {"XS","S","M","L","XL"};
HBox sizeRow = new HBox(4);
for (int pi = 0; pi < sizeLabels.length; pi++) {
Button sb = new Button(sizeLabels[pi]);
sb.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(sb, Priority.ALWAYS);
final double factor = sizeFactors[pi];
sb.setOnAction(ev -> groesseSl.setValue(factor));
sizeRow.getChildren().add(sb);
}
emitterDynamicContent.getChildren().add(sizeRow);
emitterDynamicContent.getChildren().add(buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish)); emitterDynamicContent.getChildren().add(buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish));
emitterDynamicContent.getChildren().add(buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish)); emitterDynamicContent.getChildren().add(buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit")); emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit"));
emitterDynamicContent.getChildren().add(buildSlider("Vel X", vals, 10, -10, 10, 0.1, publish)); emitterDynamicContent.getChildren().add(buildSlider("Tempo X", vals, 10, -5, 5, 0.05, publish));
emitterDynamicContent.getChildren().add(buildSlider("Vel Y", vals, 11, 0, 15, 0.1, publish)); emitterDynamicContent.getChildren().add(buildSlider("Tempo Y", vals, 11, -5, 5, 0.05, publish));
emitterDynamicContent.getChildren().add(buildSlider("Vel Z", vals, 12, -10, 10, 0.1, publish)); emitterDynamicContent.getChildren().add(buildSlider("Tempo Z", vals, 12, -5, 5, 0.05, publish));
emitterDynamicContent.getChildren().add(buildSlider("Variation", vals, 13, 0, 1, 0.05, publish)); emitterDynamicContent.getChildren().add(buildSlider("Streuung (0=eng)", vals, 13, 0, 1, 0.05, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation")); emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation"));
emitterDynamicContent.getChildren().add(buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish)); emitterDynamicContent.getChildren().add(buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish));
@@ -376,4 +432,128 @@ public class AtmosphereController {
int dot = name.lastIndexOf('.'); int dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name; return dot > 0 ? name.substring(0, dot) : name;
} }
// ── Sound-Panel ───────────────────────────────────────────────────────────
private VBox buildSoundPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
UiHelper.sectionTitle("Sound-Quellen"),
new Separator(),
UiHelper.sectionTitle("Gewählte Quelle"),
new Separator());
soundDynamicContent = new VBox(6);
Label noSel = new Label("Keine Quelle ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
soundDynamicContent.getChildren().add(noSel);
inner.getChildren().add(soundDynamicContent);
inner.getChildren().addAll(
new Separator(),
UiHelper.styledHint("Linksklick → platzieren / auswählen"),
UiHelper.styledHint("Rechtsklick → Auswahl aufheben"),
UiHelper.styledHint("Entf → Quelle löschen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
public void updateSoundPanel(String info) {
if (soundDynamicContent == null) return;
soundDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Keine Quelle ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
soundDynamicContent.getChildren().add(noSel);
return;
}
String[] parts = info.split("\\|", -1);
// format: idx|x|y|z|soundPath|volume|radius
if (parts.length < 7) return;
try {
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
float z = Float.parseFloat(parts[3]);
String path0 = parts[4];
float vol0 = Float.parseFloat(parts[5]);
float rad0 = Float.parseFloat(parts[6]);
Label posLabel = new Label(String.format("X: %.1f Y: %.1f Z: %.1f", x, y, z));
posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
soundDynamicContent.getChildren().addAll(posLabel, new Separator());
final String[] curPath = {path0};
final float[] curVol = {vol0};
final float[] curRad = {rad0};
Runnable publish = () -> {
ctx.input().pendingSoundProp.set(
new SharedInput.SoundPropertyChange(curPath[0], curVol[0], curRad[0]));
ctx.scheduleAutoSave().run();
};
// Sound-Datei wählen
Label soundLbl = new Label("Sound:");
soundLbl.setStyle("-fx-font-size: 11;");
Button soundBtn = new Button(shortFileName(curPath[0]));
soundBtn.setMaxWidth(Double.MAX_VALUE);
soundBtn.setOnAction(e -> Platform.runLater(() -> {
SoundChooser chooser = new SoundChooser(ctx.assetRoot(), SoundChooser.Mode.ALL);
chooser.showAndWait().ifPresent(p -> {
curPath[0] = p;
soundBtn.setText(shortFileName(p));
publish.run();
});
}));
soundDynamicContent.getChildren().addAll(soundLbl, soundBtn, new Separator());
// Lautstärke
Label volLbl = new Label("Lautstärke");
volLbl.setStyle("-fx-font-size: 11;");
Slider volSl = new Slider(0.0, 1.0, curVol[0]);
volSl.setBlockIncrement(0.05);
volSl.valueProperty().addListener((o, ov, nv) -> {
curVol[0] = nv.floatValue();
publish.run();
});
soundDynamicContent.getChildren().addAll(volLbl, UiHelper.withField(volSl, "%.2f"));
// Radius
Label radLbl = new Label("Radius");
radLbl.setStyle("-fx-font-size: 11;");
Slider radSl = new Slider(0.5, 200.0, curRad[0]);
radSl.setBlockIncrement(1.0);
radSl.valueProperty().addListener((o, ov, nv) -> {
curRad[0] = nv.floatValue();
publish.run();
});
soundDynamicContent.getChildren().addAll(radLbl, UiHelper.withField(radSl, "%.1f"));
Button delBtn = new Button("Loeschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> ctx.input().deleteSoundRequested = true);
soundDynamicContent.getChildren().addAll(new Separator(), delBtn);
} catch (NumberFormatException ignored) {}
}
private String shortFileName(String path) {
if (path == null || path.isEmpty()) return "(kein Sound)";
String name = java.nio.file.Paths.get(path).getFileName().toString();
int dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name;
}
} }

View File

@@ -58,6 +58,8 @@ public class ModelEditorController implements ToolController {
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>(); private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
private VBox modelEditorLightBox = null; private VBox modelEditorLightBox = null;
private VBox modelEditorEmitterBox = null; private VBox modelEditorEmitterBox = null;
private VBox modelEditorSoundBox = null;
private final java.util.List<de.blight.common.ModelMeta.AttachedSound> modelEditorSounds = new java.util.ArrayList<>();
private ComboBox<de.blight.common.ModelCategory> modelEditorCategoryCB = null; private ComboBox<de.blight.common.ModelCategory> modelEditorCategoryCB = null;
private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null; private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null;
private Spinner<Double> modelEditorInteractableXSpin = null; private Spinner<Double> modelEditorInteractableXSpin = null;
@@ -555,8 +557,8 @@ public class ModelEditorController implements ToolController {
HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn); HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn);
HBox.setHgrow(modelEditorLod2GenBtn, Priority.ALWAYS); HBox.setHgrow(modelEditorLod2GenBtn, Priority.ALWAYS);
// Anhänge: Lichter & Partikel // Anhänge: Lichter, Partikel & Sounds
Label attachTitle = new Label("Anhänge: Lichter & Partikel"); Label attachTitle = new Label("Anhänge: Lichter, Partikel & Sound");
attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
Label lightSectionLbl = new Label("Lichtquellen:"); Label lightSectionLbl = new Label("Lichtquellen:");
@@ -591,12 +593,29 @@ public class ModelEditorController implements ToolController {
addEmitterBtn.setMaxWidth(Double.MAX_VALUE); addEmitterBtn.setMaxWidth(Double.MAX_VALUE);
addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;"); addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;");
addEmitterBtn.setOnAction(ev -> { addEmitterBtn.setOnAction(ev -> {
modelEditorEmitters.add(new de.blight.common.ModelMeta.AttachedEmitter( modelEditorEmitters.add(de.blight.common.ModelMeta.AttachedEmitter.fire(0f, 0.5f, 0f));
0f, 0.5f, 0f, 0));
rebuildEmitterBox(); rebuildEmitterBox();
pushAttachmentsToJme(); pushAttachmentsToJme();
}); });
Label soundSectionLbl = new Label("Sound-Quellen:");
soundSectionLbl.setStyle("-fx-text-fill:#aaa;");
modelEditorSounds.clear();
modelEditorSounds.addAll(meta.attachedSounds());
modelEditorSoundBox = new VBox(4);
rebuildSoundBox();
Button addSoundBtn = new Button("+ Sound-Quelle");
addSoundBtn.setMaxWidth(Double.MAX_VALUE);
addSoundBtn.setStyle("-fx-background-color:#2e3a4a;");
addSoundBtn.setOnAction(ev -> {
modelEditorSounds.add(new de.blight.common.ModelMeta.AttachedSound(0f, 0.5f, 0f, "", 1.0f, 20.0f));
rebuildSoundBox();
pushAttachmentsToJme();
});
pushAttachmentsToJme(); pushAttachmentsToJme();
// Modell-Kategorie // Modell-Kategorie
@@ -811,6 +830,7 @@ public class ModelEditorController implements ToolController {
modelEditorLod1Path, modelEditorLod2Path, modelEditorLod1Path, modelEditorLod2Path,
new java.util.ArrayList<>(modelEditorLights), new java.util.ArrayList<>(modelEditorLights),
new java.util.ArrayList<>(modelEditorEmitters), new java.util.ArrayList<>(modelEditorEmitters),
new java.util.ArrayList<>(modelEditorSounds),
modelEditorInteractableCB != null modelEditorInteractableCB != null
? modelEditorInteractableCB.getValue() ? modelEditorInteractableCB.getValue()
: de.blight.common.model.InteractableType.NONE, : de.blight.common.model.InteractableType.NONE,
@@ -861,6 +881,7 @@ public class ModelEditorController implements ToolController {
attachTitle, attachTitle,
lightSectionLbl, modelEditorLightBox, addLightBtn, lightSectionLbl, modelEditorLightBox, addLightBtn,
emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn, emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn,
soundSectionLbl, modelEditorSoundBox, addSoundBtn,
new Separator(), new Separator(),
catTitle, modelEditorCategoryCB, catTitle, modelEditorCategoryCB,
new Separator(), new Separator(),
@@ -1342,6 +1363,7 @@ public class ModelEditorController implements ToolController {
String lod1Path, String lod2Path, String lod1Path, String lod2Path,
java.util.List<de.blight.common.ModelMeta.AttachedLight> lights, java.util.List<de.blight.common.ModelMeta.AttachedLight> lights,
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters, java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
java.util.List<de.blight.common.ModelMeta.AttachedSound> sounds,
de.blight.common.model.InteractableType interactableType, de.blight.common.model.InteractableType interactableType,
float interactableOffsetX, float interactableOffsetY, float interactableOffsetX, float interactableOffsetY,
float interactableOffsetZ, float interactableRotY, float interactableOffsetZ, float interactableRotY,
@@ -1351,7 +1373,7 @@ public class ModelEditorController implements ToolController {
name, category, tags, 1f, 1f, 1f, uniform, name, category, tags, 1f, 1f, 1f, uniform,
pivotY, placeY, solid, cast, receive, rndMin, rndMax, pivotY, placeY, solid, cast, receive, rndMin, rndMax,
lod1Path, lod2Path, 30f, 80f, 120f, lod1Path, lod2Path, 30f, 80f, 120f,
lights, emitters, lights, emitters, sounds,
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE, interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY, interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
footstepSurface != null ? footstepSurface : "", footstepSurface != null ? footstepSurface : "",
@@ -1460,6 +1482,7 @@ public class ModelEditorController implements ToolController {
private void pushAttachmentsToJme() { private void pushAttachmentsToJme() {
ctx.input().modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights); ctx.input().modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights);
ctx.input().modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters); ctx.input().modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters);
ctx.input().modelEditorAttachedSounds = new java.util.ArrayList<>(modelEditorSounds);
ctx.input().modelEditorAttachmentsChanged = true; ctx.input().modelEditorAttachmentsChanged = true;
} }
@@ -1553,34 +1576,134 @@ public class ModelEditorController implements ToolController {
tp.setExpanded(false); tp.setExpanded(false);
tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;");
GridPane g = new GridPane(); // Offset-Spinner
g.setHgap(6); g.setVgap(3);
g.setPadding(new Insets(4));
Spinner<Double> ox = buildOffsetSpinner(ae.offsetX()); Spinner<Double> ox = buildOffsetSpinner(ae.offsetX());
Spinner<Double> oy = buildOffsetSpinner(ae.offsetY()); Spinner<Double> oy = buildOffsetSpinner(ae.offsetY());
Spinner<Double> oz = buildOffsetSpinner(ae.offsetZ()); Spinner<Double> oz = buildOffsetSpinner(ae.offsetZ());
addLabeledRow(g, 0, "Offset X:", ox); GridPane offsetGrid = new GridPane();
addLabeledRow(g, 1, "Offset Y:", oy); offsetGrid.setHgap(6); offsetGrid.setVgap(3);
addLabeledRow(g, 2, "Offset Z:", oz); offsetGrid.setPadding(new Insets(4, 4, 0, 4));
addLabeledRow(offsetGrid, 0, "Offset X:", ox);
addLabeledRow(offsetGrid, 1, "Offset Y:", oy);
addLabeledRow(offsetGrid, 2, "Offset Z:", oz);
ComboBox<String> presetCB = new ComboBox<>(); // Mutable state for all parameters
presetCB.getItems().addAll("Feuer", "Rauch", "Funken"); float[] vals = {
presetCB.getSelectionModel().select(Math.max(0, Math.min(2, ae.preset()))); ae.startR(), ae.startG(), ae.startB(), ae.startA(), // 0-3
presetCB.setMaxWidth(Double.MAX_VALUE); ae.endR(), ae.endG(), ae.endB(), ae.endA(), // 4-7
g.add(new Label("Typ:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); ae.startSize(), ae.endSize(), // 8-9
g.add(presetCB, 1, 3); ae.velX(), ae.velY(), ae.velZ(), ae.velocityVariation(), // 10-13
ae.gravX(), ae.gravY(), ae.gravZ(), // 14-16
ae.lowLife(), ae.highLife(), ae.emitRate() // 17-19
};
int[] mxPart = {ae.maxParticles()};
Runnable sync = () -> { Runnable sync = () -> {
modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter( modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter(
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(),
presetCB.getSelectionModel().getSelectedIndex())); ae.texturePath(), ae.imagesX(), ae.imagesY(),
vals[0], vals[1], vals[2], vals[3],
vals[4], vals[5], vals[6], vals[7],
vals[8], vals[9],
vals[10], vals[11], vals[12], vals[13],
vals[14], vals[15], vals[16],
vals[17], vals[18],
mxPart[0], vals[19]));
pushAttachmentsToJme(); pushAttachmentsToJme();
}; };
ox.valueProperty().addListener((o,ov,nv) -> sync.run()); ox.valueProperty().addListener((o, ov, nv) -> sync.run());
oy.valueProperty().addListener((o,ov,nv) -> sync.run()); oy.valueProperty().addListener((o, ov, nv) -> sync.run());
oz.valueProperty().addListener((o,ov,nv) -> sync.run()); oz.valueProperty().addListener((o, ov, nv) -> sync.run());
presetCB.valueProperty().addListener((o,ov,nv) -> sync.run());
// Preset-Buttons
Button btnFire = new Button("Feuer");
Button btnSmoke = new Button("Rauch");
Button btnSparks = new Button("Funken");
for (Button b : new Button[]{btnFire, btnSmoke, btnSparks}) {
b.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(b, Priority.ALWAYS);
}
btnFire.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.fire(
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue())));
btnSmoke.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.smoke(
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue())));
btnSparks.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.sparks(
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue())));
VBox content = new VBox(4);
content.setPadding(new Insets(2));
content.getChildren().add(offsetGrid);
content.getChildren().add(new HBox(4, btnFire, btnSmoke, btnSparks));
content.getChildren().addAll(emSep(), emLabel("Startfarbe"));
content.getChildren().add(emSlider("Start R", vals, 0, 0, 1, 0.01, sync));
content.getChildren().add(emSlider("Start G", vals, 1, 0, 1, 0.01, sync));
content.getChildren().add(emSlider("Start B", vals, 2, 0, 1, 0.01, sync));
content.getChildren().add(emSlider("Start A", vals, 3, 0, 1, 0.01, sync));
content.getChildren().addAll(emSep(), emLabel("Endfarbe"));
content.getChildren().add(emSlider("Ende R", vals, 4, 0, 1, 0.01, sync));
content.getChildren().add(emSlider("Ende G", vals, 5, 0, 1, 0.01, sync));
content.getChildren().add(emSlider("Ende B", vals, 6, 0, 1, 0.01, sync));
content.getChildren().add(emSlider("Ende A", vals, 7, 0, 1, 0.01, sync));
content.getChildren().addAll(emSep(), emLabel("Partikelgroesse"));
final float initStartSizeE = vals[8], initEndSizeE = vals[9];
final float initVelXE = vals[10], initVelYE = vals[11], initVelZE = vals[12];
Slider groesseSlE = new Slider(0.05, 10.0, 1.0);
groesseSlE.setBlockIncrement(0.05);
Label groesseValE = new Label("1.00");
groesseValE.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;");
groesseValE.setMinWidth(38);
groesseSlE.valueProperty().addListener((o, ov, nv) -> {
float f = nv.floatValue();
float sqrtF = (float) Math.sqrt(f);
groesseValE.setText(String.format("%.2f", f));
vals[8] = Math.max(0.01f, initStartSizeE * sqrtF);
vals[9] = Math.max(0.01f, initEndSizeE * sqrtF);
vals[10] = initVelXE * f;
vals[11] = initVelYE * f;
vals[12] = initVelZE * f;
sync.run();
});
Label groesseNameE = new Label("Groesse");
groesseNameE.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;");
groesseNameE.setMinWidth(115);
HBox groesseRowE = new HBox(4, groesseNameE, groesseSlE, groesseValE);
HBox.setHgrow(groesseSlE, Priority.ALWAYS);
groesseRowE.setPadding(new Insets(0, 4, 0, 4));
content.getChildren().add(groesseRowE);
double[] sizeFactors = {0.1, 0.3, 1.0, 3.0, 8.0};
String[] sizeLabels = {"XS","S","M","L","XL"};
HBox sizeRow = new HBox(4);
sizeRow.setPadding(new Insets(0, 4, 0, 4));
for (int pi = 0; pi < sizeLabels.length; pi++) {
Button sb = new Button(sizeLabels[pi]);
sb.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(sb, Priority.ALWAYS);
final double factor = sizeFactors[pi];
sb.setOnAction(ev -> groesseSlE.setValue(factor));
sizeRow.getChildren().add(sb);
}
content.getChildren().add(sizeRow);
content.getChildren().add(emSlider("Start-Groesse", vals, 8, 0.01, 5, 0.05, sync));
content.getChildren().add(emSlider("Ende-Groesse", vals, 9, 0.01, 5, 0.05, sync));
content.getChildren().addAll(emSep(), emLabel("Geschwindigkeit"));
content.getChildren().add(emSlider("Tempo X", vals, 10, -5, 5, 0.05, sync));
content.getChildren().add(emSlider("Tempo Y", vals, 11, -5, 5, 0.05, sync));
content.getChildren().add(emSlider("Tempo Z", vals, 12, -5, 5, 0.05, sync));
content.getChildren().add(emSlider("Streuung (0=eng)", vals, 13, 0, 1, 0.05, sync));
content.getChildren().addAll(emSep(), emLabel("Gravitation"));
content.getChildren().add(emSlider("Grav Y", vals, 15, -20, 20, 0.5, sync));
content.getChildren().addAll(emSep(), emLabel("Lebensdauer (s)"));
content.getChildren().add(emSlider("Min", vals, 17, 0.1, 10, 0.1, sync));
content.getChildren().add(emSlider("Max", vals, 18, 0.1, 10, 0.1, sync));
content.getChildren().addAll(emSep(), emLabel("Emission"));
content.getChildren().add(emSlider("Max Partikel", mxPart, 0, 1, 500, 5, sync));
content.getChildren().add(emSlider("Rate/s", vals, 19, 0.5, 100, 0.5, sync));
Button del = new Button("✕ Entfernen"); Button del = new Button("✕ Entfernen");
del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;");
@@ -1590,13 +1713,68 @@ public class ModelEditorController implements ToolController {
rebuildEmitterBox(); rebuildEmitterBox();
pushAttachmentsToJme(); pushAttachmentsToJme();
}); });
content.getChildren().addAll(emSep(), del);
VBox content = new VBox(4, g, del);
content.setPadding(new Insets(2));
tp.setContent(content); tp.setContent(content);
return tp; return tp;
} }
private void applyEmitterPreset(int idx, de.blight.common.ModelMeta.AttachedEmitter preset) {
modelEditorEmitters.set(idx, preset);
rebuildEmitterBox();
pushAttachmentsToJme();
}
private static Separator emSep() { return new Separator(); }
private static Label emLabel(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-size:11; -fx-font-weight:bold; -fx-text-fill:#ccc;");
return l;
}
private static HBox emSlider(String name, float[] arr, int idx,
double min, double max, double step, Runnable sync) {
Label lbl = new Label(name);
lbl.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;");
lbl.setMinWidth(115);
Slider sl = new Slider(min, max, Math.max(min, Math.min(max, arr[idx])));
sl.setBlockIncrement(step);
Label val = new Label(String.format("%.2f", arr[idx]));
val.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;");
val.setMinWidth(38);
sl.valueProperty().addListener((o, ov, nv) -> {
arr[idx] = nv.floatValue();
val.setText(String.format("%.2f", arr[idx]));
sync.run();
});
HBox row = new HBox(4, lbl, sl, val);
HBox.setHgrow(sl, Priority.ALWAYS);
row.setPadding(new Insets(0, 4, 0, 4));
return row;
}
private static HBox emSlider(String name, int[] arr, int idx,
double min, double max, double step, Runnable sync) {
Label lbl = new Label(name);
lbl.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;");
lbl.setMinWidth(115);
Slider sl = new Slider(min, max, Math.max(min, Math.min(max, arr[idx])));
sl.setBlockIncrement(step);
Label val = new Label(String.format("%.0f", (double) arr[idx]));
val.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;");
val.setMinWidth(38);
sl.valueProperty().addListener((o, ov, nv) -> {
arr[idx] = nv.intValue();
val.setText(String.format("%.0f", (double) arr[idx]));
sync.run();
});
HBox row = new HBox(4, lbl, sl, val);
HBox.setHgrow(sl, Priority.ALWAYS);
row.setPadding(new Insets(0, 4, 0, 4));
return row;
}
private void importLodFile(int slot) { private void importLodFile(int slot) {
FileChooser fc = new FileChooser(); FileChooser fc = new FileChooser();
fc.setTitle("LOD " + slot + " – j3o wählen"); fc.setTitle("LOD " + slot + " – j3o wählen");
@@ -1674,4 +1852,86 @@ public class ModelEditorController implements ToolController {
new SharedInput.ModelLodGenRequest(level, method, null, reductionPct)); new SharedInput.ModelLodGenRequest(level, method, null, reductionPct));
}); });
} }
// ── Sound-Anhänge ─────────────────────────────────────────────────────────
private void rebuildSoundBox() {
if (modelEditorSoundBox == null) return;
modelEditorSoundBox.getChildren().clear();
for (int i = 0; i < modelEditorSounds.size(); i++) {
final int idx = i;
modelEditorSoundBox.getChildren().add(buildSoundEntry(idx, modelEditorSounds.get(i)));
}
}
private javafx.scene.Node buildSoundEntry(int idx,
de.blight.common.ModelMeta.AttachedSound as) {
TitledPane tp = new TitledPane();
tp.setText("Sound " + (idx + 1));
tp.setExpanded(false);
tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;");
GridPane g = new GridPane();
g.setHgap(6); g.setVgap(3);
g.setPadding(new Insets(4));
Spinner<Double> ox = buildOffsetSpinner(as.offsetX());
Spinner<Double> oy = buildOffsetSpinner(as.offsetY());
Spinner<Double> oz = buildOffsetSpinner(as.offsetZ());
addLabeledRow(g, 0, "Offset X:", ox);
addLabeledRow(g, 1, "Offset Y:", oy);
addLabeledRow(g, 2, "Offset Z:", oz);
final String[] curPath = {as.soundPath()};
Button soundBtn = new Button(curPath[0].isEmpty() ? "(kein Sound)" : java.nio.file.Paths.get(curPath[0]).getFileName().toString());
soundBtn.setMaxWidth(Double.MAX_VALUE);
g.add(new Label("Sound:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3);
g.add(soundBtn, 1, 3);
Spinner<Double> volSpin = new Spinner<>(0.0, 1.0, as.volume(), 0.05);
volSpin.setEditable(true);
addLabeledRow(g, 4, "Lautstärke:", volSpin);
Spinner<Double> radSpin = new Spinner<>(0.5, 200.0, as.radius(), 1.0);
radSpin.setEditable(true);
addLabeledRow(g, 5, "Radius:", radSpin);
Runnable sync = () -> {
modelEditorSounds.set(idx, new de.blight.common.ModelMeta.AttachedSound(
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(),
curPath[0],
volSpin.getValue().floatValue(), radSpin.getValue().floatValue()));
pushAttachmentsToJme();
};
soundBtn.setOnAction(ev -> {
de.blight.editor.ui.SoundChooser chooser = new de.blight.editor.ui.SoundChooser(
ctx.assetRoot(), de.blight.editor.ui.SoundChooser.Mode.ALL);
chooser.showAndWait().ifPresent(p -> {
curPath[0] = p;
soundBtn.setText(java.nio.file.Paths.get(p).getFileName().toString());
sync.run();
});
});
ox.valueProperty().addListener((o, ov, nv) -> sync.run());
oy.valueProperty().addListener((o, ov, nv) -> sync.run());
oz.valueProperty().addListener((o, ov, nv) -> sync.run());
volSpin.valueProperty().addListener((o, ov, nv) -> sync.run());
radSpin.valueProperty().addListener((o, ov, nv) -> sync.run());
Button del = new Button("✕ Entfernen");
del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;");
del.setMaxWidth(Double.MAX_VALUE);
del.setOnAction(ev -> {
modelEditorSounds.remove(idx);
rebuildSoundBox();
pushAttachmentsToJme();
});
VBox content = new VBox(4, g, del);
content.setPadding(new Insets(2));
tp.setContent(content);
return tp;
}
} }

View File

@@ -446,6 +446,8 @@ public class ViewportController {
input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0)); input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_EMITTERS -> case SharedInput.LAYER_EMITTERS ->
input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0)); input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_PLACED_SOUNDS ->
input.soundClickQueue.offer(new SharedInput.SoundClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_WATER -> case SharedInput.LAYER_WATER ->
input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0)); input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_SOUND_AREAS -> case SharedInput.LAYER_SOUND_AREAS ->

View 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;
}
}

View File

@@ -105,6 +105,8 @@ public class WorldScene extends BaseAppState {
private de.blight.game.state.AmbientSoundSystem ambientSounds; private de.blight.game.state.AmbientSoundSystem ambientSounds;
private de.blight.game.state.MusicSystem musicSystem; private de.blight.game.state.MusicSystem musicSystem;
private de.blight.game.state.EmitterSystem emitterSystem; 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.audio.FootstepSystem footstepSystem;
private de.blight.game.state.DrownState drownState; private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState; private WaterInteractionState waterInteractionState;
@@ -277,9 +279,12 @@ public class WorldScene extends BaseAppState {
BlightGame.status("Lade Welt-Objekte..."); BlightGame.status("Lade Welt-Objekte...");
app.getStateManager().attach(new RiverState()); 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(); emitterSystem = new de.blight.game.state.EmitterSystem();
app.getStateManager().attach(emitterSystem); app.getStateManager().attach(emitterSystem);
placedSoundSystem = new de.blight.game.state.PlacedSoundSystem();
app.getStateManager().attach(placedSoundSystem);
WorldObjectsState worldObjects = new WorldObjectsState(); WorldObjectsState worldObjects = new WorldObjectsState();
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range); worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
app.getStateManager().attach(worldObjects); app.getStateManager().attach(worldObjects);
@@ -470,7 +475,9 @@ public class WorldScene extends BaseAppState {
if (riverSound != null) riverSound.setPlayerPosition(pos); if (riverSound != null) riverSound.setPlayerPosition(pos);
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos); if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
if (musicSystem != null) musicSystem.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) if (musicSystem != null && dayNight != null)

View File

@@ -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);
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.PlacedWaterfall; import de.blight.common.PlacedWaterfall;
import de.blight.common.WaterfallIO; import de.blight.common.WaterfallIO;
import de.blight.game.audio.StereoSource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 Logger log = LoggerFactory.getLogger(WaterfallState.class);
private static final int ROWS = 24; // Tessellierungs-Zeilen vertikal private static final int ROWS = 24;
private static final int COLS = 6; // Tessellierungs-Spalten horizontal private static final int COLS = 6;
private static final float BULGE_MAX = 1.2f; // Maximale horizontale Vorwölbung am oberen Rand (m) private static final float BULGE_MAX = 1.2f;
private static final float V_RANGE = 0.30f; // Anteil der Wasserfallhöhe für den Bogen (0..1) private static final float V_RANGE = 0.30f;
private static final float SPLASH_HEIGHT = 0.9f; // Höhe des vertikalen Schaum-Vorhangs (m) 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 Node rootNode;
private final List<Geometry> geos = new ArrayList<>(); 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<Material> foamMaterials = new ArrayList<>();
private final List<ParticleEmitter> mists = new ArrayList<>(); private final List<ParticleEmitter> mists = new ArrayList<>();
private float time = 0f; 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 @Override
protected void initialize(Application app) { protected void initialize(Application application) {
rootNode = ((SimpleApplication) app).getRootNode(); rootNode = ((SimpleApplication) application).getRootNode();
AssetManager assets = app.getAssetManager(); AssetManager assets = application.getAssetManager();
audioSettings = getStateManager().getState(AudioSettingsState.class);
List<PlacedWaterfall> list; List<PlacedWaterfall> list;
try { try {
@@ -70,6 +82,7 @@ public class WaterfallState extends BaseAppState {
for (PlacedWaterfall wf : list) { for (PlacedWaterfall wf : list) {
try { try {
buildWaterfall(wf, assets); buildWaterfall(wf, assets);
buildAudioSource(wf, assets);
} catch (Exception e) { } catch (Exception e) {
log.error("Fehler beim Aufbauen eines Wasserfalls", 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()); 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 @Override
public void update(float tpf) { public void update(float tpf) {
if (!materials.isEmpty()) { if (!materials.isEmpty()) {
@@ -89,10 +124,36 @@ public class WaterfallState extends BaseAppState {
foamMaterials.get(i).setColor("Color", new ColorRGBA(1f, 1f, 1f, alpha)); 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 @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 : geos) g.removeFromParent();
for (Geometry g : foams) g.removeFromParent(); for (Geometry g : foams) g.removeFromParent();
for (ParticleEmitter e : mists) e.removeFromParent(); for (ParticleEmitter e : mists) e.removeFromParent();

View File

@@ -1,2 +1,2 @@
# x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate # x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate
0.93604 2.50000 -3.56553 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000 1.46137 2.00000 -1.08840 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.2739 0.6573 0.0000 0.7500 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000

View File

@@ -1,3 +1,2 @@
# x y z r g b intensity radius # x y z r g b intensity radius
-2.18687 3.00000 -5.43333 1.00000 1.00000 1.00000 1.00000 20.00000 1.46137 2.70000 -1.08840 1.00000 0.60000 0.40000 2.00000 8.00000
0.42349 3.00000 -10.67357 1.00000 1.00000 1.00000 1.00000 20.00000

View File

@@ -11,7 +11,6 @@ Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828
Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec NONE Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec NONE
Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE
Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE
Models/imported/wolf.j3o -1.25617 2.00000 -1.86424 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE
Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6 NONE Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6 NONE
Models/trees/pine/medium/pine_medium_20260706_190939.j3o 157.82524 2.00000 -793.89911 -2.44322 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE Models/trees/pine/medium/pine_medium_20260706_190939.j3o 157.82524 2.00000 -793.89911 -2.44322 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE
Models/trees/pine/medium/pine_medium_20260706_190939.j3o 170.43440 2.00000 -784.16235 2.81244 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE Models/trees/pine/medium/pine_medium_20260706_190939.j3o 170.43440 2.00000 -784.16235 2.81244 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE
@@ -22,3 +21,4 @@ Models/trees/pine/medium/pine_medium_20260706_190939.j3o 155.19229 2.00000 -802.
Models/northcoast/alte_fischerhytte.j3o 317.92999 2.01000 -863.58002 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/northcoast/alte_fischerhytte.j3o 317.92999 2.01000 -863.58002 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING
Models/northcoast/alte_fischerhytte2.j3o 312.98001 2.00000 -851.37000 -3.50811 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/northcoast/alte_fischerhytte2.j3o 312.98001 2.00000 -851.37000 -3.50811 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING
Models/northcoast/alte_fischerhytte.j3o 322.73001 2.00000 -856.09003 -1.83155 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/northcoast/alte_fischerhytte.j3o 322.73001 2.00000 -856.09003 -1.83155 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING
Models/imported/feuerstelle1.j3o 1.46137 2.00000 -1.08840 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 NONE

View File

@@ -0,0 +1,2 @@
# x y z soundPath volume radius
1.46137 2.50000 -1.08840 audio/ambient/fire/campfire.ogg 1.0000 20.0000

11
licences/Read.txt Normal file
View File

@@ -0,0 +1,11 @@
"Free Sounds Library"
Free Sound Effects Site.
Licence: License: Attribution 4.0 International (CC BY 4.0). You are allowed to use sound effects free of charge and royalty free in your multimedia projects for commercial or non-commercial purposes.
https://www.freesoundslibrary.com/