diff --git a/blight-common/src/main/java/de/blight/common/MapData.java b/blight-common/src/main/java/de/blight/common/MapData.java index 5391d16..f1d20e1 100644 --- a/blight-common/src/main/java/de/blight/common/MapData.java +++ b/blight-common/src/main/java/de/blight/common/MapData.java @@ -80,6 +80,22 @@ public final class MapData { /** Splatmap Alpha-Kanal Dritte Gruppe [SPLAT_SIZE²]. */ 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). */ public final String[] thirdTextures = new String[]{"", "", "", ""}; /** 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]; thirdSplatB = 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]; grassHeightMap = new byte [SPLAT_SIZE * SPLAT_SIZE]; grassTextureMap = new byte [SPLAT_SIZE * SPLAT_SIZE]; diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java index 08e7165..281b3ff 100644 --- a/blight-common/src/main/java/de/blight/common/MapIO.java +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -29,6 +29,7 @@ import java.util.zip.*; * Altes Format (v≤9) wird beim Laden bilinear hochskaliert. * 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. + * 17 – Voxel-Splatmaps (12 Kanäle RGBA×3) getrennt von Basis-Terrain-Splatmaps. */ public final class MapIO { @@ -65,7 +66,7 @@ public final class MapIO { } 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 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.thirdDisplacementMaps); 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. // 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); 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; } diff --git a/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java index d183829..6e25049 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java @@ -530,9 +530,9 @@ public class SculptedMeshEditorState extends BaseAppState { } } - com.jme3.texture.Texture2D a0 = tes.getSplatTex(); - com.jme3.texture.Texture2D a1 = tes.getUpperSplatTex(); - com.jme3.texture.Texture2D a2 = tes.getThirdSplatTex(); + com.jme3.texture.Texture2D a0 = tes.getVoxelSplatTex(); + com.jme3.texture.Texture2D a1 = tes.getVoxelUpperSplatTex(); + com.jme3.texture.Texture2D a2 = tes.getVoxelThirdSplatTex(); if (a0 != null) { mat.setTexture("AlphaMap", a0); } if (a1 != null) { mat.setTexture("AlphaMap_1", a1); } if (a2 != null) { mat.setTexture("AlphaMap_2", a2); } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 0b709e9..8f47a5b 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -142,6 +142,22 @@ public class TerrainEditorState extends BaseAppState { private Image thirdSplatImage; 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 ──────────────────────────────────────────────────────── 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)) @@ -476,6 +492,19 @@ public class TerrainEditorState extends BaseAppState { input.thirdTexturePaths, 0, MapData.TEXTURE_SLOTS); System.arraycopy(loadedMapData.thirdNormalMaps, 0, 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) input.normalMapOrder = loadedMapData.normalMapOrder.clone(); if (loadedMapData.diffuseScales != null && loadedMapData.diffuseScales.length == 12) @@ -507,6 +536,18 @@ public class TerrainEditorState extends BaseAppState { thirdSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE]; thirdSplatB = 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) @@ -545,6 +586,39 @@ public class TerrainEditorState extends BaseAppState { thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); 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 // dieser Merge). Freigeben, weil terrainHeight allein ~1 GB (16385² floats) // 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 ─────────────────────────────────────────────────────── @Override @@ -1159,27 +1366,44 @@ public class TerrainEditorState extends BaseAppState { Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal()); - Vector3f contact = raycastSurface(ray); - if (contact == null) continue; - if (ves != null && ves.terrainTypeAt(contact.x, contact.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue; + SurfaceHit hit = raycastSurfaceWithType(ray); + if (hit == null) 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(); float str = (float) input.textureTool.brushStrength.getValue(); - if (edit.action() > 0) { - // Linksklick: Slot malen - if (selIdx >= 8) { - applyThirdTexturePaint(contact, str, selIdx - 8); // 0=R(Slot9),1=G(Slot10),... - } else if (selIdx >= 4) { - applyUpperTexturePaint(contact, str, selIdx - 4); // 0=R(Slot5),1=G(Slot6),... + if (hit.voxelMesh) { + // Sculpted-Mesh-Treffer → Voxel-Splatmaps + if (edit.action() > 0) { + if (selIdx >= 8) { + applyVoxelThirdTexturePaint(contact, str, selIdx - 8); + } else if (selIdx >= 4) { + applyVoxelUpperTexturePaint(contact, str, selIdx - 4); + } else { + applyVoxelTexturePaint(contact, str, selIdx); + } } else { - applyTexturePaint(contact, str, selIdx); + applyVoxelTexturePaint(contact, str, 0); + applyVoxelUpperTexturePaint(contact, str, -1); + applyVoxelThirdTexturePaint(contact, str, -1); } } else { - // Rechtsklick: immer auf Basis (Slot 1) zurücksetzen - applyTexturePaint(contact, str, 0); // Base → Slot 1 (Gras) - applyUpperTexturePaint(contact, str, -1); // Upper-Kanäle → 0 - applyThirdTexturePaint(contact, str, -1); // Third-Kanäle → 0 + // Basis-Terrain-Treffer → normale Splatmaps + if (edit.action() > 0) { + if (selIdx >= 8) { + 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 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. * Gibt den zur Kamera nächstgelegenen Treffer zurück, oder null wenn keines getroffen wurde. */ 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(); terrain.collideWith(ray, hits); Vector3f best = hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null; float bestDistSq = best != null ? ray.getOrigin().distanceSquared(best) : Float.MAX_VALUE; + boolean bestIsVoxelMesh = false; VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); if (ves != null) { Vector3f vp = ves.raycastVoxelGeometry(ray); if (vp != null) { 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); if (sp != null) { 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. */ @@ -1268,6 +1508,18 @@ public class TerrainEditorState extends BaseAppState { final byte[] thG = thirdSplatG != null ? thirdSplatG.clone() : null; final byte[] thB = thirdSplatB != null ? thirdSplatB.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[] normalPaths = input.terrainNormalMapPaths.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(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.diffuseScales = diffScaleSnap.clone(); data.voxelFlatSlot = voxelFlatSlot; diff --git a/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java b/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java index 0e089c6..2534321 100644 --- a/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java +++ b/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java @@ -238,19 +238,22 @@ public class SculptedMeshState extends BaseAppState { private Material buildMaterial() { Material mat = new Material(app.getAssetManager(), "MatDefs/BakedTerrain.j3md"); - // DiffuseArray + DiffuseScales + AlphaMaps vom Terrain-Material übernehmen, - // damit die gemalten Splatmap-Texturen auch ingame sichtbar sind. + // DiffuseArray + DiffuseScales vom Terrain-Material übernehmen (selbe Textur-Slots). + // AlphaMaps kommen aus den Voxel-spezifischen Splatmap-Arrays in MapData. if (terrainMaterial != null) { com.jme3.material.MatParam diffArr = terrainMaterial.getParam("DiffuseArray"); if (diffArr != null) mat.setParam("DiffuseArray", diffArr.getVarType(), diffArr.getValue()); com.jme3.material.MatParam scales = terrainMaterial.getParam("DiffuseScales"); if (scales != null) mat.setParam("DiffuseScales", scales.getVarType(), scales.getValue()); - - for (String key : new String[]{"AlphaMap", "AlphaMap_1", "AlphaMap_2"}) { - com.jme3.material.MatParam p = terrainMaterial.getParam(key); - if (p != null) mat.setTexture(key, (Texture) p.getValue()); - } + } + if (mapData != null) { + com.jme3.texture.Texture2D a0 = buildVoxelAlphaMap(mapData.voxelSplatR, mapData.voxelSplatG, mapData.voxelSplatB, mapData.voxelSplatA); + 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 @@ -261,4 +264,21 @@ public class SculptedMeshState extends BaseAppState { 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; + } } diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index aa2016d..66b9cfe 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index fa61d1f..44be15d 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ