Compare commits

...

3 Commits

Author SHA1 Message Date
dd32ec65ca Commit 2026-08-17 19:41:07 +02:00
c09502ad3a Menü Buttons angepasst, Screenshot Funktion optimiert 2026-08-17 12:41:21 +02:00
ee8c235655 Fehler beim ERstellen von Voxel auf Meereshöhe behoben 2026-08-17 11:26:19 +02:00
20 changed files with 1414 additions and 30 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1477,6 +1477,7 @@ public class EditorApp extends Application {
saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S"));
saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); });
MenuItem screenshotItem = new MenuItem("Screenshot");
screenshotItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+F12"));
screenshotItem.setOnAction(e -> takeEditorScreenshot());
MenuItem settingsItem = new MenuItem("Einstellungen…");
settingsItem.setOnAction(e -> openSettings());
@@ -6442,6 +6443,7 @@ public class EditorApp extends Application {
}
private void takeEditorScreenshot() {
playScreenshotClick();
WritableImage image = primaryStage.getScene().snapshot(null);
String timestamp = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
@@ -6456,6 +6458,34 @@ public class EditorApp extends Application {
}
}
private void playScreenshotClick() {
Thread t = new Thread(() -> {
try {
int sr = 44100;
int frames = (int) (sr * 0.08);
byte[] buf = new byte[frames * 2];
java.util.Random rng = new java.util.Random();
for (int i = 0; i < frames; i++) {
double env = Math.exp(-i / (sr * 0.012));
double s = (rng.nextGaussian() * 0.55 + Math.sin(2 * Math.PI * 3200.0 * i / sr) * 0.45) * env * 0.85;
short val = (short) Math.max(-32767, Math.min(32767, s * 32767));
buf[i * 2] = (byte) (val & 0xff);
buf[i * 2 + 1] = (byte) ((val >> 8) & 0xff);
}
javax.sound.sampled.AudioFormat fmt = new javax.sound.sampled.AudioFormat(sr, 16, 1, true, false);
javax.sound.sampled.DataLine.Info info = new javax.sound.sampled.DataLine.Info(javax.sound.sampled.SourceDataLine.class, fmt);
javax.sound.sampled.SourceDataLine line = (javax.sound.sampled.SourceDataLine) javax.sound.sampled.AudioSystem.getLine(info);
line.open(fmt);
line.start();
line.write(buf, 0, buf.length);
line.drain();
line.close();
} catch (Exception ignored) {}
}, "screenshot-click");
t.setDaemon(true);
t.start();
}
private void handleTextureImport(javafx.stage.Window owner) {
FileChooser fc = new FileChooser();
fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)");

View File

@@ -994,9 +994,27 @@ public class VoxelEditorState extends BaseAppState {
if (cy == aCy) {
currentTop = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int)(anchor - cy * (float) VoxelChunk.CELLS)));
} else {
// Chunks außerhalb des Anker-Chunks ohne bestehende Voxel: überspringen.
} else if (cy < aCy) {
// Chunk unterhalb der Terrain-Oberfläche: sofort komplett auffüllen.
// Verhindert flache Schnitt-Kante bei Y=0 / Wasserrand.
float chunkBaseY = cy * (float) VoxelChunk.CELLS;
if (chunkBaseY + VoxelChunk.SIZE >= -10f) {
int yFloor = Math.max(0, (int) Math.ceil(-10f - chunkBaseY));
for (int lly = yFloor; lly < VoxelChunk.SIZE; lly++) {
chunk.setDensity(lx, lly, lz, (byte) 127);
}
}
continue;
} else {
// Chunk oberhalb des Ankers: nur weiter bauen wenn der Chunk darunter
// an seiner Oberkante (ly=SIZE-1) bereits solid ist.
// Das erlaubt Voxeln aus dem Unterwasserbereich (aCy=-1) in cy=0
// hineinzuwachsen, sobald cy=-1 bis world Y=0 aufgefüllt ist.
VoxelChunk below = chunks.get(chunkKey(cx, cy - 1, cz));
if (below == null || below.getDensity(lx, VoxelChunk.SIZE - 1, lz) <= 0) {
continue;
}
currentTop = 0;
}
}
int newTop = Math.min(VoxelChunk.SIZE - 1, currentTop + colStep);
@@ -1010,8 +1028,10 @@ public class VoxelEditorState extends BaseAppState {
if (chunk.getDensity(lx, ly, lz) > 0) { currentTop = ly; break; }
}
if (currentTop < 0) continue; // Nichts zu entfernen
int newTop = Math.max(0, currentTop - colStep);
for (int ly = newTop + 1; ly <= currentTop; ly++) {
// newTop darf negativ werden: dann werden ALLE Voxel inkl. ly=0 entfernt.
// Math.max(0,...) würde den untersten Voxel bei Y=0 permanent einfrieren.
int newTop = currentTop - colStep;
for (int ly = Math.max(0, newTop + 1); ly <= currentTop; ly++) {
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
}
}
@@ -1474,14 +1494,29 @@ public class VoxelEditorState extends BaseAppState {
if (currentTopLY >= 0) {
startLY = currentTopLY;
} else {
// Terrain-Oberfläche als Ankerpunkt
int tCy = VoxelChunk.worldYToCy(th);
if (cy == tCy) {
// Terrain-Chunk: schrittweise von der Oberfläche nach oben wachsen lassen.
startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int)(th - cy * (float) VoxelChunk.CELLS)));
} else {
// Chunk ohne bestehende Voxel und nicht der Terrain-Chunk: überspringen.
} else if (cy < tCy) {
// Chunk unterhalb der Terrain-Oberfläche: sofort komplett auffüllen
// (verhindert flache Schnitt-Kante bei Y=0 / Wasserrand).
float chunkBaseY = cy * (float) VoxelChunk.CELLS;
if (chunkBaseY + VoxelChunk.SIZE < -10f) continue; // unter Fundament-Limit
int yFloor = Math.max(0, (int) Math.ceil(-10f - chunkBaseY));
for (int ly = yFloor; ly < VoxelChunk.SIZE; ly++) {
chunk.setDensity(lx, ly, lz, (byte) 127);
}
continue;
} else {
// Chunk über der Terrain-Oberfläche (cy > tCy): weiter bauen
// wenn der Chunk darunter an seiner Oberkante solid ist.
VoxelChunk below = chunks.get(chunkKey(cx, cy - 1, cz));
if (below == null || below.getDensity(lx, VoxelChunk.SIZE - 1, lz) <= 0) {
continue;
}
startLY = 0;
}
}
int newTop = Math.min(startLY + step, targetLY); // nie über Ziel-Zelle
@@ -1549,10 +1584,21 @@ public class VoxelEditorState extends BaseAppState {
float th = terrainH(wx, wz);
if (!Float.isFinite(th)) continue;
int thCy = VoxelChunk.worldYToCy(th);
if (cy != thCy) continue; // Terrain-Oberfläche in anderem Chunk
currentTopWY = th;
startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int)(th - cy * (float) VoxelChunk.CELLS)));
if (cy == thCy) {
currentTopWY = th;
startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int)(th - cy * (float) VoxelChunk.CELLS)));
} else if (cy > thCy) {
// Chunk über dem Terrain: weiter bauen wenn Chunk darunter an Oberkante solid.
VoxelChunk below = chunks.get(chunkKey(cx, cy - 1, cz));
if (below == null || below.getDensity(lx, VoxelChunk.SIZE - 1, lz) <= 0) continue;
// ly=0 immer solid setzen step könnte 0 sein und die Lücke bei Y=0 offen lassen.
chunk.setDensity(lx, 0, lz, (byte) 127);
startLY = 0;
currentTopWY = VoxelChunk.toWorldY(cy, 0);
} else {
continue; // cy < thCy: underground, kein Bedarf für Smooth
}
}
int step = (int) Math.round((avgH - currentTopWY) * blend);
@@ -1662,10 +1708,21 @@ public class VoxelEditorState extends BaseAppState {
float th = terrainH(wx, wz);
if (!Float.isFinite(th)) continue;
int thCy = VoxelChunk.worldYToCy(th);
if (cy != thCy) continue;
currentTopWY = th;
startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int)(th - cy * (float) VoxelChunk.CELLS)));
if (cy == thCy) {
currentTopWY = th;
startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int)(th - cy * (float) VoxelChunk.CELLS)));
} else if (cy > thCy) {
// Chunk über dem Terrain: weiter bauen wenn Chunk darunter an Oberkante solid.
VoxelChunk below = chunks.get(chunkKey(cx, cy - 1, cz));
if (below == null || below.getDensity(lx, VoxelChunk.SIZE - 1, lz) <= 0) continue;
// ly=0 immer solid setzen step könnte 0 sein und die Lücke bei Y=0 offen lassen.
chunk.setDensity(lx, 0, lz, (byte) 127);
startLY = 0;
currentTopWY = VoxelChunk.toWorldY(cy, 0);
} else {
continue; // cy < thCy: underground, kein Bedarf für Slope
}
}
int step = (int) Math.round((targetH - currentTopWY) * blend);

View File

@@ -0,0 +1,46 @@
plugins {
id 'java'
}
ext {
jmeVersion = '3.9.0-stable'
mainClassName = 'de.blight.fbxtest.FbxTestApp'
}
dependencies {
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}"
implementation 'com.github.stephengold:MonkeyWrench:1.0.0'
runtimeOnly 'ch.qos.logback:logback-classic:1.5.18'
implementation 'org.slf4j:slf4j-api:2.0.17'
}
tasks.register('extractNatives', Copy) {
def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
.resolvedArtifacts
.findAll { it.name.contains('natives') }
.collect { zipTree(it.file) }
from nativeConf
into "${buildDir}/natives"
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
tasks.register('run', JavaExec) {
group = 'application'
description = 'Startet den FBX-Tester'
mainClass = mainClassName
classpath = sourceSets.main.runtimeClasspath
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(26)
}
jvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
'--enable-native-access=ALL-UNNAMED',
"-Djava.library.path=${buildDir}/natives",
]
dependsOn extractNatives
workingDir = rootDir
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %-5level [%logger{20}] %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.jme3" level="WARN"/>
<logger name="com.github.stephengold" level="INFO"/>
</configuration>

View File

@@ -50,6 +50,7 @@ public class BlightGame extends SimpleApplication {
private AudioSettings audioSettings;
private ScreenshotAppState screenshotState;
private Path screenshotDir;
private com.jme3.audio.AudioNode screenshotClickSound;
private WorldScene worldScene;
private ConfigScreen configScreen;
private GraphicsScreen graphicsScreen;
@@ -380,8 +381,12 @@ public class BlightGame extends SimpleApplication {
try {
screenshotDir = BlightHome.resolve("screenshots");
Files.createDirectories(screenshotDir);
screenshotState = new ScreenshotAppState(screenshotDir + File.separator, "screenshot");
screenshotState = new ScreenshotAppState("", "screenshot");
stateManager.attach(screenshotState);
screenshotClickSound = new com.jme3.audio.AudioNode(assetManager, "audio/static/screenshot_click.ogg", com.jme3.audio.AudioData.DataType.Buffer);
screenshotClickSound.setPositional(false);
screenshotClickSound.setLooping(false);
screenshotClickSound.setVolume(1f);
log.info("Screenshots werden gespeichert in: {}", screenshotDir.toAbsolutePath());
} catch (IOException e) {
log.warn("Screenshot-Verzeichnis konnte nicht angelegt werden", e);
@@ -389,8 +394,12 @@ public class BlightGame extends SimpleApplication {
inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
if (isPressed && screenshotState != null) {
log.info("[Screenshot] Speichere in: {}", screenshotDir.toAbsolutePath());
String ts = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
screenshotState.setFileName(screenshotDir + java.io.File.separator + "blight_" + ts + "_");
log.info("[Screenshot] Speichere: blight_{}.png", ts);
screenshotState.takeScreenshot();
if (screenshotClickSound != null) screenshotClickSound.playInstance();
}
}, "Screenshot");

View File

@@ -16,6 +16,8 @@ import com.jme3.texture.image.ColorSpace;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@@ -127,6 +129,8 @@ public class NinePatch {
// Guide-Pixel durch Rahmen-Nachbarfarbe ersetzen
stripGuides(img, w, h, l - 1, w - r, t - 1, h - b);
img = gaussianBlur(img, 0.0f);
Texture2D tex = toJmeTexture(img, w, h);
return new NinePatch(assets, tex, w, h, l, r, t, b);
}
@@ -219,6 +223,26 @@ public class NinePatch {
return 0; // Fallback: transparent
}
/**
* Weicher Gauss-Blur (3×3-Kernel) auf das Bild glättet Übergänge an Patch-Grenzen.
* sigma=0 deaktiviert den Blur.
*/
private static BufferedImage gaussianBlur(BufferedImage img, float sigma) {
if (sigma <= 0f) { return img; }
float s2 = 2f * sigma * sigma;
float[] data = {
(float) Math.exp(-2 / s2), (float) Math.exp(-1 / s2), (float) Math.exp(-2 / s2),
(float) Math.exp(-1 / s2), 1f, (float) Math.exp(-1 / s2),
(float) Math.exp(-2 / s2), (float) Math.exp(-1 / s2), (float) Math.exp(-2 / s2)
};
float sum = 0; for (float v : data) sum += v;
for (int i = 0; i < data.length; i++) data[i] /= sum;
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, data), ConvolveOp.EDGE_NO_OP, null);
BufferedImage out = new BufferedImage(img.getWidth(), img.getHeight(), img.getType());
op.filter(img, out);
return out;
}
/**
* Konvertiert ein BufferedImage in eine JME3-Textur.
* Zeilen werden von unten nach oben gelesen (entspricht JME3-Y-Flip beim PNG-Laden).
@@ -266,7 +290,7 @@ public class NinePatch {
*/
public Node build(float x, float y, float w, float h, float z) {
tex.setWrap(Texture.WrapMode.EdgeClamp);
tex.setMagFilter(Texture.MagFilter.Nearest);
tex.setMagFilter(Texture.MagFilter.Bilinear);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setTexture("ColorMap", tex);
@@ -282,22 +306,75 @@ public class NinePatch {
float sx1 = x + l, sx2 = x + w - r;
float sy1 = y + b, sy2 = y + h - t;
// Kachelgröße des Mittelbereichs in Bildschirmpixeln
float cw = (u2 - u1) * texW;
float ch = (v2 - v1) * texH;
Node n = new Node("ninepatch");
// Reihe unten
n.attachChild(q(mat, x, y, l, b, 0, 0, u1, v1, z));
n.attachChild(q(mat, sx1, y, sx2-sx1, b, u1, 0, u2, v1, z));
n.attachChild(q(mat, sx2, y, r, b, u2, 0, 1, v1, z));
// Reihe mitte
n.attachChild(q(mat, x, sy1, l, sy2-sy1, 0, v1, u1, v2, z));
n.attachChild(q(mat, sx1, sy1, sx2-sx1, sy2-sy1, u1, v1, u2, v2, z));
n.attachChild(q(mat, sx2, sy1, r, sy2-sy1, u2, v1, 1, v2, z));
// Reihe oben
n.attachChild(q(mat, x, sy2, l, t, 0, v2, u1, 1, z));
n.attachChild(q(mat, sx1, sy2, sx2-sx1, t, u1, v2, u2, 1, z));
n.attachChild(q(mat, sx2, sy2, r, t, u2, v2, 1, 1, z));
// Reihe unten Ecken fix, untere Kante Mirror-X
n.attachChild(q(mat, x, y, l, b, 0, 0, u1, v1, z));
tileHEdge(n, mat, sx1, y, sx2, b, u1, u2, 0, v1, cw, texW, z);
n.attachChild(q(mat, sx2, y, r, b, u2, 0, 1, v1, z));
// Reihe mitte Seiten Mirror-Y, Mitte Mirror-X+Y
tileVEdge(n, mat, x, sy1, l, sy2, 0, u1, v1, v2, ch, texH, z);
tileCenterQuads(n, mat, sx1, sy1, sx2, sy2, u1, v1, u2, v2, cw, ch, texW, texH, z);
tileVEdge(n, mat, sx2, sy1, r, sy2, u2, 1f, v1, v2, ch, texH, z);
// Reihe oben Ecken fix, obere Kante Mirror-X
n.attachChild(q(mat, x, sy2, l, t, 0, v2, u1, 1f, z));
tileHEdge(n, mat, sx1, sy2, sx2, t, u1, u2, v2, 1f, cw, texW, z);
n.attachChild(q(mat, sx2, sy2, r, t, u2, v2, 1f, 1f, z));
return n;
}
/** Mitte: Mirror-Repeat in X und Y. */
private static void tileCenterQuads(Node n, Material mat,
float sx1, float sy1, float sx2, float sy2,
float u1, float v1, float u2, float v2,
float cw, float ch,
float texW, float texH, float z) {
int iz = 0;
for (float py = sy1; py < sy2; py += ch, iz++) {
float th = Math.min(ch, sy2 - py);
float vA = (iz & 1) == 0 ? v1 : v2;
float vB = (iz & 1) == 0 ? v1+th/texH : v2-th/texH;
int ix = 0;
for (float px = sx1; px < sx2; px += cw, ix++) {
float tw = Math.min(cw, sx2 - px);
float uA = (ix & 1) == 0 ? u1 : u2;
float uB = (ix & 1) == 0 ? u1+tw/texW : u2-tw/texW;
n.attachChild(q(mat, px, py, tw, th, uA, vA, uB, vB, z));
}
}
}
/** Horizontaler Randstreifen (oben/unten): Mirror-Repeat nur in X. */
private static void tileHEdge(Node n, Material mat,
float sx1, float py, float sx2, float ph,
float u1, float u2, float vMin, float vMax,
float cw, float texW, float z) {
int ix = 0;
for (float px = sx1; px < sx2; px += cw, ix++) {
float tw = Math.min(cw, sx2 - px);
float uA = (ix & 1) == 0 ? u1 : u2;
float uB = (ix & 1) == 0 ? u1+tw/texW : u2-tw/texW;
n.attachChild(q(mat, px, py, tw, ph, uA, vMin, uB, vMax, z));
}
}
/** Vertikaler Randstreifen (links/rechts): Mirror-Repeat nur in Y. */
private static void tileVEdge(Node n, Material mat,
float px, float sy1, float pw, float sy2,
float uMin, float uMax, float v1, float v2,
float ch, float texH, float z) {
int iz = 0;
for (float py = sy1; py < sy2; py += ch, iz++) {
float th = Math.min(ch, sy2 - py);
float vA = (iz & 1) == 0 ? v1 : v2;
float vB = (iz & 1) == 0 ? v1+th/texH : v2-th/texH;
n.attachChild(q(mat, px, py, pw, th, uMin, vA, uMax, vB, z));
}
}
private static Geometry q(Material mat,
float x, float y, float w, float h,
float uMin, float vMin, float uMax, float vMax,

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,3 +7,4 @@ include 'blight-lang'
include 'blight-editor'
include 'blight-game'
include 'blight-vegetation-generator'
include 'blight-fbx-test'