Texturierung von Voxel Terrain angepasst

This commit is contained in:
2026-08-20 16:10:06 +02:00
parent 919ef98137
commit 024a7ffaff
7 changed files with 358 additions and 28 deletions

View File

@@ -80,6 +80,22 @@ public final class MapData {
/** Splatmap Alpha-Kanal Dritte Gruppe [SPLAT_SIZE²]. */ /** Splatmap Alpha-Kanal Dritte Gruppe [SPLAT_SIZE²]. */
public final byte[] thirdSplatA; public final byte[] thirdSplatA;
/** Voxel-Splatmap Kanal 1-4 (AlphaMap für Sculpted-Meshes, unabhängig vom Basis-Terrain) [SPLAT_SIZE²]. */
public final byte[] voxelSplatR;
public final byte[] voxelSplatG;
public final byte[] voxelSplatB;
public final byte[] voxelSplatA;
/** Voxel-Splatmap Kanal 5-8 (AlphaMap_1) [SPLAT_SIZE²]. */
public final byte[] voxelUpperSplatR;
public final byte[] voxelUpperSplatG;
public final byte[] voxelUpperSplatB;
public final byte[] voxelUpperSplatA;
/** Voxel-Splatmap Kanal 9-12 (AlphaMap_2) [SPLAT_SIZE²]. */
public final byte[] voxelThirdSplatR;
public final byte[] voxelThirdSplatG;
public final byte[] voxelThirdSplatB;
public final byte[] voxelThirdSplatA;
/** Texturpfade für dritte Gruppe (4 Slots, "" = Standard-Textur). */ /** Texturpfade für dritte Gruppe (4 Slots, "" = Standard-Textur). */
public final String[] thirdTextures = new String[]{"", "", "", ""}; public final String[] thirdTextures = new String[]{"", "", "", ""};
/** Normal-Map-Pfade für dritte Gruppe (4 Slots, "" = keine Normal-Map). */ /** Normal-Map-Pfade für dritte Gruppe (4 Slots, "" = keine Normal-Map). */
@@ -155,6 +171,18 @@ public final class MapData {
thirdSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE]; thirdSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
thirdSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE]; thirdSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
thirdSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE]; thirdSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelSplatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE]; grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassHeightMap = new byte [SPLAT_SIZE * SPLAT_SIZE]; grassHeightMap = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassTextureMap = new byte [SPLAT_SIZE * SPLAT_SIZE]; grassTextureMap = new byte [SPLAT_SIZE * SPLAT_SIZE];

View File

@@ -29,6 +29,7 @@ import java.util.zip.*;
* Altes Format (v≤9) wird beim Laden bilinear hochskaliert. * Altes Format (v≤9) wird beim Laden bilinear hochskaliert.
* 16 Kartengröße halbiert: TERRAIN_VERTS=8193, SPLAT_SIZE=1025, UPPER_CELLS=256. * 16 Kartengröße halbiert: TERRAIN_VERTS=8193, SPLAT_SIZE=1025, UPPER_CELLS=256.
* Format v10-v15 wird beim Laden bilinear auf neue Größe herunterskaliert. * Format v10-v15 wird beim Laden bilinear auf neue Größe herunterskaliert.
* 17 Voxel-Splatmaps (12 Kanäle RGBA×3) getrennt von Basis-Terrain-Splatmaps.
*/ */
public final class MapIO { public final class MapIO {
@@ -65,7 +66,7 @@ public final class MapIO {
} }
private static final int MAGIC = 0x424C4947; // "BLIG" private static final int MAGIC = 0x424C4947; // "BLIG"
private static final int VERSION = 16; private static final int VERSION = 17;
// Größen alter Saves für Migration // Größen alter Saves für Migration
private static final int V10_TERRAIN_VERTS = 16385; // v10-v15 private static final int V10_TERRAIN_VERTS = 16385; // v10-v15
@@ -172,6 +173,13 @@ public final class MapIO {
writeStrings(out, data.upperDisplacementMaps); writeStrings(out, data.upperDisplacementMaps);
writeStrings(out, data.thirdDisplacementMaps); writeStrings(out, data.thirdDisplacementMaps);
writeFloats(out, data.diffuseScales); writeFloats(out, data.diffuseScales);
// v17: voxel-splatmaps (unabhängig vom Basis-Terrain)
out.write(data.voxelSplatR); out.write(data.voxelSplatG);
out.write(data.voxelSplatB); out.write(data.voxelSplatA);
out.write(data.voxelUpperSplatR); out.write(data.voxelUpperSplatG);
out.write(data.voxelUpperSplatB); out.write(data.voxelUpperSplatA);
out.write(data.voxelThirdSplatR); out.write(data.voxelThirdSplatG);
out.write(data.voxelThirdSplatB); out.write(data.voxelThirdSplatA);
} }
// Atomares Umbenennen: erst wenn die Datei vollständig ist ersetzen wir die alte. // Atomares Umbenennen: erst wenn die Datei vollständig ist ersetzen wir die alte.
// ATOMIC_MOVE schlägt auf manchen Systemen cross-device fehl → Fallback auf REPLACE_EXISTING. // ATOMIC_MOVE schlägt auf manchen Systemen cross-device fehl → Fallback auf REPLACE_EXISTING.
@@ -377,6 +385,14 @@ public final class MapIO {
readStrings(in, data.thirdDisplacementMaps); readStrings(in, data.thirdDisplacementMaps);
readFloats(in, data.diffuseScales); readFloats(in, data.diffuseScales);
} }
if (version >= 17) {
in.readFully(data.voxelSplatR); in.readFully(data.voxelSplatG);
in.readFully(data.voxelSplatB); in.readFully(data.voxelSplatA);
in.readFully(data.voxelUpperSplatR); in.readFully(data.voxelUpperSplatG);
in.readFully(data.voxelUpperSplatB); in.readFully(data.voxelUpperSplatA);
in.readFully(data.voxelThirdSplatR); in.readFully(data.voxelThirdSplatG);
in.readFully(data.voxelThirdSplatB); in.readFully(data.voxelThirdSplatA);
}
} }
return data; return data;
} }

View File

@@ -530,9 +530,9 @@ public class SculptedMeshEditorState extends BaseAppState {
} }
} }
com.jme3.texture.Texture2D a0 = tes.getSplatTex(); com.jme3.texture.Texture2D a0 = tes.getVoxelSplatTex();
com.jme3.texture.Texture2D a1 = tes.getUpperSplatTex(); com.jme3.texture.Texture2D a1 = tes.getVoxelUpperSplatTex();
com.jme3.texture.Texture2D a2 = tes.getThirdSplatTex(); com.jme3.texture.Texture2D a2 = tes.getVoxelThirdSplatTex();
if (a0 != null) { mat.setTexture("AlphaMap", a0); } if (a0 != null) { mat.setTexture("AlphaMap", a0); }
if (a1 != null) { mat.setTexture("AlphaMap_1", a1); } if (a1 != null) { mat.setTexture("AlphaMap_1", a1); }
if (a2 != null) { mat.setTexture("AlphaMap_2", a2); } if (a2 != null) { mat.setTexture("AlphaMap_2", a2); }

View File

@@ -142,6 +142,22 @@ public class TerrainEditorState extends BaseAppState {
private Image thirdSplatImage; private Image thirdSplatImage;
private Texture2D thirdSplatTex; private Texture2D thirdSplatTex;
// ── Voxel-Splatmaps (Sculpted-Mesh, unabhängig vom Basis-Terrain) ─────────
private byte[] voxelSplatR, voxelSplatG, voxelSplatB, voxelSplatA;
private ByteBuffer voxelSplatBuf;
private Image voxelSplatImage;
private Texture2D voxelSplatTex;
private byte[] voxelUpperSplatR, voxelUpperSplatG, voxelUpperSplatB, voxelUpperSplatA;
private ByteBuffer voxelUpperSplatBuf;
private Image voxelUpperSplatImage;
private Texture2D voxelUpperSplatTex;
private byte[] voxelThirdSplatR, voxelThirdSplatG, voxelThirdSplatB, voxelThirdSplatA;
private ByteBuffer voxelThirdSplatBuf;
private Image voxelThirdSplatImage;
private Texture2D voxelThirdSplatTex;
// ── Kameraposition ──────────────────────────────────────────────────────── // ── Kameraposition ────────────────────────────────────────────────────────
private static final float DEFAULT_CAM_Y = 50f; private static final float DEFAULT_CAM_Y = 50f;
private static final float DEFAULT_PITCH = (float) (Math.PI / 4); // intern +45° → Kamera schaut 45° nach unten (JME 3.9: dir.y = -sin(pitch)) private static final float DEFAULT_PITCH = (float) (Math.PI / 4); // intern +45° → Kamera schaut 45° nach unten (JME 3.9: dir.y = -sin(pitch))
@@ -476,6 +492,19 @@ public class TerrainEditorState extends BaseAppState {
input.thirdTexturePaths, 0, MapData.TEXTURE_SLOTS); input.thirdTexturePaths, 0, MapData.TEXTURE_SLOTS);
System.arraycopy(loadedMapData.thirdNormalMaps, 0, System.arraycopy(loadedMapData.thirdNormalMaps, 0,
input.thirdNormalMapPaths, 0, MapData.TEXTURE_SLOTS); input.thirdNormalMapPaths, 0, MapData.TEXTURE_SLOTS);
// Voxel-Splatmaps (Sculpted-Meshes, seit Version 17)
voxelSplatR = loadedMapData.voxelSplatR.clone();
voxelSplatG = loadedMapData.voxelSplatG.clone();
voxelSplatB = loadedMapData.voxelSplatB.clone();
voxelSplatA = loadedMapData.voxelSplatA.clone();
voxelUpperSplatR = loadedMapData.voxelUpperSplatR.clone();
voxelUpperSplatG = loadedMapData.voxelUpperSplatG.clone();
voxelUpperSplatB = loadedMapData.voxelUpperSplatB.clone();
voxelUpperSplatA = loadedMapData.voxelUpperSplatA.clone();
voxelThirdSplatR = loadedMapData.voxelThirdSplatR.clone();
voxelThirdSplatG = loadedMapData.voxelThirdSplatG.clone();
voxelThirdSplatB = loadedMapData.voxelThirdSplatB.clone();
voxelThirdSplatA = loadedMapData.voxelThirdSplatA.clone();
if (loadedMapData.normalMapOrder != null && loadedMapData.normalMapOrder.length == 12) if (loadedMapData.normalMapOrder != null && loadedMapData.normalMapOrder.length == 12)
input.normalMapOrder = loadedMapData.normalMapOrder.clone(); input.normalMapOrder = loadedMapData.normalMapOrder.clone();
if (loadedMapData.diffuseScales != null && loadedMapData.diffuseScales.length == 12) if (loadedMapData.diffuseScales != null && loadedMapData.diffuseScales.length == 12)
@@ -507,6 +536,18 @@ public class TerrainEditorState extends BaseAppState {
thirdSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE]; thirdSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE];
thirdSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE]; thirdSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE];
thirdSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE]; thirdSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelSplatR = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatR = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelUpperSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatR = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE];
voxelThirdSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE];
} }
// A-Kanal aus alten Saves bereinigen (war für Fluss-Textur; nicht mehr verwendet) // A-Kanal aus alten Saves bereinigen (war für Fluss-Textur; nicht mehr verwendet)
@@ -545,6 +586,39 @@ public class TerrainEditorState extends BaseAppState {
thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear); thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
voxelSplatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4);
for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) {
voxelSplatBuf.put(voxelSplatR[i]).put(voxelSplatG[i]).put(voxelSplatB[i]).put(voxelSplatA[i]);
}
voxelSplatBuf.flip();
voxelSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, voxelSplatBuf, ColorSpace.Linear);
voxelSplatTex = new Texture2D(voxelSplatImage);
voxelSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
voxelSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
voxelSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
voxelUpperSplatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4);
for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) {
voxelUpperSplatBuf.put(voxelUpperSplatR[i]).put(voxelUpperSplatG[i]).put(voxelUpperSplatB[i]).put(voxelUpperSplatA[i]);
}
voxelUpperSplatBuf.flip();
voxelUpperSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, voxelUpperSplatBuf, ColorSpace.Linear);
voxelUpperSplatTex = new Texture2D(voxelUpperSplatImage);
voxelUpperSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
voxelUpperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
voxelUpperSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
voxelThirdSplatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4);
for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) {
voxelThirdSplatBuf.put(voxelThirdSplatR[i]).put(voxelThirdSplatG[i]).put(voxelThirdSplatB[i]).put(voxelThirdSplatA[i]);
}
voxelThirdSplatBuf.flip();
voxelThirdSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, voxelThirdSplatBuf, ColorSpace.Linear);
voxelThirdSplatTex = new Texture2D(voxelThirdSplatImage);
voxelThirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
voxelThirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
voxelThirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
// loadedMapData wird ab hier nicht mehr gebraucht (letzter Verbraucher war // loadedMapData wird ab hier nicht mehr gebraucht (letzter Verbraucher war
// dieser Merge). Freigeben, weil terrainHeight allein ~1 GB (16385² floats) // dieser Merge). Freigeben, weil terrainHeight allein ~1 GB (16385² floats)
// belegt und sonst die ganze Session über unnötig im Heap hängt. // belegt und sonst die ganze Session über unnötig im Heap hängt.
@@ -954,6 +1028,139 @@ public class TerrainEditorState extends BaseAppState {
} }
} }
/** Malt Voxel-Basis-Texturen (Slots 1-4) auf die Voxel-Splatmap. */
private void applyVoxelTexturePaint(Vector3f contact, float strength, int textureIndex) {
float radius = (float) input.textureTool.brushRadius.getValue();
float softEdge = radius + Math.min(radius * 0.5f, 2f * SPLAT_WE_PER_PX);
int centerPX = Math.round((contact.x + WORLD_HALF) / SPLAT_WE_PER_PX);
int centerPZ = (SPLAT_SIZE - 1) - Math.round((contact.z + WORLD_HALF) / SPLAT_WE_PER_PX);
int pixR = (int) Math.ceil(softEdge / SPLAT_WE_PER_PX);
boolean changed = false;
for (int dz = -pixR; dz <= pixR; dz++) {
int pz = centerPZ + dz;
if (pz < 0 || pz >= SPLAT_SIZE) continue;
for (int dx = -pixR; dx <= pixR; dx++) {
int px = centerPX + dx;
if (px < 0 || px >= SPLAT_SIZE) continue;
float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
if (distWE >= softEdge) continue;
float t = distWE / softEdge;
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
float blend = (textureIndex == 0) ? falloff : strength * falloff;
int idx = pz * SPLAT_SIZE + px;
float curG = (voxelSplatG[idx] & 0xFF) / 255f;
float curB = (voxelSplatB[idx] & 0xFF) / 255f;
float curA = (voxelSplatA[idx] & 0xFF) / 255f;
float tgG = (textureIndex == 1) ? 1f : 0f;
float tgB = (textureIndex == 2) ? 1f : 0f;
float tgA = (textureIndex == 3) ? 1f : 0f;
voxelSplatR[idx] = (byte) 255;
voxelSplatG[idx] = (byte) Math.round((curG + (tgG - curG) * blend) * 255f);
voxelSplatB[idx] = (byte) Math.round((curB + (tgB - curB) * blend) * 255f);
voxelSplatA[idx] = (byte) Math.round((curA + (tgA - curA) * blend) * 255f);
int bi = idx * 4;
voxelSplatBuf.put(bi, voxelSplatR[idx]);
voxelSplatBuf.put(bi + 1, voxelSplatG[idx]);
voxelSplatBuf.put(bi + 2, voxelSplatB[idx]);
voxelSplatBuf.put(bi + 3, voxelSplatA[idx]);
changed = true;
}
}
if (changed) { voxelSplatBuf.rewind(); voxelSplatImage.setUpdateNeeded(); }
}
/** Malt Voxel-Slots 5-8 auf die Voxel-Upper-Splatmap. */
private void applyVoxelUpperTexturePaint(Vector3f contact, float strength, int textureIndex) {
float radius = (float) input.textureTool.brushRadius.getValue();
float softEdge = radius + Math.min(radius * 0.5f, 2f * SPLAT_WE_PER_PX);
int centerPX = Math.round((contact.x + WORLD_HALF) / SPLAT_WE_PER_PX);
int centerPZ = (SPLAT_SIZE - 1) - Math.round((contact.z + WORLD_HALF) / SPLAT_WE_PER_PX);
int pixR = (int) Math.ceil(softEdge / SPLAT_WE_PER_PX);
boolean changed = false;
for (int dz = -pixR; dz <= pixR; dz++) {
int pz = centerPZ + dz;
if (pz < 0 || pz >= SPLAT_SIZE) continue;
for (int dx = -pixR; dx <= pixR; dx++) {
int px = centerPX + dx;
if (px < 0 || px >= SPLAT_SIZE) continue;
float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
if (distWE >= softEdge) continue;
float t = distWE / softEdge;
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
int idx = pz * SPLAT_SIZE + px;
if (textureIndex < 0) {
voxelUpperSplatR[idx] = (byte) Math.round((voxelUpperSplatR[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
voxelUpperSplatG[idx] = (byte) Math.round((voxelUpperSplatG[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
voxelUpperSplatB[idx] = (byte) Math.round((voxelUpperSplatB[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
voxelUpperSplatA[idx] = (byte) Math.round((voxelUpperSplatA[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
} else {
float blend = strength * falloff;
float curR = (voxelUpperSplatR[idx] & 0xFF) / 255f;
float curG = (voxelUpperSplatG[idx] & 0xFF) / 255f;
float curB = (voxelUpperSplatB[idx] & 0xFF) / 255f;
float curA = (voxelUpperSplatA[idx] & 0xFF) / 255f;
voxelUpperSplatR[idx] = (byte) Math.round((curR + ((textureIndex == 0 ? 1f : 0f) - curR) * blend) * 255f);
voxelUpperSplatG[idx] = (byte) Math.round((curG + ((textureIndex == 1 ? 1f : 0f) - curG) * blend) * 255f);
voxelUpperSplatB[idx] = (byte) Math.round((curB + ((textureIndex == 2 ? 1f : 0f) - curB) * blend) * 255f);
voxelUpperSplatA[idx] = (byte) Math.round((curA + ((textureIndex == 3 ? 1f : 0f) - curA) * blend) * 255f);
}
int bi = idx * 4;
voxelUpperSplatBuf.put(bi, voxelUpperSplatR[idx]);
voxelUpperSplatBuf.put(bi + 1, voxelUpperSplatG[idx]);
voxelUpperSplatBuf.put(bi + 2, voxelUpperSplatB[idx]);
voxelUpperSplatBuf.put(bi + 3, voxelUpperSplatA[idx]);
changed = true;
}
}
if (changed) { voxelUpperSplatBuf.rewind(); voxelUpperSplatImage.setUpdateNeeded(); }
}
/** Malt Voxel-Slots 9-12 auf die Voxel-Third-Splatmap. */
private void applyVoxelThirdTexturePaint(Vector3f contact, float strength, int textureIndex) {
float radius = (float) input.textureTool.brushRadius.getValue();
float softEdge = radius + Math.min(radius * 0.5f, 2f * SPLAT_WE_PER_PX);
int centerPX = Math.round((contact.x + WORLD_HALF) / SPLAT_WE_PER_PX);
int centerPZ = (SPLAT_SIZE - 1) - Math.round((contact.z + WORLD_HALF) / SPLAT_WE_PER_PX);
int pixR = (int) Math.ceil(softEdge / SPLAT_WE_PER_PX);
boolean changed = false;
for (int dz = -pixR; dz <= pixR; dz++) {
int pz = centerPZ + dz;
if (pz < 0 || pz >= SPLAT_SIZE) continue;
for (int dx = -pixR; dx <= pixR; dx++) {
int px = centerPX + dx;
if (px < 0 || px >= SPLAT_SIZE) continue;
float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
if (distWE >= softEdge) continue;
float t = distWE / softEdge;
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
int idx = pz * SPLAT_SIZE + px;
if (textureIndex < 0) {
voxelThirdSplatR[idx] = (byte) Math.round((voxelThirdSplatR[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
voxelThirdSplatG[idx] = (byte) Math.round((voxelThirdSplatG[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
voxelThirdSplatB[idx] = (byte) Math.round((voxelThirdSplatB[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
voxelThirdSplatA[idx] = (byte) Math.round((voxelThirdSplatA[idx] & 0xFF) / 255f * (1f - falloff) * 255f);
} else {
float blend = strength * falloff;
float curR = (voxelThirdSplatR[idx] & 0xFF) / 255f;
float curG = (voxelThirdSplatG[idx] & 0xFF) / 255f;
float curB = (voxelThirdSplatB[idx] & 0xFF) / 255f;
float curA = (voxelThirdSplatA[idx] & 0xFF) / 255f;
voxelThirdSplatR[idx] = (byte) Math.round((curR + ((textureIndex == 0 ? 1f : 0f) - curR) * blend) * 255f);
voxelThirdSplatG[idx] = (byte) Math.round((curG + ((textureIndex == 1 ? 1f : 0f) - curG) * blend) * 255f);
voxelThirdSplatB[idx] = (byte) Math.round((curB + ((textureIndex == 2 ? 1f : 0f) - curB) * blend) * 255f);
voxelThirdSplatA[idx] = (byte) Math.round((curA + ((textureIndex == 3 ? 1f : 0f) - curA) * blend) * 255f);
}
int bi = idx * 4;
voxelThirdSplatBuf.put(bi, voxelThirdSplatR[idx]);
voxelThirdSplatBuf.put(bi + 1, voxelThirdSplatG[idx]);
voxelThirdSplatBuf.put(bi + 2, voxelThirdSplatB[idx]);
voxelThirdSplatBuf.put(bi + 3, voxelThirdSplatA[idx]);
changed = true;
}
}
if (changed) { voxelThirdSplatBuf.rewind(); voxelThirdSplatImage.setUpdateNeeded(); }
}
// ── Update-Schleife ─────────────────────────────────────────────────────── // ── Update-Schleife ───────────────────────────────────────────────────────
@Override @Override
@@ -1159,27 +1366,44 @@ public class TerrainEditorState extends BaseAppState {
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal()); com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
Vector3f contact = raycastSurface(ray); SurfaceHit hit = raycastSurfaceWithType(ray);
if (contact == null) continue; if (hit == null) continue;
if (ves != null && ves.terrainTypeAt(contact.x, contact.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue; Vector3f contact = hit.point;
if (!hit.voxelMesh && ves != null && ves.terrainTypeAt(contact.x, contact.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue;
int selIdx = input.textureTool.textureIndex.getSelectedIndex(); int selIdx = input.textureTool.textureIndex.getSelectedIndex();
float str = (float) input.textureTool.brushStrength.getValue(); float str = (float) input.textureTool.brushStrength.getValue();
if (edit.action() > 0) { if (hit.voxelMesh) {
// Linksklick: Slot malen // Sculpted-Mesh-Treffer → Voxel-Splatmaps
if (selIdx >= 8) { if (edit.action() > 0) {
applyThirdTexturePaint(contact, str, selIdx - 8); // 0=R(Slot9),1=G(Slot10),... if (selIdx >= 8) {
} else if (selIdx >= 4) { applyVoxelThirdTexturePaint(contact, str, selIdx - 8);
applyUpperTexturePaint(contact, str, selIdx - 4); // 0=R(Slot5),1=G(Slot6),... } else if (selIdx >= 4) {
applyVoxelUpperTexturePaint(contact, str, selIdx - 4);
} else {
applyVoxelTexturePaint(contact, str, selIdx);
}
} else { } else {
applyTexturePaint(contact, str, selIdx); applyVoxelTexturePaint(contact, str, 0);
applyVoxelUpperTexturePaint(contact, str, -1);
applyVoxelThirdTexturePaint(contact, str, -1);
} }
} else { } else {
// Rechtsklick: immer auf Basis (Slot 1) zurücksetzen // Basis-Terrain-Treffer → normale Splatmaps
applyTexturePaint(contact, str, 0); // Base → Slot 1 (Gras) if (edit.action() > 0) {
applyUpperTexturePaint(contact, str, -1); // Upper-Kanäle → 0 if (selIdx >= 8) {
applyThirdTexturePaint(contact, str, -1); // Third-Kanäle → 0 applyThirdTexturePaint(contact, str, selIdx - 8);
} else if (selIdx >= 4) {
applyUpperTexturePaint(contact, str, selIdx - 4);
} else {
applyTexturePaint(contact, str, selIdx);
}
} else {
applyTexturePaint(contact, str, 0);
applyUpperTexturePaint(contact, str, -1);
applyThirdTexturePaint(contact, str, -1);
}
} }
} }
} }
@@ -1194,22 +1418,38 @@ public class TerrainEditorState extends BaseAppState {
public com.jme3.texture.Texture2D getUpperSplatTex() { return upperSplatTex; } public com.jme3.texture.Texture2D getUpperSplatTex() { return upperSplatTex; }
public com.jme3.texture.Texture2D getThirdSplatTex() { return thirdSplatTex; } public com.jme3.texture.Texture2D getThirdSplatTex() { return thirdSplatTex; }
public com.jme3.texture.Texture2D getVoxelSplatTex() { return voxelSplatTex; }
public com.jme3.texture.Texture2D getVoxelUpperSplatTex() { return voxelUpperSplatTex; }
public com.jme3.texture.Texture2D getVoxelThirdSplatTex() { return voxelThirdSplatTex; }
private static final class SurfaceHit {
final Vector3f point;
final boolean voxelMesh;
SurfaceHit(Vector3f p, boolean v) { point = p; voxelMesh = v; }
}
/** /**
* Raycast gegen Basis-Terrain, ungebackenes Voxel-Terrain und gebackene Sculpted-Meshes. * Raycast gegen Basis-Terrain, ungebackenes Voxel-Terrain und gebackene Sculpted-Meshes.
* Gibt den zur Kamera nächstgelegenen Treffer zurück, oder null wenn keines getroffen wurde. * Gibt den zur Kamera nächstgelegenen Treffer zurück, oder null wenn keines getroffen wurde.
*/ */
private Vector3f raycastSurface(com.jme3.math.Ray ray) { private Vector3f raycastSurface(com.jme3.math.Ray ray) {
SurfaceHit h = raycastSurfaceWithType(ray);
return h != null ? h.point : null;
}
private SurfaceHit raycastSurfaceWithType(com.jme3.math.Ray ray) {
CollisionResults hits = new CollisionResults(); CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits); terrain.collideWith(ray, hits);
Vector3f best = hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null; Vector3f best = hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null;
float bestDistSq = best != null ? ray.getOrigin().distanceSquared(best) : Float.MAX_VALUE; float bestDistSq = best != null ? ray.getOrigin().distanceSquared(best) : Float.MAX_VALUE;
boolean bestIsVoxelMesh = false;
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) { if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray); Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) { if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp); float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; } if (d < bestDistSq) { best = vp; bestDistSq = d; bestIsVoxelMesh = false; }
} }
} }
@@ -1218,10 +1458,10 @@ public class TerrainEditorState extends BaseAppState {
Vector3f sp = smes.raycastGeometry(ray); Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) { if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp); float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; } if (d < bestDistSq) { best = sp; bestIsVoxelMesh = true; }
} }
} }
return best; return best != null ? new SurfaceHit(best, bestIsVoxelMesh) : null;
} }
/** Gibt die Terrain-Höhe (Welt-Y) an der angegebenen Welt-XZ-Position zurück. */ /** Gibt die Terrain-Höhe (Welt-Y) an der angegebenen Welt-XZ-Position zurück. */
@@ -1268,6 +1508,18 @@ public class TerrainEditorState extends BaseAppState {
final byte[] thG = thirdSplatG != null ? thirdSplatG.clone() : null; final byte[] thG = thirdSplatG != null ? thirdSplatG.clone() : null;
final byte[] thB = thirdSplatB != null ? thirdSplatB.clone() : null; final byte[] thB = thirdSplatB != null ? thirdSplatB.clone() : null;
final byte[] thA = thirdSplatA != null ? thirdSplatA.clone() : null; final byte[] thA = thirdSplatA != null ? thirdSplatA.clone() : null;
final byte[] vxR = voxelSplatR != null ? voxelSplatR.clone() : null;
final byte[] vxG = voxelSplatG != null ? voxelSplatG.clone() : null;
final byte[] vxB = voxelSplatB != null ? voxelSplatB.clone() : null;
final byte[] vxA = voxelSplatA != null ? voxelSplatA.clone() : null;
final byte[] vxUR = voxelUpperSplatR != null ? voxelUpperSplatR.clone() : null;
final byte[] vxUG = voxelUpperSplatG != null ? voxelUpperSplatG.clone() : null;
final byte[] vxUB = voxelUpperSplatB != null ? voxelUpperSplatB.clone() : null;
final byte[] vxUA = voxelUpperSplatA != null ? voxelUpperSplatA.clone() : null;
final byte[] vxTR = voxelThirdSplatR != null ? voxelThirdSplatR.clone() : null;
final byte[] vxTG = voxelThirdSplatG != null ? voxelThirdSplatG.clone() : null;
final byte[] vxTB = voxelThirdSplatB != null ? voxelThirdSplatB.clone() : null;
final byte[] vxTA = voxelThirdSplatA != null ? voxelThirdSplatA.clone() : null;
final String[] texPaths = input.terrainTexturePaths.clone(); final String[] texPaths = input.terrainTexturePaths.clone();
final String[] normalPaths = input.terrainNormalMapPaths.clone(); final String[] normalPaths = input.terrainNormalMapPaths.clone();
final String[] upperPaths = input.upperTexturePaths.clone(); final String[] upperPaths = input.upperTexturePaths.clone();
@@ -1345,6 +1597,20 @@ public class TerrainEditorState extends BaseAppState {
System.arraycopy(thirdPaths, 0, data.thirdTextures, 0, MapData.TEXTURE_SLOTS); System.arraycopy(thirdPaths, 0, data.thirdTextures, 0, MapData.TEXTURE_SLOTS);
System.arraycopy(thirdNormPaths, 0, data.thirdNormalMaps, 0, MapData.TEXTURE_SLOTS); System.arraycopy(thirdNormPaths, 0, data.thirdNormalMaps, 0, MapData.TEXTURE_SLOTS);
} }
if (vxR != null) {
System.arraycopy(vxR, 0, data.voxelSplatR, 0, data.voxelSplatR.length);
System.arraycopy(vxG, 0, data.voxelSplatG, 0, data.voxelSplatG.length);
System.arraycopy(vxB, 0, data.voxelSplatB, 0, data.voxelSplatB.length);
System.arraycopy(vxA, 0, data.voxelSplatA, 0, data.voxelSplatA.length);
System.arraycopy(vxUR, 0, data.voxelUpperSplatR, 0, data.voxelUpperSplatR.length);
System.arraycopy(vxUG, 0, data.voxelUpperSplatG, 0, data.voxelUpperSplatG.length);
System.arraycopy(vxUB, 0, data.voxelUpperSplatB, 0, data.voxelUpperSplatB.length);
System.arraycopy(vxUA, 0, data.voxelUpperSplatA, 0, data.voxelUpperSplatA.length);
System.arraycopy(vxTR, 0, data.voxelThirdSplatR, 0, data.voxelThirdSplatR.length);
System.arraycopy(vxTG, 0, data.voxelThirdSplatG, 0, data.voxelThirdSplatG.length);
System.arraycopy(vxTB, 0, data.voxelThirdSplatB, 0, data.voxelThirdSplatB.length);
System.arraycopy(vxTA, 0, data.voxelThirdSplatA, 0, data.voxelThirdSplatA.length);
}
data.normalMapOrder = input.normalMapOrder.clone(); data.normalMapOrder = input.normalMapOrder.clone();
data.diffuseScales = diffScaleSnap.clone(); data.diffuseScales = diffScaleSnap.clone();
data.voxelFlatSlot = voxelFlatSlot; data.voxelFlatSlot = voxelFlatSlot;

View File

@@ -238,19 +238,22 @@ public class SculptedMeshState extends BaseAppState {
private Material buildMaterial() { private Material buildMaterial() {
Material mat = new Material(app.getAssetManager(), "MatDefs/BakedTerrain.j3md"); Material mat = new Material(app.getAssetManager(), "MatDefs/BakedTerrain.j3md");
// DiffuseArray + DiffuseScales + AlphaMaps vom Terrain-Material übernehmen, // DiffuseArray + DiffuseScales vom Terrain-Material übernehmen (selbe Textur-Slots).
// damit die gemalten Splatmap-Texturen auch ingame sichtbar sind. // AlphaMaps kommen aus den Voxel-spezifischen Splatmap-Arrays in MapData.
if (terrainMaterial != null) { if (terrainMaterial != null) {
com.jme3.material.MatParam diffArr = terrainMaterial.getParam("DiffuseArray"); com.jme3.material.MatParam diffArr = terrainMaterial.getParam("DiffuseArray");
if (diffArr != null) mat.setParam("DiffuseArray", diffArr.getVarType(), diffArr.getValue()); if (diffArr != null) mat.setParam("DiffuseArray", diffArr.getVarType(), diffArr.getValue());
com.jme3.material.MatParam scales = terrainMaterial.getParam("DiffuseScales"); com.jme3.material.MatParam scales = terrainMaterial.getParam("DiffuseScales");
if (scales != null) mat.setParam("DiffuseScales", scales.getVarType(), scales.getValue()); if (scales != null) mat.setParam("DiffuseScales", scales.getVarType(), scales.getValue());
}
for (String key : new String[]{"AlphaMap", "AlphaMap_1", "AlphaMap_2"}) { if (mapData != null) {
com.jme3.material.MatParam p = terrainMaterial.getParam(key); com.jme3.texture.Texture2D a0 = buildVoxelAlphaMap(mapData.voxelSplatR, mapData.voxelSplatG, mapData.voxelSplatB, mapData.voxelSplatA);
if (p != null) mat.setTexture(key, (Texture) p.getValue()); com.jme3.texture.Texture2D a1 = buildVoxelAlphaMap(mapData.voxelUpperSplatR, mapData.voxelUpperSplatG, mapData.voxelUpperSplatB, mapData.voxelUpperSplatA);
} com.jme3.texture.Texture2D a2 = buildVoxelAlphaMap(mapData.voxelThirdSplatR, mapData.voxelThirdSplatG, mapData.voxelThirdSplatB, mapData.voxelThirdSplatA);
mat.setTexture("AlphaMap", a0);
mat.setTexture("AlphaMap_1", a1);
mat.setTexture("AlphaMap_2", a2);
} }
// Klippen-Textur: steiler Slot aus MapData für Flat/Steep-Blending // Klippen-Textur: steiler Slot aus MapData für Flat/Steep-Blending
@@ -261,4 +264,21 @@ public class SculptedMeshState extends BaseAppState {
return mat; return mat;
} }
private com.jme3.texture.Texture2D buildVoxelAlphaMap(byte[] r, byte[] g, byte[] b, byte[] a) {
int size = MapData.SPLAT_SIZE;
java.nio.ByteBuffer buf = com.jme3.util.BufferUtils.createByteBuffer(size * size * 4);
for (int i = 0; i < size * size; i++) {
buf.put(r[i]).put(g[i]).put(b[i]).put(a[i]);
}
buf.flip();
com.jme3.texture.Image img = new com.jme3.texture.Image(
com.jme3.texture.Image.Format.RGBA8, size, size, buf,
com.jme3.texture.image.ColorSpace.Linear);
com.jme3.texture.Texture2D tex = new com.jme3.texture.Texture2D(img);
tex.setWrap(Texture.WrapMode.EdgeClamp);
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
tex.setMagFilter(Texture.MagFilter.Bilinear);
return tex;
}
} }