diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_155951.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_155951.png deleted file mode 100644 index ff5ba2e..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_155951.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_155953.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_155953.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_155953.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160003.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160003.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160003.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160027.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160027.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160027.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160028.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160028.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160028.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160029.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160029.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160029.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160030.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160030.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160030.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160126.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160126.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160126.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160809.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160809.png deleted file mode 100644 index 7fd1bec..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160809.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160907.png b/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160907.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_EzBaum1_20260603_160907.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_ash_large_20260608_160327.png b/blight-assets/src/main/resources/.impostors/ez_impostor_ash_large_20260608_160327.png deleted file mode 100644 index cd55ac3..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_ash_large_20260608_160327.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_aspen_large_20260608_185100.png b/blight-assets/src/main/resources/.impostors/ez_impostor_aspen_large_20260608_185100.png deleted file mode 100644 index 30cc8cd..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_aspen_large_20260608_185100.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_large_20260606_163631.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_large_20260606_163631.png deleted file mode 100644 index e36ba98..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_large_20260606_163631.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_large_20260606_163636.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_large_20260606_163636.png deleted file mode 100644 index 9aabc6c..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_large_20260606_163636.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210057.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210057.png deleted file mode 100644 index 018e0ef..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210057.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210112.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210112.png deleted file mode 100644 index a0ef111..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210112.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210903.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210903.png deleted file mode 100644 index 7fd1bec..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260603_210903.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_162818.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_162818.png deleted file mode 100644 index 7fd1bec..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_162818.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_163554.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_163554.png deleted file mode 100644 index e127e97..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_163554.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_190655.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_190655.png deleted file mode 100644 index 7fd1bec..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260606_190655.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260607_221049.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260607_221049.png deleted file mode 100644 index eaab7f4..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260607_221049.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_190845.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_190845.png deleted file mode 100644 index 60bd14a..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_190845.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_191554.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_191554.png deleted file mode 100644 index 14a53e8..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_191554.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_200434.png b/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_200434.png deleted file mode 100644 index 3c7717e..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_oak_medium_20260608_200434.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_pine_large_20260606_190805.png b/blight-assets/src/main/resources/.impostors/ez_impostor_pine_large_20260606_190805.png deleted file mode 100644 index 6f7cdca..0000000 Binary files a/blight-assets/src/main/resources/.impostors/ez_impostor_pine_large_20260606_190805.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_pine_medium_20260615_221703.png b/blight-assets/src/main/resources/.impostors/ez_impostor_pine_medium_20260615_221703.png new file mode 100644 index 0000000..2c4c2de Binary files /dev/null and b/blight-assets/src/main/resources/.impostors/ez_impostor_pine_medium_20260615_221703.png differ diff --git a/blight-assets/src/main/resources/.impostors/ez_impostor_pine_medium_20260615_221707.png b/blight-assets/src/main/resources/.impostors/ez_impostor_pine_medium_20260615_221707.png new file mode 100644 index 0000000..1adb713 Binary files /dev/null and b/blight-assets/src/main/resources/.impostors/ez_impostor_pine_medium_20260615_221707.png differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_Baum1.png b/blight-assets/src/main/resources/.impostors/impostor_Baum1.png deleted file mode 100644 index c785f37..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_Baum1.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_Baum1_20260603_160925.png b/blight-assets/src/main/resources/.impostors/impostor_Baum1_20260603_160925.png deleted file mode 100644 index ace2a5a..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_Baum1_20260603_160925.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163802.png b/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163802.png deleted file mode 100644 index ba1e54b..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163802.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163809.png b/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163809.png deleted file mode 100644 index 41ad3ad..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163809.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163819.png b/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163819.png deleted file mode 100644 index 57eeb31..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_oak_20260606_163819.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_oak_20260608_155226.png b/blight-assets/src/main/resources/.impostors/impostor_oak_20260608_155226.png deleted file mode 100644 index e2f23af..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_oak_20260608_155226.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_143905.png b/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_143905.png deleted file mode 100644 index bc038d7..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_143905.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_152427.png b/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_152427.png deleted file mode 100644 index b6d724f..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_152427.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_160151.png b/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_160151.png deleted file mode 100644 index a1161a2..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_pine_20260608_160151.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_willow_20260608_152437.png b/blight-assets/src/main/resources/.impostors/impostor_willow_20260608_152437.png deleted file mode 100644 index fc6d9b8..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_willow_20260608_152437.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.impostors/impostor_willow_20260608_152445.png b/blight-assets/src/main/resources/.impostors/impostor_willow_20260608_152445.png deleted file mode 100644 index af31637..0000000 Binary files a/blight-assets/src/main/resources/.impostors/impostor_willow_20260608_152445.png and /dev/null differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/wooden+cabin+3d+model.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/wooden+cabin+3d+model.j3o.thumb.png new file mode 100644 index 0000000..e3ce8db Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/wooden+cabin+3d+model.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/wooden+cabin+3d+model.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/wooden+cabin+3d+model.j3o.thumb.png new file mode 100644 index 0000000..e3ce8db Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/wooden+cabin+3d+model.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/MatDefs/Grass.j3md b/blight-assets/src/main/resources/MatDefs/Grass.j3md index 19f61be..24089e6 100644 --- a/blight-assets/src/main/resources/MatDefs/Grass.j3md +++ b/blight-assets/src/main/resources/MatDefs/Grass.j3md @@ -3,8 +3,11 @@ MaterialDef Grass { MaterialParameters { Color Color (Color) : 1.0 1.0 1.0 1.0 Texture2D ColorMap + Texture2D NormalMap Float WindSpeed : 0.5 Float WindStrength : 0.12 + Vector3 SunDir : 0.55 0.80 0.35 + Color SunColor : 1.0 1.0 0.95 1.0 } Technique { @@ -15,6 +18,7 @@ MaterialDef Grass { WorldViewProjectionMatrix WorldMatrix Time + AmbientLightColor } RenderState { @@ -22,7 +26,8 @@ MaterialDef Grass { } Defines { - HAS_COLORMAP : ColorMap + HAS_COLORMAP : ColorMap + HAS_NORMALMAP : NormalMap } } } diff --git a/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md b/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md new file mode 100644 index 0000000..29ac22e --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md @@ -0,0 +1,40 @@ +MaterialDef TerrainArray { + + MaterialParameters { + TextureArray DiffuseArray + FloatArray DiffuseScales + TextureArray NormalArray + Float Shininess : 32.0 + Vector3 LightDir + Vector3 SunColor + Vector3 AmbientColor + Boolean DebugNoLight + Boolean DebugSlot0Only + Texture2D DebugDirectTex + Texture2D AlphaMap + Texture2D AlphaMap_1 + Texture2D AlphaMap_2 + } + + Technique { + VertexShader GLSL150 : Shaders/TerrainArray.vert + FragmentShader GLSL150 : Shaders/TerrainArray.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + ViewProjectionMatrix + } + + Defines { + HAS_NORMAL_ARRAY : NormalArray + HAS_ALPHA_1 : AlphaMap_1 + HAS_ALPHA_2 : AlphaMap_2 + HAS_LIGHTDIR : LightDir + HAS_SCENE_LIGHT : SunColor + DEBUG_NO_LIGHT : DebugNoLight + DEBUG_SLOT0_ONLY : DebugSlot0Only + DEBUG_DIRECT_TEX : DebugDirectTex + } + } +} diff --git a/blight-assets/src/main/resources/MatDefs/Tree.j3md b/blight-assets/src/main/resources/MatDefs/Tree.j3md index 25e4729..f145101 100644 --- a/blight-assets/src/main/resources/MatDefs/Tree.j3md +++ b/blight-assets/src/main/resources/MatDefs/Tree.j3md @@ -6,6 +6,9 @@ MaterialDef Tree { Float WindSpeed : 0.5 Texture2D BarkMap Boolean HasBarkMap : false + Vector3 LightDir + Vector3 SunColor + Vector3 AmbientColor } Technique { @@ -17,5 +20,9 @@ MaterialDef Tree { WorldMatrix Time } + + Defines { + HAS_SCENE_LIGHT : SunColor + } } } diff --git a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md index d57e4bd..fe543fe 100644 --- a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md +++ b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md @@ -28,6 +28,8 @@ MaterialDef TreeLeaf { Matrix4 LightViewProjectionMatrix5 Vector3 LightPos Vector3 LightDir + Vector3 SunColor + Vector3 AmbientColor Float PCFEdge Float ShadowMapSize Boolean BackfaceShadows : false @@ -46,6 +48,10 @@ MaterialDef TreeLeaf { RenderState { FaceCull Off } + + Defines { + HAS_SCENE_LIGHT : SunColor + } } Technique PostShadow { diff --git a/blight-assets/src/main/resources/MatDefs/Voxel.j3md b/blight-assets/src/main/resources/MatDefs/Voxel.j3md index 70a3d1b..aa3e395 100644 --- a/blight-assets/src/main/resources/MatDefs/Voxel.j3md +++ b/blight-assets/src/main/resources/MatDefs/Voxel.j3md @@ -1,21 +1,71 @@ MaterialDef Voxel { MaterialParameters { - Texture2D Tex0 - Texture2D Tex1 - Texture2D Tex2 - Texture2D Tex3 - Float TexScale : 4.0 + Texture2D TexFlat + Texture2D TexSteep + Texture2D TexCeil + Texture2D NormalMapFlat + Texture2D NormalMapSteep + Texture2D NormalMapCeil + Texture2D DisplacementMapFlat + Texture2D DisplacementMapSteep + Texture2D DisplacementMapCeil + Float TexScale : 8.0 + Float DisplacementScale : 0.3 + Float TessellationLevel : 4.0 + Vector3 LightDir + Vector3 SunColor + Vector3 AmbientColor + Boolean DebugNoLight } Technique { + VertexShader GLSL150 : Shaders/Voxel.vert FragmentShader GLSL150 : Shaders/Voxel.frag WorldParameters { WorldViewProjectionMatrix WorldMatrix - NormalMatrix + } + + Defines { + HAS_NM_FLAT : NormalMapFlat + HAS_NM_STEEP : NormalMapSteep + HAS_NM_CEIL : NormalMapCeil + HAS_LIGHTDIR : LightDir + HAS_SCENE_LIGHT : SunColor + DEBUG_NO_LIGHT : DebugNoLight + } + + RenderState { + FaceCull Off + } + } + + Technique Tessellation { + + VertexShader GLSL400 : Shaders/VoxelTess.vert + TessellationControlShader GLSL400 : Shaders/Voxel.tsctrl + TessellationEvaluationShader GLSL400 : Shaders/Voxel.tseval + FragmentShader GLSL400 : Shaders/Voxel.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + ViewProjectionMatrix + CameraPosition + } + + Defines { + HAS_NM_FLAT : NormalMapFlat + HAS_NM_STEEP : NormalMapSteep + HAS_NM_CEIL : NormalMapCeil + HAS_DISP_FLAT : DisplacementMapFlat + HAS_DISP_STEEP : DisplacementMapSteep + HAS_DISP_CEIL : DisplacementMapCeil + HAS_LIGHTDIR : LightDir + HAS_SCENE_LIGHT : SunColor } RenderState { diff --git a/blight-assets/src/main/resources/Models/Campfire.j3o b/blight-assets/src/main/resources/Models/Campfire.j3o deleted file mode 100644 index d0fae90..0000000 Binary files a/blight-assets/src/main/resources/Models/Campfire.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Campfire.mtl b/blight-assets/src/main/resources/Models/Campfire.mtl deleted file mode 100644 index 6d92b23..0000000 --- a/blight-assets/src/main/resources/Models/Campfire.mtl +++ /dev/null @@ -1,24 +0,0 @@ -# Blender 4.0.2 MTL File: 'Campfire.blend' -# www.blender.org - -newmtl Campfire_MAT -Ka 0.500000 0.500000 0.500000 -Ks 0.222727 0.222727 0.222727 -Ke 0.000000 0.000000 0.000000 -Ni 1.450000 -d 1.000000 -illum 3 -map_Kd Campfire_MAT_BaseColor_01.jpg -map_Ns Campfire_MAT_Roughness.jpg -map_refl Campfire_MAT_Metallic.jpg -map_Bump -bm 1.000000 Campfire_MAT_Normal_JL.jpg - -newmtl Campfire_fire_MAT -Ns 1000.000000 -Ka 0.500000 0.500000 0.500000 -Ks 1.000000 1.000000 1.000000 -Ke 0.000000 0.000000 0.000000 -Ni 1.450000 -illum 3 -map_Kd Campfire_fire_MAT_BaseColor_Alpha.png -map_d Campfire_fire_MAT_BaseColor_Alpha.png diff --git a/blight-assets/src/main/resources/Models/FernPlantV2.j3o.meta b/blight-assets/src/main/resources/Models/FernPlantV2.j3o.meta deleted file mode 100644 index 48bcf48..0000000 --- a/blight-assets/src/main/resources/Models/FernPlantV2.j3o.meta +++ /dev/null @@ -1,15 +0,0 @@ -#Mon Jun 08 11:09:04 CEST 2026 -castShadow=true -category= -name=FernPlantV2 -pivotOffsetY=0.0 -placementOffsetY=0.0 -randomScaleMax=1.0 -randomScaleMin=1.0 -receiveShadow=true -scaleX=0.002 -scaleY=0.002 -scaleZ=0.002 -solid=false -tags= -uniformScale=true diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1.j3o deleted file mode 100644 index ced8c4e..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075053.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075053.j3o deleted file mode 100644 index 4ca6429..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075053.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075102.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075102.j3o deleted file mode 100644 index 7edb7bf..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075102.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075134.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075134.j3o deleted file mode 100644 index dc4841a..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075134.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075137.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075137.j3o deleted file mode 100644 index 4d1bb00..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260522_075137.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153405.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153405.j3o deleted file mode 100644 index f067a07..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153405.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153413.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153413.j3o deleted file mode 100644 index 9ebecba..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153413.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153419.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153419.j3o deleted file mode 100644 index 965b2ff..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153419.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153421.j3o b/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153421.j3o deleted file mode 100644 index 965b2ff..0000000 Binary files a/blight-assets/src/main/resources/Models/Palm_Palme1_20260524_153421.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o deleted file mode 100644 index 934683b..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o deleted file mode 100644 index db100e3..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o deleted file mode 100644 index 28e0d0b..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o deleted file mode 100644 index 0130e51..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o deleted file mode 100644 index da829fa..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o deleted file mode 100644 index de521fe..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o deleted file mode 100644 index 27f7491..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_7.j3o b/blight-assets/src/main/resources/Models/custom_mesh_7.j3o deleted file mode 100644 index 2721b89..0000000 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_7.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/imported/wooden+cabin+3d+model.j3o b/blight-assets/src/main/resources/Models/imported/wooden+cabin+3d+model.j3o new file mode 100644 index 0000000..84ffdee Binary files /dev/null and b/blight-assets/src/main/resources/Models/imported/wooden+cabin+3d+model.j3o differ diff --git a/blight-assets/src/main/resources/Models/mainchar.animmap b/blight-assets/src/main/resources/Models/mainchar.animmap deleted file mode 100644 index 3fafab0..0000000 --- a/blight-assets/src/main/resources/Models/mainchar.animmap +++ /dev/null @@ -1 +0,0 @@ -{"IDLE":"stand_up"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Models/manatrank.j3o b/blight-assets/src/main/resources/Models/manatrank.j3o deleted file mode 100644 index d08f1ca..0000000 Binary files a/blight-assets/src/main/resources/Models/manatrank.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/manatrank.j3o.meta b/blight-assets/src/main/resources/Models/manatrank.j3o.meta deleted file mode 100644 index 2b12563..0000000 --- a/blight-assets/src/main/resources/Models/manatrank.j3o.meta +++ /dev/null @@ -1,20 +0,0 @@ -#Tue Jun 09 21:42:20 CEST 2026 -castShadow=true -category= -cullDistance=120.0 -lod1Distance=30.0 -lod1Path= -lod2Distance=80.0 -lod2Path= -name=manatrank -pivotOffsetY=0.0 -placementOffsetY=0.0 -randomScaleMax=1.0 -randomScaleMin=1.0 -receiveShadow=true -scaleX=0.2 -scaleY=0.2 -scaleZ=0.2 -solid=false -tags= -uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/fern/fern_20260608_165628.j3o b/blight-assets/src/main/resources/Models/plants/fern/fern_20260608_165628.j3o deleted file mode 100644 index 28066c2..0000000 Binary files a/blight-assets/src/main/resources/Models/plants/fern/fern_20260608_165628.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/plants/fern/fern_20260608_165631.j3o b/blight-assets/src/main/resources/Models/plants/fern/fern_20260608_165631.j3o deleted file mode 100644 index e89d38f..0000000 Binary files a/blight-assets/src/main/resources/Models/plants/fern/fern_20260608_165631.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/schwachermanatrank.j3o b/blight-assets/src/main/resources/Models/schwachermanatrank.j3o deleted file mode 100644 index 91e42be..0000000 Binary files a/blight-assets/src/main/resources/Models/schwachermanatrank.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/starkermanatrank.j3o b/blight-assets/src/main/resources/Models/starkermanatrank.j3o deleted file mode 100644 index 91e42be..0000000 Binary files a/blight-assets/src/main/resources/Models/starkermanatrank.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/tree.j3o b/blight-assets/src/main/resources/Models/tree.j3o deleted file mode 100644 index 5f52977..0000000 Binary files a/blight-assets/src/main/resources/Models/tree.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/trees/oak/medium/oak_medium_20260608_200434.j3o b/blight-assets/src/main/resources/Models/trees/oak/medium/oak_medium_20260608_200434.j3o deleted file mode 100644 index e7bc320..0000000 Binary files a/blight-assets/src/main/resources/Models/trees/oak/medium/oak_medium_20260608_200434.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260615_221703.j3o b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260615_221703.j3o new file mode 100644 index 0000000..c092a0d Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260615_221703.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260615_221707.j3o b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260615_221707.j3o new file mode 100644 index 0000000..616b67e Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260615_221707.j3o differ diff --git a/blight-assets/src/main/resources/Shaders/Grass.frag b/blight-assets/src/main/resources/Shaders/Grass.frag index 3fbc02f..3635fd5 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.frag +++ b/blight-assets/src/main/resources/Shaders/Grass.frag @@ -1,10 +1,20 @@ uniform vec4 m_Color; +uniform vec3 m_SunDir; +uniform vec4 m_SunColor; +uniform vec4 g_AmbientLightColor; #ifdef HAS_COLORMAP uniform sampler2D m_ColorMap; #endif +#ifdef HAS_NORMALMAP +uniform sampler2D m_NormalMap; +in vec3 wTangent; +in vec3 wBitangent; +#endif + in vec2 texCoord; +in vec3 wNormal; out vec4 outFragColor; void main() { @@ -12,7 +22,20 @@ void main() { #ifdef HAS_COLORMAP color *= texture(m_ColorMap, texCoord); #endif - // Alpha-Discard: transparente Pixel sofort verwerfen, Z-Buffer bleibt sauber if (color.a < 0.5) discard; + + vec3 n = normalize(wNormal); + +#ifdef HAS_NORMALMAP + vec3 nm = texture(m_NormalMap, texCoord).rgb * 2.0 - 1.0; + mat3 tbn = mat3(normalize(wTangent), normalize(wBitangent), n); + n = normalize(tbn * nm); +#endif + + // Beleuchtung: Sonne (two-sided via abs für Grashalme) + Ambient + float diffuse = abs(dot(n, normalize(m_SunDir))); + vec3 lit = g_AmbientLightColor.rgb + m_SunColor.rgb * diffuse; + color.rgb *= lit; + outFragColor = color; } diff --git a/blight-assets/src/main/resources/Shaders/Grass.vert b/blight-assets/src/main/resources/Shaders/Grass.vert index 49ba07d..3e80ce9 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.vert +++ b/blight-assets/src/main/resources/Shaders/Grass.vert @@ -7,8 +7,16 @@ uniform float m_WindStrength; in vec3 inPosition; in vec2 inTexCoord; +in vec3 inNormal; +in vec4 inTangent; out vec2 texCoord; +out vec3 wNormal; + +#ifdef HAS_NORMALMAP +out vec3 wTangent; +out vec3 wBitangent; +#endif void main() { vec4 pos = vec4(inPosition, 1.0); @@ -29,5 +37,14 @@ void main() { } texCoord = inTexCoord; + + mat3 rot = mat3(g_WorldMatrix); + wNormal = normalize(rot * inNormal); + +#ifdef HAS_NORMALMAP + wTangent = normalize(rot * inTangent.xyz); + wBitangent = cross(wNormal, wTangent) * inTangent.w; +#endif + gl_Position = g_WorldViewProjectionMatrix * pos; } diff --git a/blight-assets/src/main/resources/Shaders/TerrainArray.frag b/blight-assets/src/main/resources/Shaders/TerrainArray.frag new file mode 100644 index 0000000..dc9cd91 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/TerrainArray.frag @@ -0,0 +1,133 @@ +uniform sampler2DArray m_DiffuseArray; +#ifdef DEBUG_DIRECT_TEX +uniform sampler2D m_DebugDirectTex; +#endif +uniform float m_DiffuseScales[12]; +uniform sampler2D m_AlphaMap; + +#ifdef HAS_ALPHA_1 +uniform sampler2D m_AlphaMap_1; +#endif +#ifdef HAS_ALPHA_2 +uniform sampler2D m_AlphaMap_2; +#endif + +#ifdef HAS_NORMAL_ARRAY +uniform sampler2DArray m_NormalArray; +#endif + +#ifdef HAS_LIGHTDIR +uniform vec3 m_LightDir; +#endif + +#ifdef HAS_SCENE_LIGHT +uniform vec3 m_SunColor; +uniform vec3 m_AmbientColor; +#endif + +in vec2 vSplatUV; +in vec3 vWorldPos; +in vec3 vNormal; +in vec4 vTangent; + +out vec4 outColor; + +void main() { + vec4 a0 = texture(m_AlphaMap, vSplatUV); + vec4 a1 = vec4(0.0); + vec4 a2 = vec4(0.0); +#ifdef HAS_ALPHA_1 + a1 = texture(m_AlphaMap_1, vSplatUV); +#endif +#ifdef HAS_ALPHA_2 + a2 = texture(m_AlphaMap_2, vSplatUV); +#endif + + // Slot-Mapping: + // Array[0] = Editor-Slot 1 (Basis, immer sichtbar — a0.r ist hardcoded 1.0) + // Array[1] = Editor-Slot 2, alpha = a0.g + // Array[2] = Editor-Slot 3, alpha = a0.b + // Array[3] = Editor-Slot 4, alpha = a0.a + // Array[4] = Editor-Slot 5, alpha = a1.r + // Array[5] = Editor-Slot 6, alpha = a1.g + // Array[6] = Editor-Slot 7, alpha = a1.b + // Array[7] = Editor-Slot 8, alpha = a1.a + // Array[8] = Editor-Slot 9, alpha = a2.r + // Array[9] = Editor-Slot 10, alpha = a2.g + // Array[10] = Editor-Slot 11, alpha = a2.b + // Array[11] = Editor-Slot 12, alpha = a2.a + // + // Blending: sequential mix — jeder Overlay ersetzt den Basis-Layer schrittweise. + // a0.r (Slot 1) wird vom Painting-Tool immer auf 1.0 gesetzt und dient nur als + // "Basis ist belegt"-Marker; er ist kein echter Alpha-Wert. + + float overlays[11] = float[](a0.g, a0.b, a0.a, + a1.r, a1.g, a1.b, a1.a, + a2.r, a2.g, a2.b, a2.a); + +#ifdef DEBUG_DIRECT_TEX + vec4 directCol = texture(m_DebugDirectTex, vWorldPos.xz / m_DiffuseScales[0]); + outColor = vec4(directCol.rgb, directCol.a); + return; +#endif + + // Basis (Slot 1) + vec4 col = texture(m_DiffuseArray, vec3(vWorldPos.xz / m_DiffuseScales[0], 0.0)); + +#ifdef DEBUG_SLOT0_ONLY + // Debug: nur Slot 0 anzeigen, keine Alpha-Blending-Schleife + outColor = vec4(col.rgb, col.a); + return; +#endif + + // Overlays (Slots 2-12) sequentiell darüber mischen + for (int i = 0; i < 11; i++) { + if (overlays[i] > 0.001) { + vec2 uv = vWorldPos.xz / m_DiffuseScales[i + 1]; + col = mix(col, texture(m_DiffuseArray, vec3(uv, float(i + 1))), overlays[i]); + } + } + + vec3 N = normalize(vNormal); + +#ifdef HAS_NORMAL_ARRAY + vec3 T = normalize(vTangent.xyz); + vec3 B = cross(N, T) * vTangent.w; + mat3 TBN = mat3(T, B, N); + + // Normal-Map Basis (Slot 1) + vec2 uv0 = vWorldPos.xz / m_DiffuseScales[0]; + vec3 nm0 = texture(m_NormalArray, vec3(uv0, 0.0)).rgb * 2.0 - 1.0; + vec3 pertN = TBN * nm0; + + // Normal-Maps Overlays (Slots 2-12) sequentiell mischen + for (int i = 0; i < 11; i++) { + if (overlays[i] > 0.001) { + vec2 uv = vWorldPos.xz / m_DiffuseScales[i + 1]; + vec3 nm = texture(m_NormalArray, vec3(uv, float(i + 1))).rgb * 2.0 - 1.0; + pertN = mix(pertN, TBN * nm, overlays[i]); + } + } + N = normalize(pertN); +#endif + +#ifdef HAS_LIGHTDIR + vec3 lightDir = normalize(m_LightDir); +#else + vec3 lightDir = normalize(vec3(0.6, 1.0, 0.4)); +#endif + float diff = max(dot(N, lightDir), 0.0); + +#ifdef HAS_SCENE_LIGHT + vec3 light = m_AmbientColor + m_SunColor * diff; +#else + vec3 light = vec3(diff * 0.65 + 0.35); +#endif + + const float BRIGHTNESS = 0.80; +#ifdef DEBUG_NO_LIGHT + outColor = vec4(col.rgb * BRIGHTNESS, col.a); +#else + outColor = vec4(col.rgb * light * BRIGHTNESS, col.a); +#endif +} diff --git a/blight-assets/src/main/resources/Shaders/TerrainArray.vert b/blight-assets/src/main/resources/Shaders/TerrainArray.vert new file mode 100644 index 0000000..af1b7be --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/TerrainArray.vert @@ -0,0 +1,27 @@ +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; +uniform mat4 g_ViewProjectionMatrix; + +in vec3 inPosition; +in vec3 inNormal; +in vec2 inTexCoord; +in vec4 inTangent; + +out vec2 vSplatUV; +out vec3 vWorldPos; +out vec3 vNormal; +out vec4 vTangent; + +void main() { + vec4 worldPos4 = g_WorldMatrix * vec4(inPosition, 1.0); + mat3 worldMat3 = mat3(g_WorldMatrix); + vec3 worldPos = worldPos4.xyz; + vec3 worldNorm = normalize(worldMat3 * inNormal); + + vSplatUV = inTexCoord; + vWorldPos = worldPos; + vNormal = worldNorm; + vTangent = vec4(normalize(worldMat3 * inTangent.xyz), inTangent.w); + + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); +} diff --git a/blight-assets/src/main/resources/Shaders/Tree.frag b/blight-assets/src/main/resources/Shaders/Tree.frag index 4232df7..37a8722 100644 --- a/blight-assets/src/main/resources/Shaders/Tree.frag +++ b/blight-assets/src/main/resources/Shaders/Tree.frag @@ -4,26 +4,35 @@ uniform vec4 m_Diffuse; uniform sampler2D m_BarkMap; uniform bool m_HasBarkMap; +#ifdef HAS_SCENE_LIGHT +uniform vec3 m_LightDir; +uniform vec3 m_SunColor; +uniform vec3 m_AmbientColor; +#endif + in vec2 texCoord; in vec3 worldNormal; void main() { vec3 n = normalize(worldNormal); - // Sun at ~45° elevation from SE — good contrast on vertical cylinders - vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4)); - vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4)); - vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7)); - - float sun = max(dot(n, sunDir), 0.0); - float fill = max(dot(n, fillDir), 0.0) * 0.22; - float rim = max(dot(n, rimDir), 0.0) * 0.14; - float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; // [0.0, 0.8] - float light = sun * 0.75 + fill + rim + sky * 0.09 + 0.05; - vec3 baseColor = m_HasBarkMap ? texture2D(m_BarkMap, texCoord).rgb * m_Diffuse.rgb : m_Diffuse.rgb; - gl_FragColor = vec4(baseColor * clamp(light, 0.0, 1.0), m_Diffuse.a); +#ifdef HAS_SCENE_LIGHT + float diff = max(dot(n, normalize(m_LightDir)), 0.0); + vec3 light = clamp(m_AmbientColor + m_SunColor * diff, 0.0, 1.0); +#else + vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4)); + vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4)); + vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7)); + float sun = max(dot(n, sunDir), 0.0); + float fill = max(dot(n, fillDir), 0.0) * 0.22; + float rim = max(dot(n, rimDir), 0.0) * 0.14; + float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; + vec3 light = vec3(clamp(sun * 0.75 + fill + rim + sky * 0.09 + 0.05, 0.0, 1.0)); +#endif + + gl_FragColor = vec4(baseColor * light, m_Diffuse.a); } diff --git a/blight-assets/src/main/resources/Shaders/TreeLeaf.frag b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag index d805691..2e7edbd 100644 --- a/blight-assets/src/main/resources/Shaders/TreeLeaf.frag +++ b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag @@ -4,6 +4,12 @@ uniform vec4 m_Diffuse; uniform sampler2D m_LeafMap; uniform bool m_HasLeafMap; +#ifdef HAS_SCENE_LIGHT +uniform vec3 m_LightDir; +uniform vec3 m_SunColor; +uniform vec3 m_AmbientColor; +#endif + in vec2 texCoord; in vec3 worldNormal; @@ -15,13 +21,18 @@ void main() { if (tex.a < 0.5) discard; baseColor = tex.rgb * m_Diffuse.rgb; } else { - // Fallback: kreisförmiger Clip vec2 uv = texCoord * 2.0 - 1.0; if (dot(uv, uv) > 0.95) discard; float edge = 1.0 - dot(uv, uv); baseColor = m_Diffuse.rgb * (0.7 + 0.3 * edge); } - // Leaves transmit light — no directional shading, uniform brightness - gl_FragColor = vec4(baseColor, 1.0); +#ifdef HAS_SCENE_LIGHT + // Blätter transmittieren Licht — abs() für doppelseitige Beleuchtung + float diff = abs(dot(normalize(worldNormal), normalize(m_LightDir))); + vec3 light = clamp(m_AmbientColor + m_SunColor * diff * 0.6, 0.0, 1.0); + gl_FragColor = vec4(baseColor * light, 1.0); +#else + gl_FragColor = vec4(baseColor * 0.8, 1.0); +#endif } diff --git a/blight-assets/src/main/resources/Shaders/Voxel.frag b/blight-assets/src/main/resources/Shaders/Voxel.frag index f7b33b3..891bd0c 100644 --- a/blight-assets/src/main/resources/Shaders/Voxel.frag +++ b/blight-assets/src/main/resources/Shaders/Voxel.frag @@ -1,54 +1,125 @@ -uniform sampler2D m_Tex0; -uniform sampler2D m_Tex1; -uniform sampler2D m_Tex2; -uniform sampler2D m_Tex3; +uniform sampler2D m_TexFlat; +uniform sampler2D m_TexSteep; +uniform sampler2D m_TexCeil; uniform float m_TexScale; -in vec3 vWorldPos; -in vec3 vNormal; -in vec4 vMatWeights; +#ifdef HAS_NM_FLAT +uniform sampler2D m_NormalMapFlat; +#endif +#ifdef HAS_NM_STEEP +uniform sampler2D m_NormalMapSteep; +#endif +#ifdef HAS_NM_CEIL +uniform sampler2D m_NormalMapCeil; +#endif + +in vec3 vWorldPos; +in vec3 vNormal; out vec4 outColor; +#ifdef HAS_LIGHTDIR +uniform vec3 m_LightDir; +#endif +#ifdef HAS_SCENE_LIGHT +uniform vec3 m_SunColor; +uniform vec3 m_AmbientColor; +#endif + +vec4 triplanar(sampler2D tex, vec2 uvX, vec2 uvY, vec2 uvZ, vec3 bw) { + return texture(tex, uvX) * bw.x + + texture(tex, uvY) * bw.y + + texture(tex, uvZ) * bw.z; +} + +// Triplanare Normal-Map-Mischung (Whiteout-Blending, world-space Ausgabe). +// N = geometrische Weltoberflächen-Normale, bw = triplanare Gewichte. +vec3 triplanarNormal(sampler2D nmap, vec2 uvX, vec2 uvY, vec2 uvZ, vec3 bw, vec3 N) { + vec3 tnX = texture(nmap, uvX).rgb * 2.0 - 1.0; + vec3 tnY = texture(nmap, uvY).rgb * 2.0 - 1.0; + vec3 tnZ = texture(nmap, uvZ).rgb * 2.0 - 1.0; + + // Reorientiertes Normal-Mapping (RNM / Whiteout): Tangent → World + tnX = vec3(tnX.xy + N.zy, abs(tnX.z) * N.x); + tnY = vec3(tnY.xy + N.xz, abs(tnY.z) * N.y); + tnZ = vec3(tnZ.xy + N.xy, abs(tnZ.z) * N.z); + + return normalize(tnX.zyx * bw.x + tnY.xzy * bw.y + tnZ.xyz * bw.z); +} + void main() { - vec3 blendWeights = abs(vNormal); - blendWeights = max(blendWeights - 0.2, 0.0); - blendWeights /= (blendWeights.x + blendWeights.y + blendWeights.z + 0.001); + vec3 bw = pow(abs(vNormal), vec3(4.0)); + bw /= (bw.x + bw.y + bw.z + 0.001); vec2 uvX = vWorldPos.yz / m_TexScale; vec2 uvY = vWorldPos.xz / m_TexScale; vec2 uvZ = vWorldPos.xy / m_TexScale; - vec4 col = vec4(0.0); + // Flach ab ~11° Gefälle (20% grade, normal.y≈0.98); Fels darunter. + float flatBlend = smoothstep(0.94, 0.99, vNormal.y); + float ceilBlend = 1.0 - smoothstep(-0.6, -0.3, vNormal.y); + float steepBlend = max(0.0, 1.0 - flatBlend - ceilBlend); - float w0 = vMatWeights.r; - if (w0 > 0.001) { - col += w0 * (texture(m_Tex0, uvX) * blendWeights.x - + texture(m_Tex0, uvY) * blendWeights.y - + texture(m_Tex0, uvZ) * blendWeights.z); - } - float w1 = vMatWeights.g; - if (w1 > 0.001) { - col += w1 * (texture(m_Tex1, uvX) * blendWeights.x - + texture(m_Tex1, uvY) * blendWeights.y - + texture(m_Tex1, uvZ) * blendWeights.z); - } - float w2 = vMatWeights.b; - if (w2 > 0.001) { - col += w2 * (texture(m_Tex2, uvX) * blendWeights.x - + texture(m_Tex2, uvY) * blendWeights.y - + texture(m_Tex2, uvZ) * blendWeights.z); - } - float w3 = vMatWeights.a; - if (w3 > 0.001) { - col += w3 * (texture(m_Tex3, uvX) * blendWeights.x - + texture(m_Tex3, uvY) * blendWeights.y - + texture(m_Tex3, uvZ) * blendWeights.z); - } + // Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar. + // Steep/Ceil: Triplanar bleibt, da es dort keine eindeutige Projektion gibt. + vec4 col = texture(m_TexFlat, uvY) * flatBlend + + triplanar(m_TexSteep, uvX, uvY, uvZ, bw) * steepBlend + + triplanar(m_TexCeil, uvX, uvY, uvZ, bw) * ceilBlend; - vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3)); - float diff = max(dot(vNormal, lightDir), 0.0) * 0.7 + 0.3; + // Geometrie-Normale für Beleuchtung, ggf. durch Normal-Map ersetzt. + vec3 N = normalize(vNormal); - outColor = vec4(col.rgb * diff, col.a); +#if defined(HAS_NM_FLAT) || defined(HAS_NM_STEEP) || defined(HAS_NM_CEIL) + vec3 pertN = vec3(0.0); + float totalBlend = 0.0; +#ifdef HAS_NM_FLAT + if (flatBlend > 0.001) { + vec3 nmFlat = texture(m_NormalMapFlat, uvY).rgb * 2.0 - 1.0; + // Y-Projektion RNM (identisch mit triplanarNormal Y-Achse bei bw.y=1) + nmFlat = vec3(nmFlat.xy + N.xz, abs(nmFlat.z) * N.y); + pertN += normalize(nmFlat.xzy) * flatBlend; + totalBlend += flatBlend; + } +#endif +#ifdef HAS_NM_STEEP + if (steepBlend > 0.001) { + pertN += triplanarNormal(m_NormalMapSteep, uvX, uvY, uvZ, bw, N) * steepBlend; + totalBlend += steepBlend; + } +#endif +#ifdef HAS_NM_CEIL + if (ceilBlend > 0.001) { + pertN += triplanarNormal(m_NormalMapCeil, uvX, uvY, uvZ, bw, N) * ceilBlend; + totalBlend += ceilBlend; + } +#endif + if (totalBlend > 0.001) { + N = normalize(pertN / totalBlend); + } +#endif + + // Flache Voxel-Flächen Richtung (0,1,0) korrigieren, um Abweichungen der + // Marching-Cubes-Normalen auszugleichen und das Terrain-Lighting zu matchen. + N = normalize(mix(N, vec3(0.0, 1.0, 0.0), flatBlend)); + +#ifdef HAS_LIGHTDIR + vec3 lightDir = normalize(m_LightDir); +#else + vec3 lightDir = normalize(vec3(0.6, 1.0, 0.4)); +#endif + float diff = max(dot(N, lightDir), 0.0); + +#ifdef HAS_SCENE_LIGHT + vec3 light = m_AmbientColor + m_SunColor * diff; +#else + vec3 light = vec3(diff * 0.65 + 0.35); +#endif + + const float BRIGHTNESS = 0.80; +#ifdef DEBUG_NO_LIGHT + outColor = vec4(col.rgb * BRIGHTNESS, col.a); +#else + outColor = vec4(col.rgb * light * BRIGHTNESS, col.a); +#endif if (outColor.a < 0.1) discard; } diff --git a/blight-assets/src/main/resources/Shaders/Voxel.tsctrl b/blight-assets/src/main/resources/Shaders/Voxel.tsctrl new file mode 100644 index 0000000..54b5778 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Voxel.tsctrl @@ -0,0 +1,29 @@ +layout(vertices = 3) out; + +in vec3 vWorldPos[]; +in vec3 vNormal[]; + +out vec3 tcWorldPos[]; +out vec3 tcNormal[]; + +uniform float m_TessellationLevel; +uniform vec3 g_CameraPosition; + +void main() { + tcWorldPos[gl_InvocationID] = vWorldPos[gl_InvocationID]; + tcNormal[gl_InvocationID] = vNormal[gl_InvocationID]; + + if (gl_InvocationID == 0) { + vec3 center = (vWorldPos[0] + vWorldPos[1] + vWorldPos[2]) / 3.0; + float dist = distance(g_CameraPosition, center); + + // Maximum subdivision close-up, falls to 1 at 64 m + float level = mix(m_TessellationLevel, 1.0, clamp(dist / 64.0, 0.0, 1.0)); + level = max(1.0, level); + + gl_TessLevelOuter[0] = level; + gl_TessLevelOuter[1] = level; + gl_TessLevelOuter[2] = level; + gl_TessLevelInner[0] = level; + } +} diff --git a/blight-assets/src/main/resources/Shaders/Voxel.tseval b/blight-assets/src/main/resources/Shaders/Voxel.tseval new file mode 100644 index 0000000..68d25a1 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Voxel.tseval @@ -0,0 +1,82 @@ +layout(triangles, equal_spacing, ccw) in; + +in vec3 tcWorldPos[]; +in vec3 tcNormal[]; + +out vec3 vWorldPos; +out vec3 vNormal; + +uniform mat4 g_ViewProjectionMatrix; +uniform float m_TexScale; +uniform float m_DisplacementScale; + +#ifdef HAS_DISP_FLAT +uniform sampler2D m_DisplacementMapFlat; +#endif +#ifdef HAS_DISP_STEEP +uniform sampler2D m_DisplacementMapSteep; +#endif +#ifdef HAS_DISP_CEIL +uniform sampler2D m_DisplacementMapCeil; +#endif + +void main() { + vec3 bc = gl_TessCoord; + + // Interpolate position and normal from patch vertices + vec3 pos = bc.x * tcWorldPos[0] + bc.y * tcWorldPos[1] + bc.z * tcWorldPos[2]; + vec3 nor = normalize(bc.x * tcNormal[0] + bc.y * tcNormal[1] + bc.z * tcNormal[2]); + + // Triplanar blend weights (same as fragment shader) + vec3 bw = pow(abs(nor), vec3(4.0)); + bw /= (bw.x + bw.y + bw.z + 0.001); + vec2 uvX = pos.yz / m_TexScale; + vec2 uvY = pos.xz / m_TexScale; + vec2 uvZ = pos.xy / m_TexScale; + + float flatBlend = smoothstep(0.94, 0.99, nor.y); + float ceilBlend = 1.0 - smoothstep(-0.6, -0.3, nor.y); + float steepBlend = max(0.0, 1.0 - flatBlend - ceilBlend); + +#if defined(HAS_DISP_FLAT) || defined(HAS_DISP_STEEP) || defined(HAS_DISP_CEIL) + float disp = 0.0; + float totalW = 0.0; + +#ifdef HAS_DISP_FLAT + if (flatBlend > 0.001) { + float d = texture(m_DisplacementMapFlat, uvX).r * bw.x + + texture(m_DisplacementMapFlat, uvY).r * bw.y + + texture(m_DisplacementMapFlat, uvZ).r * bw.z; + disp += d * flatBlend; + totalW += flatBlend; + } +#endif +#ifdef HAS_DISP_STEEP + if (steepBlend > 0.001) { + float d = texture(m_DisplacementMapSteep, uvX).r * bw.x + + texture(m_DisplacementMapSteep, uvY).r * bw.y + + texture(m_DisplacementMapSteep, uvZ).r * bw.z; + disp += d * steepBlend; + totalW += steepBlend; + } +#endif +#ifdef HAS_DISP_CEIL + if (ceilBlend > 0.001) { + float d = texture(m_DisplacementMapCeil, uvX).r * bw.x + + texture(m_DisplacementMapCeil, uvY).r * bw.y + + texture(m_DisplacementMapCeil, uvZ).r * bw.z; + disp += d * ceilBlend; + totalW += ceilBlend; + } +#endif + + if (totalW > 0.001) disp /= totalW; + + // 0.5 = neutral (no displacement), 0 = sink, 1 = raise + pos += nor * ((disp - 0.5) * m_DisplacementScale); +#endif + + vWorldPos = pos; + vNormal = nor; + gl_Position = g_ViewProjectionMatrix * vec4(pos, 1.0); +} diff --git a/blight-assets/src/main/resources/Shaders/Voxel.vert b/blight-assets/src/main/resources/Shaders/Voxel.vert index f92e564..b06796d 100644 --- a/blight-assets/src/main/resources/Shaders/Voxel.vert +++ b/blight-assets/src/main/resources/Shaders/Voxel.vert @@ -1,19 +1,15 @@ uniform mat4 g_WorldViewProjectionMatrix; uniform mat4 g_WorldMatrix; -uniform mat3 g_NormalMatrix; in vec3 inPosition; in vec3 inNormal; -in vec4 inColor; // material blend weights (r=tex0, g=tex1, b=tex2, a=tex3) out vec3 vWorldPos; out vec3 vNormal; -out vec4 vMatWeights; void main() { - vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); - vWorldPos = worldPos.xyz; - vNormal = normalize(g_NormalMatrix * inNormal); - vMatWeights = inColor; - gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); + vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); + vWorldPos = worldPos.xyz; + vNormal = normalize(mat3(g_WorldMatrix) * inNormal); + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); } diff --git a/blight-assets/src/main/resources/Shaders/VoxelTess.vert b/blight-assets/src/main/resources/Shaders/VoxelTess.vert new file mode 100644 index 0000000..8852ae0 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/VoxelTess.vert @@ -0,0 +1,15 @@ +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; + +in vec3 inPosition; +in vec3 inNormal; + +out vec3 vWorldPos; +out vec3 vNormal; + +void main() { + vec4 wpos = g_WorldMatrix * vec4(inPosition, 1.0); + vWorldPos = wpos.xyz; + vNormal = normalize(mat3(g_WorldMatrix) * inNormal); + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); +} diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Gravel040_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Gravel040_1K-PNG_Color.json new file mode 100644 index 0000000..4fb57d9 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Gravel040_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/gravel1/Gravel040_1K-PNG_NormalGL.png","displacementMap":"Textures/gravel1/Gravel040_1K-PNG_Displacement.png","roughnessMap":"Textures/gravel1/Gravel040_1K-PNG_Roughness.png","aoMap":"Textures/gravel1/Gravel040_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground003_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground003_1K-PNG_Color.json new file mode 100644 index 0000000..e8a41f4 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground003_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/gras2/Ground003_1K-PNG_NormalGL.png","displacementMap":"Textures/gras2/Ground003_1K-PNG_Displacement.png","roughnessMap":"Textures/gras2/Ground003_1K-PNG_Roughness.png","aoMap":""} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground019_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground019_1K-PNG_Color.json new file mode 100644 index 0000000..afa51b2 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground019_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/leaves2/Ground019_1K-PNG_NormalGL.png","displacementMap":"Textures/leaves2/Ground019_1K-PNG_Displacement.png","roughnessMap":"Textures/leaves2/Ground019_1K-PNG_Roughness.png","aoMap":"Textures/leaves2/Ground019_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground023_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground023_1K-PNG_Color.json new file mode 100644 index 0000000..9350b99 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground023_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/leaves1/Ground023_1K-PNG_NormalGL.png","displacementMap":"Textures/leaves1/Ground023_1K-PNG_Displacement.png","roughnessMap":"Textures/leaves1/Ground023_1K-PNG_Roughness.png","aoMap":"Textures/leaves1/Ground023_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground037_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground037_1K-PNG_Color.json new file mode 100644 index 0000000..c13c4bf --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground037_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/gras1/Ground037_1K-PNG_NormalGL.png","displacementMap":"Textures/gras1/Ground037_1K-PNG_Displacement.png","roughnessMap":"Textures/gras1/Ground037_1K-PNG_Roughness.png","aoMap":"Textures/gras1/Ground037_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground048_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground048_1K-PNG_Color.json new file mode 100644 index 0000000..85bd477 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground048_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/dirt1/Ground048_1K-PNG_NormalGL.png","displacementMap":"Textures/dirt1/Ground048_1K-PNG_Displacement.png","roughnessMap":"Textures/dirt1/Ground048_1K-PNG_Roughness.png","aoMap":"Textures/dirt1/Ground048_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground054_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground054_1K-PNG_Color.json new file mode 100644 index 0000000..8c16b34 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground054_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/sand1/Ground054_1K-PNG_NormalGL.png","displacementMap":"Textures/sand1/Ground054_1K-PNG_Displacement.png","roughnessMap":"Textures/sand1/Ground054_1K-PNG_Roughness.png","aoMap":"Textures/sand1/Ground054_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground060_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground060_1K-PNG_Color.json new file mode 100644 index 0000000..8ea9289 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground060_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/sand2/Ground060_1K-PNG_NormalGL.png","displacementMap":"Textures/sand2/Ground060_1K-PNG_Displacement.png","roughnessMap":"Textures/sand2/Ground060_1K-PNG_Roughness.png","aoMap":"Textures/sand2/Ground060_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground068_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground068_1K-PNG_Color.json new file mode 100644 index 0000000..89ef654 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground068_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/gras3/Ground068_1K-PNG_NormalGL.png","displacementMap":"Textures/gras3/Ground068_1K-PNG_Displacement.png","roughnessMap":"Textures/gras3/Ground068_1K-PNG_Roughness.png","aoMap":"Textures/gras3/Ground068_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Ground079S_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Ground079S_1K-PNG_Color.json new file mode 100644 index 0000000..bd4537e --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Ground079S_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/dirt2/Ground079S_1K-PNG_NormalGL.png","displacementMap":"Textures/dirt2/Ground079S_1K-PNG_Displacement.png","roughnessMap":"Textures/dirt2/Ground079S_1K-PNG_Roughness.png","aoMap":"Textures/dirt2/Ground079S_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Rock030_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Rock030_1K-PNG_Color.json new file mode 100644 index 0000000..1fc2cc4 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Rock030_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/rock2/Rock030_1K-PNG_NormalGL.png","displacementMap":"Textures/rock2/Rock030_1K-PNG_Displacement.png","roughnessMap":"Textures/rock2/Rock030_1K-PNG_Roughness.png","aoMap":"Textures/rock2/Rock030_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/.mapdata/Rock033_1K-PNG_Color.json b/blight-assets/src/main/resources/Textures/.mapdata/Rock033_1K-PNG_Color.json new file mode 100644 index 0000000..bcfe220 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/.mapdata/Rock033_1K-PNG_Color.json @@ -0,0 +1 @@ +{"normalMap":"Textures/rock1/Rock033_1K-PNG_NormalGL.png","displacementMap":"Textures/rock1/Rock033_1K-PNG_Displacement.png","roughnessMap":"Textures/rock1/Rock033_1K-PNG_Roughness.png","aoMap":"Textures/rock1/Rock033_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/fern/Fern02_Diffuse.tga b/blight-assets/src/main/resources/Textures/fern/Fern02_Diffuse.tga deleted file mode 100644 index f54f5e0..0000000 Binary files a/blight-assets/src/main/resources/Textures/fern/Fern02_Diffuse.tga and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/fern/Fern02_Normal.tga b/blight-assets/src/main/resources/Textures/fern/Fern02_Normal.tga deleted file mode 100644 index 6fd6e0b..0000000 Binary files a/blight-assets/src/main/resources/Textures/fern/Fern02_Normal.tga and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/fern/Fern02_Specular.tga b/blight-assets/src/main/resources/Textures/fern/Fern02_Specular.tga deleted file mode 100644 index d440482..0000000 Binary files a/blight-assets/src/main/resources/Textures/fern/Fern02_Specular.tga and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/gras.png b/blight-assets/src/main/resources/Textures/gras.png deleted file mode 100644 index 2f131cb..0000000 Binary files a/blight-assets/src/main/resources/Textures/gras.png and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/gras/vegetation_grass_card_03.png b/blight-assets/src/main/resources/Textures/gras/vegetation_grass_card_03.png deleted file mode 100644 index aca6e12..0000000 Binary files a/blight-assets/src/main/resources/Textures/gras/vegetation_grass_card_03.png and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_Color.jpg b/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_Color.jpg deleted file mode 100644 index 64ebdb3..0000000 Binary files a/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_Color.jpg and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_NormalGL.jpg b/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_NormalGL.jpg deleted file mode 100644 index e1237cc..0000000 Binary files a/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_NormalGL.jpg and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_Color.jpg b/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_Color.jpg deleted file mode 100644 index 4584efb..0000000 Binary files a/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_Color.jpg and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_NormalDX.jpg b/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_NormalDX.jpg deleted file mode 100644 index e4d12e0..0000000 Binary files a/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_NormalDX.jpg and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/Rocks015_1K-JPG_Color.jpg b/blight-assets/src/main/resources/Textures/ground/Rocks015_1K-JPG_Color.jpg deleted file mode 100644 index 5a1bf1c..0000000 Binary files a/blight-assets/src/main/resources/Textures/ground/Rocks015_1K-JPG_Color.jpg and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/Rocks015_1K-JPG_NormalDX.jpg b/blight-assets/src/main/resources/Textures/ground/Rocks015_1K-JPG_NormalDX.jpg deleted file mode 100644 index 83d4b84..0000000 Binary files a/blight-assets/src/main/resources/Textures/ground/Rocks015_1K-JPG_NormalDX.jpg and /dev/null differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048.png new file mode 100644 index 0000000..9f5dc31 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..1371cec Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Color.png new file mode 100644 index 0000000..1ba95ae Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Displacement.png new file mode 100644 index 0000000..0fdefef Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_NormalDX.png new file mode 100644 index 0000000..b92c600 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_NormalGL.png new file mode 100644 index 0000000..f77bbfe Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Roughness.png new file mode 100644 index 0000000..bfcb317 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt1/Ground048_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt1/textureset.json b/blight-assets/src/main/resources/Textures/ground/dirt1/textureset.json new file mode 100644 index 0000000..52cb258 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/dirt1/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/dirt1/Ground048_1K-PNG_Color.png", "normalMap": "Textures/ground/dirt1/Ground048_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/dirt1/Ground048_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/dirt1/Ground048_1K-PNG_Roughness.png", "aoMap": "Textures/ground/dirt1/Ground048_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S.png new file mode 100644 index 0000000..e7385fb Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..717e9fa Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Color.png new file mode 100644 index 0000000..1ea84aa Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Displacement.png new file mode 100644 index 0000000..a1678f5 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_NormalDX.png new file mode 100644 index 0000000..42772d8 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_NormalGL.png new file mode 100644 index 0000000..93fec3a Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Roughness.png new file mode 100644 index 0000000..8579e98 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/dirt2/Ground079S_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/dirt2/textureset.json b/blight-assets/src/main/resources/Textures/ground/dirt2/textureset.json new file mode 100644 index 0000000..6754ba1 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/dirt2/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/dirt2/Ground079S_1K-PNG_Color.png", "normalMap": "Textures/ground/dirt2/Ground079S_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/dirt2/Ground079S_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/dirt2/Ground079S_1K-PNG_Roughness.png", "aoMap": "Textures/ground/dirt2/Ground079S_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037.png new file mode 100644 index 0000000..3d40e84 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..fbd9f30 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Color.png new file mode 100644 index 0000000..43ca3e3 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Displacement.png new file mode 100644 index 0000000..d8381bf Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_NormalDX.png new file mode 100644 index 0000000..2f7e135 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_NormalGL.png new file mode 100644 index 0000000..089265d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Roughness.png new file mode 100644 index 0000000..f6d0d81 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras1/Ground037_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras1/textureset.json b/blight-assets/src/main/resources/Textures/ground/gras1/textureset.json new file mode 100644 index 0000000..9be2a1c --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/gras1/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/gras1/Ground037_1K-PNG_Color.png", "normalMap": "Textures/ground/gras1/Ground037_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/gras1/Ground037_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/gras1/Ground037_1K-PNG_Roughness.png", "aoMap": "Textures/ground/gras1/Ground037_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/Ground003.png b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003.png new file mode 100644 index 0000000..6969728 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Color.png new file mode 100644 index 0000000..7091657 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Displacement.png new file mode 100644 index 0000000..e2946de Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_NormalDX.png new file mode 100644 index 0000000..4c9fe2e Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_NormalGL.png new file mode 100644 index 0000000..fc14144 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Roughness.png new file mode 100644 index 0000000..2bab270 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras2/Ground003_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras2/textureset.json b/blight-assets/src/main/resources/Textures/ground/gras2/textureset.json new file mode 100644 index 0000000..388ff7a --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/gras2/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/gras2/Ground003_1K-PNG_Color.png", "normalMap": "Textures/ground/gras2/Ground003_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/gras2/Ground003_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/gras2/Ground003_1K-PNG_Roughness.png", "aoMap": ""} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068.png new file mode 100644 index 0000000..9eb2b1c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..15e4c02 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Color.png new file mode 100644 index 0000000..71400b6 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Displacement.png new file mode 100644 index 0000000..c416d5f Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_NormalDX.png new file mode 100644 index 0000000..074f60a Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_NormalGL.png new file mode 100644 index 0000000..3d6b924 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Roughness.png new file mode 100644 index 0000000..6b50eb3 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gras3/Ground068_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gras3/textureset.json b/blight-assets/src/main/resources/Textures/ground/gras3/textureset.json new file mode 100644 index 0000000..a68093e --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/gras3/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/gras3/Ground068_1K-PNG_Color.png", "normalMap": "Textures/ground/gras3/Ground068_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/gras3/Ground068_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/gras3/Ground068_1K-PNG_Roughness.png", "aoMap": "Textures/ground/gras3/Ground068_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040.png new file mode 100644 index 0000000..e1a7177 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..6c993a8 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Color.png new file mode 100644 index 0000000..9bdf3b2 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Displacement.png new file mode 100644 index 0000000..62afd29 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_NormalDX.png new file mode 100644 index 0000000..b4f4fb6 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_NormalGL.png new file mode 100644 index 0000000..f26ad07 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Roughness.png new file mode 100644 index 0000000..9c1181b Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/gravel1/Gravel040_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/gravel1/textureset.json b/blight-assets/src/main/resources/Textures/ground/gravel1/textureset.json new file mode 100644 index 0000000..1060287 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/gravel1/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/gravel1/Gravel040_1K-PNG_Color.png", "normalMap": "Textures/ground/gravel1/Gravel040_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/gravel1/Gravel040_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/gravel1/Gravel040_1K-PNG_Roughness.png", "aoMap": "Textures/ground/gravel1/Gravel040_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023.png new file mode 100644 index 0000000..ed95dd6 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..30529ef Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Color.png new file mode 100644 index 0000000..93f969f Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Displacement.png new file mode 100644 index 0000000..0d74425 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_NormalDX.png new file mode 100644 index 0000000..3094203 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_NormalGL.png new file mode 100644 index 0000000..2f703e0 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Roughness.png new file mode 100644 index 0000000..0a0ad6f Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves1/Ground023_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves1/textureset.json b/blight-assets/src/main/resources/Textures/ground/leaves1/textureset.json new file mode 100644 index 0000000..981d3e0 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/leaves1/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/leaves1/Ground023_1K-PNG_Color.png", "normalMap": "Textures/ground/leaves1/Ground023_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/leaves1/Ground023_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/leaves1/Ground023_1K-PNG_Roughness.png", "aoMap": "Textures/ground/leaves1/Ground023_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019.png new file mode 100644 index 0000000..8235b34 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..937f08f Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Color.png new file mode 100644 index 0000000..9cd1c46 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Displacement.png new file mode 100644 index 0000000..c825f09 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_NormalDX.png new file mode 100644 index 0000000..5351643 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_NormalGL.png new file mode 100644 index 0000000..fc7adb4 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Roughness.png new file mode 100644 index 0000000..e48d5aa Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/leaves2/Ground019_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/leaves2/textureset.json b/blight-assets/src/main/resources/Textures/ground/leaves2/textureset.json new file mode 100644 index 0000000..be317e3 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/leaves2/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/leaves2/Ground019_1K-PNG_Color.png", "normalMap": "Textures/ground/leaves2/Ground019_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/leaves2/Ground019_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/leaves2/Ground019_1K-PNG_Roughness.png", "aoMap": "Textures/ground/leaves2/Ground019_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141.png new file mode 100644 index 0000000..6023c4d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..c98be55 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Color.png new file mode 100644 index 0000000..f66144d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Displacement.png new file mode 100644 index 0000000..35b42de Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_NormalDX.png new file mode 100644 index 0000000..37c27e8 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_NormalGL.png new file mode 100644 index 0000000..772b06c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Roughness.png new file mode 100644 index 0000000..e0d2843 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/pavement1/PavingStones141_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/pavement1/textureset.json b/blight-assets/src/main/resources/Textures/ground/pavement1/textureset.json new file mode 100644 index 0000000..56b43a0 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/pavement1/textureset.json @@ -0,0 +1 @@ +{"colorMap":"Textures/pavement1/PavingStones141_1K-PNG_Color.png","normalMap":"Textures/pavement1/PavingStones141_1K-PNG_NormalGL.png","displacementMap":"Textures/pavement1/PavingStones141_1K-PNG_Displacement.png","roughnessMap":"Textures/pavement1/PavingStones141_1K-PNG_Roughness.png","aoMap":"Textures/pavement1/PavingStones141_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033.png new file mode 100644 index 0000000..e62ef4c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..469c90c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Color.png new file mode 100644 index 0000000..3b33229 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Displacement.png new file mode 100644 index 0000000..f8b09bf Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_NormalDX.png new file mode 100644 index 0000000..be938a2 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_NormalGL.png new file mode 100644 index 0000000..12ca202 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Roughness.png new file mode 100644 index 0000000..8a79eb2 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock1/Rock033_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock1/textureset.json b/blight-assets/src/main/resources/Textures/ground/rock1/textureset.json new file mode 100644 index 0000000..fad17b8 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/rock1/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/rock1/Rock033_1K-PNG_Color.png", "normalMap": "Textures/ground/rock1/Rock033_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/rock1/Rock033_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/rock1/Rock033_1K-PNG_Roughness.png", "aoMap": "Textures/ground/rock1/Rock033_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030.png new file mode 100644 index 0000000..1b04e9c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..eabb433 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Color.png new file mode 100644 index 0000000..3c448b1 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Displacement.png new file mode 100644 index 0000000..c9303ee Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_NormalDX.png new file mode 100644 index 0000000..c5aa679 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_NormalGL.png new file mode 100644 index 0000000..f83596d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Roughness.png new file mode 100644 index 0000000..6670e01 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/rock2/Rock030_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/rock2/textureset.json b/blight-assets/src/main/resources/Textures/ground/rock2/textureset.json new file mode 100644 index 0000000..e55aff8 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/rock2/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/rock2/Rock030_1K-PNG_Color.png", "normalMap": "Textures/ground/rock2/Rock030_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/rock2/Rock030_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/rock2/Rock030_1K-PNG_Roughness.png", "aoMap": "Textures/ground/rock2/Rock030_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054.png new file mode 100644 index 0000000..bd2cd72 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..7ad7a03 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Color.png new file mode 100644 index 0000000..d61deb1 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Displacement.png new file mode 100644 index 0000000..e2989c6 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_NormalDX.png new file mode 100644 index 0000000..63dc4dc Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_NormalGL.png new file mode 100644 index 0000000..b19eaa7 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Roughness.png new file mode 100644 index 0000000..9aa6077 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand1/Ground054_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand1/textureset.json b/blight-assets/src/main/resources/Textures/ground/sand1/textureset.json new file mode 100644 index 0000000..c68d08b --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/sand1/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/sand1/Ground054_1K-PNG_Color.png", "normalMap": "Textures/ground/sand1/Ground054_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/sand1/Ground054_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/sand1/Ground054_1K-PNG_Roughness.png", "aoMap": "Textures/ground/sand1/Ground054_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060.png new file mode 100644 index 0000000..87e87ad Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_AmbientOcclusion.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_AmbientOcclusion.png new file mode 100644 index 0000000..1b4dbbc Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_AmbientOcclusion.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Color.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Color.png new file mode 100644 index 0000000..eaa5100 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Displacement.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Displacement.png new file mode 100644 index 0000000..fd6aad4 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Displacement.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_NormalDX.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_NormalDX.png new file mode 100644 index 0000000..ae58d58 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_NormalDX.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_NormalGL.png new file mode 100644 index 0000000..aa139df Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Roughness.png b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Roughness.png new file mode 100644 index 0000000..c762a32 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/sand2/Ground060_1K-PNG_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/sand2/textureset.json b/blight-assets/src/main/resources/Textures/ground/sand2/textureset.json new file mode 100644 index 0000000..feac355 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/sand2/textureset.json @@ -0,0 +1 @@ +{"colorMap": "Textures/ground/sand2/Ground060_1K-PNG_Color.png", "normalMap": "Textures/ground/sand2/Ground060_1K-PNG_NormalGL.png", "displacementMap": "Textures/ground/sand2/Ground060_1K-PNG_Displacement.png", "roughnessMap": "Textures/ground/sand2/Ground060_1K-PNG_Roughness.png", "aoMap": "Textures/ground/sand2/Ground060_1K-PNG_AmbientOcclusion.png"} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/gras/gras1.png b/blight-assets/src/main/resources/Textures/ground/vegetation/gras1.png similarity index 100% rename from blight-assets/src/main/resources/Textures/gras/gras1.png rename to blight-assets/src/main/resources/Textures/ground/vegetation/gras1.png diff --git a/blight-assets/src/main/resources/Textures/gras/gras2.png b/blight-assets/src/main/resources/Textures/ground/vegetation/gras2.png similarity index 100% rename from blight-assets/src/main/resources/Textures/gras/gras2.png rename to blight-assets/src/main/resources/Textures/ground/vegetation/gras2.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Color.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Color.png new file mode 100644 index 0000000..c4c5f05 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_NormalGL.png new file mode 100644 index 0000000..f6d5b45 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Roughness.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Roughness.png new file mode 100644 index 0000000..6139dfe Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/textureset.json b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/textureset.json new file mode 100644 index 0000000..f4f9cee --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/textureset.json @@ -0,0 +1,7 @@ +{ + "colorMap": "Textures/ground/plant1/plant1_Color.png", + "normalMap": "Textures/ground/plant1/plant1_NormalGL.png", + "displacementMap": "", + "roughnessMap": "Textures/ground/plant1/plant1_Roughness.png", + "aoMap": "" +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Color.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Color.png new file mode 100644 index 0000000..f784768 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_NormalGL.png new file mode 100644 index 0000000..a511617 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Roughness.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Roughness.png new file mode 100644 index 0000000..e01080e Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/textureset.json b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/textureset.json new file mode 100644 index 0000000..20e22a7 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/textureset.json @@ -0,0 +1,7 @@ +{ + "colorMap": "Textures/vegetation/plant2/plant2_Color.png", + "normalMap": "Textures/vegetation/plant2/plant2_NormalGL.png", + "displacementMap": "", + "roughnessMap": "Textures/vegetation/plant2/plant2_Roughness.png", + "aoMap": "" +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Color.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Color.png new file mode 100644 index 0000000..245c253 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_NormalGL.png new file mode 100644 index 0000000..c290247 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Roughness.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Roughness.png new file mode 100644 index 0000000..bf3dbab Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/textureset.json b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/textureset.json new file mode 100644 index 0000000..f01e2b8 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/textureset.json @@ -0,0 +1,7 @@ +{ + "colorMap": "Textures/vegetation/plant3/plant3_Color.png", + "normalMap": "Textures/vegetation/plant3/plant3_NormalGL.png", + "displacementMap": "", + "roughnessMap": "Textures/vegetation/plant3/plant3_Roughness.png", + "aoMap": "" +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Color.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Color.png new file mode 100644 index 0000000..bea1781 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_NormalGL.png new file mode 100644 index 0000000..52fe615 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Roughness.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Roughness.png new file mode 100644 index 0000000..488164d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/textureset.json b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/textureset.json new file mode 100644 index 0000000..d29b9f2 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/textureset.json @@ -0,0 +1,7 @@ +{ + "colorMap": "Textures/ground/plant4/plant4_Color.png", + "normalMap": "Textures/ground/plant4/plant4_NormalGL.png", + "displacementMap": "", + "roughnessMap": "Textures/ground/plant4/plant4_Roughness.png", + "aoMap": "" +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Color.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Color.png new file mode 100644 index 0000000..3ee6aeb Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Color.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_NormalGL.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_NormalGL.png new file mode 100644 index 0000000..5c8ce53 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_NormalGL.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Roughness.png b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Roughness.png new file mode 100644 index 0000000..aa00a2e Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Roughness.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/textureset.json b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/textureset.json new file mode 100644 index 0000000..e3e8752 --- /dev/null +++ b/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/textureset.json @@ -0,0 +1,7 @@ +{ + "colorMap": "Textures/ground/plant5/plant5_Color.png", + "normalMap": "Textures/ground/plant5/plant5_NormalGL.png", + "displacementMap": "", + "roughnessMap": "Textures/ground/plant5/plant5_Roughness.png", + "aoMap": "" +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg b/blight-assets/src/main/resources/Textures/internal/bark/Bark001_Color.jpg similarity index 100% rename from blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg rename to blight-assets/src/main/resources/Textures/internal/bark/Bark001_Color.jpg diff --git a/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg b/blight-assets/src/main/resources/Textures/internal/bark/Bark002_Color.jpg similarity index 100% rename from blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg rename to blight-assets/src/main/resources/Textures/internal/bark/Bark002_Color.jpg diff --git a/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg b/blight-assets/src/main/resources/Textures/internal/bark/Bark003_Color.jpg similarity index 100% rename from blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg rename to blight-assets/src/main/resources/Textures/internal/bark/Bark003_Color.jpg diff --git a/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg b/blight-assets/src/main/resources/Textures/internal/bark/Bark008_Color.jpg similarity index 100% rename from blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg rename to blight-assets/src/main/resources/Textures/internal/bark/Bark008_Color.jpg diff --git a/blight-assets/src/main/resources/Textures/bark/Bark_Palm.png b/blight-assets/src/main/resources/Textures/internal/bark/Bark_Palm.png similarity index 100% rename from blight-assets/src/main/resources/Textures/bark/Bark_Palm.png rename to blight-assets/src/main/resources/Textures/internal/bark/Bark_Palm.png diff --git a/blight-assets/src/main/resources/Textures/bark/Bark_Palm2.png b/blight-assets/src/main/resources/Textures/internal/bark/Bark_Palm2.png similarity index 100% rename from blight-assets/src/main/resources/Textures/bark/Bark_Palm2.png rename to blight-assets/src/main/resources/Textures/internal/bark/Bark_Palm2.png diff --git a/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Diffuse.png b/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Diffuse.png new file mode 100644 index 0000000..3ec9bdd Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Diffuse.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Normal.png b/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Normal.png new file mode 100644 index 0000000..dfe4a24 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Normal.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Specular.png b/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Specular.png new file mode 100644 index 0000000..3452297 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fern/Fern02_Specular.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/ash.png b/blight-assets/src/main/resources/Textures/internal/leaves/ash.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/ash.png rename to blight-assets/src/main/resources/Textures/internal/leaves/ash.png diff --git a/blight-assets/src/main/resources/Textures/leaves/aspen.png b/blight-assets/src/main/resources/Textures/internal/leaves/aspen.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/aspen.png rename to blight-assets/src/main/resources/Textures/internal/leaves/aspen.png diff --git a/blight-assets/src/main/resources/Textures/leaves/oak.png b/blight-assets/src/main/resources/Textures/internal/leaves/oak.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/oak.png rename to blight-assets/src/main/resources/Textures/internal/leaves/oak.png diff --git a/blight-assets/src/main/resources/Textures/leaves/palm.png b/blight-assets/src/main/resources/Textures/internal/leaves/palm.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/palm.png rename to blight-assets/src/main/resources/Textures/internal/leaves/palm.png diff --git a/blight-assets/src/main/resources/Textures/leaves/palm2.png b/blight-assets/src/main/resources/Textures/internal/leaves/palm2.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/palm2.png rename to blight-assets/src/main/resources/Textures/internal/leaves/palm2.png diff --git a/blight-assets/src/main/resources/Textures/leaves/palmcrown.png b/blight-assets/src/main/resources/Textures/internal/leaves/palmcrown.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/palmcrown.png rename to blight-assets/src/main/resources/Textures/internal/leaves/palmcrown.png diff --git a/blight-assets/src/main/resources/Textures/leaves/pine.png b/blight-assets/src/main/resources/Textures/internal/leaves/pine.png similarity index 100% rename from blight-assets/src/main/resources/Textures/leaves/pine.png rename to blight-assets/src/main/resources/Textures/internal/leaves/pine.png diff --git a/blight-assets/src/main/resources/Textures/water/river.jpg b/blight-assets/src/main/resources/Textures/internal/water/river.jpg similarity index 100% rename from blight-assets/src/main/resources/Textures/water/river.jpg rename to blight-assets/src/main/resources/Textures/internal/water/river.jpg diff --git a/blight-assets/src/main/resources/Textures/water/river_normal.jpg b/blight-assets/src/main/resources/Textures/internal/water/river_normal.jpg similarity index 100% rename from blight-assets/src/main/resources/Textures/water/river_normal.jpg rename to blight-assets/src/main/resources/Textures/internal/water/river_normal.jpg diff --git a/blight-assets/src/main/resources/Textures/water/waterfall_diffuse.png b/blight-assets/src/main/resources/Textures/internal/water/waterfall_diffuse.png similarity index 100% rename from blight-assets/src/main/resources/Textures/water/waterfall_diffuse.png rename to blight-assets/src/main/resources/Textures/internal/water/waterfall_diffuse.png diff --git a/blight-assets/src/main/resources/Textures/water/waterfall_normal.png b/blight-assets/src/main/resources/Textures/internal/water/waterfall_normal.png similarity index 100% rename from blight-assets/src/main/resources/Textures/water/waterfall_normal.png rename to blight-assets/src/main/resources/Textures/internal/water/waterfall_normal.png 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 c22a7cb..ba53137 100644 --- a/blight-common/src/main/java/de/blight/common/MapData.java +++ b/blight-common/src/main/java/de/blight/common/MapData.java @@ -71,6 +71,33 @@ public final class MapData { /** Normal-Map-Pfade für Gebirge (4 Slots, "" = keine Normal-Map). */ public final String[] upperNormalMaps = new String[]{"", "", "", ""}; + /** Splatmap Rot-Kanal Dritte Gruppe (Slots 9-12, AlphaMap_2) [SPLAT_SIZE²]. */ + public final byte[] thirdSplatR; + /** Splatmap Grün-Kanal Dritte Gruppe [SPLAT_SIZE²]. */ + public final byte[] thirdSplatG; + /** Splatmap Blau-Kanal Dritte Gruppe [SPLAT_SIZE²]. */ + public final byte[] thirdSplatB; + /** Splatmap Alpha-Kanal Dritte Gruppe [SPLAT_SIZE²]. */ + public final byte[] thirdSplatA; + + /** 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). */ + public final String[] thirdNormalMaps = new String[]{"", "", "", ""}; + + /** Normal-Map-Priorisierungsreihenfolge (Slot-Index 0-11, absteigend nach Priorität). */ + public int[] normalMapOrder = {0,1,2,3,4,5,6,7,8,9,10,11}; + + /** Displacement-Map-Pfade für Basis-Terrain (4 Slots, "" = keine). */ + public String[] terrainDisplacementMaps = {"","","",""}; + /** Displacement-Map-Pfade für Gebirge (4 Slots, "" = keine). */ + public String[] upperDisplacementMaps = {"","","",""}; + /** Displacement-Map-Pfade für dritte Gruppe (4 Slots, "" = keine). */ + public String[] thirdDisplacementMaps = {"","","",""}; + + /** UV-Scale pro Slot in Welteinheiten (Meter pro Textur-Tile), 12 Werte für Slots 1-12. */ + public float[] diffuseScales = {8f,8f,8f,8f, 8f,8f,8f,8f, 8f,8f,8f,8f}; + /** Gras-Dichte [SPLAT_SIZE²], Bytes 0–255 (0=kein Gras, 255=max Dichte). */ public final byte[] grassDensity; @@ -102,6 +129,13 @@ public final class MapData { /** Spawnpunkt Z-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */ public float spawnZ = 0f; + /** Terrain-Slot (0-7) für flache Voxel-Flächen (TexFlat), -1 = nicht gesetzt. */ + public int voxelFlatSlot = -1; + /** Terrain-Slot (0-7) für steile Wände (TexSteep), -1 = nicht gesetzt. */ + public int voxelSteepSlot = -1; + /** Terrain-Slot (0-7) für Decken/Unterseiten (TexCeil), -1 = nicht gesetzt. */ + public int voxelCeilSlot = -1; + public MapData() { terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS]; upperTop = new float[UPPER_VERTS * UPPER_VERTS]; @@ -114,6 +148,10 @@ public final class MapData { upperSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE]; upperSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE]; upperSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatR = new byte [SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatA = 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 4b828a4..b41c1b7 100644 --- a/blight-common/src/main/java/de/blight/common/MapIO.java +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -1,5 +1,8 @@ package de.blight.common; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.*; import java.nio.*; import java.nio.file.*; @@ -27,6 +30,8 @@ import java.util.zip.*; */ public final class MapIO { + private static final Logger log = LoggerFactory.getLogger(MapIO.class); + /** System-Property, das vom Editor beim Spielstart auf eine temporäre Session-Kopie gesetzt wird. */ public static final String PROP_SESSION_MAP = "blight.session.map.path"; @@ -58,7 +63,7 @@ public final class MapIO { } private static final int MAGIC = 0x424C4947; // "BLIG" - private static final int VERSION = 11; + private static final int VERSION = 14; // Größen älterer Saves (v≤9) – für Migrations-Upsampling private static final int OLD_TERRAIN_VERTS = 4097; @@ -70,17 +75,17 @@ public final class MapIO { public static boolean exists() { Path p = getMapPath(); - System.out.println("[MapIO] Suche Karte: " + p.toAbsolutePath()); + log.info("[MapIO] Suche Karte: {}", p.toAbsolutePath()); if (Files.exists(p)) return true; // Einmalige Migration vom alten Speicherort (world/blight_map.blm) – nur im Nicht-Session-Modus if (System.getProperty(PROP_SESSION_MAP) == null && Files.exists(MAP_PATH_OLD)) { try { Files.createDirectories(MAP_PATH.getParent()); Files.move(MAP_PATH_OLD, MAP_PATH); - System.out.println("[MapIO] Karte migriert: " + MAP_PATH_OLD + " → " + MAP_PATH); + log.info("[MapIO] Karte migriert: {} → {}", MAP_PATH_OLD, MAP_PATH); return true; } catch (IOException e) { - System.err.println("[MapIO] Migration fehlgeschlagen: " + e.getMessage()); + log.error("[MapIO] Migration fehlgeschlagen: {}", e.getMessage()); } } return false; @@ -143,6 +148,22 @@ public final class MapIO { // v11: normal-map-pfade writeStrings(out, data.terrainNormalMaps); writeStrings(out, data.upperNormalMaps); + // v12: voxel-slot-indizes + out.writeInt(data.voxelFlatSlot); + out.writeInt(data.voxelSteepSlot); + out.writeInt(data.voxelCeilSlot); + // v13: dritte splatmap + texturpfade + out.write(data.thirdSplatR); + out.write(data.thirdSplatG); + out.write(data.thirdSplatB); + out.write(data.thirdSplatA); + writeStrings(out, data.thirdTextures); + writeStrings(out, data.thirdNormalMaps); + // v14: displacement-maps + uv-scales + writeStrings(out, data.terrainDisplacementMaps); + writeStrings(out, data.upperDisplacementMaps); + writeStrings(out, data.thirdDisplacementMaps); + writeFloats(out, data.diffuseScales); } // 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. @@ -266,6 +287,23 @@ public final class MapIO { readStrings(in, data.terrainNormalMaps); readStrings(in, data.upperNormalMaps); } + if (version >= 12) { + data.voxelFlatSlot = in.readInt(); + data.voxelSteepSlot = in.readInt(); + data.voxelCeilSlot = in.readInt(); + } + if (version >= 13) { + in.readFully(data.thirdSplatR); in.readFully(data.thirdSplatG); + in.readFully(data.thirdSplatB); in.readFully(data.thirdSplatA); + readStrings(in, data.thirdTextures); + readStrings(in, data.thirdNormalMaps); + } + if (version >= 14) { + readStrings(in, data.terrainDisplacementMaps); + readStrings(in, data.upperDisplacementMaps); + readStrings(in, data.thirdDisplacementMaps); + readFloats(in, data.diffuseScales); + } } return data; } diff --git a/blight-common/src/main/java/de/blight/common/ModelMeta.java b/blight-common/src/main/java/de/blight/common/ModelMeta.java index e310bc5..fe7f82b 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMeta.java +++ b/blight-common/src/main/java/de/blight/common/ModelMeta.java @@ -1,5 +1,7 @@ package de.blight.common; +import java.util.List; + /** * Metadaten einer Model-Asset-Datei (.j3o.meta neben dem .j3o). * Skalierung ist per-Achse; uniformScale=true → X/Y/Z werden gemeinsam angepasst. @@ -19,16 +21,30 @@ public record ModelMeta( boolean receiveShadow, float randomScaleMin, float randomScaleMax, - String lod1Path, // relativer Asset-Pfad; "" = nicht gesetzt + String lod1Path, String lod2Path, - float lod1Distance, // ab dieser Distanz LOD1 anzeigen - float lod2Distance, // ab dieser Distanz LOD2 anzeigen - float cullDistance // ab dieser Distanz ausblenden + float lod1Distance, + float lod2Distance, + float cullDistance, + List attachedLights, + List attachedEmitters ) { + /** Lichtquelle relativ zum Modell-Ursprung. */ + public record AttachedLight( + float offsetX, float offsetY, float offsetZ, + float r, float g, float b, + float intensity, float radius) {} + + /** Partikel-Emitter relativ zum Modell-Ursprung. Preset: 0=Feuer, 1=Rauch, 2=Funken. */ + public record AttachedEmitter( + float offsetX, float offsetY, float offsetZ, + int preset) {} + public static ModelMeta defaults(String j3oFileName) { String name = j3oFileName.replaceFirst("\\.j3o$", ""); return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f, false, true, true, 1f, 1f, - "", "", 30f, 80f, 120f); + "", "", 30f, 80f, 120f, + List.of(), List.of()); } } diff --git a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java index e2549e8..2dd0328 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java +++ b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java @@ -2,7 +2,7 @@ package de.blight.common; import java.io.*; import java.nio.file.*; -import java.util.Properties; +import java.util.*; /** Liest und schreibt {@code .j3o.meta}-Dateien neben dem jeweiligen {@code .j3o}. */ public final class ModelMetaIO { @@ -34,6 +34,28 @@ public final class ModelMetaIO { p.setProperty("lod1Distance", String.valueOf(m.lod1Distance())); p.setProperty("lod2Distance", String.valueOf(m.lod2Distance())); p.setProperty("cullDistance", String.valueOf(m.cullDistance())); + + // Anhänge: Lichter + List lights = m.attachedLights(); + p.setProperty("attachedLights.count", String.valueOf(lights.size())); + for (int i = 0; i < lights.size(); i++) { + ModelMeta.AttachedLight l = lights.get(i); + p.setProperty("attachedLight." + i, String.format(Locale.ROOT, + "%.5f|%.5f|%.5f|%.5f|%.5f|%.5f|%.5f|%.5f", + l.offsetX(), l.offsetY(), l.offsetZ(), + l.r(), l.g(), l.b(), l.intensity(), l.radius())); + } + + // Anhänge: Emitter + List emitters = m.attachedEmitters(); + p.setProperty("attachedEmitters.count", String.valueOf(emitters.size())); + for (int i = 0; i < emitters.size(); i++) { + ModelMeta.AttachedEmitter e = emitters.get(i); + p.setProperty("attachedEmitter." + i, String.format(Locale.ROOT, + "%.5f|%.5f|%.5f|%d", + e.offsetX(), e.offsetY(), e.offsetZ(), e.preset())); + } + try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) { p.store(w, null); } @@ -48,6 +70,42 @@ public final class ModelMetaIO { } catch (IOException ignored) {} } String defName = j3oPath.getFileName().toString().replaceFirst("\\.j3o$", ""); + + // Anhänge: Lichter + int lightCount = parseInt(p, "attachedLights.count", 0); + List lights = new ArrayList<>(lightCount); + for (int i = 0; i < lightCount; i++) { + String v = p.getProperty("attachedLight." + i, ""); + if (!v.isEmpty()) { + String[] f = v.split("\\|", -1); + if (f.length >= 8) { + try { + lights.add(new ModelMeta.AttachedLight( + Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]), + Float.parseFloat(f[3]), Float.parseFloat(f[4]), Float.parseFloat(f[5]), + Float.parseFloat(f[6]), Float.parseFloat(f[7]))); + } catch (NumberFormatException ignored) {} + } + } + } + + // Anhänge: Emitter + int emitterCount = parseInt(p, "attachedEmitters.count", 0); + List emitters = new ArrayList<>(emitterCount); + for (int i = 0; i < emitterCount; i++) { + String v = p.getProperty("attachedEmitter." + i, ""); + if (!v.isEmpty()) { + String[] f = v.split("\\|", -1); + if (f.length >= 4) { + try { + emitters.add(new ModelMeta.AttachedEmitter( + Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]), + Integer.parseInt(f[3]))); + } catch (NumberFormatException ignored) {} + } + } + } + return new ModelMeta( p.getProperty("name", defName), p.getProperty("category", ""), @@ -67,7 +125,9 @@ public final class ModelMetaIO { p.getProperty("lod2Path", ""), parseFloat(p, "lod1Distance", 30f), parseFloat(p, "lod2Distance", 80f), - parseFloat(p, "cullDistance", 120f) + parseFloat(p, "cullDistance", 120f), + Collections.unmodifiableList(lights), + Collections.unmodifiableList(emitters) ); } @@ -75,4 +135,9 @@ public final class ModelMetaIO { try { return Float.parseFloat(p.getProperty(key, String.valueOf(def))); } catch (NumberFormatException e) { return def; } } + + private static int parseInt(Properties p, String key, int def) { + try { return Integer.parseInt(p.getProperty(key, String.valueOf(def))); } + catch (NumberFormatException e) { return def; } + } } diff --git a/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java b/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java index cb9ffef..ab82477 100644 --- a/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java +++ b/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java @@ -42,6 +42,18 @@ public final class VoxelChunkIO { return VoxelChunk.deserialize(Files.readAllBytes(getPath(cx, cy, cz)), cx, cy, cz); } + /** Pfad der gebackenen J3O-Datei für einen LOD-Level. */ + public static Path getBakedPath(int cx, int cy, int cz, int lod) { + String cyStr = cy < 0 ? "m" + (-cy) : String.valueOf(cy); + return ChunkTerrainIO.chunksDir() + .resolve(String.format("voxel_%02d_%s_%02d_baked_lod%d.j3o", cx, cyStr, cz, lod)); + } + + /** true wenn für (cx,cy,cz) ein gebackenes LOD0-Mesh vorliegt. */ + public static boolean bakedExists(int cx, int cy, int cz) { + return Files.exists(getBakedPath(cx, cy, cz, 0)); + } + /** * Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis. * Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert. diff --git a/blight-editor/build.gradle b/blight-editor/build.gradle index b6f4b96..2fc487f 100644 --- a/blight-editor/build.gradle +++ b/blight-editor/build.gradle @@ -26,7 +26,7 @@ dependencies { implementation project(':blight-common') implementation project(':blight-assets') implementation project(':blight-map') - implementation project(':ez-tree-jme') + implementation project(':blight-vegetation-generator') // Spiel-Klassen + deren Abhängigkeiten (jme3-jbullet, gson) auf dem Runtime- // Classpath, damit der Editor BlightApp als Subprocess starten kann. implementation project(':blight-game') diff --git a/blight-editor/src/main/java/de/blight/editor/BlightEditor.java b/blight-editor/src/main/java/de/blight/editor/BlightEditor.java index 59ca2c6..265922b 100644 --- a/blight-editor/src/main/java/de/blight/editor/BlightEditor.java +++ b/blight-editor/src/main/java/de/blight/editor/BlightEditor.java @@ -2,12 +2,31 @@ package de.blight.editor; import org.slf4j.bridge.SLF4JBridgeHandler; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + /** * Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt * aufgelöst wird, bevor Application.launch() aufgerufen wird. */ public class BlightEditor { + + private static FileLock instanceLock; + private static FileChannel lockChannel; + public static void main(String[] args) { + if (!acquireSingleInstanceLock()) { + javax.swing.JOptionPane.showMessageDialog( + null, + "Der Blight Editor läuft bereits.\nEs kann nur eine Instanz gleichzeitig gestartet werden.", + "Bereits gestartet", + javax.swing.JOptionPane.WARNING_MESSAGE); + System.exit(0); + } + SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); @@ -17,4 +36,21 @@ public class BlightEditor { ProjectRoot.PATH.toString(); // Trigger static init EditorApp.main(args); } + + private static boolean acquireSingleInstanceLock() { + try { + Path lockFile = Paths.get(System.getProperty("java.io.tmpdir"), "blight-editor.lock"); + lockChannel = FileChannel.open(lockFile, + StandardOpenOption.CREATE, StandardOpenOption.WRITE); + instanceLock = lockChannel.tryLock(); + if (instanceLock == null) return false; + // Sperre beim JVM-Exit freigeben + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { instanceLock.release(); lockChannel.close(); } catch (Exception ignored) {} + })); + return true; + } catch (Exception e) { + return true; // Im Fehlerfall trotzdem starten + } + } } diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index c32ea24..cd2dd9e 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -9,6 +9,7 @@ import de.blight.editor.tree.PalmOptions; import de.blight.editor.tree.TreeParams; import de.blight.editor.ui.MapObjectsView; import de.blight.editor.ui.TextureChooser; +import de.blight.editor.ui.TextureDetailDialog; import de.blight.eztree.Billboard; import de.blight.eztree.TreeOptions; import de.blight.eztree.TreePresets; @@ -32,18 +33,31 @@ import javafx.scene.layout.*; import javafx.stage.FileChooser; import javafx.stage.Stage; +import com.google.gson.Gson; +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; import java.io.File; import java.io.IOException; +import java.io.Reader; +import java.io.Writer; import java.net.URL; +import java.nio.ByteBuffer; import java.nio.file.*; import java.util.Comparator; import java.util.HashMap; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class EditorApp extends Application { + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EditorApp.class); + // ── Initiale JME-Startauflösung (wird sofort durch Fenster-Größe überschrieben) ─ private static final int INITIAL_VP_W = 1280; private static final int INITIAL_VP_H = 720; @@ -61,16 +75,18 @@ public class EditorApp extends Application { private BorderPane root; private Stage gameConsoleStage; private TextArea gameConsoleArea; - private java.nio.file.attribute.FileTime lastLivePosTime = - java.nio.file.attribute.FileTime.fromMillis(0); + private final java.util.concurrent.ConcurrentLinkedQueue consoleBuffer = + new java.util.concurrent.ConcurrentLinkedQueue<>(); private VBox assetPanel; private MapObjectsView mapObjectsView; private StackPane worldViewport; + private javafx.scene.canvas.Canvas minimapCanvas; private VBox topBar; // MenuBar + aktuelle Toolbar private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons) private ImageView treePreviewView; // aktualisiert wenn JME3 Bild neu erstellt private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren private Stage primaryStage; + private JmeEditorApp jmeApp; // Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt) private TreeParams treeParams = TreeParams.oak(); @@ -95,9 +111,10 @@ public class EditorApp extends Application { private Runnable onF6 = () -> {}; // Zufälligen Seed wählen // Asset-Panel: Pfad-Map + DnD-Zustand - private final Map, Path> itemPaths = new HashMap<>(); - private final Map, String> jmePaths = new HashMap<>(); - private final java.util.Set> jmeFolderNodes = new java.util.HashSet<>(); + private final Map, Path> itemPaths = new HashMap<>(); + private final Map, String> jmePaths = new HashMap<>(); + private final java.util.Set> jmeFolderNodes = new java.util.HashSet<>(); + private final java.util.Set> textureSetNodes = new java.util.HashSet<>(); private TreeItem draggedItem = null; // Objekt-Werkzeug-Zustand @@ -109,6 +126,7 @@ public class EditorApp extends Application { private CheckBox multiPlaceCB; // Mehrfach-Platzierungs-Modus private TextField objXField, objYField, objZField; private TextField objRotXField, objRotYField, objRotZField; + private TextField objScaleField; private Label objTexLabel; private Label objNormalMapLabel; private Label objMatLabel; @@ -207,10 +225,27 @@ public class EditorApp extends Application { private CheckBox modelEditorUniformCB; private boolean modelEditorSuppressListeners = false; private String modelEditorCurrentPath = null; - private String modelEditorLod1Path = ""; - private String modelEditorLod2Path = ""; - private Label modelEditorLod1Label; - private Label modelEditorLod2Label; + private String modelEditorLod1Path = ""; + private String modelEditorLod2Path = ""; + private Label modelEditorLod1Label; + private Label modelEditorLod2Label; + private ToggleButton modelEditorLod1Btn; + private ToggleButton modelEditorLod2Btn; + private ToggleButton importLod1Btn; + private ToggleButton importLod2Btn; + // Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen) + private final java.util.List modelEditorLights = new java.util.ArrayList<>(); + private final java.util.List modelEditorEmitters = new java.util.ArrayList<>(); + private javafx.scene.layout.VBox modelEditorLightBox = null; + private javafx.scene.layout.VBox modelEditorEmitterBox = null; + + // Modell-Import-Zustand + private Label modelImportLod1StatusLabel; + private Label modelImportLod2StatusLabel; + private Label modelImportExportStatusLabel; + private String modelImportCurrentPath = null; + /** Warteschlange für Mehrfach-Import – nächste Datei wird nach Export der aktuellen geladen. */ + private final java.util.Deque modelImportQueue = new java.util.ArrayDeque<>(); // Asset-Overlay-Zustand private boolean assetOverlayOpen = false; @@ -255,7 +290,8 @@ public class EditorApp extends Application { private javafx.animation.Timeline editTimer; // Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein) - private TreeItem assetTreeRoot; + private TreeItem assetTreeRoot; + private TreeView assetTree; private TreeItem modelsNode; private TreeItem texturesNode; private TreeItem audioNode; @@ -299,7 +335,7 @@ public class EditorApp extends Application { splash.show(); jfxImage = new WritableImage(INITIAL_VP_W, INITIAL_VP_H); - JmeEditorApp.launch(input, jfxImage, INITIAL_VP_W, INITIAL_VP_H); + jmeApp = JmeEditorApp.launch(input, jfxImage, INITIAL_VP_W, INITIAL_VP_H); assetPanel = buildAssetPanel(); toolPanel = buildToolPanel(); @@ -347,7 +383,12 @@ public class EditorApp extends Application { stage.setScene(scene); stage.setMinWidth(900); stage.setMinHeight(600); - stage.setOnCloseRequest(e -> { saveCameraPrefs(); Platform.exit(); }); + stage.setOnCloseRequest(e -> { + saveCameraPrefs(); + if (jmeApp != null) jmeApp.stop(); + Platform.exit(); + System.exit(0); + }); stage.setMaximized(true); javafx.animation.Timeline[] pollerRef = new javafx.animation.Timeline[1]; @@ -486,8 +527,20 @@ public class EditorApp extends Application { updateWaterHeightDisplay(input.waterCurrentHeight); } - // Live-Spielerposition aus laufendem Spiel - pollLivePlayerPosition(); + drawMinimap(); + + // Spiel-Konsole: gepufferte Zeilen gebündelt ausgeben (max 200 auf einmal) + if (!consoleBuffer.isEmpty() && gameConsoleArea != null) { + StringBuilder sb = new StringBuilder(); + int count = 0; + String ln; + while (count < 200 && (ln = consoleBuffer.poll()) != null) { + sb.append(ln).append('\n'); + count++; + } + gameConsoleArea.appendText(sb.toString()); + gameConsoleArea.setScrollTop(Double.MAX_VALUE); + } if (input.soundAreaSelectionChanged) { input.soundAreaSelectionChanged = false; @@ -536,6 +589,41 @@ public class EditorApp extends Application { modelEditorLod2Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;"); } } + // Model-Editor LOD-Buttons freischalten/sperren + if (modelEditorLod1Btn != null) + modelEditorLod1Btn.setDisable(!input.modelEditorHasEmbeddedLods && modelEditorLod1Path.isBlank()); + if (modelEditorLod2Btn != null) + modelEditorLod2Btn.setDisable(!input.modelEditorHasEmbeddedLods && modelEditorLod2Path.isBlank()); + // Importer LOD-Buttons freischalten sobald generiert + if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1); + if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2); + + // Modell-Import: LOD-Status + String lodStatus = input.modelLodGenStatus; + if (lodStatus != null) { + input.modelLodGenStatus = null; + if (modelImportLod1StatusLabel != null && lodStatus.startsWith("LOD1")) + modelImportLod1StatusLabel.setText(lodStatus); + else if (modelImportLod2StatusLabel != null && (lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor"))) + modelImportLod2StatusLabel.setText(lodStatus); + setStatus(lodStatus); + } + // Modell-Import: Export-Ergebnis + String expStatus = input.modelImportExportStatus; + if (expStatus != null) { + input.modelImportExportStatus = null; + if (modelImportExportStatusLabel != null) modelImportExportStatusLabel.setText(expStatus); + int remaining = modelImportQueue.size(); + setStatus("Import: " + expStatus + (remaining > 0 ? " (" + remaining + " weitere)" : "")); + if (!expStatus.startsWith("FEHLER")) { + input.refreshAssets = true; + } + // Nächste Datei aus Warteschlange laden + if (!modelImportQueue.isEmpty()) { + File next = modelImportQueue.poll(); + startImportFile(next); + } + } // Kamera-Koordinaten aktualisieren camCoordsLabel.setText(String.format( @@ -561,6 +649,11 @@ public class EditorApp extends Application { ht.plateauHeightChanged = false; showToolParameters(toolPanel, input.activeTool); } + if (input.activeTool instanceof de.blight.editor.tool.VoxelTool vt + && vt.plateauTargetChanged) { + vt.plateauTargetChanged = false; + showToolParameters(toolPanel, input.activeTool); + } // Neues JME-Image nach Viewport-Resize übernehmen javafx.scene.image.WritableImage newImg = input.resizedImage.getAndSet(null); @@ -669,20 +762,29 @@ public class EditorApp extends Application { label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); ToggleButton lod0Btn = new ToggleButton("LOD 0"); - ToggleButton lod1Btn = new ToggleButton("LOD 1"); - ToggleButton lod2Btn = new ToggleButton("LOD 2"); + modelEditorLod1Btn = new ToggleButton("LOD 1"); + modelEditorLod2Btn = new ToggleButton("LOD 2"); javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup(); - lod0Btn.setToggleGroup(lodGroup); lod1Btn.setToggleGroup(lodGroup); lod2Btn.setToggleGroup(lodGroup); + lod0Btn.setToggleGroup(lodGroup); + modelEditorLod1Btn.setToggleGroup(lodGroup); + modelEditorLod2Btn.setToggleGroup(lodGroup); lod0Btn.setSelected(true); String lodStyle = "-fx-font-size:11; -fx-padding:2 8 2 8;"; - lod0Btn.setStyle(lodStyle); lod1Btn.setStyle(lodStyle); lod2Btn.setStyle(lodStyle); + lod0Btn.setStyle(lodStyle); + modelEditorLod1Btn.setStyle(lodStyle); + modelEditorLod2Btn.setStyle(lodStyle); lod0Btn.setOnAction(e -> { input.modelEditorLodPreview = 0; input.modelEditorLodChanged = true; }); - lod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; }); - lod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; }); + modelEditorLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; }); + modelEditorLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; }); + // Initial deaktiviert – werden freigegeben sobald LOD-Pfade/eingebettete LODs bekannt sind + boolean hasLod1 = input.modelEditorHasEmbeddedLods || !modelEditorLod1Path.isBlank(); + boolean hasLod2 = input.modelEditorHasEmbeddedLods || !modelEditorLod2Path.isBlank(); + modelEditorLod1Btn.setDisable(!hasLod1); + modelEditorLod2Btn.setDisable(!hasLod2); ToolBar tb = new ToolBar(); tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, - new Separator(Orientation.VERTICAL), lod0Btn, lod1Btn, lod2Btn); + new Separator(Orientation.VERTICAL), lod0Btn, modelEditorLod1Btn, modelEditorLod2Btn); return tb; } @@ -883,7 +985,21 @@ public class EditorApp extends Application { MenuItem saveItem = new MenuItem("Speichern"); saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S")); saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); }); - fileMenu.getItems().addAll(newItem, saveItem); + MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…"); + MenuItem importTexItem = new MenuItem("Texturen…"); + MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…"); + MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…"); + MenuItem importAudioItem = new MenuItem("Audio…"); + MenuItem importAnimItem = new MenuItem("Animationen…"); + importModelLodItem.setOnAction(e -> openModelImport(primaryStage)); + importTexItem.setOnAction(e -> handleTextureImport(primaryStage)); + importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); + importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); + importAudioItem.setOnAction(e -> handleAudioImport(primaryStage)); + importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage)); + fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(), + importModelLodItem, importTexItem, importTexZipItem, importTexSetItem, + importAudioItem, importAnimItem); Menu toolsMenu = new Menu("Werkzeuge"); MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); @@ -1532,7 +1648,7 @@ public class EditorApp extends Application { && palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png"; leafTexBox.setValue(currentLeaf); leafTexBox.setMaxWidth(Double.MAX_VALUE); - leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/leaves/" + leafTexBox.getValue()); + leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/internal/leaves/" + leafTexBox.getValue()); inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox)); // Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner) @@ -1552,7 +1668,7 @@ public class EditorApp extends Application { barkTexBox.setMaxWidth(Double.MAX_VALUE); barkTexBox.setOnAction(e -> { if (barkTexBox.getValue() != null) - palmOptions.barkTexture = "Textures/bark/" + barkTexBox.getValue(); + palmOptions.barkTexture = "Textures/internal/bark/" + barkTexBox.getValue(); }); inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox)); @@ -1761,7 +1877,7 @@ public class EditorApp extends Application { // ── Textur (optional, nur für Primitive) ───────────────────────────── inner.getChildren().addAll(sectionTitle("Textur"), new Separator()); Label texLabel = new Label(input.pendingTexturePath != null && !input.pendingTexturePath.isEmpty() - ? java.nio.file.Paths.get(input.pendingTexturePath).getFileName().toString() + ? labelFromPath(input.pendingTexturePath) : "(keine)"); texLabel.setWrapText(true); texLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); @@ -2408,7 +2524,7 @@ public class EditorApp extends Application { 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;"); - Label texLabel = new Label("Textur: " + texPath); + Label texLabel = new Label("Textur: " + labelFromPath(texPath)); texLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #777;"); texLabel.setWrapText(true); emitterDynamicContent.getChildren().addAll(posLabel, texLabel, new Separator()); @@ -2546,6 +2662,7 @@ public class EditorApp extends Application { // Null field references first to prevent stale focus-lost events from firing objXField = objYField = objZField = null; objRotXField = objRotYField = objRotZField = null; + objScaleField = null; objSolidCB = null; objCastShadowCB = null; objReceiveShadowCB = null; @@ -2582,6 +2699,7 @@ public class EditorApp extends Application { float rotXDeg = (float) Math.toDegrees(parseF(parts[6])); float rotYDeg = (float) Math.toDegrees(parseF(parts[7])); float rotZDeg = (float) Math.toDegrees(parseF(parts[8])); + float scale = parseF(parts[9]); String texPath = parts[10]; String normalPath = parts.length >= 12 ? parts[11] : ""; String matPath = parts.length >= 13 ? parts[12] : ""; @@ -2667,13 +2785,22 @@ public class EditorApp extends Application { objXField = makeFloatField(x); objYField = makeFloatField(y); objZField = makeFloatField(z); - hookApply(objXField); hookApply(objYField); hookApply(objZField); // Rotation objRotXField = makeFloatField(rotXDeg); objRotYField = makeFloatField(rotYDeg); objRotZField = makeFloatField(rotZDeg); - hookApply(objRotXField); hookApply(objRotYField); hookApply(objRotZField); + + // Skalierung + objScaleField = makeFloatField(scale); + + // "Übernehmen"-Button: liest Position, Rotation und Scale aus den Feldern + // und schreibt sie atomar in die JME-Szene. Kein Auto-Apply bei Focus-Lost, + // damit versehentliche Tab-Tastendrücke das Objekt nicht verschieben. + Button uebernehmenBtn = new Button("✔ Übernehmen"); + uebernehmenBtn.setMaxWidth(Double.MAX_VALUE); + uebernehmenBtn.setDefaultButton(true); + uebernehmenBtn.setOnAction(ev -> enqueuePropertyChange(null, null, null)); // Solid / Schatten objSolidCB = new CheckBox("Solid (Charakter-Kollision)"); @@ -2708,15 +2835,21 @@ public class EditorApp extends Application { input.pendingAnimClip = "(keine)".equals(val) ? "" : val; }); + // Scale-Feld: breiteren TextField, da nur ein Wert + HBox scaleRow = new HBox(6, new Label("Skalierung:"), objScaleField); + scaleRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + objDynamicContent.getChildren().addAll( modelLbl, bold("Textur:"), texRow, bold("Normal Map:"), normRow, bold("Material:"), matRow, new Separator(), bold("Position (m)"), - labeledRow("X:", objXField, "Y:", objYField, "Z:", objZField), new Separator(), + labeledRow("X:", objXField, "Y:", objYField, "Z:", objZField), bold("Rotation (°)"), - labeledRow("X:", objRotXField, "Y:", objRotYField, "Z:", objRotZField), new Separator(), + labeledRow("X:", objRotXField, "Y:", objRotYField, "Z:", objRotZField), + scaleRow, + uebernehmenBtn, new Separator(), objSolidCB, objCastShadowCB, objReceiveShadowCB, new Separator(), bold("Animation:"), animCombo); @@ -2772,17 +2905,18 @@ public class EditorApp extends Application { private void enqueuePropertyChange(String texOverride, String normalOverride, String matOverride) { if (objXField == null || objYField == null || objZField == null) return; try { - float x = parseFloatField(objXField.getText()); - float y = parseFloatField(objYField.getText()); - float z = parseFloatField(objZField.getText()); - float rotX = (float) Math.toRadians(parseFloatField(objRotXField.getText())); - float rotY = (float) Math.toRadians(parseFloatField(objRotYField.getText())); - float rotZ = (float) Math.toRadians(parseFloatField(objRotZField.getText())); + float x = parseFloatField(objXField.getText()); + float y = parseFloatField(objYField.getText()); + float z = parseFloatField(objZField.getText()); + float rotX = (float) Math.toRadians(parseFloatField(objRotXField.getText())); + float rotY = (float) Math.toRadians(parseFloatField(objRotYField.getText())); + float rotZ = (float) Math.toRadians(parseFloatField(objRotZField.getText())); + float scale = objScaleField != null ? parseFloatField(objScaleField.getText()) : 1f; boolean solid = objSolidCB != null && objSolidCB.isSelected(); boolean castShadow = objCastShadowCB == null || objCastShadowCB.isSelected(); boolean receiveShadow = objReceiveShadowCB == null || objReceiveShadowCB.isSelected(); input.objectPropertyQueue.offer( - new SharedInput.ObjectPropertyChange(x, y, z, rotX, rotY, rotZ, + new SharedInput.ObjectPropertyChange(x, y, z, rotX, rotY, rotZ, scale, solid, castShadow, receiveShadow, texOverride, normalOverride, matOverride)); } catch (NumberFormatException ignored) {} @@ -2813,9 +2947,6 @@ public class EditorApp extends Application { if (tool instanceof de.blight.editor.tool.TextureTool && param == ((de.blight.editor.tool.TextureTool) tool).textureIndex) { panel.getChildren().addAll(nameLabel, buildTextureChoiceUI(param)); - } else if (tool instanceof de.blight.editor.tool.VoxelTool vt - && param == vt.textureSlot) { - panel.getChildren().addAll(nameLabel, buildVoxelTextureChoiceUI(param)); } else if (tool instanceof de.blight.editor.tool.VoxelTool vt2 && param == vt2.mode) { panel.getChildren().addAll(nameLabel, buildVoxelModeUI(param)); @@ -2891,15 +3022,112 @@ public class EditorApp extends Application { // Textur-Slot-Konfigurator (nur beim TextureTool) if (tool instanceof de.blight.editor.tool.TextureTool) { - panel.getChildren().addAll(new javafx.scene.control.Separator(), + // Globaler UV-Scale-Spinner (ändert alle 12 Slots gleichzeitig) + float curScale = (input.diffuseScales != null) ? input.diffuseScales[0] : 8f; + javafx.scene.control.Spinner globalScaleSpin = + new javafx.scene.control.Spinner<>(0.5, 1024.0, curScale, 0.5); + globalScaleSpin.setEditable(true); + globalScaleSpin.setMaxWidth(Double.MAX_VALUE); + globalScaleSpin.setTooltip(new Tooltip("UV-Scale in Metern — gilt für alle Slots gleichzeitig")); + globalScaleSpin.valueProperty().addListener((ob, ov, nv) -> { + if (nv == null) return; + float v = nv.floatValue(); + java.util.Arrays.fill(input.diffuseScales, v); + input.diffuseScalesChanged = true; + }); + Label scaleLbl = new Label("UV-Scale alle Slots (m):"); + scaleLbl.setStyle("-fx-font-size: 11; -fx-text-fill: #333;"); + + // Slot-Gruppen in ScrollPane damit die Werkzeugleiste nicht überquillt + VBox slots = new VBox(5, buildTextureSlotsUI(false), new javafx.scene.control.Separator(), - buildTextureSlotsUI(true)); + buildTextureSlotsUI(true), + new javafx.scene.control.Separator(), + buildThirdTextureSlotsUI()); + slots.setPadding(new javafx.geometry.Insets(4)); + ScrollPane slotsScroll = new ScrollPane(slots); + slotsScroll.setFitToWidth(true); + slotsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + VBox.setVgrow(slotsScroll, Priority.ALWAYS); + + panel.getChildren().addAll( + new javafx.scene.control.Separator(), + scaleLbl, globalScaleSpin, + new javafx.scene.control.Separator(), + slotsScroll); } // Textur-Picker (nur beim GrassTool) if (tool instanceof GrassTool) { - panel.getChildren().addAll(new Separator(), buildGrassTextureUI()); + javafx.scene.Node grassUI = buildGrassTextureUI(); + VBox.setVgrow(grassUI, javafx.scene.layout.Priority.ALWAYS); + panel.getChildren().addAll(new Separator(), grassUI); + } + + // Voxel-Textur-Slots (nur beim VoxelTool) + if (tool instanceof de.blight.editor.tool.VoxelTool) { + panel.getChildren().addAll(new Separator(), buildVoxelTextureSlotsUI()); + } + + // Backen-Button (nur beim VoxelTool) + if (tool instanceof de.blight.editor.tool.VoxelTool) { + Label bakeStatus = new Label(""); + bakeStatus.setWrapText(true); + bakeStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + + javafx.scene.control.ProgressBar bakeBar = new javafx.scene.control.ProgressBar(0); + bakeBar.setMaxWidth(Double.MAX_VALUE); + bakeBar.setVisible(false); + bakeBar.setManaged(false); + + Label bakeBarLabel = new Label(""); + bakeBarLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); + bakeBarLabel.setVisible(false); + bakeBarLabel.setManaged(false); + + Button bakeBtn = new Button("Voxels backen (J3O)"); + bakeBtn.setMaxWidth(Double.MAX_VALUE); + bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); + bakeBtn.setOnAction(e -> { + input.bakeVoxelsRequested = true; + bakeStatus.setText(""); + bakeBtn.setDisable(true); + bakeBar.setProgress(0); + bakeBar.setVisible(true); + bakeBar.setManaged(true); + bakeBarLabel.setVisible(true); + bakeBarLabel.setManaged(true); + bakeBarLabel.setText("Starte..."); + }); + + javafx.animation.Timeline poller = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { + int total = input.bakeTotal; + int done = input.bakeDone; + if (total > 0) { + double prog = (double) done / total; + bakeBar.setProgress(prog); + bakeBarLabel.setText(done + " / " + total + " Chunks"); + } + String msg = input.bakeStatusMsg; + if (msg != null) { + input.bakeStatusMsg = null; + input.bakeTotal = 0; + input.bakeDone = 0; + bakeStatus.setText(msg); + bakeBar.setVisible(false); + bakeBar.setManaged(false); + bakeBarLabel.setVisible(false); + bakeBarLabel.setManaged(false); + bakeBtn.setDisable(false); + } + }) + ); + poller.setCycleCount(javafx.animation.Animation.INDEFINITE); + poller.play(); + + panel.getChildren().addAll(new Separator(), bakeBtn, bakeBar, bakeBarLabel, bakeStatus); } } @@ -2909,6 +3137,14 @@ public class EditorApp extends Application { Label title = new Label("Textur-Slots"); title.setStyle("-fx-text-fill: #111111; -fx-font-weight: bold;"); + // Aspect-Ratios für bereits geladene Slots vorab berechnen + float[] initArs = input.grassAspectRatios.clone(); + for (int i = 0; i < 8; i++) { + String p = getGrassSlotPath(i); + if (p != null && !p.isEmpty()) initArs[i] = readAspectRatio(p); + } + input.grassAspectRatios = initArs; + ToggleGroup slotGroup = new ToggleGroup(); VBox slotRows = new VBox(4); @@ -2923,7 +3159,7 @@ public class EditorApp extends Application { String prefix = slot + " ▸ "; String currentPath = getGrassSlotPath(slot); String fname = (currentPath != null && !currentPath.isEmpty()) - ? java.nio.file.Paths.get(currentPath).getFileName().toString() + ? labelFromPath(currentPath) : "(Leer)"; Label nameLabel = new Label(prefix + fname); @@ -2937,6 +3173,16 @@ public class EditorApp extends Application { chooseBtn.setOnAction(e -> { TextureChooser chooser = new TextureChooser(ASSET_ROOT, false); chooser.showAndWait().ifPresent(path -> { + // Normal-Map + Seitenverhältnis aus dem Textur-Set auslesen + Path colorAbs = ASSET_ROOT.resolve(path.replace('/', java.io.File.separatorChar)); + TextureMapData grassMeta = loadTexMeta(colorAbs); + String[] nmCopy = input.grassNormalMapPaths.clone(); + nmCopy[slot] = grassMeta.normalMap != null ? grassMeta.normalMap : ""; + input.grassNormalMapPaths = nmCopy; + float[] arCopy = input.grassAspectRatios.clone(); + arCopy[slot] = readAspectRatio(path); + input.grassAspectRatios = arCopy; + if (slot == 0) { input.grassTexturePath = path; input.grassSettingsChanged = true; @@ -2946,8 +3192,7 @@ public class EditorApp extends Application { input.grassTextureSlots = copy; input.grassSlotsChanged = true; } - String fn = java.nio.file.Paths.get(path).getFileName().toString(); - nameLabel.setText(slot + " ▸ " + fn); + nameLabel.setText(slot + " ▸ " + labelFromPath(path)); ImageView newThumb = makeSlotThumb(path); thumb.setImage(newThumb.getImage()); }); @@ -2955,6 +3200,9 @@ public class EditorApp extends Application { Button clearBtn = new Button("X"); clearBtn.setOnAction(e -> { + String[] nmCopy = input.grassNormalMapPaths.clone(); + nmCopy[slot] = ""; + input.grassNormalMapPaths = nmCopy; if (slot == 0) { input.grassTexturePath = ""; input.grassSettingsChanged = true; @@ -2982,14 +3230,29 @@ public class EditorApp extends Application { ScrollPane scroll = new ScrollPane(slotRows); scroll.setFitToWidth(true); - scroll.setMaxHeight(300); + scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scroll.setStyle("-fx-background-color: transparent;"); + VBox.setVgrow(scroll, javafx.scene.layout.Priority.ALWAYS); VBox box = new VBox(5, title, scroll); box.setPadding(new Insets(4, 0, 0, 0)); + VBox.setVgrow(box, javafx.scene.layout.Priority.ALWAYS); return box; } + private float readAspectRatio(String assetRelPath) { + if (assetRelPath == null || assetRelPath.isEmpty()) return 1f; + try { + java.io.File f = ASSET_ROOT.resolve( + assetRelPath.replace('/', java.io.File.separatorChar)).toFile(); + if (!f.exists()) return 1f; + javafx.scene.image.Image img = new javafx.scene.image.Image(f.toURI().toString()); + double w = img.getWidth(), h = img.getHeight(); + return (w > 0 && h > 0) ? (float) (w / h) : 1f; + } catch (Exception e) { return 1f; } + } + private String getGrassSlotPath(int slot) { if (slot == 0) return input.grassTexturePath != null ? input.grassTexturePath : ""; String[] slots = input.grassTextureSlots; @@ -3020,8 +3283,8 @@ public class EditorApp extends Application { "" }; - String[] allPaths = new String[8]; - String[] allLabels = new String[8]; + String[] allPaths = new String[12]; + String[] allLabels = new String[12]; for (int i = 0; i < 4; i++) { String p = input.terrainTexturePaths[i]; allPaths[i] = (p != null && !p.isEmpty()) ? p : defaultsTerrain[i]; @@ -3032,6 +3295,11 @@ public class EditorApp extends Application { allPaths[4 + i] = (p != null && !p.isEmpty()) ? p : ""; allLabels[4 + i] = "S" + (i + 5) + " " + labelFromPath(allPaths[4 + i]); } + for (int i = 0; i < 4; i++) { + String p = input.thirdTexturePaths[i]; + allPaths[8 + i] = (p != null && !p.isEmpty()) ? p : ""; + allLabels[8 + i] = "S" + (i + 9) + " " + labelFromPath(allPaths[8 + i]); + } ToggleGroup tg = new ToggleGroup(); javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); @@ -3039,7 +3307,7 @@ public class EditorApp extends Application { tile.setVgap(4); tile.setPrefColumns(4); - for (int j = 0; j < 8; j++) { + for (int j = 0; j < 12; j++) { final int idx = j; String path = allPaths[j]; @@ -3155,6 +3423,61 @@ public class EditorApp extends Application { return tile; } + private VBox buildVoxelTextureSlotsUI() { + VBox box = new VBox(6); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + Label title = new Label("Voxel-Texturen (aus Textur-Brush)"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + box.getChildren().add(title); + + String[] roleLabels = { "Flache Flächen", "Steile Wände", "Decke / Unterseite" }; + int[] current = { input.voxelFlatSlot, input.voxelSteepSlot, input.voxelCeilSlot }; + + // Alle 12 Textur-Slots aus dem Brush zusammenbauen (Slot 0-3 unten, 4-7 oben, 8-11 dritte) + String[] slotNames = new String[13]; // Index 0 = "(keiner)", 1..12 = Slot 0..11 + slotNames[0] = "(keiner)"; + for (int s = 0; s < 4; s++) { + String p = input.terrainTexturePaths[s]; + slotNames[s + 1] = "Slot " + s + (p != null && !p.isEmpty() + ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); + } + for (int s = 0; s < 4; s++) { + String p = input.upperTexturePaths[s]; + slotNames[s + 5] = "Slot " + (s + 4) + (p != null && !p.isEmpty() + ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); + } + for (int s = 0; s < 4; s++) { + String p = input.thirdTexturePaths[s]; + slotNames[s + 9] = "Slot " + (s + 8) + (p != null && !p.isEmpty() + ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); + } + + for (int i = 0; i < 3; i++) { + final int roleIdx = i; + Label roleLbl = new Label(roleLabels[i]); + roleLbl.setStyle("-fx-text-fill: #444; -fx-font-size: 11;"); + + javafx.scene.control.ComboBox combo = new javafx.scene.control.ComboBox<>(); + combo.getItems().addAll(slotNames); + // current[i] ist -1..7 → ComboBox-Index 0..(current+1) + combo.getSelectionModel().select(Math.max(0, current[i] + 1)); + combo.setMaxWidth(Double.MAX_VALUE); + combo.setOnAction(ev -> { + int chosen = combo.getSelectionModel().getSelectedIndex() - 1; // -1 = keiner + switch (roleIdx) { + case 0 -> input.voxelFlatSlot = chosen; + case 1 -> input.voxelSteepSlot = chosen; + case 2 -> input.voxelCeilSlot = chosen; + } + input.voxelTexturesChanged = true; + }); + + VBox roleBox = new VBox(2, roleLbl, combo); + box.getChildren().add(roleBox); + } + return box; + } + private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) { ToggleGroup tg = new ToggleGroup(); javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); @@ -3234,7 +3557,13 @@ public class EditorApp extends Application { private static String labelFromPath(String path) { if (path == null || path.isEmpty()) return "(leer)"; - String name = java.nio.file.Paths.get(path).getFileName().toString(); + Path p = ASSET_ROOT.resolve(path.replace('/', java.io.File.separatorChar)); + // Wenn die Datei in einem Textur-Set-Ordner liegt, den Ordnernamen verwenden + Path parent = p.getParent(); + if (parent != null && Files.exists(parent.resolve("textureset.json"))) { + return parent.getFileName().toString(); + } + String name = p.getFileName().toString(); int dot = name.lastIndexOf('.'); return dot > 0 ? name.substring(0, dot) : name; } @@ -3247,19 +3576,21 @@ public class EditorApp extends Application { title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); box.getChildren().add(title); - String[] paths = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; - String[] nmPaths = isUpper ? input.upperNormalMapPaths : input.terrainNormalMapPaths; + String[] paths = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; String[] defaults = isUpper ? new String[]{"Textures/Terrain/Rock2/rock.jpg","","",""} : new String[]{"Textures/Terrain/splat/grass.jpg","Textures/Terrain/Rock2/rock.jpg","Textures/Terrain/splat/dirt.jpg",""}; + final int group = isUpper ? 1 : 0; + for (int i = 0; i < 4; i++) { - final int slot = i; + final int slot = i; + final int globalIdx = (isUpper ? 4 : 0) + i; // ── Diffuse-Zeile ───────────────────────────────────────────────── String current = (paths[i] != null && !paths[i].isEmpty()) - ? java.nio.file.Paths.get(paths[i]).getFileName().toString() - : (defaults[i].isEmpty() ? "(kein)" : java.nio.file.Paths.get(defaults[i]).getFileName().toString() + " (std)"); + ? labelFromPath(paths[i]) + : (defaults[i].isEmpty() ? "(kein)" : labelFromPath(defaults[i]) + " (std)"); Label slotLbl = new Label("Slot " + (isUpper ? i + 5 : i + 1) + ": " + current); slotLbl.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); @@ -3283,45 +3614,14 @@ public class EditorApp extends Application { HBox row = new HBox(4, slotLbl, chooseBtn, clearBtn); HBox.setHgrow(slotLbl, Priority.ALWAYS); row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - - // ── Normal-Map-Zeile ────────────────────────────────────────────── - String nmCurrent = (nmPaths[i] != null && !nmPaths[i].isEmpty()) - ? java.nio.file.Paths.get(nmPaths[i]).getFileName().toString() - : "(keine)"; - Label nmLbl = new Label(" Norm: " + nmCurrent); - nmLbl.setStyle("-fx-text-fill: #777; -fx-font-size: 10;"); - nmLbl.setMaxWidth(Double.MAX_VALUE); - nmLbl.setMinWidth(0); - nmLbl.setEllipsisString("…"); - - javafx.scene.control.Button nmChooseBtn = new javafx.scene.control.Button("Wählen"); - nmChooseBtn.setOnAction(ev -> { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - chooser.showAndWait().ifPresent(p -> { - String[] nm = isUpper ? input.upperNormalMapPaths : input.terrainNormalMapPaths; - nm[slot] = p; - if (isUpper) input.upperNormalMapsChanged = true; - else input.terrainNormalMapsChanged = true; - showToolParameters(toolPanel, input.activeTool); - }); + row.setOnContextMenuRequested(ev -> { + String tex = (isUpper ? input.upperTexturePaths : input.terrainTexturePaths)[slot]; + if (tex != null && !tex.isEmpty()) + showTextureSlotDetails(tex, slot, group, row, ev.getScreenX(), ev.getScreenY()); + ev.consume(); }); - javafx.scene.control.Button nmClearBtn = new javafx.scene.control.Button("✕"); - nmClearBtn.setTooltip(new Tooltip("Normal Map entfernen")); - nmClearBtn.setOnAction(ev -> { - String[] nm = isUpper ? input.upperNormalMapPaths : input.terrainNormalMapPaths; - nm[slot] = ""; - if (isUpper) input.upperNormalMapsChanged = true; - else input.terrainNormalMapsChanged = true; - showToolParameters(toolPanel, input.activeTool); - }); - - HBox nmRow = new HBox(4, nmLbl, nmChooseBtn, nmClearBtn); - HBox.setHgrow(nmLbl, Priority.ALWAYS); - nmRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - - box.getChildren().addAll(row, nmRow); + box.getChildren().add(row); } return box; } @@ -3335,10 +3635,214 @@ public class EditorApp extends Application { paths[slot] = path; if (isUpper) input.upperTexturesChanged = true; else input.terrainTexturesChanged = true; + applyMetaToSlot(path, slot, isUpper ? 1 : 0); showToolParameters(toolPanel, input.activeTool); }); } + /** + * Liest die textureset.json für texRelPath und trägt Normal- und Displacement-Map + * automatisch in den entsprechenden Slot ein. + * group: 0 = terrain, 1 = upper, 2 = third + */ + private void applyMetaToSlot(String texRelPath, int slot, int group) { + if (texRelPath == null || texRelPath.isEmpty()) return; + Path absPath = ASSET_ROOT.resolve(texRelPath.replace('/', java.io.File.separatorChar)); + TextureMapData meta = loadTexMeta(absPath); + boolean changed = false; + + if (!meta.normalMap.isEmpty()) { + switch (group) { + case 0 -> { input.terrainNormalMapPaths[slot] = meta.normalMap; input.terrainNormalMapsChanged = true; } + case 1 -> { input.upperNormalMapPaths[slot] = meta.normalMap; input.upperNormalMapsChanged = true; } + case 2 -> { input.thirdNormalMapPaths[slot] = meta.normalMap; input.thirdNormalMapsChanged = true; } + } + changed = true; + } + if (!meta.displacementMap.isEmpty()) { + switch (group) { + case 0 -> input.terrainDisplacementMapPaths[slot] = meta.displacementMap; + case 1 -> input.upperDisplacementMapPaths[slot] = meta.displacementMap; + case 2 -> input.thirdDisplacementMapPaths[slot] = meta.displacementMap; + } + changed = true; + } + if (changed) input.voxelTexturesChanged = true; + } + + /** Baut die 4-Slot-Auswahl für die dritte Gruppe (Slots 9-12). */ + private VBox buildThirdTextureSlotsUI() { + VBox box = new VBox(5); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + Label title = new Label("Texturen Slots 9-12"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + box.getChildren().add(title); + + String[] paths = input.thirdTexturePaths; + + for (int i = 0; i < 4; i++) { + final int slot = i; + final int globalIdx = 8 + i; + + String current = (paths[i] != null && !paths[i].isEmpty()) + ? labelFromPath(paths[i]) + : "(kein)"; + + Label slotLbl = new Label("Slot " + (i + 9) + ": " + current); + slotLbl.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); + slotLbl.setMaxWidth(Double.MAX_VALUE); + slotLbl.setMinWidth(0); + slotLbl.setEllipsisString("…"); + + javafx.scene.control.Button chooseBtn = new javafx.scene.control.Button("Wählen"); + chooseBtn.setOnAction(ev -> { + TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); + if (primaryStage != null) chooser.initOwner(primaryStage); + chooser.showAndWait().ifPresent(p -> { + input.thirdTexturePaths[slot] = p; + input.thirdTexturesChanged = true; + applyMetaToSlot(p, slot, 2); + showToolParameters(toolPanel, input.activeTool); + }); + }); + + javafx.scene.control.Button clearBtn = new javafx.scene.control.Button("✕"); + clearBtn.setTooltip(new Tooltip("Auf Standard zurücksetzen")); + clearBtn.setOnAction(ev -> { + input.thirdTexturePaths[slot] = ""; + input.thirdTexturesChanged = true; + showToolParameters(toolPanel, input.activeTool); + }); + + HBox row = new HBox(4, slotLbl, chooseBtn, clearBtn); + HBox.setHgrow(slotLbl, Priority.ALWAYS); + row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + row.setOnContextMenuRequested(ev -> { + String tex = input.thirdTexturePaths[slot]; + if (tex != null && !tex.isEmpty()) + showTextureSlotDetails(tex, slot, 2, row, ev.getScreenX(), ev.getScreenY()); + ev.consume(); + }); + box.getChildren().add(row); + } + return box; + } + + /** + * Normal-Map-Prioritätsliste: zeigt alle 12 Slots, lässt den Nutzer die Reihenfolge + * per ↑/↓ ändern. Slots ohne Normal-Map sind ausgegraut; ein Budget-Hinweis zeigt, + * wie viele Normal Maps tatsächlich an die GPU gebunden werden. + */ + private VBox buildNormalMapPriorityUI() { + VBox box = new VBox(5); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + + Label title = new Label("Normal Map Priorität"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + + // Budget-Hinweis (dynamisch berechnet) + Label budgetLbl = new Label(); + budgetLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #555;"); + budgetLbl.setWrapText(true); + + // Alle 12 Normalpfade (flat, Index 0-11 = Slot 1-12) + java.util.function.Supplier normPaths = () -> { + String[] p = new String[12]; + System.arraycopy(input.terrainNormalMapPaths, 0, p, 0, 4); + System.arraycopy(input.upperNormalMapPaths, 0, p, 4, 4); + System.arraycopy(input.thirdNormalMapPaths, 0, p, 8, 4); + return p; + }; + + // Slot-Labels (Slot 1-12) + java.util.function.Function slotLabel = idx -> "S" + (idx + 1); + + // Observable-Liste der globalen Slot-Indizes in Prioritätsreihenfolge + javafx.collections.ObservableList items = + javafx.collections.FXCollections.observableArrayList(); + for (int v : input.normalMapOrder) items.add(v); + + // Budget neu berechnen und Label aktualisieren + Runnable refreshBudget = () -> { + String[] np = normPaths.get(); + long withNorm = Arrays.stream(np).filter(s -> s != null && !s.isEmpty()).count(); + boolean hasUpper = Arrays.stream(input.upperTexturePaths).anyMatch(s -> s != null && !s.isEmpty()); + boolean hasThird = Arrays.stream(input.thirdTexturePaths).anyMatch(s -> s != null && !s.isEmpty()); + int alphaCost = 1 + (hasUpper ? 1 : 0) + (hasThird ? 1 : 0); + long diffCount = Arrays.stream(input.terrainTexturePaths).filter(s -> s != null && !s.isEmpty()).count() + + Arrays.stream(input.upperTexturePaths).filter(s -> s != null && !s.isEmpty()).count() + + Arrays.stream(input.thirdTexturePaths).filter(s -> s != null && !s.isEmpty()).count(); + int normBudget = Math.max(0, 16 - alphaCost - (int) diffCount); + budgetLbl.setText("Budget: " + normBudget + " Normal Map" + (normBudget != 1 ? "s" : "") + + " (von " + withNorm + " konfiguriert). Oben = höchste Prio."); + }; + refreshBudget.run(); + + // ListView + javafx.scene.control.ListView list = new javafx.scene.control.ListView<>(items); + list.setPrefHeight(200); + list.setMaxHeight(240); + list.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(Integer idx, boolean empty) { + super.updateItem(idx, empty); + if (empty || idx == null) { setText(null); setStyle(""); return; } + String[] np = normPaths.get(); + String nm = (np[idx] != null && !np[idx].isEmpty()) ? labelFromPath(np[idx]) : "–"; + setText(slotLabel.apply(idx) + " → " + nm); + boolean hasNorm = np[idx] != null && !np[idx].isEmpty(); + setStyle(hasNorm ? "-fx-text-fill: #111;" : "-fx-text-fill: #aaa;"); + } + }); + + // Schreibe Reihenfolge zurück nach SharedInput + Runnable commit = () -> { + int[] order = new int[12]; + for (int i = 0; i < 12; i++) order[i] = items.get(i); + input.normalMapOrder = order; + input.normalMapOrderChanged = true; + refreshBudget.run(); + list.refresh(); + }; + + // Auf Standardreihenfolge zurücksetzen + javafx.scene.control.Button resetBtn = new javafx.scene.control.Button("Reset"); + resetBtn.setTooltip(new Tooltip("Reihenfolge auf 1–12 zurücksetzen")); + resetBtn.setOnAction(e -> { + items.setAll(0,1,2,3,4,5,6,7,8,9,10,11); + commit.run(); + }); + + // ↑ / ↓ Buttons + javafx.scene.control.Button upBtn = new javafx.scene.control.Button("↑"); + javafx.scene.control.Button downBtn = new javafx.scene.control.Button("↓"); + + upBtn.setOnAction(e -> { + int sel = list.getSelectionModel().getSelectedIndex(); + if (sel <= 0) return; + Integer tmp = items.get(sel - 1); + items.set(sel - 1, items.get(sel)); + items.set(sel, tmp); + list.getSelectionModel().select(sel - 1); + commit.run(); + }); + downBtn.setOnAction(e -> { + int sel = list.getSelectionModel().getSelectedIndex(); + if (sel < 0 || sel >= items.size() - 1) return; + Integer tmp = items.get(sel + 1); + items.set(sel + 1, items.get(sel)); + items.set(sel, tmp); + list.getSelectionModel().select(sel + 1); + commit.run(); + }); + + HBox btnRow = new HBox(4, upBtn, downBtn, resetBtn); + btnRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + + box.getChildren().addAll(title, budgetLbl, list, btnRow); + return box; + } + // ── Linke Seite: Asset-Panel (Welteneditor) ────────────────────────────── private VBox buildAssetPanel() { @@ -3358,6 +3862,7 @@ public class EditorApp extends Application { populateAssetTree(assetTreeRoot); TreeView tree = new TreeView<>(assetTreeRoot); + assetTree = tree; tree.setShowRoot(false); VBox.setVgrow(tree, Priority.ALWAYS); tree.setCellFactory(tv -> buildAssetCell()); @@ -3436,13 +3941,19 @@ public class EditorApp extends Application { Button importBtn = new Button("⊕ Import…"); importBtn.setMaxWidth(Double.MAX_VALUE); importBtn.setOnAction(e -> { - TreeItem sel = tree.getSelectionModel().getSelectedItem(); - TreeItem cat = sel != null ? getCategoryRoot(sel) : null; - if (cat == animationsNode || sel == animationsNode) { - handleAnimationImport(tree.getScene().getWindow()); - } else { - handleImport(tree.getScene().getWindow()); - } + ContextMenu popup = new ContextMenu(); + MenuItem miModel = new MenuItem("Modell mit LOD-Editor…"); + MenuItem miTex = new MenuItem("Texturen…"); + MenuItem miTexZip = new MenuItem("Texturen-ZIP…"); + MenuItem miAudio = new MenuItem("Audio…"); + MenuItem miAnim = new MenuItem("Animationen…"); + miModel.setOnAction(ev -> openModelImport(importBtn.getScene().getWindow() instanceof javafx.stage.Stage s ? s : primaryStage)); + miTex.setOnAction(ev -> handleTextureImport(importBtn.getScene().getWindow())); + miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow())); + miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow())); + miAnim.setOnAction(ev -> handleAnimationImport(importBtn.getScene().getWindow())); + popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim); + popup.show(importBtn, javafx.geometry.Side.TOP, 0, 0); }); Button refreshBtn = new Button("↻"); @@ -3623,17 +4134,41 @@ public class EditorApp extends Application { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) { setText(null); setStyle(""); return; } + if (empty || item == null) { setText(null); setGraphic(null); setStyle(""); return; } + javafx.scene.shape.Rectangle swatch = null; + if (textureSetNodes.contains(getTreeItem())) { + swatch = new javafx.scene.shape.Rectangle(10, 10); + swatch.setFill(javafx.scene.paint.Color.web("#8b5cf6")); + } else { + Path tp = itemPaths.get(getTreeItem()); + if (tp != null && getCategoryRoot(getTreeItem()) == texturesNode) { + String lo = tp.getFileName().toString().toLowerCase(); + if (lo.endsWith(".png") || lo.endsWith(".jpg") + || lo.endsWith(".jpeg") || lo.endsWith(".dds")) { + swatch = new javafx.scene.shape.Rectangle(10, 10); + swatch.setFill(javafx.scene.paint.Color.web("#22c55e")); + } + } + } + if (swatch != null) { + swatch.setArcWidth(3); swatch.setArcHeight(3); + setGraphic(swatch); + } else { + setGraphic(null); + } setText(item); setCellStyle(this, false); } }; - // ── Drag-Quelle: nur Dateien (keine Ordner), JME-Texturen schreibgeschützt ─ + // ── Drag-Quelle: Dateien + Unterordner (keine Kategorie-Wurzeln, keine JME-Einträge) ─ cell.setOnDragDetected(e -> { TreeItem item = cell.getTreeItem(); - if (item == null || isAssetFolder(item)) return; - if (jmePaths.containsKey(item)) return; + if (item == null) return; + if (jmePaths.containsKey(item) || jmeFolderNodes.contains(item)) return; + // Kategorie-Wurzeln (Models, Textures, …) nicht verschiebbar + if (item == modelsNode || item == texturesNode || item == audioNode + || item == animationsNode || item == itemsNode) return; draggedItem = item; Dragboard db = cell.startDragAndDrop(TransferMode.MOVE); ClipboardContent cc = new ClipboardContent(); @@ -3666,15 +4201,27 @@ public class EditorApp extends Application { Path dest = destDir.resolve(src.getFileName()); if (!src.equals(dest)) { if (Files.exists(dest)) { - setStatus("Fehler: Datei mit diesem Namen existiert bereits im Zielordner."); + setStatus("Fehler: " + src.getFileName() + " existiert bereits im Zielordner."); } else { + // isDirectory VOR dem Verschieben prüfen (danach ist src weg) + boolean srcIsFolder = Files.isDirectory(src); try { Files.move(src, dest); draggedItem.getParent().getChildren().remove(draggedItem); - itemPaths.remove(draggedItem); - TreeItem moved = new TreeItem<>(src.getFileName().toString()); - itemPaths.put(moved, dest); - target.getChildren().add(moved); + if (srcIsFolder) { + // Ordner: TreeItem mit gesamtem Subtree reparentieren, + // alle itemPaths im Subtree auf neuen Pfad umbiegen + updateItemPathsUnder(draggedItem, src, dest); + itemPaths.put(draggedItem, dest); + } else { + // Datei: TreeItem neu anlegen; Set-Markierung übertragen + boolean wasSet = textureSetNodes.remove(draggedItem); + itemPaths.remove(draggedItem); + draggedItem = new TreeItem<>(src.getFileName().toString()); + itemPaths.put(draggedItem, dest); + if (wasSet) textureSetNodes.add(draggedItem); + } + target.getChildren().add(draggedItem); target.setExpanded(true); setStatus("Verschoben: " + src.getFileName()); ok = true; @@ -3698,6 +4245,49 @@ public class EditorApp extends Application { if (jmeFolderNodes.contains(item) || jmePaths.containsKey(item)) return; ContextMenu ctx = new ContextMenu(); + // Texture-Set-Knoten: eigenes Menü + if (textureSetNodes.contains(item)) { + Path setDir = itemPaths.get(item); + boolean setReadOnly = isInternalTexture(setDir); + MenuItem details = new MenuItem("Details…"); + details.setOnAction(ev -> { + TextureDetailDialog dlg = TextureDetailDialog.forSet(ASSET_ROOT, setDir, setReadOnly); + dlg.initOwner(ctx.getOwnerWindow()); + dlg.showAndWait().filter(Boolean.TRUE::equals).ifPresent(ok -> + refreshCategoryNode(texturesNode, true)); + }); + MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); + reveal.setOnAction(ev -> { + try { Runtime.getRuntime().exec(new String[]{"xdg-open", setDir.toString()}); } + catch (IOException ignored) {} + }); + MenuItem del = new MenuItem("🗑 Löschen"); + del.setStyle("-fx-text-fill: #c0392b;"); + del.setDisable(setReadOnly); + del.setOnAction(ev -> { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + "Textur-Set wirklich löschen?\n" + setDir.getFileName() + + "\n(Alle enthaltenen Dateien werden gelöscht!)", + ButtonType.OK, ButtonType.CANCEL); + confirm.setHeaderText(null); + confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { + try { + deleteDirectoryRecursive(setDir); + item.getParent().getChildren().remove(item); + itemPaths.remove(item); + textureSetNodes.remove(item); + setStatus("Textur-Set gelöscht: " + setDir.getFileName()); + } catch (IOException ex) { + setStatus("Fehler: " + ex.getMessage()); + } + }); + }); + ctx.getItems().addAll(details, new SeparatorMenuItem(), reveal, new SeparatorMenuItem(), del); + ctx.show(cell, e.getScreenX(), e.getScreenY()); + e.consume(); + return; + } + if (isAssetFolder(item)) { MenuItem newSub = new MenuItem("📁 Neuer Unterordner…"); newSub.setOnAction(ev -> createSubfolder(item)); @@ -3800,6 +4390,24 @@ public class EditorApp extends Application { ctx.getItems().addAll(placeItem, editItem, new SeparatorMenuItem()); } + // Textur-Map-Zuordnung (nur für Bilddateien im Textures-Zweig) + String loName = pStr.toLowerCase(); + boolean isImage = loName.endsWith(".png") || loName.endsWith(".jpg") + || loName.endsWith(".jpeg") || loName.endsWith(".dds"); + if (isImage && getCategoryRoot(item) == texturesNode && !jmePaths.containsKey(item)) { + boolean imgReadOnly = isInternalTexture(p); + MenuItem imgDetails = new MenuItem("Details…"); + imgDetails.setOnAction(ev -> { + TextureDetailDialog dlg = TextureDetailDialog.forFile(ASSET_ROOT, p, imgReadOnly); + dlg.initOwner(ctx.getOwnerWindow()); + dlg.setOnConvertedToSet(newSetDir -> + Platform.runLater(() -> refreshCategoryNode(texturesNode, true))); + dlg.showAndWait().filter(Boolean.TRUE::equals).ifPresent(ok -> + refreshCategoryNode(texturesNode, true)); + }); + ctx.getItems().addAll(imgDetails, new SeparatorMenuItem()); + } + MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); reveal.setOnAction(ev -> { try { Runtime.getRuntime().exec( @@ -3838,7 +4446,9 @@ public class EditorApp extends Application { } private void setCellStyle(TreeCell cell, boolean highlighted) { - String base = isAssetFolder(cell.getTreeItem()) ? "-fx-font-weight:bold;" : ""; + TreeItem ti = cell.getTreeItem(); + boolean isBold = isAssetFolder(ti) && !textureSetNodes.contains(ti); + String base = isBold ? "-fx-font-weight:bold;" : ""; cell.setStyle(highlighted ? base + "-fx-background-color:#cce5ff;" : base); } @@ -3860,17 +4470,53 @@ public class EditorApp extends Application { return null; } + /** Gibt true zurück wenn der Pfad unter Textures/internal/ liegt (schreibgeschützt). */ + private static boolean isInternalTexture(Path p) { + if (p == null) return false; + for (Path part : p) { + if (part.toString().equals("internal")) return true; + } + return false; + } + /** Gibt true zurück wenn draggedItem auf dieses target verschoben werden darf. */ private boolean isValidDropTarget(TreeItem target) { if (draggedItem == null || target == null) return false; if (!isAssetFolder(target)) return false; - if (jmeFolderNodes.contains(target)) return false; // JME-Ordner sind schreibgeschützt - if (target == draggedItem.getParent()) return false; // schon dort + if (jmeFolderNodes.contains(target)) return false; + if (textureSetNodes.contains(target)) return false; // Set-Ordner sind keine Drop-Ziele + if (target == draggedItem.getParent()) return false; + if (target == draggedItem) return false; + // Ordner darf nicht in einen eigenen Nachfolger gezogen werden + if (isAssetFolder(draggedItem) && isDescendantOf(draggedItem, target)) return false; TreeItem dragCat = getCategoryRoot(draggedItem); TreeItem dropCat = getCategoryRoot(target); return dragCat != null && dragCat == dropCat; } + private static boolean isDescendantOf(TreeItem ancestor, TreeItem item) { + TreeItem cur = item.getParent(); + while (cur != null) { + if (cur == ancestor) return true; + cur = cur.getParent(); + } + return false; + } + + /** Aktualisiert rekursiv alle itemPaths-Einträge unter einem verschobenen Ordner. */ + private void updateItemPathsUnder(TreeItem node, Path oldBase, Path newBase) { + for (TreeItem child : node.getChildren()) { + Path old = itemPaths.get(child); + if (old != null) { + Path relative = oldBase.relativize(old); + itemPaths.put(child, newBase.resolve(relative)); + } + if (!child.getChildren().isEmpty()) { + updateItemPathsUnder(child, oldBase, newBase); + } + } + } + /** Erstellt einen neuen Unterordner unter parentItem (Dialog + Filesystem). */ private void createSubfolder(TreeItem parentItem) { Path parentPath = itemPaths.get(parentItem); @@ -4087,18 +4733,27 @@ public class EditorApp extends Application { .thenComparing(p -> p.getFileName().toString().toLowerCase())) .toList(); for (Path p : paths) { + String name = p.getFileName().toString(); if (Files.isDirectory(p)) { - TreeItem sub = new TreeItem<>(p.getFileName().toString()); - itemPaths.put(sub, p); - parent.getChildren().add(sub); - loadAssetsRecursive(sub, p, exts); + // Texture-Set-Ordner: enthält textureset.json → als einzelner Blattknoten + if (Files.exists(p.resolve("textureset.json"))) { + TreeItem sub = new TreeItem<>(name); + itemPaths.put(sub, p); + textureSetNodes.add(sub); + parent.getChildren().add(sub); + } else { + TreeItem sub = new TreeItem<>(name); + itemPaths.put(sub, p); + parent.getChildren().add(sub); + loadAssetsRecursive(sub, p, exts); + } } else { - String lo = p.getFileName().toString().toLowerCase(); + String lo = name.toLowerCase(); if (lo.endsWith(".meta")) continue; boolean include = exts.length == 0 || java.util.Arrays.stream(exts).anyMatch(lo::endsWith); if (include) { - TreeItem file = new TreeItem<>(p.getFileName().toString()); + TreeItem file = new TreeItem<>(name); itemPaths.put(file, p); parent.getChildren().add(file); } @@ -4143,6 +4798,7 @@ public class EditorApp extends Application { audioNode = null; animationsNode = null; itemsNode = null; jmeModelsNode = null; jmeTexturesNode = null; jmeFolderNodes.clear(); + textureSetNodes.clear(); List topDirs; try (var s = Files.list(ASSET_ROOT)) { @@ -4257,6 +4913,7 @@ public class EditorApp extends Application { for (TreeItem child : item.getChildren()) clearItemPathsFor(child); if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode) itemPaths.remove(item); + textureSetNodes.remove(item); } /** @@ -4414,16 +5071,561 @@ public class EditorApp extends Application { } } + private void handleTextureImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)"); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Bildateien", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"), + new FileChooser.ExtensionFilter("PNG", "*.png"), + new FileChooser.ExtensionFilter("JPEG", "*.jpg","*.jpeg"), + new FileChooser.ExtensionFilter("Weitere", "*.bmp","*.tga","*.dds")); + List files = fc.showOpenMultipleDialog(owner); + if (files == null) return; + Path destDir = ASSET_ROOT.resolve("Textures"); + int ok = 0; + for (File file : files) { + try { + Files.createDirectories(destDir); + Path dest = importTextureAsPng(file, destDir); + TreeItem item = new TreeItem<>(dest.getFileName().toString()); + itemPaths.put(item, dest); + texturesNode.getChildren().add(item); + ok++; + } catch (IOException ex) { + setStatus("Fehler beim Import von " + file.getName() + ": " + ex.getMessage()); + } + } + if (ok > 0) { + texturesNode.setExpanded(true); + setStatus(ok + " Textur(en) importiert"); + } + } + + /** + * Kopiert oder konvertiert eine Bilddatei als PNG in destDir. + * PNG + DDS werden unverändert kopiert; JPG/BMP/TGA werden nach PNG konvertiert. + * Gibt den Pfad der resultierenden Datei zurück. + */ + private static Path importTextureAsPng(File src, Path destDir) throws IOException { + String nameL = src.getName().toLowerCase(); + String base = src.getName().replaceFirst("\\.[^.]+$", ""); + + // DDS bleibt unverändert (GPU-komprimiertes Format, JME3 lädt es nativ) + if (nameL.endsWith(".dds") || nameL.endsWith(".png")) { + Path dest = destDir.resolve(src.getName()); + Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); + return dest; + } + + Path destPng = destDir.resolve(base + ".png"); + BufferedImage img = null; + + if (nameL.endsWith(".tga")) { + img = loadTgaAsBufferedImage(src); + } else { + // JPG, JPEG, BMP, GIF – von Java ImageIO unterstützt + img = ImageIO.read(src); + } + + if (img == null) { + // Fallback: Datei unverändert kopieren + Path dest = destDir.resolve(src.getName()); + Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); + return dest; + } + + // Sicherstellen, dass das Bild ARGB hat (für verlustfreies PNG) + if (img.getType() != BufferedImage.TYPE_INT_ARGB) { + BufferedImage rgba = new BufferedImage(img.getWidth(), img.getHeight(), + BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = rgba.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + img = rgba; + } + + ImageIO.write(img, "PNG", destPng.toFile()); + return destPng; + } + + /** Lädt eine TGA-Datei mit JME3's TGALoader und gibt ein BufferedImage zurück. */ + private static BufferedImage loadTgaAsBufferedImage(File tgaFile) throws IOException { + com.jme3.texture.plugins.TGALoader loader = new com.jme3.texture.plugins.TGALoader(); + com.jme3.asset.TextureKey key = new com.jme3.asset.TextureKey(tgaFile.getName(), true); + com.jme3.asset.AssetInfo info = new com.jme3.asset.AssetInfo(null, key) { + @Override public java.io.InputStream openStream() { + try { return new java.io.FileInputStream(tgaFile); } + catch (java.io.FileNotFoundException e) { throw new RuntimeException(e); } + } + }; + + com.jme3.texture.Image jmeImg = (com.jme3.texture.Image) loader.load(info); + int w = jmeImg.getWidth(), h = jmeImg.getHeight(); + ByteBuffer data = jmeImg.getData(0); + data.rewind(); + + BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + int[] pixels = new int[w * h]; + com.jme3.texture.Image.Format fmt = jmeImg.getFormat(); + for (int i = 0; i < w * h; i++) { + int r, g, b, a; + switch (fmt) { + case RGBA8 -> { r = data.get()&0xFF; g = data.get()&0xFF; b = data.get()&0xFF; a = data.get()&0xFF; } + case RGB8 -> { r = data.get()&0xFF; g = data.get()&0xFF; b = data.get()&0xFF; a = 255; } + case BGR8 -> { b = data.get()&0xFF; g = data.get()&0xFF; r = data.get()&0xFF; a = 255; } + case BGRA8 -> { b = data.get()&0xFF; g = data.get()&0xFF; r = data.get()&0xFF; a = data.get()&0xFF; } + default -> { r = g = b = a = 128; } + } + pixels[i] = (a<<24)|(r<<16)|(g<<8)|b; + } + bi.setRGB(0, 0, w, h, pixels, 0, w); + return bi; + } + + // ── Textur-Set-Import ───────────────────────────────────────────────────── + + private void openTextureSetImport(javafx.stage.Window owner) { + de.blight.editor.ui.TextureImportDialog dlg = new de.blight.editor.ui.TextureImportDialog(ASSET_ROOT); + if (owner != null) dlg.initOwner(owner); + dlg.showAndWait(); + Path imported = dlg.getLastImportedDir(); + if (imported == null) return; + refreshCategoryNode(texturesNode, true); + // importierten Knoten im Baum vorselektieren + for (Map.Entry, Path> e : itemPaths.entrySet()) { + if (imported.equals(e.getValue())) { + TreeItem target = e.getKey(); + if (assetTree != null) { + assetTree.getSelectionModel().select(target); + int idx = assetTree.getRow(target); + if (idx >= 0) assetTree.scrollTo(idx); + } + break; + } + } + } + + // ── Texturen-ZIP-Import ─────────────────────────────────────────────────── + + /** Fragt nach einer oder mehreren ZIP-Dateien, entpackt sie und verknüpft Maps. */ + private void handleZipTextureImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Texturen-ZIPs importieren"); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("ZIP-Archiv", "*.zip")); + List zipFiles = fc.showOpenMultipleDialog(owner); + if (zipFiles == null || zipFiles.isEmpty()) return; + + int totalExtracted = 0; + int totalLinked = 0; + List errors = new java.util.ArrayList<>(); + + for (File zipFile : zipFiles) { + try { + int[] result = importSingleZip(zipFile); + totalExtracted += result[0]; + totalLinked += result[1]; + } catch (IOException ex) { + errors.add(zipFile.getName() + ": " + ex.getMessage()); + } + } + + refreshCategoryNode(texturesNode, true); + if (texturesNode != null) texturesNode.setExpanded(true); + + if (!errors.isEmpty()) { + setStatus("Fehler: " + String.join(", ", errors)); + } else { + String s = totalExtracted + " Dateien aus " + zipFiles.size() + " ZIP(s) importiert"; + if (totalLinked > 0) s += " – " + totalLinked + " Map(s) verknüpft"; + setStatus(s); + } + } + + /** + * Importiert eine einzelne ZIP-Datei nach Textures/{zipBaseName}/. + * @return int[2]: [0] = extrahierte Dateien, [1] = verknüpfte Maps (0 oder 1) + */ + private int[] importSingleZip(File zipFile) throws IOException { + String zipBase = zipFile.getName().replaceFirst("\\.zip$", ""); + Path destDir = ASSET_ROOT.resolve("Textures").resolve(zipBase); + Files.createDirectories(destDir); + + Map typeToPath = new HashMap<>(); + int extracted = 0; + + try (ZipInputStream zis = new ZipInputStream( + new java.io.BufferedInputStream(new java.io.FileInputStream(zipFile)))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) { zis.closeEntry(); continue; } + String entryName = java.nio.file.Paths.get(entry.getName()).getFileName().toString(); + if (!isSupportedAsset(entryName)) { zis.closeEntry(); continue; } + Path tmp = destDir.resolve(entryName); + Files.copy(zis, tmp, StandardCopyOption.REPLACE_EXISTING); + Path png = importTextureAsPng(tmp.toFile(), destDir); + if (!png.equals(tmp)) Files.deleteIfExists(tmp); + String type = detectTexMapType(entryName); + if (type != null) typeToPath.merge(type, png, (a, b) -> a); // GL bevorzugt + extracted++; + zis.closeEntry(); + } + } + + // NormalGL bevorzugen, NormalDX nur als Fallback + if (!typeToPath.containsKey("normal")) { + if (typeToPath.containsKey("normalGL")) typeToPath.put("normal", typeToPath.get("normalGL")); + else if (typeToPath.containsKey("normalDX")) typeToPath.put("normal", typeToPath.get("normalDX")); + } + + Path colorPath = typeToPath.get("color"); + int linked = 0; + if (colorPath != null) { + TextureMapData meta = new TextureMapData(); + meta.colorMap = ASSET_ROOT.relativize(colorPath).toString().replace('\\', '/'); + Path normPath = typeToPath.get("normal"); + Path dispPath = typeToPath.get("displacement"); + if (normPath != null) meta.normalMap = ASSET_ROOT.relativize(normPath).toString().replace('\\', '/'); + if (dispPath != null) meta.displacementMap = ASSET_ROOT.relativize(dispPath).toString().replace('\\', '/'); + if (typeToPath.containsKey("roughness")) meta.roughnessMap = ASSET_ROOT.relativize(typeToPath.get("roughness")).toString().replace('\\', '/'); + if (typeToPath.containsKey("ao")) meta.aoMap = ASSET_ROOT.relativize(typeToPath.get("ao")).toString().replace('\\', '/'); + saveTexMeta(colorPath, meta); + autoAssignMapsForTexture(meta.colorMap, meta); + linked = 1; + } + + return new int[]{ extracted, linked }; + } + + /** Gibt true zurück wenn die Datei ein unterstütztes Asset ist (Bild, Audio, Modell …). */ + private static boolean isSupportedAsset(String filename) { + String lo = filename.toLowerCase(); + return lo.endsWith(".png") || lo.endsWith(".jpg") || lo.endsWith(".jpeg") + || lo.endsWith(".tga") || lo.endsWith(".bmp") || lo.endsWith(".dds") + || lo.endsWith(".exr") || lo.endsWith(".hdr") + || lo.endsWith(".ogg") || lo.endsWith(".wav") + || lo.endsWith(".gltf") || lo.endsWith(".glb") || lo.endsWith(".fbx") + || lo.endsWith(".obj") || lo.endsWith(".j3o"); + } + + /** Erkennt den Map-Typ anhand des Dateinamens (Suffix vor der Extension). */ + private static String detectTexMapType(String filename) { + String base = filename.toLowerCase().replaceFirst("\\.[^.]+$", ""); + if (base.endsWith("_color") || base.endsWith("_colour") || base.endsWith("_diffuse") || base.endsWith("_albedo")) + return "color"; + if (base.endsWith("_normalgl") || base.endsWith("_normal_gl") || base.endsWith("_normalgl_directx")) + return "normalGL"; + if (base.endsWith("_normaldx") || base.endsWith("_normal_dx") || base.endsWith("_normaldirectx")) + return "normalDX"; + if (base.endsWith("_normal")) + return "normalGL"; // generic normal = GL convention assumed + if (base.endsWith("_displacement") || base.endsWith("_height") || base.endsWith("_disp")) + return "displacement"; + if (base.endsWith("_roughness") || base.endsWith("_rough")) + return "roughness"; + if (base.endsWith("_ambientocclusion") || base.endsWith("_ao") || base.endsWith("_occlusion")) + return "ao"; + return null; + } + + // ── Texture-Map-Kontextmenü-Aktion ──────────────────────────────────────── + + /** + * Standalone-Textur: erstellt einen Set-Ordner, verschiebt die Textur hinein, + * lässt den User eine Normal- oder Displacement-Map wählen und schreibt textureset.json. + */ + private void assignMapToTexture(Path colorTexPath, boolean isDisplacement, javafx.stage.Window owner) { + TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); + if (primaryStage != null) chooser.initOwner(primaryStage); + chooser.showAndWait().ifPresent(picked -> { + if (picked == null || picked.isEmpty()) return; + + // Set-Ordner anlegen (gleicher Name wie Textur ohne Extension) + String baseName = colorTexPath.getFileName().toString().replaceFirst("\\.[^.]+$", ""); + Path setDir = colorTexPath.getParent().resolve(baseName); + Path newColor; + try { + Files.createDirectories(setDir); + newColor = setDir.resolve(colorTexPath.getFileName()); + if (!Files.exists(newColor)) Files.move(colorTexPath, newColor); + } catch (IOException ex) { + setStatus("Fehler beim Erstellen des Set-Ordners: " + ex.getMessage()); + return; + } + + String oldColorRel = ASSET_ROOT.relativize(colorTexPath).toString().replace('\\', '/'); + String newColorRel = ASSET_ROOT.relativize(newColor).toString().replace('\\', '/'); + + TextureMapData meta = new TextureMapData(); + meta.colorMap = newColorRel; + if (isDisplacement) meta.displacementMap = picked; + else meta.normalMap = picked; + saveTexMeta(newColor, meta); + + // Slots aktualisieren: alten Pfad → neuen Pfad + updateSlotsColorPath(oldColorRel, newColorRel); + autoAssignMapsForTexture(newColorRel, meta); + + // Baum aktualisieren: Datei-Item → Set-Item + TreeItem fileItem = itemPaths.entrySet().stream() + .filter(en -> colorTexPath.equals(en.getValue())) + .map(Map.Entry::getKey).findFirst().orElse(null); + if (fileItem != null) { + TreeItem parentItem = fileItem.getParent(); + if (parentItem != null) parentItem.getChildren().remove(fileItem); + itemPaths.remove(fileItem); + } + TreeItem parentByPath = itemPaths.entrySet().stream() + .filter(en -> setDir.getParent().equals(en.getValue())) + .map(Map.Entry::getKey).findFirst().orElse(null); + if (parentByPath != null) { + TreeItem setItem = new TreeItem<>(baseName); + itemPaths.put(setItem, setDir); + textureSetNodes.add(setItem); + parentByPath.getChildren().add(setItem); + } + + String type = isDisplacement ? "Displacement" : "Normal"; + setStatus(type + "-Map für " + baseName + " gesetzt – Textur-Set erstellt"); + }); + } + + /** + * Textur-Set-Knoten: lädt textureset.json, lässt den User eine Map wählen, speichert. + */ + private void assignMapToSetNode(TreeItem setItem, Path setDir, boolean isDisplacement, + javafx.stage.Window owner) { + TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); + if (primaryStage != null) chooser.initOwner(primaryStage); + chooser.showAndWait().ifPresent(picked -> { + if (picked == null || picked.isEmpty()) return; + TextureMapData meta = loadTexMeta(setDir); + if (isDisplacement) meta.displacementMap = picked; + else meta.normalMap = picked; + saveTexMeta(setDir, meta); + if (!meta.colorMap.isEmpty()) autoAssignMapsForTexture(meta.colorMap, meta); + String type = isDisplacement ? "Displacement" : "Normal"; + setStatus(type + "-Map für " + setDir.getFileName() + " gesetzt"); + }); + } + + /** Ersetzt in allen Slots einen alten Texturpfad durch den neuen (nach Datei-Verschiebung). */ + private void updateSlotsColorPath(String oldRel, String newRel) { + for (int i = 0; i < 4; i++) { + if (oldRel.equals(input.terrainTexturePaths[i])) input.terrainTexturePaths[i] = newRel; + if (oldRel.equals(input.upperTexturePaths[i])) input.upperTexturePaths[i] = newRel; + if (oldRel.equals(input.thirdTexturePaths[i])) input.thirdTexturePaths[i] = newRel; + } + input.terrainTexturesChanged = true; + input.upperTexturesChanged = true; + input.voxelTexturesChanged = true; + } + + /** + * Wenn colorTexRelPath in irgendeinem Terrain/Upper/Third-Slot steckt, + * wird die Normal-Map aus meta dort automatisch eingetragen. + */ + private void autoAssignMapsForTexture(String colorTexRelPath, TextureMapData meta) { + boolean anyChanged = false; + for (int i = 0; i < 4; i++) { + if (colorTexRelPath.equals(input.terrainTexturePaths[i])) { + if (!meta.normalMap.isEmpty()) { input.terrainNormalMapPaths[i] = meta.normalMap; anyChanged = true; } + if (!meta.displacementMap.isEmpty()) { input.terrainDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } + } + if (colorTexRelPath.equals(input.upperTexturePaths[i])) { + if (!meta.normalMap.isEmpty()) { input.upperNormalMapPaths[i] = meta.normalMap; anyChanged = true; } + if (!meta.displacementMap.isEmpty()) { input.upperDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } + } + if (colorTexRelPath.equals(input.thirdTexturePaths[i])) { + if (!meta.normalMap.isEmpty()) { input.thirdNormalMapPaths[i] = meta.normalMap; anyChanged = true; } + if (!meta.displacementMap.isEmpty()) { input.thirdDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } + } + } + if (anyChanged) { + input.terrainNormalMapsChanged = true; + input.upperNormalMapsChanged = true; + input.thirdNormalMapsChanged = true; + input.voxelTexturesChanged = true; + } + } + + // ── Texture-Metadaten (textureset.json) ─────────────────────────────────── + + private static final Gson TEX_GSON = new Gson(); + + /** Löscht ein Verzeichnis mitsamt aller Inhalte. */ + private static void deleteDirectoryRecursive(Path dir) throws IOException { + try (var walk = Files.walk(dir)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); + } + } + + private Path texMetaFile(Path colorTexPath) { + Path dir = Files.isDirectory(colorTexPath) ? colorTexPath : colorTexPath.getParent(); + return dir.resolve("textureset.json"); + } + + private TextureMapData loadTexMeta(Path colorTexPath) { + Path mf = texMetaFile(colorTexPath); + if (!Files.exists(mf)) return new TextureMapData(); + try (Reader r = Files.newBufferedReader(mf)) { + TextureMapData d = TEX_GSON.fromJson(r, TextureMapData.class); + return d != null ? d : new TextureMapData(); + } catch (IOException e) { + return new TextureMapData(); + } + } + + private void saveTexMeta(Path colorTexPath, TextureMapData meta) { + Path mf = texMetaFile(colorTexPath); + try { + Files.createDirectories(mf.getParent()); + try (Writer w = Files.newBufferedWriter(mf)) { + TEX_GSON.toJson(meta, w); + } + } catch (IOException ex) { + setStatus("Fehler beim Speichern der Textur-Metadaten: " + ex.getMessage()); + } + } + + // ── Textur-Slot Details-Popup ───────────────────────────────────────────── + + private void showTextureSlotDetails(String texRelPath, int slot, int group, + javafx.scene.Node owner, double screenX, double screenY) { + Path texAbs = ASSET_ROOT.resolve(texRelPath.replace('/', java.io.File.separatorChar)); + TextureMapData meta = loadTexMeta(texAbs); + + String normPath = switch (group) { + case 0 -> input.terrainNormalMapPaths[slot]; + case 1 -> input.upperNormalMapPaths[slot]; + default -> input.thirdNormalMapPaths[slot]; + }; + + javafx.scene.layout.VBox box = new javafx.scene.layout.VBox(5); + box.setPadding(new Insets(10)); + box.setStyle("-fx-background-color:#fafafa; -fx-border-color:#888; -fx-border-width:1;" + + "-fx-effect:dropshadow(gaussian,rgba(0,0,0,0.25),8,0,2,2);"); + box.setMinWidth(270); + + addDetailRow(box, "🖼 Textur", texAbs); + + if (normPath != null && !normPath.isEmpty()) { + box.getChildren().add(new javafx.scene.control.Separator()); + addDetailRow(box, "🔵 Normal", ASSET_ROOT.resolve(normPath.replace('/', java.io.File.separatorChar))); + } + if (!meta.roughnessMap.isEmpty()) { + box.getChildren().add(new javafx.scene.control.Separator()); + addDetailRow(box, "🟤 Roughness", ASSET_ROOT.resolve(meta.roughnessMap.replace('/', java.io.File.separatorChar))); + } + if (!meta.aoMap.isEmpty()) { + box.getChildren().add(new javafx.scene.control.Separator()); + addDetailRow(box, "⬛ AO", ASSET_ROOT.resolve(meta.aoMap.replace('/', java.io.File.separatorChar))); + } + + javafx.stage.Popup popup = new javafx.stage.Popup(); + popup.getContent().add(box); + popup.setAutoHide(true); + if (owner.getScene() != null) + popup.show(owner.getScene().getWindow(), screenX, screenY); + } + + private static void addDetailRow(javafx.scene.layout.VBox box, String label, Path file) { + Label nameLbl = new Label(label + ": " + file.getFileName()); + nameLbl.setStyle("-fx-font-weight:bold; -fx-font-size:11;"); + box.getChildren().add(nameLbl); + + if (Files.exists(file)) { + StringBuilder sb = new StringBuilder(" "); + int[] dims = readImageDimensions(file); + if (dims != null) sb.append(dims[0]).append(" × ").append(dims[1]).append(" · "); + String ext = file.getFileName().toString(); + int dot = ext.lastIndexOf('.'); + if (dot >= 0) sb.append(ext.substring(dot + 1).toUpperCase()).append(" · "); + try { sb.append(formatFileSize(Files.size(file))); } catch (IOException ignored) { sb.append("?"); } + Label info = new Label(sb.toString()); + info.setStyle("-fx-text-fill:#555; -fx-font-size:10;"); + box.getChildren().add(info); + } else { + Label missing = new Label(" (Datei nicht gefunden)"); + missing.setStyle("-fx-text-fill:#c00; -fx-font-size:10;"); + box.getChildren().add(missing); + } + } + + private static int[] readImageDimensions(Path p) { + try (var iis = javax.imageio.ImageIO.createImageInputStream(p.toFile())) { + java.util.Iterator readers = javax.imageio.ImageIO.getImageReaders(iis); + if (readers.hasNext()) { + javax.imageio.ImageReader reader = readers.next(); + try { + reader.setInput(iis); + return new int[]{ reader.getWidth(0), reader.getHeight(0) }; + } finally { + reader.dispose(); + } + } + } catch (Exception ignored) {} + return null; + } + + private static String formatFileSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + return String.format("%.1f MB", bytes / (1024.0 * 1024)); + } + + private void handleAudioImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Audio importieren"); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Audio-Dateien", "*.ogg","*.wav","*.mp3"), + new FileChooser.ExtensionFilter("OGG", "*.ogg"), + new FileChooser.ExtensionFilter("WAV / MP3", "*.wav","*.mp3")); + List files = fc.showOpenMultipleDialog(owner); + if (files == null) return; + Path destDir = ASSET_ROOT.resolve("audio"); + int ok = 0; + for (File file : files) { + try { + Files.createDirectories(destDir); + String name = file.getName().toLowerCase(); + String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); + Path dest = destDir.resolve(baseName + ".ogg"); + if (name.endsWith(".ogg")) { + Files.copy(file.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); + } else { + setStatus("Konvertiere " + file.getName() + " → OGG …"); + convertToOgg(file.toPath(), dest); + } + TreeItem item = new TreeItem<>(dest.getFileName().toString()); + itemPaths.put(item, dest); + audioNode.getChildren().add(item); + ok++; + } catch (IOException ex) { + setStatus("Fehler: " + ex.getMessage()); + } + } + if (ok > 0) { + audioNode.setExpanded(true); + setStatus(ok + " Audio-Datei(en) importiert"); + } + } + // ── Modell-Editor ──────────────────────────────────────────────────────── private void openModelEditor(String relPath, java.nio.file.Path absolutePath) { modelEditorCurrentPath = relPath; currentTool = "modeleditor"; - topBar.getChildren().set(1, buildModelEditorToolBar()); input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; + setCenterView(worldViewport); openAssetOverlay(); if (relPath == null) { + // Pfade + Flags sofort zurücksetzen bevor Toolbar gebaut wird + modelEditorLod1Path = ""; + modelEditorLod2Path = ""; + input.modelEditorHasEmbeddedLods = false; + topBar.getChildren().set(1, buildModelEditorToolBar()); // Kein Modell vorausgewählt – Platzhalter zeigen VBox placeholder = new VBox(12); placeholder.setPadding(new javafx.geometry.Insets(16)); @@ -4445,18 +5647,21 @@ public class EditorApp extends Application { ? de.blight.common.ModelMetaIO.load(absolutePath) : de.blight.common.ModelMeta.defaults(relPath.replaceAll(".*/", "")); - // JME-State anweisen - input.modelEditorScaleX = meta.scaleX(); - input.modelEditorScaleY = meta.scaleY(); - input.modelEditorScaleZ = meta.scaleZ(); - input.modelEditorPivotY = meta.pivotOffsetY(); - input.modelEditorOpenPath = relPath; + // LOD-Pfade + Embedded-LOD-Flag VOR Toolbar-Build setzen + modelEditorLod1Path = meta.lod1Path(); + modelEditorLod2Path = meta.lod2Path(); + input.modelEditorLod1Path = modelEditorLod1Path; + input.modelEditorLod2Path = modelEditorLod2Path; + input.modelEditorHasEmbeddedLods = false; // wird vom JME-Thread ggf. auf true gesetzt + input.modelEditorLodPreview = 0; + topBar.getChildren().set(1, buildModelEditorToolBar()); - modelEditorLod1Path = meta.lod1Path(); - modelEditorLod2Path = meta.lod2Path(); - input.modelEditorLod1Path = modelEditorLod1Path; - input.modelEditorLod2Path = modelEditorLod2Path; - input.modelEditorLodPreview = 0; + // JME-State anweisen + input.modelEditorScaleX = meta.scaleX(); + input.modelEditorScaleY = meta.scaleY(); + input.modelEditorScaleZ = meta.scaleZ(); + input.modelEditorPivotY = meta.pivotOffsetY(); + input.modelEditorOpenPath = relPath; root.setRight(buildModelEditorPanel(relPath, absolutePath, meta)); setStatus("Modell-Editor: " + relPath); @@ -4643,6 +5848,53 @@ public class EditorApp extends Application { HBox.setHgrow(lod1ImportBtn, javafx.scene.layout.Priority.ALWAYS); HBox.setHgrow(lod2ImportBtn, javafx.scene.layout.Priority.ALWAYS); + // ── Anhänge: Lichter & Partikel ─────────────────────────────────────── + Label attachTitle = new Label("Anhänge: Lichter & Partikel"); + attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + // Lichter + Label lightSectionLbl = new Label("Lichtquellen:"); + lightSectionLbl.setStyle("-fx-text-fill:#aaa;"); + + modelEditorLights.clear(); + modelEditorLights.addAll(meta.attachedLights()); + + modelEditorLightBox = new javafx.scene.layout.VBox(4); + rebuildLightBox(); + + Button addLightBtn = new Button("+ Lichtquelle"); + addLightBtn.setMaxWidth(Double.MAX_VALUE); + addLightBtn.setStyle("-fx-background-color:#4a4a2e;"); + addLightBtn.setOnAction(ev -> { + modelEditorLights.add(new de.blight.common.ModelMeta.AttachedLight( + 0f, 1f, 0f, 1f, 0.85f, 0.5f, 2f, 8f)); + rebuildLightBox(); + pushAttachmentsToJme(); + }); + + // Emitter + Label emitterSectionLbl = new Label("Partikel-Emitter:"); + emitterSectionLbl.setStyle("-fx-text-fill:#aaa;"); + + modelEditorEmitters.clear(); + modelEditorEmitters.addAll(meta.attachedEmitters()); + + modelEditorEmitterBox = new javafx.scene.layout.VBox(4); + rebuildEmitterBox(); + + Button addEmitterBtn = new Button("+ Partikel-Emitter"); + addEmitterBtn.setMaxWidth(Double.MAX_VALUE); + addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;"); + addEmitterBtn.setOnAction(ev -> { + modelEditorEmitters.add(new de.blight.common.ModelMeta.AttachedEmitter( + 0f, 0.5f, 0f, 0)); + rebuildEmitterBox(); + pushAttachmentsToJme(); + }); + + // Initiale Gizmos pushen + pushAttachmentsToJme(); + // ── Buttons ─────────────────────────────────────────────────────────── Button saveBtn = new Button("💾 Speichern"); saveBtn.setMaxWidth(Double.MAX_VALUE); @@ -4672,7 +5924,9 @@ public class EditorApp extends Application { receiveCB.isSelected(), (float)(double) rndMinSpin.getValue(), (float)(double) rndMaxSpin.getValue(), - modelEditorLod1Path, modelEditorLod2Path)); + modelEditorLod1Path, modelEditorLod2Path, + new java.util.ArrayList<>(modelEditorLights), + new java.util.ArrayList<>(modelEditorEmitters))); placeBtn.setOnAction(e -> { input.modelEditorCloseRequest = true; @@ -4709,11 +5963,384 @@ public class EditorApp extends Application { modelEditorLod1Label, lod1Row, modelEditorLod2Label, lod2Row, new Separator(), + attachTitle, + lightSectionLbl, modelEditorLightBox, addLightBtn, + emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn, + new Separator(), saveBtn, placeBtn, closeBtn ); return panel; } + // ── Modell-Import ───────────────────────────────────────────────────────── + + private void openModelImport(Stage stage) { + FileChooser fc = new FileChooser(); + fc.setTitle("Modell importieren"); + fc.getExtensionFilters().add( + new FileChooser.ExtensionFilter("3D-Modelle", "*.j3o", "*.gltf", "*.glb", "*.obj")); + List files = fc.showOpenMultipleDialog(stage); + if (files == null || files.isEmpty()) return; + + modelImportQueue.clear(); + if (files.size() == 1) { + startImportFile(files.get(0)); + } else { + // Mehrfach-Import: erste Datei sofort, Rest in Warteschlange + modelImportQueue.addAll(files.subList(1, files.size())); + setStatus("Mehrfach-Import: " + files.size() + " Dateien – starte mit " + files.get(0).getName()); + startImportFile(files.get(0)); + } + } + + private void startImportFile(File file) { + String name = file.getName(); + String baseName = name.replaceFirst("\\.[^.]+$", ""); + Path destDir = ASSET_ROOT.resolve("Models").resolve("imported"); + try { Files.createDirectories(destDir); } + catch (IOException ex) { setStatus("Fehler: " + ex.getMessage()); return; } + + boolean isJ3o = name.toLowerCase().endsWith(".j3o"); + Path destFile = destDir.resolve(name); + Path destJ3o = destDir.resolve(baseName + ".j3o"); + + try { + Files.copy(file.toPath(), destFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + setStatus("Fehler beim Kopieren: " + ex.getMessage()); + return; + } + + if (isJ3o) { + String relPath = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/'); + activateModelImport(relPath, baseName); + } else { + String assetPath = ASSET_ROOT.relativize(destFile).toString().replace('\\', '/'); + boolean keepCtrls = name.matches(".*\\.(gltf|glb)"); + input.modelConvertQueue.offer( + new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile, keepCtrls)); + setStatus("Konvertiere " + name + " → .j3o …"); + String finalRel = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/'); + new Thread(() -> { + for (int i = 0; i < 300; i++) { + try { Thread.sleep(100); } catch (InterruptedException ignored) {} + if (Files.exists(destJ3o)) break; + } + Platform.runLater(() -> activateModelImport(finalRel, baseName)); + }, "import-wait").start(); + } + } + + private void activateModelImport(String relPath, String suggestedName) { + modelImportCurrentPath = relPath; + modelImportLod1StatusLabel = null; + modelImportLod2StatusLabel = null; + modelImportExportStatusLabel = null; + input.modelImportHasLod1 = false; + input.modelImportHasLod2 = false; + input.modelImportExportName = null; + input.modelImportExportStatus = null; + input.modelImportDummyVisible = false; + input.modelImportDummyX = 0f; + input.modelImportDummyY = 0f; + input.modelImportDummyZ = 0f; + input.modelImportLod1PreviewNode.set(null); + input.modelImportLod2PreviewNode.set(null); + if (importLod1Btn != null) { importLod1Btn.setDisable(true); importLod1Btn.setSelected(false); } + if (importLod2Btn != null) { importLod2Btn.setDisable(true); importLod2Btn.setSelected(false); } + + currentTool = "modelimport"; + topBar.getChildren().set(1, buildModelImportToolBar()); + input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; + setCenterView(worldViewport); + openAssetOverlay(); + + // Reset scale + input.modelEditorScaleX = 1f; + input.modelEditorScaleY = 1f; + input.modelEditorScaleZ = 1f; + input.modelEditorPivotY = 0f; + input.modelEditorOpenPath = relPath; + + root.setRight(buildModelImportPanel(relPath, suggestedName)); + setStatus("Modell-Import: " + relPath); + } + + private ToolBar buildModelImportToolBar() { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> { + input.modelEditorCloseRequest = true; + input.activeLayer = 0; + if (baseBtn != null) baseBtn.setSelected(true); + root.setRight(toolPanel); + switchToWorldEditor(); + }); + Label label = new Label("Modell importieren"); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + + ToggleButton lod0Btn = new ToggleButton("LOD 0"); + importLod1Btn = new ToggleButton("LOD 1"); + importLod2Btn = new ToggleButton("LOD 2"); + javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup(); + lod0Btn.setToggleGroup(lodGroup); importLod1Btn.setToggleGroup(lodGroup); importLod2Btn.setToggleGroup(lodGroup); + lod0Btn.setSelected(true); + String lodStyle = "-fx-font-size:11; -fx-padding:2 8 2 8;"; + lod0Btn.setStyle(lodStyle); importLod1Btn.setStyle(lodStyle); importLod2Btn.setStyle(lodStyle); + lod0Btn.setOnAction(e -> { input.modelEditorLodPreview = 0; input.modelEditorLodChanged = true; }); + importLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; }); + importLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; }); + // Initial deaktiviert – werden freigegeben sobald LOD generiert wurde + importLod1Btn.setDisable(true); + importLod2Btn.setDisable(true); + + ToggleButton cylBtn = new ToggleButton("⇕ 2m"); + cylBtn.setStyle("-fx-font-size:11; -fx-padding:2 8 2 8;"); + cylBtn.setTooltip(new Tooltip("Vergleichsfigur (2m Zylinder) ein-/ausblenden")); + cylBtn.setSelected(input.modelImportDummyVisible); + cylBtn.setOnAction(e -> input.modelImportDummyVisible = cylBtn.isSelected()); + + String spnStyle = "-fx-pref-width:64; -fx-font-size:11;"; + javafx.scene.control.Spinner spnX = new javafx.scene.control.Spinner<>(-50.0, 50.0, 0.0, 0.25); + javafx.scene.control.Spinner spnY = new javafx.scene.control.Spinner<>(-20.0, 20.0, 0.0, 0.25); + javafx.scene.control.Spinner spnZ = new javafx.scene.control.Spinner<>(-50.0, 50.0, 0.0, 0.25); + spnX.setStyle(spnStyle); spnY.setStyle(spnStyle); spnZ.setStyle(spnStyle); + spnX.setEditable(true); spnY.setEditable(true); spnZ.setEditable(true); + spnX.setTooltip(new Tooltip("Vergleichsfigur X-Offset")); + spnY.setTooltip(new Tooltip("Vergleichsfigur Y-Offset")); + spnZ.setTooltip(new Tooltip("Vergleichsfigur Z-Offset")); + spnX.valueProperty().addListener((ob, o, v) -> input.modelImportDummyX = v.floatValue()); + spnY.valueProperty().addListener((ob, o, v) -> input.modelImportDummyY = v.floatValue()); + spnZ.valueProperty().addListener((ob, o, v) -> input.modelImportDummyZ = v.floatValue()); + + Label lblX = new Label("X"); lblX.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + Label lblY = new Label("Y"); lblY.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + Label lblZ = new Label("Z"); lblZ.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, + new Separator(Orientation.VERTICAL), lod0Btn, importLod1Btn, importLod2Btn, + new Separator(Orientation.VERTICAL), cylBtn, + lblX, spnX, lblY, spnY, lblZ, spnZ); + return tb; + } + + private VBox buildModelImportPanel(String relPath, String suggestedName) { + VBox panel = new VBox(8); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(300); + panel.setStyle("-fx-background-color: #2a2a3e;"); + + // ── Titel ───────────────────────────────────────────────────────────── + Label title = new Label("Modell importieren"); + title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); + + // ── Skalierung ──────────────────────────────────────────────────────── + Label scaleTitle = new Label("Skalierung:"); + scaleTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + modelEditorUniformCB = new CheckBox("Gleichmäßig skalieren (X=Y=Z)"); + modelEditorUniformCB.setSelected(true); + modelEditorUniformCB.setStyle("-fx-text-fill:#ccc;"); + + modelEditorSpinX = buildScaleSpinner(1.0); + modelEditorSpinY = buildScaleSpinner(1.0); + modelEditorSpinZ = buildScaleSpinner(1.0); + + GridPane scaleGrid = new GridPane(); + scaleGrid.setHgap(6); scaleGrid.setVgap(4); + addLabeledRow(scaleGrid, 0, "X:", modelEditorSpinX); + addLabeledRow(scaleGrid, 1, "Y:", modelEditorSpinY); + addLabeledRow(scaleGrid, 2, "Z:", modelEditorSpinZ); + + modelEditorSpinX.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinY.getValueFactory().setValue(nv); + modelEditorSpinZ.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorSpinY.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinX.getValueFactory().setValue(nv); + modelEditorSpinZ.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorSpinZ.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinX.getValueFactory().setValue(nv); + modelEditorSpinY.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorUniformCB.setOnAction(e -> { + if (modelEditorUniformCB.isSelected()) { + double xVal = modelEditorSpinX.getValue(); + modelEditorSuppressListeners = true; + modelEditorSpinY.getValueFactory().setValue(xVal); + modelEditorSpinZ.getValueFactory().setValue(xVal); + modelEditorSuppressListeners = false; + pushScaleToJme(); + } + }); + + modelEditorDimLabel = new Label("Maße: – × – × – m"); + modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;"); + + // ── LOD-Algorithmus ─────────────────────────────────────────────────── + Label algoTitle = new Label("Reduktions-Algorithmus"); + algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + RadioButton algoBlightRB = new RadioButton("Blight (Edge Collapse, Dihedral-aware)"); + RadioButton algoJmeRB = new RadioButton("JME (Progressive Mesh)"); + algoBlightRB.setStyle("-fx-text-fill:#ccc;"); + algoJmeRB.setStyle("-fx-text-fill:#ccc;"); + javafx.scene.control.ToggleGroup algoGroup = new javafx.scene.control.ToggleGroup(); + algoBlightRB.setToggleGroup(algoGroup); + algoJmeRB.setToggleGroup(algoGroup); + algoBlightRB.setSelected(true); + algoBlightRB.setOnAction(e -> input.modelLodAlgorithm = "blight"); + algoJmeRB.setOnAction(e -> input.modelLodAlgorithm = "jme"); + + // ── LOD 1 ───────────────────────────────────────────────────────────── + Label lod1Title = new Label("LOD 1"); + lod1Title.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + RadioButton lod1AutoRB = new RadioButton("Auto (Mesh-Reduktion)"); + RadioButton lod1FileRB = new RadioButton("Aus Datei…"); + lod1AutoRB.setStyle("-fx-text-fill:#ccc;"); + lod1FileRB.setStyle("-fx-text-fill:#ccc;"); + javafx.scene.control.ToggleGroup lod1Group = new javafx.scene.control.ToggleGroup(); + lod1AutoRB.setToggleGroup(lod1Group); + lod1FileRB.setToggleGroup(lod1Group); + lod1AutoRB.setSelected(true); + + Button lod1GenBtn = new Button("LOD 1 generieren"); + lod1GenBtn.setMaxWidth(Double.MAX_VALUE); + lod1GenBtn.setOnAction(e -> { + if (lod1AutoRB.isSelected()) { + input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(1, "auto_mesh", null)); + } else { + FileChooser lfc = new FileChooser(); + lfc.setTitle("LOD 1 – j3o wählen"); + lfc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); + File lFile = lfc.showOpenDialog(primaryStage); + if (lFile == null) return; + Path lodDir = ASSET_ROOT.resolve("Models").resolve("lods"); + try { + Files.createDirectories(lodDir); + Path dest = lodDir.resolve(lFile.getName()); + Files.copy(lFile.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + String assetPath = ASSET_ROOT.relativize(dest).toString().replace('\\', '/'); + input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(1, "file", assetPath)); + } catch (IOException ex) { + setStatus("LOD 1 Import fehlgeschlagen: " + ex.getMessage()); + } + } + }); + + modelImportLod1StatusLabel = new Label("(nicht gesetzt)"); + modelImportLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelImportLod1StatusLabel.setWrapText(true); + + // ── LOD 2 ───────────────────────────────────────────────────────────── + Label lod2Title = new Label("LOD 2"); + lod2Title.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + RadioButton lod2MeshRB = new RadioButton("Auto (Starke Mesh-Reduktion, ~15%)"); + RadioButton lod2FileRB = new RadioButton("Aus Datei…"); + for (RadioButton rb : new RadioButton[]{lod2MeshRB, lod2FileRB}) + rb.setStyle("-fx-text-fill:#ccc;"); + javafx.scene.control.ToggleGroup lod2Group = new javafx.scene.control.ToggleGroup(); + lod2MeshRB.setToggleGroup(lod2Group); + lod2FileRB.setToggleGroup(lod2Group); + lod2MeshRB.setSelected(true); + + Button lod2GenBtn = new Button("LOD 2 generieren"); + lod2GenBtn.setMaxWidth(Double.MAX_VALUE); + lod2GenBtn.setOnAction(e -> { + if (lod2MeshRB.isSelected()) { + input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(2, "auto_mesh_heavy", null)); + } else { + FileChooser lfc = new FileChooser(); + lfc.setTitle("LOD 2 – j3o wählen"); + lfc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); + File lFile = lfc.showOpenDialog(primaryStage); + if (lFile == null) return; + Path lodDir = ASSET_ROOT.resolve("Models").resolve("lods"); + try { + Files.createDirectories(lodDir); + Path dest = lodDir.resolve(lFile.getName()); + Files.copy(lFile.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + String assetPath = ASSET_ROOT.relativize(dest).toString().replace('\\', '/'); + input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(2, "file", assetPath)); + } catch (IOException ex) { + setStatus("LOD 2 Import fehlgeschlagen: " + ex.getMessage()); + } + } + }); + + modelImportLod2StatusLabel = new Label("(nicht gesetzt)"); + modelImportLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelImportLod2StatusLabel.setWrapText(true); + + // ── Export ──────────────────────────────────────────────────────────── + Label exportTitle = new Label("Export"); + exportTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + Label assetNameLabel = new Label("Asset-Name:"); + assetNameLabel.setStyle("-fx-text-fill:#aaa;"); + TextField nameTF = new TextField(suggestedName); + nameTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); + + modelImportExportStatusLabel = new Label(""); + modelImportExportStatusLabel.setStyle("-fx-text-fill:#8cf; -fx-font-size:11;"); + modelImportExportStatusLabel.setWrapText(true); + + Button exportBtn = new Button("📦 Als Asset importieren"); + exportBtn.setMaxWidth(Double.MAX_VALUE); + exportBtn.setStyle("-fx-background-color:#2e7d32; -fx-text-fill:#fff; -fx-font-weight:bold;"); + exportBtn.setOnAction(e -> { + String n = nameTF.getText().trim(); + if (!n.isEmpty()) input.modelImportExportName = n; + }); + + Button cancelBtn = new Button("✕ Abbrechen"); + cancelBtn.setMaxWidth(Double.MAX_VALUE); + cancelBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); + cancelBtn.setOnAction(e -> { + input.modelEditorCloseRequest = true; + input.activeLayer = 0; + if (baseBtn != null) baseBtn.setSelected(true); + root.setRight(toolPanel); + switchToWorldEditor(); + }); + + panel.getChildren().addAll( + title, + new Separator(), + scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, + new Separator(), + algoTitle, algoBlightRB, algoJmeRB, + new Separator(), + lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel, + new Separator(), + lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel, + new Separator(), + exportTitle, assetNameLabel, nameTF, modelImportExportStatusLabel, + exportBtn, cancelBtn + ); + return panel; + } + private Spinner buildScaleSpinner(double initial) { SpinnerValueFactory.DoubleSpinnerValueFactory f = new SpinnerValueFactory.DoubleSpinnerValueFactory(0.001, 1000.0, initial, 0.1); @@ -4786,11 +6413,14 @@ public class EditorApp extends Application { float pivotY, float placeY, boolean solid, boolean cast, boolean receive, float rndMin, float rndMax, - String lod1Path, String lod2Path) { + String lod1Path, String lod2Path, + java.util.List lights, + java.util.List emitters) { de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( name, category, tags, sx, sy, sz, uniform, pivotY, placeY, solid, cast, receive, rndMin, rndMax, - lod1Path, lod2Path, 30f, 80f, 120f); + lod1Path, lod2Path, 30f, 80f, 120f, + lights, emitters); if (absolutePath == null || !absolutePath.toFile().exists()) { setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert"); @@ -4851,6 +6481,148 @@ public class EditorApp extends Application { input.modelEditorThumbnailRequest = finalJ3o; } + // ── Anhang-Hilfsmethoden (Modell-Editor) ────────────────────────────────── + + private void pushAttachmentsToJme() { + input.modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights); + input.modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters); + input.modelEditorAttachmentsChanged = true; + } + + private void rebuildLightBox() { + if (modelEditorLightBox == null) return; + modelEditorLightBox.getChildren().clear(); + for (int i = 0; i < modelEditorLights.size(); i++) { + final int idx = i; + de.blight.common.ModelMeta.AttachedLight al = modelEditorLights.get(i); + modelEditorLightBox.getChildren().add(buildLightEntry(idx, al)); + } + } + + private javafx.scene.Node buildLightEntry(int idx, + de.blight.common.ModelMeta.AttachedLight al) { + javafx.scene.control.TitledPane tp = new javafx.scene.control.TitledPane(); + tp.setText("Licht " + (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 javafx.geometry.Insets(4)); + + Spinner ox = buildOffsetSpinner(al.offsetX()); + Spinner oy = buildOffsetSpinner(al.offsetY()); + Spinner oz = buildOffsetSpinner(al.offsetZ()); + addLabeledRow(g, 0, "Offset X:", ox); + addLabeledRow(g, 1, "Offset Y:", oy); + addLabeledRow(g, 2, "Offset Z:", oz); + + javafx.scene.control.ColorPicker cp = new javafx.scene.control.ColorPicker( + javafx.scene.paint.Color.color( + Math.min(1, al.r()), Math.min(1, al.g()), Math.min(1, al.b()))); + cp.setMaxWidth(Double.MAX_VALUE); + g.add(new Label("Farbe:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); + g.add(cp, 1, 3); + + Spinner intSpin = new Spinner<>(0.0, 20.0, al.intensity(), 0.5); + intSpin.setEditable(true); + addLabeledRow(g, 4, "Intensität:", intSpin); + + Spinner radSpin = new Spinner<>(0.5, 50.0, al.radius(), 1.0); + radSpin.setEditable(true); + addLabeledRow(g, 5, "Radius:", radSpin); + + Runnable sync = () -> { + javafx.scene.paint.Color c = cp.getValue(); + modelEditorLights.set(idx, new de.blight.common.ModelMeta.AttachedLight( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), + (float) c.getRed(), (float) c.getGreen(), (float) c.getBlue(), + intSpin.getValue().floatValue(), radSpin.getValue().floatValue())); + pushAttachmentsToJme(); + }; + ox.valueProperty().addListener((o,ov,nv) -> sync.run()); + oy.valueProperty().addListener((o,ov,nv) -> sync.run()); + oz.valueProperty().addListener((o,ov,nv) -> sync.run()); + cp.valueProperty().addListener((o,ov,nv) -> sync.run()); + intSpin.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 -> { + modelEditorLights.remove(idx); + rebuildLightBox(); + pushAttachmentsToJme(); + }); + + VBox content = new VBox(4, g, del); + content.setPadding(new javafx.geometry.Insets(2)); + tp.setContent(content); + return tp; + } + + private void rebuildEmitterBox() { + if (modelEditorEmitterBox == null) return; + modelEditorEmitterBox.getChildren().clear(); + for (int i = 0; i < modelEditorEmitters.size(); i++) { + final int idx = i; + de.blight.common.ModelMeta.AttachedEmitter ae = modelEditorEmitters.get(i); + modelEditorEmitterBox.getChildren().add(buildEmitterEntry(idx, ae)); + } + } + + private javafx.scene.Node buildEmitterEntry(int idx, + de.blight.common.ModelMeta.AttachedEmitter ae) { + javafx.scene.control.TitledPane tp = new javafx.scene.control.TitledPane(); + tp.setText("Emitter " + (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 javafx.geometry.Insets(4)); + + Spinner ox = buildOffsetSpinner(ae.offsetX()); + Spinner oy = buildOffsetSpinner(ae.offsetY()); + Spinner oz = buildOffsetSpinner(ae.offsetZ()); + addLabeledRow(g, 0, "Offset X:", ox); + addLabeledRow(g, 1, "Offset Y:", oy); + addLabeledRow(g, 2, "Offset Z:", oz); + + ComboBox presetCB = new ComboBox<>(); + presetCB.getItems().addAll("Feuer", "Rauch", "Funken"); + presetCB.getSelectionModel().select(Math.max(0, Math.min(2, ae.preset()))); + presetCB.setMaxWidth(Double.MAX_VALUE); + g.add(new Label("Typ:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); + g.add(presetCB, 1, 3); + + Runnable sync = () -> { + modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), + presetCB.getSelectionModel().getSelectedIndex())); + pushAttachmentsToJme(); + }; + ox.valueProperty().addListener((o,ov,nv) -> sync.run()); + oy.valueProperty().addListener((o,ov,nv) -> sync.run()); + oz.valueProperty().addListener((o,ov,nv) -> sync.run()); + presetCB.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 -> { + modelEditorEmitters.remove(idx); + rebuildEmitterBox(); + pushAttachmentsToJme(); + }); + + VBox content = new VBox(4, g, del); + content.setPadding(new javafx.geometry.Insets(2)); + tp.setContent(content); + return tp; + } + private void importLodFile(int slot) { FileChooser fc = new FileChooser(); fc.setTitle("LOD " + slot + " – j3o wählen"); @@ -4887,7 +6659,13 @@ public class EditorApp extends Application { viewport.setPreserveRatio(false); viewport.setFocusTraversable(true); - StackPane pane = new StackPane(viewport); + minimapCanvas = new javafx.scene.canvas.Canvas(164, 164); + minimapCanvas.setMouseTransparent(true); + StackPane.setAlignment(minimapCanvas, javafx.geometry.Pos.BOTTOM_RIGHT); + StackPane.setMargin(minimapCanvas, new Insets(0, 10, 10, 0)); + drawMinimap(); // Initiales Zeichnen (leer) + + StackPane pane = new StackPane(viewport, minimapCanvas); pane.setStyle("-fx-background-color: #1a1a2e;"); javafx.animation.PauseTransition resizeDebounce = @@ -5067,7 +6845,7 @@ public class EditorApp extends Application { input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y)); } case SharedInput.LAYER_VOXEL -> - input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y)); + input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y, action)); } } @@ -5148,20 +6926,18 @@ public class EditorApp extends Application { openGameConsole(); }); - // Stdout des Spiels in Konsolen-Fenster streamen + // Stdout des Spiels in Puffer schreiben — Flush erfolgt gebündelt im UI-Timer try (java.io.BufferedReader br = new java.io.BufferedReader( new java.io.InputStreamReader(proc.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { - final String l = line; - Platform.runLater(() -> appendToConsole(l)); + consoleBuffer.offer(line); } } - // Spiel beendet → Live-Position löschen, Button freigeben - input.livePlayerX = Float.NaN; + // Spiel beendet → Button freigeben + consoleBuffer.offer("--- Spiel beendet ---"); Platform.runLater(() -> { - appendToConsole("--- Spiel beendet ---"); if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); @@ -5230,28 +7006,65 @@ public class EditorApp extends Application { gameConsoleArea.setScrollTop(Double.MAX_VALUE); } - private void pollLivePlayerPosition() { - Path posFile = de.blight.common.MapIO.getMapPath().resolveSibling("blight_live.pos"); - try { - if (Files.exists(posFile)) { - java.nio.file.attribute.FileTime mt = Files.getLastModifiedTime(posFile); - if (mt.compareTo(lastLivePosTime) > 0) { - lastLivePosTime = mt; - String[] parts = Files.readString(posFile).strip().split("\\|"); - if (parts.length == 3) { - input.livePlayerX = Float.parseFloat(parts[0]); - input.livePlayerY = Float.parseFloat(parts[1]); - input.livePlayerZ = Float.parseFloat(parts[2]); - } - } - } else if (!Float.isNaN(input.livePlayerX)) { - input.livePlayerX = Float.NaN; - lastLivePosTime = java.nio.file.attribute.FileTime.fromMillis(0); - } - } catch (Exception ignored) {} + // ── Minimap ─────────────────────────────────────────────────────────────── + + private static final float WORLD_HALF = 2048f; // Welt geht von -2048 bis +2048 + + private void drawMinimap() { + if (minimapCanvas == null) return; + final double SIZE = minimapCanvas.getWidth(); // 164 + final double INNER = SIZE - 4; // 160 innere Kartenfläche + final double OFFSET = 2; + + javafx.scene.canvas.GraphicsContext gc = minimapCanvas.getGraphicsContext2D(); + gc.clearRect(0, 0, SIZE, SIZE); + + // Hintergrund + Rahmen + gc.setFill(javafx.scene.paint.Color.rgb(10, 10, 20, 0.75)); + gc.fillRoundRect(0, 0, SIZE, SIZE, 6, 6); + gc.setStroke(javafx.scene.paint.Color.rgb(120, 120, 180, 0.8)); + gc.setLineWidth(1); + gc.strokeRoundRect(0.5, 0.5, SIZE - 1, SIZE - 1, 6, 6); + + // Hilfslinien (Weltmitte) + gc.setStroke(javafx.scene.paint.Color.rgb(80, 80, 110, 0.5)); + gc.setLineWidth(0.5); + double mid = OFFSET + INNER / 2.0; + gc.strokeLine(mid, OFFSET, mid, OFFSET + INNER); + gc.strokeLine(OFFSET, mid, OFFSET + INNER, mid); + + // Kameraposition (blauer Pfeil/Dreieck) + if (Float.isFinite(input.camX) && Float.isFinite(input.camZ)) { + double mx = worldToMap(input.camX, INNER, OFFSET); + double mz = worldToMap(input.camZ, INNER, OFFSET); + double yaw = Math.toRadians(input.camYaw); // Yaw: 0 = Norden, positiv = Uhrzeigersinn + + gc.save(); + gc.translate(mx, mz); + gc.rotate(Math.toDegrees(yaw)); + gc.setFill(javafx.scene.paint.Color.rgb(80, 160, 255, 0.95)); + gc.setStroke(javafx.scene.paint.Color.WHITE); + gc.setLineWidth(0.8); + // Kleines Dreieck zeigt Blickrichtung + double[] px = { 0, -4.5, 4.5 }; + double[] pz = { -7, 5, 5 }; + gc.fillPolygon(px, pz, 3); + gc.strokePolygon(px, pz, 3); + gc.restore(); + } + + // Beschriftung + gc.setFill(javafx.scene.paint.Color.rgb(180, 180, 220, 0.7)); + gc.setFont(javafx.scene.text.Font.font(8)); + gc.fillText("N", mid - 3, OFFSET + 9); + } + + private static double worldToMap(float world, double innerSize, double offset) { + return offset + (world + WORLD_HALF) / (WORLD_HALF * 2) * innerSize; } private void saveCameraPrefs() { + if (!Float.isFinite(input.camX) || !Float.isFinite(input.camY) || !Float.isFinite(input.camZ)) return; java.util.Properties p = new java.util.Properties(); p.setProperty("cam.x", String.valueOf(input.camX)); p.setProperty("cam.y", String.valueOf(input.camY)); @@ -5264,7 +7077,7 @@ public class EditorApp extends Application { p.store(w, "Blight Editor – Kamera-Einstellungen"); } } catch (IOException e) { - System.err.println("[Editor] Kamera-Prefs konnten nicht gespeichert werden: " + e.getMessage()); + log.warn("[Editor] Kamera-Prefs konnten nicht gespeichert werden: {}", e.getMessage()); } } @@ -5300,7 +7113,8 @@ public class EditorApp extends Application { case D -> input.right = pressed; case Q -> input.up = pressed; case E -> input.down = pressed; - case SHIFT -> input.shiftHeld = pressed; + case SHIFT -> input.shiftHeld = pressed; + case CONTROL -> input.ctrlHeld = pressed; case ESCAPE -> { if (pressed && (input.activeLayer == SharedInput.LAYER_SOUND_AREAS || input.activeLayer == SharedInput.LAYER_AREAS @@ -5351,6 +7165,9 @@ public class EditorApp extends Application { if (objEditBtn != null) Platform.runLater(() -> objEditBtn.fire()); } } + case F8 -> { + if (pressed && input.ctrlHeld) input.debugNoLightToggle = true; + } } } diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index 0b0d051..7b044a1 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -2,11 +2,21 @@ package de.blight.editor; import com.jme3.app.SimpleApplication; import com.jme3.asset.plugins.FileLocator; +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.bounding.BoundingVolume; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.queue.GeometryComparator; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; import com.jme3.system.AppSettings; import com.jme3.system.JmeContext; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture2D; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import de.blight.editor.state.AnimPreviewState; import de.blight.editor.state.ItemPlacementState; import de.blight.editor.state.AreaState; @@ -25,12 +35,15 @@ import de.blight.editor.state.SceneObjectState; import de.blight.editor.state.TerrainEditorState; import de.blight.editor.state.TreeGeneratorState; import de.blight.editor.state.VoxelEditorState; +import de.blight.editor.state.ModelImportState; import de.blight.game.console.JmeConsole; import de.blight.game.state.DayNightState; import javafx.scene.image.WritableImage; public class JmeEditorApp extends SimpleApplication { + private static final Logger log = LoggerFactory.getLogger(JmeEditorApp.class); + private final SharedInput input; private final WritableImage initialImage; private final int vpWidth; @@ -41,6 +54,81 @@ public class JmeEditorApp extends SimpleApplication { private int currentW; private int currentH; + // ── NaN-sichere Comparatoren ────────────────────────────────────────────── + + /** Transparent-Bucket: back-to-front nach distanceToEdge, NaN-sicher. */ + private static final class SafeTransparentComparator implements GeometryComparator { + private Camera cam; + + @Override public void setCamera(Camera c) { this.cam = c; } + + private float dist(Geometry g) { + BoundingVolume wb = g.getWorldBound(); + if (wb == null) { + log.warn("[SORT-NaN] Null worldBound – Geo='{}' parent='{}'", + g.getName(), g.getParent() != null ? g.getParent().getName() : "none"); + return 0f; + } + Vector3f c = wb.getCenter(); + if (Float.isNaN(c.x) || Float.isNaN(c.y) || Float.isNaN(c.z)) { + log.warn("[SORT-NaN] NaN center – Geo='{}' wb={}", g.getName(), wb); + return 0f; + } + float d = wb.distanceToEdge(cam.getLocation()); + if (!Float.isFinite(d)) { + log.warn("[SORT-NaN] Nicht-finite Distanz={} – Geo='{}' wb={}", d, g.getName(), wb); + return 0f; + } + return d; + } + + @Override public int compare(Geometry o1, Geometry o2) { + float d1 = dist(o1), d2 = dist(o2); + if (d1 == d2) return 0; + return d1 < d2 ? 1 : -1; // weit → nah + } + } + + /** Opaque-Bucket: front-to-back nach Kamera-Dot, NaN-sicher. */ + private static final class SafeOpaqueComparator implements GeometryComparator { + private Camera cam; + private final Vector3f tmp1 = new Vector3f(); + private final Vector3f tmp2 = new Vector3f(); + + @Override public void setCamera(Camera c) { this.cam = c; } + + private float dist(Geometry g) { + if (g.queueDistance != Float.NEGATIVE_INFINITY) return g.queueDistance; + Vector3f pos = g.getWorldBound() != null + ? g.getWorldBound().getCenter() + : g.getWorldTranslation(); + Vector3f dir = cam.getDirection(tmp2); + pos.subtract(cam.getLocation(), tmp1); + float d = tmp1.dot(dir); + if (!Float.isFinite(d)) { + String matId = g.getMaterial() != null + ? (g.getMaterial().getName() != null ? g.getMaterial().getName() + : g.getMaterial().getMaterialDef().getName()) + : "NULL"; + log.warn("[SORT-NaN] Opaque NaN Distanz – Geo='{}' pos={} mat='{}'", + g.getName(), pos, matId); + d = 0f; + } + g.queueDistance = d; + return d; + } + + @Override public int compare(Geometry o1, Geometry o2) { + int matCmp = Integer.compare( + o1.getMaterial().getSortId(), + o2.getMaterial().getSortId()); + if (matCmp != 0) return matCmp; + float d1 = dist(o1), d2 = dist(o2); + if (d1 == d2) return 0; + return d1 < d2 ? -1 : 1; // nah → fern + } + } + public JmeEditorApp(SharedInput input, WritableImage jfxImage, int vpWidth, int vpHeight) { this.input = input; this.initialImage = jfxImage; @@ -81,6 +169,7 @@ public class JmeEditorApp extends SimpleApplication { java.nio.file.Path blightAssets = ProjectRoot.resolve("blight-assets", "src", "main", "resources"); if (java.nio.file.Files.isDirectory(blightAssets)) { assetManager.registerLocator(blightAssets.toAbsolutePath().toString(), FileLocator.class); + assetManager.registerLocator(blightAssets.toAbsolutePath().toString(), LegacyAssetRedirectLocator.class); } input.loadingStatus = "Initialisiere Renderer..."; @@ -107,6 +196,11 @@ public class JmeEditorApp extends SimpleApplication { stateManager.attach(new ModelEditorState(input)); stateManager.attach(new ItemPlacementState(input)); stateManager.attach(new VoxelEditorState(input)); + stateManager.attach(new ModelImportState(input)); + + // NaN-sichere Comparatoren einsetzen (verhindern den TimSort-Crash bei kaputten Bounds) + viewPort.getQueue().setGeometryComparator(RenderQueue.Bucket.Transparent, new SafeTransparentComparator()); + viewPort.getQueue().setGeometryComparator(RenderQueue.Bucket.Opaque, new SafeOpaqueComparator()); input.loadingStatus = "Initialisiere Konsole..."; jmeConsole = new JmeConsole(false); diff --git a/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java b/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java new file mode 100644 index 0000000..9315b2f --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java @@ -0,0 +1,69 @@ +package de.blight.editor; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLocator; +import com.jme3.asset.AssetManager; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +/** + * Leitet veraltete Textur-Pfade (vor der internal/-Migration) auf die neuen + * Speicherorte um. Behandelt auch .tga → .png Konvertierungen. + * + * Registriert mit root = blight-assets/src/main/resources. + */ +public class LegacyAssetRedirectLocator implements AssetLocator { + + private static final Map PREFIXES = Map.of( + "Textures/bark/", "Textures/internal/bark/", + "Textures/leaves/", "Textures/internal/leaves/", + "Textures/fern/", "Textures/internal/fern/", + "Textures/water/", "Textures/internal/water/", + "Textures/Water/", "Textures/internal/water/" + ); + + private Path root; + + @Override + public void setRootPath(String rootPath) { + root = Paths.get(rootPath); + } + + @Override + public AssetInfo locate(AssetManager manager, AssetKey key) { + String name = key.getName(); + for (var entry : PREFIXES.entrySet()) { + if (name.startsWith(entry.getKey())) { + String remapped = entry.getValue() + name.substring(entry.getKey().length()); + AssetInfo info = tryFile(manager, key, remapped); + if (info != null) return info; + // .tga → .png Fallback für konvertierte Texturen + if (remapped.toLowerCase().endsWith(".tga")) { + info = tryFile(manager, key, remapped.substring(0, remapped.length() - 4) + ".png"); + if (info != null) return info; + } + break; + } + } + return null; + } + + private AssetInfo tryFile(AssetManager manager, AssetKey key, String rel) { + Path p = root.resolve(rel); + if (!Files.exists(p)) return null; + return new AssetInfo(manager, key) { + @Override + public InputStream openStream() { + try { return new FileInputStream(p.toFile()); } + catch (FileNotFoundException ex) { throw new RuntimeException(ex); } + } + }; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index a26f880..2b16a51 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -52,8 +52,12 @@ public class SharedInput { public final java.util.concurrent.atomic.AtomicInteger scrollAccum = new java.util.concurrent.atomic.AtomicInteger(); - // ── Shift: horizontale Bewegung (Y-Höhe wird beibehalten) ─────────────── + // ── Shift / Ctrl ───────────────────────────────────────────────────────── public volatile boolean shiftHeld; + public volatile boolean ctrlHeld; + + // ── Debug-Toggle: Strg+F8 schaltet Raw-Texture-Modus (kein Lighting) ───── + public volatile boolean debugNoLightToggle; // ── Kamerarotation (Maus-Drag mit mittlerer Taste) ─────────────────────── private final AtomicInteger mouseDxAccum = new AtomicInteger(); @@ -95,6 +99,10 @@ public class SharedInput { public volatile boolean grassSettingsChanged = false; /** Texturpfade für Gras-Slots 1..7 (Index 0 = Slot 1). Neues Array zuweisen bei Änderung. */ public volatile String[] grassTextureSlots = new String[]{"", "", "", "", "", "", ""}; + /** Normal-Map-Pfade für Gras-Slots 0..7 (Index 0 = grassTexturePath-Slot, 1..7 = grassTextureSlots). */ + public volatile String[] grassNormalMapPaths = new String[]{"", "", "", "", "", "", "", ""}; + /** Seitenverhältnis (Breite/Höhe) der Textur je Slot – steuert die Klingbreite des Grashalms. */ + public volatile float[] grassAspectRatios = new float[]{1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f}; /** Aktiver Maler-Slot (0 = grassTexturePath/Slot0, 1–7 = grassTextureSlots[slot-1]). */ public volatile int grassActiveSlot = 0; /** JFX setzt true wenn Slot-Texturen geändert wurden. */ @@ -121,6 +129,30 @@ public class SharedInput { public volatile String[] upperNormalMapPaths = new String[]{"", "", "", ""}; public volatile boolean upperNormalMapsChanged = false; + /** Pfade der 4 dritten Gruppe (Slots 9-12, "" = Standard). */ + public volatile String[] thirdTexturePaths = new String[]{"", "", "", ""}; + public volatile boolean thirdTexturesChanged = false; + /** Normal-Map-Pfade der dritten Gruppe (4 Slots, "" = keine). */ + public volatile String[] thirdNormalMapPaths = new String[]{"", "", "", ""}; + public volatile boolean thirdNormalMapsChanged = false; + + /** Reihenfolge (Slot-Index 0-11), in der Normal Maps an GPU-Units vergeben werden. + * Index 0 = höchste Priorität. JFX schreibt neue Array-Referenz, JME liest. */ + public volatile int[] normalMapOrder = {0,1,2,3,4,5,6,7,8,9,10,11}; + public volatile boolean normalMapOrderChanged = false; + + /** Displacement-Map-Pfade der 4 Terrain-Slots ("" = keine) — nur für Voxel-Tessellation. */ + public volatile String[] terrainDisplacementMapPaths = new String[]{"", "", "", ""}; + /** Displacement-Map-Pfade der 4 Gebirge-Slots ("" = keine) — nur für Voxel-Tessellation. */ + public volatile String[] upperDisplacementMapPaths = new String[]{"", "", "", ""}; + /** Displacement-Map-Pfade der dritten Gruppe (4 Slots, "" = keine) — nur für Voxel-Tessellation. */ + public volatile String[] thirdDisplacementMapPaths = new String[]{"", "", "", ""}; + + /** UV-Scale pro Slot in Welteinheiten (Meter pro Textur-Tile), 12 Werte für Slots 1-12. */ + public volatile float[] diffuseScales = {8f,8f,8f,8f, 8f,8f,8f,8f, 8f,8f,8f,8f}; + /** JFX setzt true wenn DiffuseScales geändert wurden; JME liest + resettet. */ + public volatile boolean diffuseScalesChanged = false; + /** JME setzt true nach Map-Load → JFX kann Textur-UI aktualisieren. */ public volatile boolean texturePathsLoaded = false; @@ -245,6 +277,7 @@ public class SharedInput { public record ObjectPropertyChange( float x, float y, float z, float rotX, float rotY, float rotZ, + float scale, boolean solid, boolean castShadow, boolean receiveShadow, @@ -494,11 +527,6 @@ public class SharedInput { public volatile float tempSpawnX = Float.NaN; public volatile float tempSpawnZ = Float.NaN; - /** Live-Spielerposition aus dem laufenden Spiel (NaN = kein Spiel aktiv). */ - public volatile float livePlayerX = Float.NaN; - public volatile float livePlayerY = Float.NaN; - public volatile float livePlayerZ = Float.NaN; - // ── Animations-Vorschau ────────────────────────────────────────────────── public volatile float animPreviewRotY = 0f; public volatile float animPreviewRotX = 25f; @@ -586,6 +614,11 @@ public class SharedInput { /** activeLayer==20 → Modell-Editor (3-D-Vorschau + Metadaten-Konfiguration) */ public static final int LAYER_MODEL_EDITOR = 20; + /** JFX → JME: aktualisierte Anhang-Listen; JME rendert Gizmos neu. */ + public volatile java.util.List modelEditorAttachedLights = java.util.List.of(); + public volatile java.util.List modelEditorAttachedEmitters = java.util.List.of(); + public volatile boolean modelEditorAttachmentsChanged = false; + /** JFX → JME: Pfad des zu ladenden Modells (relativ zu blight-assets/src/main/resources/). */ public volatile String modelEditorOpenPath = null; @@ -628,9 +661,28 @@ public class SharedInput { public static final int LAYER_VOXEL = 16; /** Klick/Drag im Viewport im Voxel-Modus. */ - public record VoxelEdit(float screenX, float screenY) {} + /** action +1 = Linksklick (erhöhen/hinzufügen), -1 = Rechtsklick (senken/entfernen). */ + public record VoxelEdit(float screenX, float screenY, int action) {} public final ConcurrentLinkedQueue voxelEditQueue = new ConcurrentLinkedQueue<>(); + /** JFX → JME: alle Voxel-Chunks als geglättete J3O-Meshes backen. */ + public volatile boolean bakeVoxelsRequested = false; + /** JME → JFX: Anzahl bereits gebackener Chunks (0 = nicht gestartet). */ + public volatile int bakeDone = 0; + /** JME → JFX: Gesamtzahl der zu backenden Chunks (0 = nicht gestartet). */ + public volatile int bakeTotal = 0; + /** JME → JFX: Status-Meldung nach Abschluss des Backens. */ + public volatile String bakeStatusMsg = null; + + /** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */ + public volatile int voxelFlatSlot = -1; + /** Terrain-Slot (0-7) für steile Wände. */ + public volatile int voxelSteepSlot = -1; + /** Terrain-Slot (0-7) für Decken/Unterseiten. */ + public volatile int voxelCeilSlot = -1; + /** JFX setzt true wenn Voxel-Texturen geändert wurden; JME liest + resettet. */ + public volatile boolean voxelTexturesChanged = false; + // ── Item-Platzierung ────────────────────────────────────────────────────── /** activeLayer==21 → Item-Pickup auf die Karte platzieren */ public static final int LAYER_ITEMS = 21; @@ -640,4 +692,43 @@ public class SharedInput { /** Klick-Events für Item-Platzierung (analoges Format zu objectClickQueue). */ public final ConcurrentLinkedQueue itemClickQueue = new ConcurrentLinkedQueue<>(); + + // ── Modell-Import ───────────────────────────────────────────────────────── + /** activeLayer==22 → Modell importieren (LOD-Aufbau + Export) */ + public static final int LAYER_MODEL_IMPORT = 22; + + /** JFX → JME: Dummy-Objekt im Viewport anzeigen (Platzierungsvorschau). */ + public volatile boolean modelImportDummyVisible = false; + public volatile float modelImportDummyX = 0f; + public volatile float modelImportDummyY = 0f; + public volatile float modelImportDummyZ = 0f; + + /** In-memory LOD-Previews (aus Importer, kein Dateisystem nötig). */ + public final AtomicReference modelImportLod1PreviewNode = new AtomicReference<>(null); + public final AtomicReference modelImportLod2PreviewNode = new AtomicReference<>(null); + + /** + * JFX → JME: LOD-Generierungsanfrage. + * level: 1 oder 2 + * method: "auto_mesh" | "file" | "auto_impostor" + * fileAssetPath: nur bei method=="file" relevant + */ + public record ModelLodGenRequest(int level, String method, String fileAssetPath) {} + public final AtomicReference modelLodGenRequest = + new AtomicReference<>(); + + /** JME → JFX: Status-Meldung nach LOD-Generierung. */ + public volatile String modelLodGenStatus = null; + + /** JFX → JME: LOD-Reduktions-Algorithmus. "blight" = Dihedral Edge Collapse, "jme" = JME3 Progressive Mesh. */ + public volatile String modelLodAlgorithm = "blight"; + + /** JME → JFX: LOD-Stufen vorhanden. */ + public volatile boolean modelImportHasLod1 = false; + public volatile boolean modelImportHasLod2 = false; + + /** JFX → JME: Dateiname (ohne Erweiterung) für den Export-Auftrag. null = kein Auftrag. */ + public volatile String modelImportExportName = null; + /** JME → JFX: Status-Meldung nach dem Export (relativer Pfad oder "FEHLER: …"). */ + public volatile String modelImportExportStatus = null; } diff --git a/blight-editor/src/main/java/de/blight/editor/TextureMapData.java b/blight-editor/src/main/java/de/blight/editor/TextureMapData.java new file mode 100644 index 0000000..cf1cc81 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/TextureMapData.java @@ -0,0 +1,10 @@ +package de.blight.editor; + +/** Persistent metadata for a texture set (stored as textureset.json inside the set folder). */ +public class TextureMapData { + public String colorMap = ""; // asset-root-relative path to the diffuse/color texture + public String normalMap = ""; + public String displacementMap = ""; + public String roughnessMap = ""; + public String aoMap = ""; +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index e9b4a37..e586c9c 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -102,8 +102,11 @@ public class AnimPreviewState extends BaseAppState { new ColorRGBA(1.8f, 1.7f, 1.4f, 1f))); previewScene.addLight(new DirectionalLight( new Vector3f(0.6f, -0.4f, 0.7f).normalizeLocal(), - new ColorRGBA(0.45f, 0.5f, 0.7f, 1f))); - previewScene.addLight(new AmbientLight(new ColorRGBA(0.4f, 0.4f, 0.45f, 1f))); + new ColorRGBA(0.5f, 0.55f, 0.75f, 1f))); + previewScene.addLight(new DirectionalLight( + new Vector3f(0f, 0.3f, -1f).normalizeLocal(), + new ColorRGBA(0.4f, 0.4f, 0.5f, 1f))); + previewScene.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f))); previewHolder = new Node("animHolder"); previewScene.attachChild(previewHolder); @@ -188,7 +191,7 @@ public class AnimPreviewState extends BaseAppState { double length = currentAction.getLength(); double time = ac.getTime(); if (length <= 0) { - System.err.println("[AnimPreview] Loop-Check: length=0, Clip hat keine Dauer!"); + LOG.warn("[AnimPreview] Loop-Check: length=0, Clip hat keine Dauer!"); } else if (time >= length - 0.02) { LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length); if (input.animPreviewLoop) { @@ -323,8 +326,7 @@ public class AnimPreviewState extends BaseAppState { currentClipName = clipName; if (currentAction != null) { currentAction.setSpeed(input.animPreviewSpeed); - System.err.println("[AnimPreview] Play '" + clipName - + "' length=" + currentAction.getLength()); + LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength()); } } catch (Exception e) { input.animPreviewStatus = "Abspielen fehlgeschlagen: " + e.getMessage(); @@ -413,12 +415,12 @@ public class AnimPreviewState extends BaseAppState { String info = "Kein AnimComposer | Controls: " + controls + " | Nodes: " + countNodes(animSource); input.animPreviewStatus = info; - System.err.println("[AnimPreview] addAnimation – " + info + " – Datei: " + animAssetPath); + LOG.warn("[AnimPreview] addAnimation – {} – Datei: {}", info, animAssetPath); return; } if (sourceAC.getAnimClips().isEmpty()) { input.animPreviewStatus = "AnimComposer leer (0 Clips) in: " + animAssetPath; - System.err.println("[AnimPreview] AnimComposer leer in " + animAssetPath); + LOG.warn("[AnimPreview] AnimComposer leer in {}", animAssetPath); return; } SkinningControl sourceSC = findControl(animSource, SkinningControl.class); @@ -426,22 +428,22 @@ public class AnimPreviewState extends BaseAppState { com.jme3.anim.Armature dstArm = targetSC != null ? targetSC.getArmature() : null; if (srcArm != null) { - System.err.println("[Retarget] Quell-Knochen (" + srcArm.getJointCount() + "):"); - for (var j : srcArm.getJointList()) System.err.println(" src: " + j.getName()); + LOG.info("[Retarget] Quell-Knochen ({}):", srcArm.getJointCount()); + for (var j : srcArm.getJointList()) LOG.info(" src: {}", j.getName()); } else { - System.err.println("[Retarget] Keine SkinningControl in Quelle!"); + LOG.warn("[Retarget] Keine SkinningControl in Quelle!"); } if (dstArm != null) { - System.err.println("[Retarget] Ziel-Knochen (" + dstArm.getJointCount() + "):"); - for (var j : dstArm.getJointList()) System.err.println(" dst: " + j.getName()); + LOG.info("[Retarget] Ziel-Knochen ({}):", dstArm.getJointCount()); + for (var j : dstArm.getJointList()) LOG.info(" dst: {}", j.getName()); } else { - System.err.println("[Retarget] Keine SkinningControl im Modell!"); + LOG.warn("[Retarget] Keine SkinningControl im Modell!"); } boolean retarget = srcArm != null && dstArm != null && srcArm != dstArm; if (retarget) { var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(srcArm, dstArm); - System.err.println("[Retarget] Mapping (" + mapping.size() + " Treffer): " + mapping); + LOG.info("[Retarget] Mapping ({} Treffer): {}", mapping.size(), mapping); } java.util.Set srcNames = new java.util.HashSet<>(); @@ -456,7 +458,7 @@ public class AnimPreviewState extends BaseAppState { if (name.matches(".*\\.\\d{3}$")) { String base = name.substring(0, name.length() - 4); if (srcNames.contains(base)) { - System.err.println("[AnimPreview] Überspringe Blender-Duplikat: " + name); + LOG.info("[AnimPreview] Überspringe Blender-Duplikat: {}", name); continue; } } @@ -609,17 +611,17 @@ public class AnimPreviewState extends BaseAppState { private void saveModel() { if (currentModelPath == null || currentModel == null) return; if (!currentModelPath.endsWith(".j3o")) { - System.err.println("[AnimPreview] Speichern übersprungen – kein .j3o: " + currentModelPath); + LOG.warn("[AnimPreview] Speichern übersprungen – kein .j3o: {}", currentModelPath); return; } Path file = ASSET_ROOT.resolve(currentModelPath.replace('/', java.io.File.separatorChar)); try { BinaryExporter.getInstance().save(currentModel, file.toFile()); - System.err.println("[AnimPreview] Modell gespeichert: " + currentModelPath); + LOG.info("[AnimPreview] Modell gespeichert: {}", currentModelPath); assets.deleteFromCache(new ModelKey(currentModelPath)); } catch (Exception e) { input.animPreviewStatus += " | Speicherfehler: " + e.getMessage(); - System.err.println("[AnimPreview] Speicherfehler: " + e); + LOG.error("[AnimPreview] Speicherfehler: {}", e.toString()); } } @@ -674,23 +676,23 @@ public class AnimPreviewState extends BaseAppState { private void dumpCurrentModel() { if (currentModel == null) { - System.err.println("[Dump] Kein Modell geladen."); + LOG.info("[Dump] Kein Modell geladen."); return; } - System.err.println("=".repeat(70)); - System.err.println("[Dump] Modell: " + currentModelPath); + LOG.info("══════════════════════════════════════════════════════════════════════"); + LOG.info("[Dump] Modell: {}", currentModelPath); com.jme3.anim.SkinningControl sc = findControl(currentModel, com.jme3.anim.SkinningControl.class); com.jme3.anim.AnimComposer ac = findControl(currentModel, com.jme3.anim.AnimComposer.class); if (sc == null) { - System.err.println("[Dump] Kein SkinningControl gefunden – kein Skelett."); + LOG.info("[Dump] Kein SkinningControl gefunden – kein Skelett."); } else { com.jme3.anim.Armature arm = sc.getArmature(); java.util.Map ms = buildMS(arm); - System.err.println("[Dump] Skelett: " + arm.getJointCount() + " Knochen"); - System.err.println("[Dump] ── Knochen (Name | Elternteil | bind-local° | live-local° | ms°) ──"); + LOG.info("[Dump] Skelett: {} Knochen", arm.getJointCount()); + LOG.info("[Dump] ── Knochen (Name | Elternteil | bind-local° | live-local° | ms°) ──"); for (com.jme3.anim.Joint j : arm.getJointList()) { String parent = j.getParent() != null ? j.getParent().getName() : "(root)"; @@ -705,33 +707,41 @@ public class AnimPreviewState extends BaseAppState { float[] le = liveRot.toAngles(null); float[] mse = ms.get(j).toAngles(null); - System.err.printf("[Dump] %-30s parent=%-25s bind=[%6.1f %6.1f %6.1f]° live=[%6.1f %6.1f %6.1f]° ms=[%6.1f %6.1f %6.1f]°%n", + LOG.info("[Dump] {:<30} parent={:<25} bind=[{} {} {}]° live=[{} {} {}]° ms=[{} {} {}]°", j.getName(), parent, - Math.toDegrees(be[0]), Math.toDegrees(be[1]), Math.toDegrees(be[2]), - Math.toDegrees(le[0]), Math.toDegrees(le[1]), Math.toDegrees(le[2]), - Math.toDegrees(mse[0]), Math.toDegrees(mse[1]), Math.toDegrees(mse[2])); + String.format("%6.1f", Math.toDegrees(be[0])), + String.format("%6.1f", Math.toDegrees(be[1])), + String.format("%6.1f", Math.toDegrees(be[2])), + String.format("%6.1f", Math.toDegrees(le[0])), + String.format("%6.1f", Math.toDegrees(le[1])), + String.format("%6.1f", Math.toDegrees(le[2])), + String.format("%6.1f", Math.toDegrees(mse[0])), + String.format("%6.1f", Math.toDegrees(mse[1])), + String.format("%6.1f", Math.toDegrees(mse[2]))); } // Bone-Name-Mapping gegen Standard-Namen var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(arm, arm); - System.err.println("[Dump] ── Bone-Name-Normalisierung ──"); + LOG.info("[Dump] ── Bone-Name-Normalisierung ──"); for (com.jme3.anim.Joint j : arm.getJointList()) { String norm = de.blight.game.animation.BoneNameMapping.normalize(j.getName()); - System.err.printf("[Dump] %-30s → normalized: %s%n", j.getName(), norm); + LOG.info("[Dump] {} → normalized: {}", j.getName(), norm); } } if (ac == null) { - System.err.println("[Dump] Kein AnimComposer gefunden – keine Clips."); + LOG.info("[Dump] Kein AnimComposer gefunden – keine Clips."); } else { - System.err.println("[Dump] ── Clips ──"); + LOG.info("[Dump] ── Clips ──"); for (com.jme3.anim.AnimClip clip : ac.getAnimClips()) { - System.err.printf("[Dump] Clip: %-40s Dauer=%.3fs Tracks=%d%n", - clip.getName(), clip.getLength(), clip.getTracks().length); + LOG.info("[Dump] Clip: {} Dauer={}s Tracks={}", + clip.getName(), + String.format("%.3f", clip.getLength()), + clip.getTracks().length); for (com.jme3.anim.AnimTrack track : clip.getTracks()) { if (track instanceof com.jme3.anim.TransformTrack tt && tt.getTarget() instanceof com.jme3.anim.Joint j) { - System.err.printf("[Dump] Track: %-28s frames=%d%n", + LOG.info("[Dump] Track: {} frames={}", j.getName(), tt.getTimes() != null ? tt.getTimes().length : 0); } @@ -739,8 +749,8 @@ public class AnimPreviewState extends BaseAppState { } } - System.err.println("=".repeat(70)); - input.animPreviewStatus = "Dump ins Log geschrieben (stderr)"; + LOG.info("══════════════════════════════════════════════════════════════════════"); + input.animPreviewStatus = "Dump ins Log geschrieben"; } // ── Clip umbenennen / exportieren ───────────────────────────────────────── @@ -818,7 +828,7 @@ public class AnimPreviewState extends BaseAppState { int embedded = 0; for (String clipName : set.getClips()) { if (!Files.exists(clipsDir.resolve(clipName + ".j3o"))) { - System.err.println("[AnimEmbed] Clip nicht gefunden: " + clipName); + LOG.warn("[AnimEmbed] Clip nicht gefunden: {}", clipName); continue; } try { @@ -840,7 +850,7 @@ public class AnimPreviewState extends BaseAppState { if (target != null) { charAC.addAnimClip(target); embedded++; } } } catch (Exception e) { - System.err.println("[AnimEmbed] Fehler bei Clip " + clipName + ": " + e.getMessage()); + LOG.error("[AnimEmbed] Fehler bei Clip {}: {}", clipName, e.getMessage()); } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java b/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java index 428894c..893b396 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java @@ -18,11 +18,16 @@ import com.jme3.terrain.geomipmap.TerrainQuad; import de.blight.common.PlacedEmitter; import de.blight.editor.SharedInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.List; public class EmitterState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(EmitterState.class); + private static final float MARKER_RADIUS = 0.4f; private static final float ACTIVATION_ALPHA = 0.07f; @@ -290,8 +295,7 @@ public class EmitterState extends BaseAppState { effect.setLocalTranslation(pe.x(), pe.y(), pe.z()); return effect; } catch (Exception e) { - System.err.println("[EmitterState] Textur nicht ladbar: " + pe.texturePath() - + " — " + e.getMessage()); + log.warn("[EmitterState] Textur nicht ladbar: {} — {}", pe.texturePath(), e.getMessage()); return null; } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java index 5e024b5..949ed57 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -125,7 +125,7 @@ public class EzTreeState extends BaseAppState { hdNode.setLocalScale(1f / 3f); hdNode.updateGeometricState(); - Node ld1Node = buildLod1Node(req.options()); + Node ld1Node = buildLod1Node(req); ld1Node.setLocalScale(1f / 3f); BoundingBox bb = boundsOf(hdNode); @@ -160,15 +160,21 @@ public class EzTreeState extends BaseAppState { } } - private Node buildLod1Node(TreeOptions opts) { + private Node buildLod1Node(SharedInput.EzTreeGenRequest req) { + TreeOptions opts = req.options(); + // Versuche Node.js mit halber Blattanzahl – gleiche Baumform/-größe wie LOD0. TreeOptions ld = opts.copy(); - // Levels NICHT reduzieren – gleiche Baumform und -größe wie LOD0 behalten. - // Nur Polygon-Anzahl pro Ast halbieren (sections/segments) und Blattanzahl reduzieren. + ld.leaves.count = Math.max(0, opts.leaves.count / 2); + Node n = tryNodeJsGeneration(new SharedInput.EzTreeGenRequest(ld, req.presetName(), false)); + if (n != null) { + n.setName("EzTree_ld1"); + return n; + } + // Java-Fallback: Sections/Segments halbieren for (int lv = 0; lv <= ld.branch.levels; lv++) { ld.branch.sections.put(lv, Math.max(3, opts.branch.getSections(lv) / 2)); ld.branch.segments.put(lv, Math.max(3, opts.branch.getSegments(lv) / 2)); } - ld.leaves.count = Math.max(0, opts.leaves.count / 2); Tree tree = new Tree(ld); tree.generate(); applyMaterials(tree, opts); @@ -548,6 +554,7 @@ public class EzTreeState extends BaseAppState { private static Node cloneForCapture(Node src) { Node copy = new Node("ezCap"); copy.setLocalTranslation(src.getLocalTranslation()); + copy.setLocalScale(src.getLocalScale()); for (Spatial child : src.getChildren()) { if (child instanceof Geometry g) { Geometry gc = new Geometry(g.getName(), g.getMesh()); @@ -646,6 +653,7 @@ public class EzTreeState extends BaseAppState { hd.setCullHint(Spatial.CullHint.Inherit); ld1.setCullHint(Spatial.CullHint.Always); lod2.setCullHint(Spatial.CullHint.Always); + lod2.setShadowMode(RenderQueue.ShadowMode.Off); root.addControl(new EzTreeLodControl(app.getCamera(), hd, ld1, lod2, 40f, 120f)); return root; diff --git a/blight-editor/src/main/java/de/blight/editor/state/FernGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/FernGeneratorState.java index 07bd7a9..c61b73b 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/FernGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/FernGeneratorState.java @@ -88,12 +88,12 @@ public class FernGeneratorState extends BaseAppState { mat.setFloat("WindStrength", opts.windStrength); mat.setFloat("WindSpeed", opts.windSpeed); try { - Texture diff = assets.loadTexture("Textures/fern/Fern02_Diffuse.tga"); + Texture diff = assets.loadTexture("Textures/internal/fern/Fern02_Diffuse.png"); mat.setTexture("DiffuseMap", diff); mat.setBoolean("HasDiffuseMap", true); } catch (Exception ignored) {} try { - Texture norm = assets.loadTexture("Textures/fern/Fern02_Normal.tga"); + Texture norm = assets.loadTexture("Textures/internal/fern/Fern02_Normal.png"); mat.setTexture("NormalMap", norm); mat.setBoolean("HasNormalMap", true); } catch (Exception ignored) {} diff --git a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java index 5d15577..f504ef3 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java @@ -22,6 +22,9 @@ import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexIO; import de.blight.editor.SharedInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -32,6 +35,8 @@ import java.util.Random; */ public class GrassVertexState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(GrassVertexState.class); + // ── Chunks ──────────────────────────────────────────────────────────────── private static final int TERRAIN_HALF = 2048; private static final int CHUNK_SIZE = 128; @@ -94,7 +99,7 @@ public class GrassVertexState extends BaseAppState { if (ci >= 0) { chunkBlades[ci].add(b); dirtyChunks[ci] = true; } } } catch (Exception e) { - System.err.println("[GrassVertexState] Daten nicht ladbar: " + e.getMessage()); + log.warn("[GrassVertexState] Daten nicht ladbar: {}", e.getMessage()); } } @@ -124,7 +129,7 @@ public class GrassVertexState extends BaseAppState { mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); return mat; } catch (Exception e) { - System.err.println("[GrassVertexState] Material nicht ladbar: " + e.getMessage()); + log.warn("[GrassVertexState] Material nicht ladbar: {}", e.getMessage()); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f)); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java index e0ddcd9..98dbf53 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java @@ -9,15 +9,22 @@ import com.jme3.export.binary.BinaryExporter; import com.jme3.export.binary.BinaryImporter; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; +import com.jme3.light.PointLight; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.renderer.Camera; import com.jme3.scene.*; import com.jme3.scene.shape.Box; +import com.jme3.scene.shape.Cylinder; +import com.jme3.scene.shape.Sphere; import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; import java.nio.file.Path; /** @@ -28,6 +35,8 @@ import java.nio.file.Path; */ public class ModelEditorState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(ModelEditorState.class); + private static final java.nio.file.Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); @@ -51,6 +60,12 @@ public class ModelEditorState extends BaseAppState { private Node modelWrapper; /** Gitter-Geometry. */ private Geometry gridGeo; + /** Vergleichsfigur: 2m-Zylinder als Maßstabsreferenz. */ + private Geometry compCylinder = null; + private boolean compCylinderVisible = false; + + /** Node, der alle Anhang-Gizmos (Lichter + Emitter) enthält. */ + private Node attachmentGizmos = null; // gespeicherter Kamerazustand aus dem Editor-Modus private Vector3f savedCamPos; @@ -60,6 +75,12 @@ public class ModelEditorState extends BaseAppState { private String currentPath = null; private String mainModelPath = null; + /** Originales Spatial wie vom Asset-Manager geladen – wird durch LOD-Previews nicht überschrieben. */ + private Spatial originalSpatial = null; + + /** Ob previewRoot bereits am Viewport hängt – verhindert Doppel-Attach und First-Frame-Black. */ + private boolean previewSceneAttached = false; + // Eingebettete LOD-Stufen (falls j3o ein LOD-Node-Baum ist) private boolean hasEmbeddedLods = false; private Spatial[] embeddedLodSpatials = null; @@ -96,6 +117,7 @@ public class ModelEditorState extends BaseAppState { public void update(float tpf) { if (input.activeLayer != SharedInput.LAYER_MODEL_EDITOR) { if (previewRoot != null) exitPreview(); + input.modelEditorCloseRequest = false; return; } @@ -105,6 +127,8 @@ public class ModelEditorState extends BaseAppState { input.modelEditorOpenPath = null; mainModelPath = openPath; input.modelEditorLodPreview = 0; + input.modelImportLod1PreviewNode.set(null); + input.modelImportLod2PreviewNode.set(null); loadModel(openPath); } @@ -118,16 +142,27 @@ public class ModelEditorState extends BaseAppState { i == lodIdx ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); } } else { - String path = switch (input.modelEditorLodPreview) { - case 1 -> input.modelEditorLod1Path; - case 2 -> input.modelEditorLod2Path; - default -> mainModelPath; - }; - if (path == null || path.isBlank()) { - path = mainModelPath; - input.modelEditorLodPreview = 0; + // In-memory Preview-Node aus Importer hat Vorrang vor Dateipfaden + Spatial previewNode = null; + if (input.modelEditorLodPreview == 1) + previewNode = input.modelImportLod1PreviewNode.get(); + else if (input.modelEditorLodPreview == 2) + previewNode = input.modelImportLod2PreviewNode.get(); + + if (previewNode != null) { + showSpatialDirectly(previewNode.clone()); + } else { + String path = switch (input.modelEditorLodPreview) { + case 1 -> input.modelEditorLod1Path; + case 2 -> input.modelEditorLod2Path; + default -> mainModelPath; + }; + if (path == null || path.isBlank()) { + path = mainModelPath; + input.modelEditorLodPreview = 0; + } + if (path != null) loadModel(path); } - if (path != null) loadModel(path); } } @@ -155,6 +190,14 @@ public class ModelEditorState extends BaseAppState { applyPivot(input.modelEditorPivotY); } + // Anhang-Gizmos aktualisieren + if (input.modelEditorAttachmentsChanged) { + input.modelEditorAttachmentsChanged = false; + rebuildAttachmentGizmos( + input.modelEditorAttachedLights, + input.modelEditorAttachedEmitters); + } + // Thumbnail auf Anforderung generieren Path thumbReq = input.modelEditorThumbnailRequest; if (thumbReq != null && modelWrapper != null) { @@ -176,6 +219,20 @@ public class ModelEditorState extends BaseAppState { orbitDist = Math.max(0.5f, orbitDist - scroll * orbitDist * ZOOM_FACTOR); } + // Vergleichszylinder ein-/ausblenden + Position aktualisieren + boolean wantCylinder = input.modelImportDummyVisible; + if (wantCylinder != compCylinderVisible) { + compCylinderVisible = wantCylinder; + if (wantCylinder) { + ensureCompCylinder(); + } else if (compCylinder != null) { + compCylinder.setCullHint(Spatial.CullHint.Always); + } + } + if (compCylinderVisible) { + repositionCompCylinder(); + } + applyOrbitCamera(); previewRoot.updateLogicalState(tpf); @@ -200,8 +257,16 @@ public class ModelEditorState extends BaseAppState { embeddedLodSpatials = null; input.modelEditorHasEmbeddedLods = false; try { + // Cache vor dem Laden löschen – verhindert, dass zwischengespeicherte + // Materialzustände (z.B. aus vorherigen Thumbnail-Renders) das erste + // Rendering schwarz erscheinen lassen. + try { app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(assetPath)); } + catch (Exception ignored) {} Spatial model = app.getAssetManager().loadModel(assetPath); stripControls(model); + recalcNormalsIfMissing(model); + markBuffersUpdateNeeded(model); + originalSpatial = model; modelWrapper.attachChild(model); // Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei Kind 0 sichtbar @@ -217,8 +282,7 @@ public class ModelEditorState extends BaseAppState { } } } catch (Exception e) { - System.err.println("[ModelEditor] Fehler beim Laden von '" + assetPath + "': " + e.getMessage()); - e.printStackTrace(); + log.error("[ModelEditor] Fehler beim Laden von '{}': {}", assetPath, e.getMessage(), e); input.modelEditorBoundsReady = false; // Fallback: roter Quader als visuelles Signal Geometry box = new Geometry("error_box", new Box(0.5f, 0.5f, 0.5f)); @@ -243,6 +307,43 @@ public class ModelEditorState extends BaseAppState { orbitDist = maxExt * 3f; orbitCenter = bb.getCenter().clone(); } + + // Vergleichszylinder neu positionieren + repositionCompCylinder(); + + previewRoot.updateGeometricState(); + + // Scene erst nach vollständigem Setup am Viewport registrieren – + // verhindert, dass JME3 beim ersten Render eine noch leere Szene sieht. + if (!previewSceneAttached) { + app.getViewPort().attachScene(previewRoot); + previewSceneAttached = true; + } + } + + private void showSpatialDirectly(Spatial spatial) { + if (previewRoot == null) enterPreview(); + if (modelWrapper != null) previewRoot.detachChild(modelWrapper); + modelWrapper = new Node("model_wrapper"); + stripControls(spatial); + modelWrapper.attachChild(spatial); + applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ); + applyPivot(input.modelEditorPivotY); + previewRoot.attachChild(modelWrapper); + rebuildGrid(); + updateBounds(); + BoundingBox bb = getBoundingBox(); + if (bb != null) { + float maxExt = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); + orbitDist = maxExt * 3f; + orbitCenter = bb.getCenter().clone(); + } + repositionCompCylinder(); + previewRoot.updateGeometricState(); + if (!previewSceneAttached) { + app.getViewPort().attachScene(previewRoot); + previewSceneAttached = true; + } } private void applyScale(float sx, float sy, float sz) { @@ -281,18 +382,20 @@ public class ModelEditorState extends BaseAppState { app.getRootNode().setCullHint(Spatial.CullHint.Always); previewRoot = new Node("model_editor_preview"); - // Beleuchtung analog zum Animations-Editor previewRoot.addLight(new DirectionalLight( new Vector3f(-0.5f, -1f, -0.4f).normalizeLocal(), new ColorRGBA(1.8f, 1.7f, 1.4f, 1f))); previewRoot.addLight(new DirectionalLight( new Vector3f(0.6f, -0.4f, 0.7f).normalizeLocal(), - new ColorRGBA(0.45f, 0.5f, 0.7f, 1f))); - previewRoot.addLight(new AmbientLight(new ColorRGBA(0.4f, 0.4f, 0.45f, 1f))); + new ColorRGBA(0.5f, 0.55f, 0.75f, 1f))); + previewRoot.addLight(new DirectionalLight( + new Vector3f(0f, 0.3f, -1f).normalizeLocal(), + new ColorRGBA(0.4f, 0.4f, 0.5f, 1f))); + previewRoot.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f))); - // Direkt am Viewport registrieren – NICHT als Kind von rootNode, - // da rootNode.CullHint=Always die komplette Child-Hierarchie ausblendet. - app.getViewPort().attachScene(previewRoot); + // Hintergrundfarbe setzen; Viewport-Attach erfolgt NACH dem Modell-Load + // (in loadModel / showSpatialDirectly), damit JME3 beim ersten Render + // eine vollständig initialisierte Szene vorfindet und nicht schwarz rendert. app.getViewPort().setBackgroundColor(new ColorRGBA(0.15f, 0.15f, 0.18f, 1f)); orbitYaw = 30f; @@ -301,13 +404,20 @@ public class ModelEditorState extends BaseAppState { private void exitPreview() { if (previewRoot != null) { - app.getViewPort().detachScene(previewRoot); - previewRoot = null; - modelWrapper = null; - gridGeo = null; + if (previewSceneAttached) { + app.getViewPort().detachScene(previewRoot); + previewSceneAttached = false; + } + previewRoot = null; + modelWrapper = null; + gridGeo = null; + compCylinder = null; + attachmentGizmos = null; } + compCylinderVisible = false; hasEmbeddedLods = false; embeddedLodSpatials = null; + originalSpatial = null; input.modelEditorHasEmbeddedLods = false; app.getRootNode().setCullHint(Spatial.CullHint.Inherit); @@ -375,6 +485,42 @@ public class ModelEditorState extends BaseAppState { previewRoot.attachChild(gridGeo); } + // ── Vergleichszylinder (2 m Mensch-Figur) ──────────────────────────────── + + private void ensureCompCylinder() { + if (compCylinder != null) { compCylinder.setCullHint(Spatial.CullHint.Inherit); return; } + // Zylinder: Höhe 2m, Radius 0.25m (stilisierte Menschfigur) + Cylinder cyl = new Cylinder(2, 16, 0.25f, 2f, true); + compCylinder = new Geometry("comp_cylinder", cyl); + // JME3-Zylinder liegt entlang Z → 90° um X drehen damit er aufrecht steht + compCylinder.setLocalRotation( + new com.jme3.math.Quaternion().fromAngleAxis( + com.jme3.math.FastMath.HALF_PI, + com.jme3.math.Vector3f.UNIT_X)); + // Unshaded, semi-transparent blau-weiß, damit er sich vom Modell abhebt + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.4f, 0.7f, 1.0f, 0.55f)); + mat.getAdditionalRenderState().setBlendMode( + com.jme3.material.RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode( + com.jme3.material.RenderState.FaceCullMode.Off); + compCylinder.setMaterial(mat); + compCylinder.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent); + compCylinder.setCullHint(Spatial.CullHint.Inherit); + previewRoot.attachChild(compCylinder); + } + + private void repositionCompCylinder() { + if (compCylinder == null) return; + // Basis-X: rechts neben dem Modell; Y=1 damit der Boden bei Y=0 liegt + BoundingBox bb = getBoundingBox(); + float baseX = bb != null ? bb.getXExtent() + 0.6f : 1.5f; + compCylinder.setLocalTranslation( + baseX + input.modelImportDummyX, + 1f + input.modelImportDummyY, + input.modelImportDummyZ); + } + // ── Bounds publizieren ───────────────────────────────────────────────────── private void updateBounds() { @@ -408,13 +554,152 @@ public class ModelEditorState extends BaseAppState { cam.lookAt(orbitCenter, Vector3f.UNIT_Y); } + // ── Anhang-Gizmos ───────────────────────────────────────────────────────── + + private void rebuildAttachmentGizmos( + java.util.List lights, + java.util.List emitters) { + + if (previewRoot == null) return; + + // Alten Gizmo-Node + Punkt-Lichter entfernen + if (attachmentGizmos != null) { + // Alle PointLights im alten Gizmo-Node aus previewRoot austragen + removeGizmoLights(attachmentGizmos); + previewRoot.detachChild(attachmentGizmos); + attachmentGizmos = null; + } + + if (lights.isEmpty() && emitters.isEmpty()) return; + + attachmentGizmos = new Node("attachment_gizmos"); + + // Licht-Gizmos: kleine Kugel in Lichtfarbe + PointLight + for (de.blight.common.ModelMeta.AttachedLight al : lights) { + Geometry sphere = new Geometry("light_gizmo", + new Sphere(8, 8, 0.12f)); + Material mat = new Material(app.getAssetManager(), + "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(al.r(), al.g(), al.b(), 1f)); + sphere.setMaterial(mat); + sphere.setLocalTranslation(al.offsetX(), al.offsetY(), al.offsetZ()); + attachmentGizmos.attachChild(sphere); + + PointLight pl = new PointLight( + new Vector3f(al.offsetX(), al.offsetY(), al.offsetZ()), + new ColorRGBA(al.r() * al.intensity(), + al.g() * al.intensity(), + al.b() * al.intensity(), 1f), + al.radius()); + // PointLight merken wir uns via UserData auf der Kugel für späteres Entfernen + sphere.setUserData("pointLight", pl); + previewRoot.addLight(pl); + } + + // Emitter-Gizmos: kleine Kugel in Preset-Farbe + for (de.blight.common.ModelMeta.AttachedEmitter ae : emitters) { + ColorRGBA color = switch (ae.preset()) { + case 1 -> new ColorRGBA(0.5f, 0.5f, 0.5f, 1f); // Rauch + case 2 -> new ColorRGBA(1.0f, 0.9f, 0.1f, 1f); // Funken + default -> new ColorRGBA(1.0f, 0.4f, 0.0f, 1f); // Feuer + }; + Geometry sphere = new Geometry("emitter_gizmo", + new Sphere(8, 8, 0.10f)); + Material mat = new Material(app.getAssetManager(), + "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + sphere.setMaterial(mat); + sphere.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ()); + attachmentGizmos.attachChild(sphere); + } + + previewRoot.attachChild(attachmentGizmos); + } + + /** Entfernt alle PointLights, die über Gizmo-Kugeln in previewRoot registriert wurden. */ + private void removeGizmoLights(Node gizmos) { + for (Spatial child : gizmos.getChildren()) { + PointLight pl = child.getUserData("pointLight"); + if (pl != null) previewRoot.removeLight(pl); + } + } + // ── Hilfsmethoden ───────────────────────────────────────────────────────── + /** Markiert alle Vertex-Buffer als upload-needed – zwingt JME3 zu korrektem erstem Frame. */ + private static void markBuffersUpdateNeeded(Spatial s) { + if (s instanceof Geometry g) { + Mesh mesh = g.getMesh(); + for (VertexBuffer.Type type : VertexBuffer.Type.values()) { + VertexBuffer vb = mesh.getBuffer(type); + if (vb != null) vb.setUpdateNeeded(); + } + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) markBuffersUpdateNeeded(child); + } + } + private static void stripControls(Spatial s) { while (s.getNumControls() > 0) s.removeControl(s.getControl(0)); if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls); } + /** + * Berechnet Smooth-Normals immer neu aus der Geometrie. + * Behebt TripoAI-Modelle mit fehlenden, null oder nach innen zeigenden Normals. + */ + private static void recalcNormalsIfMissing(Spatial s) { + if (s instanceof Geometry g) { + Mesh mesh = g.getMesh(); + if (mesh.getMode() != Mesh.Mode.Triangles) return; + + FloatBuffer posBuf = mesh.getFloatBuffer(VertexBuffer.Type.Position); + if (posBuf == null) return; + posBuf.rewind(); + float[] pos = new float[posBuf.limit()]; + posBuf.get(pos); + int vertCount = pos.length / 3; + + VertexBuffer idxBuf = mesh.getBuffer(VertexBuffer.Type.Index); + int[] idx; + if (idxBuf != null) { + if (idxBuf.getFormat() == VertexBuffer.Format.UnsignedShort) { + ShortBuffer sb = (ShortBuffer) idxBuf.getData(); sb.rewind(); + idx = new int[sb.limit()]; + for (int i = 0; i < idx.length; i++) idx[i] = sb.get() & 0xFFFF; + } else { + IntBuffer ib = (IntBuffer) idxBuf.getData(); ib.rewind(); + idx = new int[ib.limit()]; + for (int i = 0; i < idx.length; i++) idx[i] = ib.get(); + } + } else { + idx = new int[vertCount]; + for (int i = 0; i < vertCount; i++) idx[i] = i; + } + + float[] normals = new float[vertCount * 3]; + for (int t = 0; t < idx.length / 3; t++) { + int a = idx[t*3], b = idx[t*3+1], c = idx[t*3+2]; + float ax=pos[b*3]-pos[a*3], ay=pos[b*3+1]-pos[a*3+1], az=pos[b*3+2]-pos[a*3+2]; + float bx=pos[c*3]-pos[a*3], by=pos[c*3+1]-pos[a*3+1], bz=pos[c*3+2]-pos[a*3+2]; + float nx=ay*bz-az*by, ny=az*bx-ax*bz, nz=ax*by-ay*bx; + normals[a*3]+=nx; normals[a*3+1]+=ny; normals[a*3+2]+=nz; + normals[b*3]+=nx; normals[b*3+1]+=ny; normals[b*3+2]+=nz; + normals[c*3]+=nx; normals[c*3+1]+=ny; normals[c*3+2]+=nz; + } + for (int i = 0; i < vertCount; i++) { + float x=normals[i*3], y=normals[i*3+1], z=normals[i*3+2]; + float len=(float)Math.sqrt(x*x+y*y+z*z); + if (len>0){normals[i*3]/=len; normals[i*3+1]/=len; normals[i*3+2]/=len;} + } + FloatBuffer newNb = BufferUtils.createFloatBuffer(normals.length); + newNb.put(normals); newNb.flip(); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, newNb); + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) recalcNormalsIfMissing(child); + } + } + // ── Thumbnail ───────────────────────────────────────────────────────────── private void generateThumbnail(Path j3oPath) { @@ -427,7 +712,7 @@ public class ModelEditorState extends BaseAppState { ThumbnailRenderer.saveSidecar(thumb, j3oPath, ASSET_ROOT); embedThumbnail(thumb, j3oPath); } catch (Exception e) { - System.err.println("[ModelEditor] Thumbnail-Fehler: " + e.getMessage()); + log.warn("[ModelEditor] Thumbnail-Fehler: {}", e.getMessage()); } } @@ -441,9 +726,31 @@ public class ModelEditorState extends BaseAppState { BinaryExporter.getInstance().save(root, j3oPath.toFile()); } } catch (Exception e) { - System.err.println("[ModelEditor] j3o-Embed fehlgeschlagen: " + e.getMessage()); + log.error("[ModelEditor] j3o-Embed fehlgeschlagen: {}", e.getMessage(), e); } } public String getCurrentPath() { return currentPath; } + + /** + * Gibt das erste Kind des modelWrapper zurück (das eigentliche Modell-Spatial), + * oder null wenn kein Modell geladen ist. + */ + public Spatial getModelSpatial() { + if (modelWrapper == null || modelWrapper.getChildren().isEmpty()) return null; + return modelWrapper.getChild(0); + } + + /** + * Gibt das originale, direkt vom Asset-Manager geladene Spatial zurück – + * unabhängig davon, ob gerade ein LOD-Preview angezeigt wird. + */ + public Spatial getOriginalSpatial() { return originalSpatial; } + + /** + * Gibt die aktuellen Weltgrenzen des modelWrapper zurück, oder null wenn nicht verfügbar. + */ + public BoundingBox getCurrentBounds() { + return getBoundingBox(); + } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java new file mode 100644 index 0000000..3f6ba7b --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java @@ -0,0 +1,721 @@ +package de.blight.editor.state; + +import java.io.File; +import java.io.IOException; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.math.*; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.*; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.control.AbstractControl; +import com.jme3.util.BufferUtils; + +import de.blight.editor.SharedInput; +import de.blight.editor.util.MeshUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jme3tools.optimize.LodGenerator; + +/** + * JME3 AppState für den "Import Model"-Workflow. + * Verwaltet LOD1/LOD2-Mesh-Reduktion und den finalen .j3o-Export + * mit eingebettetem ImportLodControl. + */ +public class ModelImportState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(ModelImportState.class); + + private static final Path ASSET_ROOT = + de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); + + private final SharedInput input; + private SimpleApplication app; + private AssetManager assets; + + private ModelEditorState modelEditorState; + + private Node lod1Node = null; + private Node lod2Node = null; + + public ModelImportState(SharedInput input) { + this.input = input; + } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.assets = app.getAssetManager(); + } + + @Override protected void cleanup(Application app) {} + @Override protected void onEnable() {} + @Override protected void onDisable() {} + + // ── Update-Schleife ─────────────────────────────────────────────────────── + + @Override + public void update(float tpf) { + if (modelEditorState == null) { + modelEditorState = getStateManager().getState(ModelEditorState.class); + if (modelEditorState == null) return; + } + + SharedInput.ModelLodGenRequest req = input.modelLodGenRequest.getAndSet(null); + if (req != null) { + switch (req.level()) { + case 1 -> { + if ("auto_mesh".equals(req.method())) generateLodAuto(1, 0.5f); + else if ("file".equals(req.method())) generateLodFromFile(1, req.fileAssetPath()); + } + case 2 -> { + if ("auto_mesh_heavy".equals(req.method())) generateLodAuto(2, 0.85f); + else if ("file".equals(req.method())) generateLodFromFile(2, req.fileAssetPath()); + } + default -> log.warn("[ModelImport] Unbekannte LOD-Stufe: {}", req.level()); + } + } + + String exportName = input.modelImportExportName; + if (exportName != null) { + performExport(exportName); + } + } + + // ── LOD: automatische Mesh-Reduktion ────────────────────────────────────── + + private void generateLodAuto(int level, float reduction) { + boolean isLod1 = (level == 1); + if (isLod1) input.modelImportHasLod1 = false; + else input.modelImportHasLod2 = false; + + Spatial modelSpatial = modelEditorState.getOriginalSpatial(); + if (modelSpatial == null) { + input.modelLodGenStatus = "FEHLER: Kein Modell geladen"; + return; + } + try { + Spatial clone = modelSpatial.clone(); + stripControls(clone); + + int trisBefore = countTriangles(clone); + + // Welding schafft geteilte Vertices → Manifold-Kanten für beide Algorithmen nötig + weldVerticesRecursive(clone, 0.01f); + + if ("jme".equals(input.modelLodAlgorithm)) { + decimateRecursiveJme(clone, 1.0f - reduction); + } else { + decimateRecursiveBlight(clone, 1.0f - reduction); + } + + Node wrapper; + if (clone instanceof Node n) wrapper = n; + else { wrapper = new Node("lod" + level); wrapper.attachChild(clone); } + wrapper.setName("lod" + level); + + int trisAfter = countTriangles(wrapper); + int pct = trisBefore > 0 ? (100 * trisAfter / trisBefore) : 0; + String statusMsg = String.format("LOD%d generiert: %d → %d Dreiecke (%d%%)", + level, trisBefore, trisAfter, pct); + + if (isLod1) { + lod1Node = wrapper; + input.modelImportLod1PreviewNode.set(wrapper); + input.modelImportHasLod1 = true; + } else { + wrapper.setShadowMode(RenderQueue.ShadowMode.Off); + lod2Node = wrapper; + input.modelImportLod2PreviewNode.set(wrapper); + input.modelImportHasLod2 = true; + } + input.modelLodGenStatus = statusMsg; + input.modelEditorLodPreview = level; + input.modelEditorLodChanged = true; + log.info("[ModelImport] {}", statusMsg); + } catch (Exception e) { + log.error("[ModelImport] LOD{}-Generierung fehlgeschlagen: {}", level, e.getMessage(), e); + input.modelLodGenStatus = "FEHLER LOD" + level + ": " + e.getMessage(); + } + } + + // ── LOD: aus Datei laden ────────────────────────────────────────────────── + + private void generateLodFromFile(int level, String assetPath) { + boolean isLod1 = (level == 1); + if (isLod1) input.modelImportHasLod1 = false; + else input.modelImportHasLod2 = false; + try { + Spatial loaded = assets.loadModel(assetPath); + stripControls(loaded); + Node wrapper; + if (loaded instanceof Node n) wrapper = n; + else { wrapper = new Node("lod" + level); wrapper.attachChild(loaded); } + wrapper.setName("lod" + level); + if (isLod1) { + lod1Node = wrapper; + input.modelImportLod1PreviewNode.set(wrapper); + input.modelImportHasLod1 = true; + input.modelLodGenStatus = "LOD1 geladen: " + countTriangles(wrapper) + " Dreiecke"; + } else { + lod2Node = wrapper; + input.modelImportLod2PreviewNode.set(wrapper); + input.modelImportHasLod2 = true; + input.modelLodGenStatus = "LOD2 geladen: " + countTriangles(wrapper) + " Dreiecke"; + } + input.modelEditorLodPreview = level; + input.modelEditorLodChanged = true; + log.info("[ModelImport] LOD{} aus Datei: {}", level, assetPath); + } catch (Exception e) { + log.error("[ModelImport] LOD{}-Laden fehlgeschlagen ({}): {}", level, assetPath, e.getMessage()); + input.modelLodGenStatus = "FEHLER LOD" + level + ": " + e.getMessage(); + } + } + + // ── Export ──────────────────────────────────────────────────────────────── + + private void performExport(String fileName) { + input.modelImportExportName = null; + + Spatial srcSpatial = modelEditorState.getOriginalSpatial(); + if (srcSpatial == null) { + input.modelImportExportStatus = "FEHLER: Kein Modell geladen"; + return; + } + + try { + float sx = input.modelEditorScaleX; + float sy = input.modelEditorScaleY; + float sz = input.modelEditorScaleZ; + float pivotY = input.modelEditorPivotY; + + // Clone original, apply target scale, compute bottom-aligned pivot + Spatial lod0 = srcSpatial.clone(); + stripControls(lod0); + lod0.setLocalScale(sx, sy, sz); + lod0.setLocalTranslation(Vector3f.ZERO); + lod0.updateGeometricState(); + + float translationY = 0f; + com.jme3.bounding.BoundingVolume bv = lod0.getWorldBound(); + if (bv instanceof com.jme3.bounding.BoundingBox bb) { + translationY = -(bb.getCenter().y - bb.getYExtent()) + pivotY; + } + lod0.setLocalTranslation(0f, translationY, 0f); + bakeTransform(lod0, new Matrix4f()); + lod0.setName("lod0"); + + Node root = new Node(fileName); + root.attachChild(lod0); + + boolean hasLods = (lod1Node != null || lod2Node != null); + + if (lod1Node != null) { + lod1Node.setName("lod1"); + lod1Node.setLocalScale(sx, sy, sz); + lod1Node.setLocalTranslation(0f, translationY, 0f); + lod1Node.setCullHint(Spatial.CullHint.Always); + bakeSpatialInPlace(lod1Node); + root.attachChild(lod1Node); + } + if (lod2Node != null) { + lod2Node.setName("lod2"); + lod2Node.setLocalScale(sx, sy, sz); + lod2Node.setLocalTranslation(0f, translationY, 0f); + lod2Node.setCullHint(Spatial.CullHint.Always); + lod2Node.setShadowMode(RenderQueue.ShadowMode.Off); + bakeSpatialInPlace(lod2Node); + root.attachChild(lod2Node); + } + + if (hasLods) { + Node lod0Node = (lod0 instanceof Node n) ? n : wrapInNode(lod0, "lod0"); + if (lod0 != lod0Node) { root.detachChild(lod0); root.attachChild(lod0Node); } + root.addControl(new ImportLodControl( + app.getCamera(), lod0Node, lod1Node, lod2Node, 40f, 120f)); + while (root.getNumControls() > 0) + root.removeControl(root.getControl(0)); + } + + Path modelDir = ASSET_ROOT.resolve("Models").resolve("imported"); + Files.createDirectories(modelDir); + File outFile = modelDir.resolve(fileName + ".j3o").toFile(); + + BinaryExporter.getInstance().save(root, outFile); + log.info("[ModelImport] Exportiert: {}", outFile.getAbsolutePath()); + + String relPath = "Models/imported/" + fileName + ".j3o"; + + // Asset-Cache invalidieren: JME cached geladene Modelle. Ohne diesen + // Schritt liefert loadModel() beim nächsten Platzieren noch das alte + // (unscalierte) Modell aus dem Cache – die richtige Größe erscheint erst + // nach einem Editor-Neustart oder im Spiel (dort kein Cache vorhanden). + assets.deleteFromCache(new com.jme3.asset.ModelKey(relPath)); + + input.modelImportExportStatus = "Gespeichert: " + relPath; + input.refreshAssets = true; + + lod1Node = null; + lod2Node = null; + input.modelImportHasLod1 = false; + input.modelImportHasLod2 = false; + input.modelImportLod1PreviewNode.set(null); + input.modelImportLod2PreviewNode.set(null); + + } catch (IOException e) { + log.error("[ModelImport] Export-Fehler: {}", e.getMessage(), e); + input.modelImportExportStatus = "FEHLER: " + e.getMessage(); + } + } + + // ── Vertex-Welding (Merge by Distance) ─────────────────────────────────── + + private static void weldVerticesRecursive(Spatial s, float threshold) { + if (s instanceof Geometry g) { + Mesh welded = MeshUtils.mergeByDistance(g.getMesh(), threshold); + if (welded != g.getMesh()) g.setMesh(welded); + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) weldVerticesRecursive(child, threshold); + } + } + + // ── Mesh-Decimation ─────────────────────────────────────────────────────── + + /** JME3 Progressive Mesh: verwendet JME's LodGenerator direkt. */ + private static void decimateRecursiveJme(Spatial s, float keepRatio) { + if (s instanceof Geometry g) { + Mesh mesh = g.getMesh(); + int origTris = mesh.getTriangleCount(); + try { + new LodGenerator(g).bakeLods( + LodGenerator.TriangleReductionMethod.PROPORTIONAL, 1.0f - keepRatio); + // JME stores the ORIGINAL index buffer at level 0 and the + // reduced version at level 1+. Successful reduction → numLodLevels > 1. + if (mesh.getNumLodLevels() > 1) { + Mesh reduced = extractLodAsNewMesh(mesh, mesh.getNumLodLevels() - 1); + mesh.setLodLevels(null); + g.setMesh(reduced); + log.info("[ModelImport] JME LOD: {} → {} Dreiecke", origTris, reduced.getTriangleCount()); + } else { + mesh.setLodLevels(null); + log.warn("[ModelImport] JME: keine Reduktion erzielt (numLodLevels={})", mesh.getNumLodLevels()); + } + } catch (Exception e) { + try { mesh.setLodLevels(null); } catch (Exception ignored) {} + log.warn("[ModelImport] JME bakeLods fehlgeschlagen für {}: {}", g.getName(), e.getMessage()); + } + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) decimateRecursiveJme(child, keepRatio); + } + } + + /** + * Blight Edge Collapse: verschmilzt Kanten (je zwei geteilte Dreiecke) iterativ, + * bis die Zielanzahl erreicht ist. Das Mesh bleibt durchgehend geschlossen. + */ + private static void decimateRecursiveBlight(Spatial s, float keepRatio) { + if (s instanceof Geometry g) { + g.setMesh(edgeCollapseMesh(g.getMesh(), keepRatio)); + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) decimateRecursiveBlight(child, keepRatio); + } + } + + /** + * Extrahiert ein LOD-Level aus JME3's bakeLods-Ergebnis als eigenständiges Mesh. + * Handhabt alle möglichen Index-Buffer-Formate (UnsignedShort, UnsignedInt, Float). + */ + private static Mesh extractLodAsNewMesh(Mesh orig, int lodLevel) { + VertexBuffer lodIdx = orig.getLodLevel(lodLevel); + if (lodIdx == null) throw new IllegalStateException("kein LOD-Level " + lodLevel); + + // Lese Indices unabhängig vom Buffer-Typ (Float, Short, Int sind alle möglich) + java.nio.Buffer rawData = lodIdx.getData(); + rawData.rewind(); + int count = rawData.limit(); + int[] indices = new int[count]; + if (rawData instanceof FloatBuffer fb) { + for (int i = 0; i < count; i++) indices[i] = (int) fb.get(); + } else if (rawData instanceof ShortBuffer sb) { + for (int i = 0; i < count; i++) indices[i] = sb.get() & 0xFFFF; + } else { + IntBuffer ib = (IntBuffer) rawData; + for (int i = 0; i < count; i++) indices[i] = ib.get(); + } + + Mesh newMesh = new Mesh(); + newMesh.setMode(orig.getMode()); + for (VertexBuffer.Type type : VertexBuffer.Type.values()) { + if (type == VertexBuffer.Type.Index) continue; + if (type.name().startsWith("LoDIndex")) continue; // JME-interne LOD-Puffer überspringen + VertexBuffer vb = orig.getBuffer(type); + if (vb != null) newMesh.setBuffer(vb); + } + + // Wähle Index-Format basierend auf dem tatsächlich auftretenden Maximalwert + int maxIdx = 0; + for (int idx : indices) if (idx > maxIdx) maxIdx = idx; + + VertexBuffer newIdx = new VertexBuffer(VertexBuffer.Type.Index); + if (maxIdx <= 65535) { + ShortBuffer dst = BufferUtils.createShortBuffer(indices.length); + for (int idx : indices) dst.put((short)(idx & 0xFFFF)); + dst.flip(); + newIdx.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedShort, dst); + } else { + IntBuffer dst = BufferUtils.createIntBuffer(indices.length); + for (int idx : indices) dst.put(idx); + dst.flip(); + newIdx.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedInt, dst); + } + newMesh.setBuffer(newIdx); + newMesh.updateCounts(); + newMesh.updateBound(); + return newMesh; + } + + /** + * Edge-Collapse-Decimation: verschmilzt Kanten (je zwei geteilte Dreiecke) iterativ, + * bis die Zielanzahl erreicht ist. Das Mesh bleibt durchgehend geschlossen. + */ + private static Mesh edgeCollapseMesh(Mesh src, float keepRatio) { + int vertCount = src.getVertexCount(); + VertexBuffer idxBuf = src.getBuffer(VertexBuffer.Type.Index); + if (idxBuf == null) return src; + + boolean isShort = (idxBuf.getFormat() == VertexBuffer.Format.UnsignedShort); + int totalTris; + int[] indices; + if (isShort) { + ShortBuffer sb = (ShortBuffer) idxBuf.getData(); sb.rewind(); + totalTris = sb.limit() / 3; + indices = new int[totalTris * 3]; + for (int i = 0; i < indices.length; i++) indices[i] = sb.get() & 0xFFFF; + } else { + IntBuffer ib = (IntBuffer) idxBuf.getData(); ib.rewind(); + totalTris = ib.limit() / 3; + indices = new int[totalTris * 3]; + for (int i = 0; i < indices.length; i++) indices[i] = ib.get(); + } + + int targetTris = Math.max(1, Math.round(totalTris * keepRatio)); + if (totalTris <= targetTris) return src; + + // Positionen vorab einlesen – benötigt für Kantenlängen und Mittelpunkte + FloatBuffer srcPos = src.getFloatBuffer(VertexBuffer.Type.Position); + float[] positions = null; + if (srcPos != null) { + srcPos.rewind(); + positions = new float[srcPos.limit()]; + srcPos.get(positions); + } + + // Kanten-Adjazenz aufbauen: edgeKey → Liste der anliegenden Dreiecks-Indizes + HashMap> edgeTris = new HashMap<>(); + for (int t = 0; t < totalTris; t++) { + int a = indices[t*3], b = indices[t*3+1], c = indices[t*3+2]; + ecAddEdge(edgeTris, a, b, t); + ecAddEdge(edgeTris, b, c, t); + ecAddEdge(edgeTris, a, c, t); + } + + // Per-Dreieck-Normalen für diedrale Kantenkosten + float[] triNormals = new float[totalTris * 3]; + if (positions != null) { + for (int t = 0; t < totalTris; t++) { + int a = indices[t*3], b = indices[t*3+1], c = indices[t*3+2]; + float ax=positions[b*3]-positions[a*3], ay=positions[b*3+1]-positions[a*3+1], az=positions[b*3+2]-positions[a*3+2]; + float bx=positions[c*3]-positions[a*3], by=positions[c*3+1]-positions[a*3+1], bz=positions[c*3+2]-positions[a*3+2]; + float nx=ay*bz-az*by, ny=az*bx-ax*bz, nz=ax*by-ay*bx; + float len=(float)Math.sqrt(nx*nx+ny*ny+nz*nz); + if (len>1e-10f){nx/=len;ny/=len;nz/=len;} + triNormals[t*3]=nx; triNormals[t*3+1]=ny; triNormals[t*3+2]=nz; + } + } + + // Manifold-Kanten sortieren: Kosten = Länge² × (1 + 20×Diedralstrafe)² + // Koplanare Kanten erhalten niedrige Kosten → werden zuerst kollabiert (erhält Formkanten). + List sortedEdges = new ArrayList<>(); + for (Map.Entry> entry : edgeTris.entrySet()) { + if (entry.getValue().size() != 2) continue; + long key = entry.getKey(); + float cost = 0; + if (positions != null) { + int va = (int)(key >> 20), vb = (int)(key & 0xFFFFF); + float dx = positions[va*3]-positions[vb*3]; + float dy = positions[va*3+1]-positions[vb*3+1]; + float dz = positions[va*3+2]-positions[vb*3+2]; + float lenSq = dx*dx+dy*dy+dz*dz; + int t0 = entry.getValue().get(0), t1 = entry.getValue().get(1); + float dot = triNormals[t0*3]*triNormals[t1*3] + + triNormals[t0*3+1]*triNormals[t1*3+1] + + triNormals[t0*3+2]*triNormals[t1*3+2]; + float dp = (1f - Math.max(-1f, Math.min(1f, dot))) * 0.5f; // 0=koplanar, 1=entgegengesetzt + float w = 1f + 20f * dp; + cost = lenSq * w * w; + } + sortedEdges.add(new long[]{key, Float.floatToRawIntBits(cost)}); + } + sortedEdges.sort((a, b) -> Float.compare( + Float.intBitsToFloat((int) a[1]), + Float.intBitsToFloat((int) b[1]))); + + // Union-Find für Vertex-Verschmelzung + int[] uf = new int[vertCount]; + for (int i = 0; i < vertCount; i++) uf[i] = i; + + boolean[] dead = new boolean[totalTris]; + int deadCount = 0; + + // Manifold-Kanten (kürzeste zuerst) kollabieren bis Ziel erreicht + for (long[] edgeEntry : sortedEdges) { + if (totalTris - deadCount <= targetTris) break; + long key = edgeEntry[0]; + List tris = edgeTris.get(key); + int t0 = tris.get(0), t1 = tris.get(1); + if (dead[t0] || dead[t1]) continue; + + int va = (int)(key >> 20); + int vb = (int)(key & 0xFFFFF); + + dead[t0] = dead[t1] = true; + deadCount += 2; + // vb wird zu va zusammengefasst; Positionen bleiben unverändert – + // nur die Topologie (Index-Buffer) wird angepasst. + ecUnion(uf, va, vb); + } + + // Index-Buffer neu aufbauen: tote + degenerierte Dreiecke überspringen + List newIdx = new ArrayList<>(targetTris * 3); + for (int t = 0; t < totalTris; t++) { + if (dead[t]) continue; + int a = ecFind(uf, indices[t*3]); + int b = ecFind(uf, indices[t*3+1]); + int c = ecFind(uf, indices[t*3+2]); + if (a != b && b != c && a != c) { + newIdx.add(a); newIdx.add(b); newIdx.add(c); + } + } + + int[] idxArr = newIdx.stream().mapToInt(Integer::intValue).toArray(); + + // Neues Mesh zusammenbauen + Mesh newMesh = new Mesh(); + newMesh.setMode(src.getMode()); + for (VertexBuffer.Type type : VertexBuffer.Type.values()) { + if (type == VertexBuffer.Type.Index) continue; + if (type == VertexBuffer.Type.Normal) continue; // werden unten neu berechnet + VertexBuffer vb = src.getBuffer(type); + if (vb == null || !(vb.getData() instanceof FloatBuffer)) continue; + int comps = vb.getNumComponents(); + float[] data; + if (type == VertexBuffer.Type.Position && positions != null) { + data = positions; + } else { + FloatBuffer fb = (FloatBuffer) vb.getData(); fb.rewind(); + data = new float[fb.limit()]; fb.get(data); + } + FloatBuffer dst = BufferUtils.createFloatBuffer(data.length); + dst.put(data); dst.flip(); + newMesh.setBuffer(type, comps, dst); + } + + if (vertCount <= 65535) { + ShortBuffer dst = BufferUtils.createShortBuffer(idxArr.length); + for (int i : idxArr) dst.put((short)(i & 0xFFFF)); + dst.flip(); + newMesh.setBuffer(VertexBuffer.Type.Index, 3, VertexBuffer.Format.UnsignedShort, dst); + } else { + IntBuffer dst = BufferUtils.createIntBuffer(idxArr.length); + for (int i : idxArr) dst.put(i); + dst.flip(); + newMesh.setBuffer(VertexBuffer.Type.Index, 3, VertexBuffer.Format.UnsignedInt, dst); + } + + // Smooth-Normalen aus der finalen Geometrie neu berechnen + if (positions != null) { + float[] newNormals = new float[vertCount * 3]; + for (int t = 0; t < idxArr.length / 3; t++) { + int a = idxArr[t*3], b = idxArr[t*3+1], c = idxArr[t*3+2]; + float ax = positions[b*3] - positions[a*3]; + float ay = positions[b*3+1] - positions[a*3+1]; + float az = positions[b*3+2] - positions[a*3+2]; + float bx = positions[c*3] - positions[a*3]; + float by = positions[c*3+1] - positions[a*3+1]; + float bz = positions[c*3+2] - positions[a*3+2]; + float nx = ay*bz - az*by; + float ny = az*bx - ax*bz; + float nz = ax*by - ay*bx; + newNormals[a*3]+=nx; newNormals[a*3+1]+=ny; newNormals[a*3+2]+=nz; + newNormals[b*3]+=nx; newNormals[b*3+1]+=ny; newNormals[b*3+2]+=nz; + newNormals[c*3]+=nx; newNormals[c*3+1]+=ny; newNormals[c*3+2]+=nz; + } + for (int i = 0; i < vertCount; i++) { + float x = newNormals[i*3], y = newNormals[i*3+1], z = newNormals[i*3+2]; + float len = (float) Math.sqrt(x*x + y*y + z*z); + if (len > 0) { newNormals[i*3]/=len; newNormals[i*3+1]/=len; newNormals[i*3+2]/=len; } + } + FloatBuffer normalBuf = BufferUtils.createFloatBuffer(newNormals.length); + normalBuf.put(newNormals); normalBuf.flip(); + newMesh.setBuffer(VertexBuffer.Type.Normal, 3, normalBuf); + } + + newMesh.updateCounts(); + newMesh.updateBound(); + log.info("[ModelImport] Edge Collapse: {} → {} Dreiecke", totalTris, idxArr.length / 3); + return newMesh; + } + + private static void ecAddEdge(HashMap> map, int v0, int v1, int tri) { + long key = ((long)Math.min(v0, v1) << 20) | (long)Math.max(v0, v1); + map.computeIfAbsent(key, k -> new ArrayList<>()).add(tri); + } + + private static int ecFind(int[] uf, int x) { + while (uf[x] != x) { uf[x] = uf[uf[x]]; x = uf[x]; } + return x; + } + + private static void ecUnion(int[] uf, int x, int y) { + x = ecFind(uf, x); y = ecFind(uf, y); + if (x != y) uf[y] = x; + } + + private static int countTriangles(Spatial s) { + if (s instanceof Geometry g) return g.getMesh().getTriangleCount(); + if (s instanceof Node n) + return n.getChildren().stream().mapToInt(ModelImportState::countTriangles).sum(); + return 0; + } + + // ── Transform-Baking ────────────────────────────────────────────────────── + + private Spatial bakeSpatial(Spatial source) { + Spatial clone = source.clone(); + stripControls(clone); + bakeTransform(clone, new Matrix4f()); + return clone; + } + + private void bakeSpatialInPlace(Spatial s) { + stripControls(s); + bakeTransform(s, new Matrix4f()); + } + + private static void bakeTransform(Spatial s, Matrix4f accum) { + Matrix4f localMat = new Matrix4f(); + s.getLocalTransform().toTransformMatrix(localMat); + Matrix4f combined = accum.mult(localMat); + + if (s instanceof Geometry g) { + applyMatrixToMesh(g, combined); + g.setLocalTranslation(Vector3f.ZERO); + g.setLocalScale(1f); + g.setLocalRotation(Quaternion.IDENTITY); + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) bakeTransform(child, combined); + n.setLocalTranslation(Vector3f.ZERO); + n.setLocalScale(1f); + n.setLocalRotation(Quaternion.IDENTITY); + } + } + + private static void applyMatrixToMesh(Geometry g, Matrix4f mat) { + Mesh newMesh = g.getMesh().deepClone(); + + FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position); + if (pos != null) { + pos.rewind(); + Vector3f v = new Vector3f(); + while (pos.remaining() >= 3) { + int idx = pos.position(); + v.set(pos.get(), pos.get(), pos.get()); + mat.mult(v, v); + pos.put(idx, v.x); pos.put(idx + 1, v.y); pos.put(idx + 2, v.z); + } + pos.rewind(); + newMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded(); + } + + FloatBuffer norm = newMesh.getFloatBuffer(VertexBuffer.Type.Normal); + if (norm != null) { + Matrix3f normalMat = buildNormalMatrix(mat); + norm.rewind(); + Vector3f n = new Vector3f(); + while (norm.remaining() >= 3) { + int idx = norm.position(); + n.set(norm.get(), norm.get(), norm.get()); + normalMat.mult(n, n); n.normalizeLocal(); + norm.put(idx, n.x); norm.put(idx + 1, n.y); norm.put(idx + 2, n.z); + } + norm.rewind(); + newMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded(); + } + + newMesh.updateBound(); + g.setMesh(newMesh); + } + + private static Matrix3f buildNormalMatrix(Matrix4f mat) { + Matrix3f m3 = new Matrix3f( + mat.m00, mat.m01, mat.m02, + mat.m10, mat.m11, mat.m12, + mat.m20, mat.m21, mat.m22); + try { m3.invertLocal(); m3.transposeLocal(); } + catch (Exception e) { m3.loadIdentity(); } + return m3; + } + + private static void stripControls(Spatial s) { + while (s.getNumControls() > 0) s.removeControl(s.getControl(0)); + if (s instanceof Node n) n.getChildren().forEach(ModelImportState::stripControls); + } + + private static Node wrapInNode(Spatial s, String name) { + Node n = new Node(name); n.attachChild(s); return n; + } + + // ── LOD-Control ─────────────────────────────────────────────────────────── + + private static final class ImportLodControl extends AbstractControl { + private final Camera cam; + private final Node lod0, lod1, lod2; + private final float d01sq, d12sq; + + ImportLodControl(Camera cam, Node lod0, Node lod1, Node lod2, float d01, float d12) { + this.cam = cam; + this.lod0 = lod0; this.lod1 = lod1; this.lod2 = lod2; + this.d01sq = d01 * d01; this.d12sq = d12 * d12; + } + + @Override + protected void controlUpdate(float tpf) { + float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation()); + lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + if (lod1 != null) + lod1.setCullHint(dSq >= d01sq && dSq < d12sq + ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + if (lod2 != null) + lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + + @Override protected void controlRender(RenderManager rm, ViewPort vp) {} + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java index 155f7cd..70f56e5 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java @@ -1,45 +1,80 @@ package de.blight.editor.state; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import javax.imageio.ImageIO; + import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.jme3.asset.AssetManager; import com.jme3.bounding.BoundingBox; import com.jme3.export.binary.BinaryExporter; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.material.RenderState; -import com.jme3.texture.Texture; import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; import com.jme3.math.Vector3f; +import com.jme3.post.SceneProcessor; +import com.jme3.profile.AppProfiler; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; import com.jme3.scene.Node; import com.jme3.scene.Spatial; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.control.AbstractControl; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.util.BufferUtils; + import de.blight.editor.SharedInput; import de.blight.editor.tree.PalmMeshBuilder; import de.blight.editor.tree.PalmOptions; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PalmGeneratorState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(PalmGeneratorState.class); - private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); + private static final int IMPOSTOR_SIZE = 512; + private static final int ATLAS_DIRS = 4; + private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS; // 2048 + private static final int ATLAS_H = IMPOSTOR_SIZE; // 512 + private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); private final SharedInput input; private SimpleApplication app; private AssetManager assets; private TreeGeneratorState previewHost; + // ── Pending-Capture-Kontext ─────────────────────────────────────────────── + private SharedInput.PalmGenRequest pendingRequest = null; + private Node pendingLod0 = null; + private Node pendingLod1 = null; + private BoundingBox pendingBb = null; + private String pendingFileName = null; + private ViewPort captureVP = null; + private FrameBuffer captureFB = null; + private volatile boolean captureReady = false; + private int capturePass = 0; + private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS]; + public PalmGeneratorState(SharedInput input) { this.input = input; } @Override @@ -59,8 +94,21 @@ public class PalmGeneratorState extends BaseAppState { if (previewHost == null) return; } + if (pendingRequest != null && captureReady) { + finishCapture(); + return; + } + + if (pendingRequest != null) return; // capture still in progress + SharedInput.PalmGenRequest req = input.palmGenQueue.poll(); - if (req == null) return; + if (req != null) startGeneration(req); + } + + // ── Phase 1: Generierung + Capture-Setup ───────────────────────────────── + + private void startGeneration(SharedInput.PalmGenRequest req) { + cleanupCapture(); PalmOptions opts = req.options(); if (opts.leafTexture != null) { @@ -73,32 +121,291 @@ public class PalmGeneratorState extends BaseAppState { } catch (Exception ignored) {} } - Node palm = PalmMeshBuilder.build(opts); - applyMaterials(palm, opts); - palm.updateGeometricState(); + Node lod0 = PalmMeshBuilder.build(opts); + applyMaterials(lod0, opts); + lod0.updateGeometricState(); - BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null; - float dist = bb != null - ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f - : 20f; - Vector3f target = bb != null - ? new Vector3f(0f, bb.getCenter().y, 0f) - : new Vector3f(0f, 6f, 0f); + BoundingBox bb = lod0.getWorldBound() instanceof BoundingBox b ? b : null; + if (bb == null) bb = new BoundingBox(Vector3f.ZERO, 3f, 8f, 3f); - final float finalDist = dist; - final Vector3f finalTarget = target; - final Node finalPalm = palm; - final PalmOptions finalOpts = req.options(); - final boolean doExport = req.exportAfter(); + float dist = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f; + Vector3f target = new Vector3f(0f, bb.getCenter().y, 0f); - app.enqueue(() -> { - previewHost.setPreviewContent(finalPalm, finalDist, finalTarget); - if (doExport) exportPalm(finalPalm); + previewHost.setPreviewContent(lod0, dist, target); + + String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); + + pendingRequest = req; + pendingLod0 = lod0; + pendingLod1 = buildLod1Palm(opts); + pendingBb = bb; + pendingFileName = "palm_" + timestamp; + capturePass = 0; + capturePixels = new ByteBuffer[ATLAS_DIRS]; + + startCapturePass(0); + input.treeGenStatusMsg = "Palme: Rendere Impostor (1/" + ATLAS_DIRS + ")…"; + } + + private Node buildLod1Palm(PalmOptions opts) { + PalmOptions ld = opts.copy(); + ld.trunkSegments = Math.max(4, opts.trunkSegments / 2); + ld.trunkSections = Math.max(4, opts.trunkSections / 2); + ld.frondCount = Math.max(8, opts.frondCount / 2); + Node n = PalmMeshBuilder.build(ld); + applyMaterials(n, ld); + return n; + } + + // ── Phase 2: Capture ───────────────────────────────────────────────────── + + private void startCapturePass(int pass) { + Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); + captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); + captureFB.addColorTexture(capTex); + captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); + float angle = pass * FastMath.HALF_PI; + captureVP = buildCaptureViewPort(pendingLod0, pendingBb, captureFB, angle); + captureReady = false; + } + + private void finishCapture() { + ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); + app.getRenderer().readFrameBuffer(captureFB, pixels); + capturePixels[capturePass] = pixels; + cleanupCapture(); + + if (capturePass < ATLAS_DIRS - 1) { + capturePass++; + input.treeGenStatusMsg = "Palme: Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…"; + startCapturePass(capturePass); + return; + } + + // Alle Richtungen erfasst + String impostorName = "palm_impostor_" + pendingFileName.substring("palm_".length()); + ByteBuffer atlas = combineAtlas(capturePixels); + Texture2D impTex = saveImpostor(atlas, impostorName, ATLAS_W, ATLAS_H); + + if (pendingRequest.exportAfter()) { + Node lodNode = assembleLodNode(impTex); + exportPalm(lodNode, pendingFileName); + } else { + input.treeGenStatusMsg = "Palme: Vorschau"; + } + + pendingRequest = null; + pendingLod0 = null; + pendingLod1 = null; + pendingBb = null; + pendingFileName = null; + capturePixels = new ByteBuffer[ATLAS_DIRS]; + } + + // ── LOD-Aufbau ──────────────────────────────────────────────────────────── + + private Node assembleLodNode(Texture2D impostorTex) { + Node root = new Node("palm"); + root.attachChild(pendingLod0); + root.attachChild(pendingLod1); + + Node lod2 = makeImpostorNode(pendingBb, impostorTex); + root.attachChild(lod2); + + pendingLod1.setCullHint(Spatial.CullHint.Always); + lod2.setCullHint(Spatial.CullHint.Always); + lod2.setShadowMode(RenderQueue.ShadowMode.Off); + + root.addControl(new PalmLodControl(app.getCamera(), + pendingLod0, pendingLod1, lod2, 50f, 150f)); + return root; + } + + private Node makeImpostorNode(BoundingBox bb, Texture2D tex) { + float h = bb.getYExtent() * 2f; + float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f; + float size = Math.max(h, w); + float yOff = bb.getCenter().y + 2f; + + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + if (tex != null) mat.setTexture("ColorMap", tex); + else mat.setColor("Color", new ColorRGBA(0.22f, 0.65f, 0.14f, 0.9f)); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + + Node n = new Node("lod2"); + for (int d = 0; d < ATLAS_DIRS; d++) { + float uMin = (float) d / ATLAS_DIRS; + float uMax = (float)(d + 1) / ATLAS_DIRS; + n.attachChild(buildBillboardQuad("quad_" + d, d * FastMath.HALF_PI, + yOff, size, mat.clone(), uMin, uMax)); + } + n.setQueueBucket(RenderQueue.Bucket.Transparent); + return n; + } + + private Geometry buildBillboardQuad(String name, float yRot, float yCent, + float size, Material mat, float uMin, float uMax) { + float hw = size * 0.5f; + float hh = size * 0.5f; + float cos = FastMath.cos(yRot); + float sin = FastMath.sin(yRot); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{ + -hw*cos, yCent-hh, -hw*sin, + hw*cos, yCent-hh, hw*sin, + hw*cos, yCent+hh, hw*sin, + -hw*cos, yCent+hh, -hw*sin + }); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ + uMin, 0, uMax, 0, uMax, 1, uMin, 1 + }); + mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0}); + mesh.updateBound(); + + Geometry g = new Geometry(name, mesh); + g.setMaterial(mat); + return g; + } + + // ── Offscreen-ViewPort ──────────────────────────────────────────────────── + + private ViewPort buildCaptureViewPort(Node palmNode, BoundingBox bb, FrameBuffer fb, float angle) { + Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE); + Vector3f center = bb.getCenter().add(0f, 2f, 0f); + float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); + float dist = extent * 3.0f; + + float camX = FastMath.sin(angle) * dist; + float camZ = FastMath.cos(angle) * dist; + cam.setLocation(center.add(camX, 0f, camZ)); + cam.lookAt(center, Vector3f.UNIT_Y); + cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f); + + ViewPort vp = app.getRenderManager() + .createPostView("palmCap_" + System.nanoTime(), cam); + vp.setOutputFrameBuffer(fb); + vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f)); + vp.setClearFlags(true, true, true); + + Node scene = new Node("palmCapScene"); + scene.addLight(new DirectionalLight( + new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), + new ColorRGBA(2.0f, 1.85f, 1.5f, 1f))); + scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f))); + + Node capPalm = cloneForCapture(palmNode); + scene.attachChild(capPalm); + vp.attachScene(scene); + scene.updateGeometricState(); + + vp.addProcessor(new SceneProcessor() { + @Override public void initialize(RenderManager rm, ViewPort v) {} + @Override public void reshape(ViewPort v, int w, int h) {} + @Override public boolean isInitialized() { return true; } + @Override public void preFrame(float t) {} + @Override public void postQueue(RenderQueue rq) {} + @Override public void cleanup() {} + @Override public void setProfiler(AppProfiler profiler) {} + @Override public void postFrame(FrameBuffer out) { + vp.removeProcessor(this); + captureReady = true; + } }); - input.treeGenStatusMsg = doExport ? "Palme: exportiere…" : "Palme: Vorschau"; + return vp; } + private Node cloneForCapture(Node src) { + Node copy = new Node(src.getName() + "_cap"); + copy.setLocalTranslation(src.getLocalTranslation()); + copy.setLocalScale(src.getLocalScale()); + for (Spatial child : src.getChildren()) { + if (child instanceof Geometry g) { + Geometry gc = new Geometry(g.getName() + "_c", g.getMesh()); + gc.setMaterial(g.getMaterial().clone()); + copy.attachChild(gc); + } + } + return copy; + } + + // ── Atlas kombinieren + Impostor speichern ──────────────────────────────── + + private ByteBuffer combineAtlas(ByteBuffer[] passes) { + ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4); + for (int d = 0; d < ATLAS_DIRS; d++) { + ByteBuffer src = passes[d]; + src.rewind(); + for (int y = 0; y < IMPOSTOR_SIZE; y++) { + for (int x = 0; x < IMPOSTOR_SIZE; x++) { + int srcOff = (y * IMPOSTOR_SIZE + x) * 4; + int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4; + atlas.put(dstOff, src.get(srcOff)); + atlas.put(dstOff + 1, src.get(srcOff + 1)); + atlas.put(dstOff + 2, src.get(srcOff + 2)); + atlas.put(dstOff + 3, src.get(srcOff + 3)); + } + } + } + return atlas; + } + + private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) { + try { + pixels.rewind(); + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int r = pixels.get() & 0xFF; + int g = pixels.get() & 0xFF; + int b = pixels.get() & 0xFF; + int a = pixels.get() & 0xFF; + img.setRGB(x, height - 1 - y, (a<<24)|(r<<16)|(g<<8)|b); + } + } + Path texDir = ASSET_ROOT.resolve(ThumbnailRenderer.IMPOSTOR_DIR); + Files.createDirectories(texDir); + File pngFile = texDir.resolve(name + ".png").toFile(); + ImageIO.write(img, "PNG", pngFile); + log.info("[Palme] Impostor: {}", pngFile.getAbsolutePath()); + + try { + return (Texture2D) assets.loadTexture(ThumbnailRenderer.IMPOSTOR_DIR + "/" + name + ".png"); + } catch (Exception loadEx) { + pixels.rewind(); + Image jmeImg = new Image(Image.Format.RGBA8, width, height, + pixels, null, com.jme3.texture.image.ColorSpace.sRGB); + return new Texture2D(jmeImg); + } + } catch (IOException e) { + log.error("[Palme] Impostor-Fehler: {}", e.getMessage()); + return null; + } + } + + // ── Export ──────────────────────────────────────────────────────────────── + + private void exportPalm(Node lodNode, String fileName) { + try { + Path modelDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("palm"); + Files.createDirectories(modelDir); + File out = modelDir.resolve(fileName + ".j3o").toFile(); + while (lodNode.getNumControls() > 0) + lodNode.removeControl(lodNode.getControl(0)); + BinaryExporter.getInstance().save(lodNode, out); + log.info("[Palme] Gespeichert: {}", out.getAbsolutePath()); + input.treeGenStatusMsg = "Gespeichert: Models/trees/palm/" + fileName + ".j3o"; + input.refreshAssets = true; + } catch (IOException e) { + log.error("[Palme] Export-Fehler: {}", e.getMessage()); + input.treeGenStatusMsg = "Palme Export-Fehler: " + e.getMessage(); + } + } + + // ── Material-Helpers ────────────────────────────────────────────────────── + private void applyMaterials(Node palm, PalmOptions opts) { for (Spatial child : palm.getChildren()) { if (!(child instanceof Geometry g)) continue; @@ -187,19 +494,42 @@ public class PalmGeneratorState extends BaseAppState { } } - private void exportPalm(Node palmNode) { - try { - Path modelDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("palm"); - Files.createDirectories(modelDir); - String fileName = "palm_" + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); - File out = modelDir.resolve(fileName + ".j3o").toFile(); - BinaryExporter.getInstance().save(palmNode, out); - log.info("[Palme] Gespeichert: {}", out.getAbsolutePath()); - input.treeGenStatusMsg = "Gespeichert: Models/trees/palm/" + fileName + ".j3o"; - input.refreshAssets = true; - } catch (IOException e) { - log.error("[Palme] Export-Fehler: {}", e.getMessage()); - input.treeGenStatusMsg = "Palme Export-Fehler: " + e.getMessage(); + // ── Aufräumen ───────────────────────────────────────────────────────────── + + private void cleanupCapture() { + if (captureVP != null) { + app.getRenderManager().removePostView(captureVP); + captureVP = null; } + if (captureFB != null) { + try { captureFB.dispose(); } catch (Exception ignored) {} + captureFB = null; + } + captureReady = false; + } + + // ── LOD-Control ─────────────────────────────────────────────────────────── + + private static final class PalmLodControl extends AbstractControl { + private final Camera cam; + private final Node lod0, lod1, lod2; + private final float d01sq, d12sq; + + PalmLodControl(Camera cam, Node l0, Node l1, Node l2, float d01, float d12) { + this.cam = cam; + this.lod0 = l0; this.lod1 = l1; this.lod2 = l2; + this.d01sq = d01 * d01; + this.d12sq = d12 * d12; + } + + @Override + protected void controlUpdate(float tpf) { + float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation()); + lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + lod1.setCullHint(dSq>=d01sq && dSq= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + + @Override protected void controlRender(RenderManager rm, ViewPort vp) {} } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java index 5957b88..627b3f0 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java @@ -24,6 +24,9 @@ import de.blight.common.GrassTuftIO; import de.blight.common.MapData; import de.blight.editor.SharedInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.*; @@ -38,6 +41,8 @@ import java.util.*; */ public class PlacedObjectState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(PlacedObjectState.class); + // ── Terrain ─────────────────────────────────────────────────────────────── private static final int TERRAIN_HALF = 2048; @@ -100,7 +105,7 @@ public class PlacedObjectState extends BaseAppState { } } } catch (Exception e) { - System.err.println("[PlacedObjectState] Grasdaten nicht ladbar: " + e.getMessage()); + log.warn("[PlacedObjectState] Grasdaten nicht ladbar: {}", e.getMessage()); } if (loadedData != null) { @@ -161,6 +166,17 @@ public class PlacedObjectState extends BaseAppState { } processGrassEdits(); rebuildDirtyChunks(); + + de.blight.game.state.DayNightState dns = + getApplication().getStateManager().getState(de.blight.game.state.DayNightState.class); + if (dns != null) { + for (Material mat : slotMaterials.values()) { + if ("Grass".equals(mat.getMaterialDef().getName())) { + mat.setVector3("SunDir", dns.getSunDirection().negate()); + mat.setColor("SunColor", dns.getSunLight().getColor()); + } + } + } } // ── Materialien ─────────────────────────────────────────────────────────── @@ -175,6 +191,8 @@ public class PlacedObjectState extends BaseAppState { mat.setColor("Color", new ColorRGBA(0.25f, 0.70f, 0.15f, 1f)); mat.setFloat("WindSpeed", 0.5f); mat.setFloat("WindStrength", 0.12f); + mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f)); + mat.setColor("SunColor", ColorRGBA.White); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); return mat; } catch (Exception e) { @@ -187,18 +205,21 @@ public class PlacedObjectState extends BaseAppState { private void applyAllSlotMaterials() { if (assetManager == null) return; - applyTexToMat(getMaterialForSlot(0), input.grassTexturePath); + String[] nm = input.grassNormalMapPaths; + applyTexToMat(getMaterialForSlot(0), input.grassTexturePath, + nm.length > 0 ? nm[0] : ""); String[] slots = input.grassTextureSlots; for (int i = 0; i < slots.length; i++) { if (slots[i] != null && !slots[i].isEmpty()) - applyTexToMat(getMaterialForSlot(i + 1), slots[i]); + applyTexToMat(getMaterialForSlot(i + 1), slots[i], + nm.length > i + 1 ? nm[i + 1] : ""); } } - private void applyTexToMat(Material mat, String path) { - if (path != null && !path.isEmpty()) { + private void applyTexToMat(Material mat, String colorPath, String normalPath) { + if (colorPath != null && !colorPath.isEmpty()) { try { - mat.setTexture("ColorMap", assetManager.loadTexture(path)); + mat.setTexture("ColorMap", assetManager.loadTexture(colorPath)); mat.setColor("Color", ColorRGBA.White); } catch (Exception e) { try { mat.clearParam("ColorMap"); } catch (Exception ignored) {} @@ -208,6 +229,17 @@ public class PlacedObjectState extends BaseAppState { try { mat.clearParam("ColorMap"); } catch (Exception ignored) {} mat.setColor("Color", new ColorRGBA(0.25f, 0.70f, 0.15f, 1f)); } + if (normalPath != null && !normalPath.isEmpty()) { + try { + com.jme3.texture.Texture nTex = assetManager.loadTexture(normalPath); + nTex.setMagFilter(com.jme3.texture.Texture.MagFilter.Bilinear); + mat.setTexture("NormalMap", nTex); + } catch (Exception e) { + try { mat.clearParam("NormalMap"); } catch (Exception ignored) {} + } + } else { + try { mat.clearParam("NormalMap"); } catch (Exception ignored) {} + } } // ── Pinsel-Interaktion ──────────────────────────────────────────────────── @@ -321,11 +353,14 @@ public class PlacedObjectState extends BaseAppState { float chunkCX = wXMin + CHUNK_SIZE * 0.5f; float chunkCZ = wZMin + CHUNK_SIZE * 0.5f; Node node = new Node("grassChunk_" + idx); + float[] ars = input.grassAspectRatios; for (Map.Entry> entry : bySlot.entrySet()) { if (entry.getValue().isEmpty()) continue; - Geometry geo = new Geometry("grassChunk_" + idx + "_s" + entry.getKey(), - buildGrassMesh(entry.getValue())); - geo.setMaterial(getMaterialForSlot(entry.getKey())); + int slotKey = entry.getKey(); + float ar = (ars != null && slotKey < ars.length) ? ars[slotKey] : 1f; + Geometry geo = new Geometry("grassChunk_" + idx + "_s" + slotKey, + buildGrassMesh(entry.getValue(), ar)); + geo.setMaterial(getMaterialForSlot(slotKey)); node.attachChild(geo); } if (node.getChildren().isEmpty()) return; @@ -336,23 +371,30 @@ public class PlacedObjectState extends BaseAppState { // ── Mesh ────────────────────────────────────────────────────────────────── - private static Mesh buildGrassMesh(List blades) { + private static Mesh buildGrassMesh(List blades, float ar) { int n = blades.size(); FloatBuffer pos = BufferUtils.createFloatBuffer(n * 8 * 3); FloatBuffer uv = BufferUtils.createFloatBuffer(n * 8 * 2); + FloatBuffer nor = BufferUtils.createFloatBuffer(n * 8 * 3); + FloatBuffer tan = BufferUtils.createFloatBuffer(n * 8 * 4); IntBuffer idx = BufferUtils.createIntBuffer(n * 12); int vi = 0; for (float[] blade : blades) { float x = blade[0], y = blade[1], z = blade[2], h = blade[3]; - float w = Math.max(0.05f, h * BLADE_WIDTH); + // Klingenbreite: Hälfte der Gesamtbreite (2w × h = Quad-Seitenverhältnis ar) + float w = Math.max(0.05f, h * ar * 0.5f); + // Quad 1: liegt in XY-Ebene (konstantes z) – Normal zeigt +Z, Tangent zeigt +X pos.put(x-w).put(y ).put(z); uv.put(0).put(0); pos.put(x+w).put(y ).put(z); uv.put(1).put(0); pos.put(x+w).put(y+h).put(z); uv.put(1).put(1); pos.put(x-w).put(y+h).put(z); uv.put(0).put(1); + for (int i = 0; i < 4; i++) { nor.put(0).put(0).put(1); tan.put(1).put(0).put(0).put(1); } + // Quad 2: liegt in ZY-Ebene (konstantes x) – Normal zeigt -X, Tangent zeigt +Z pos.put(x).put(y ).put(z-w); uv.put(0).put(0); pos.put(x).put(y ).put(z+w); uv.put(1).put(0); pos.put(x).put(y+h).put(z+w); uv.put(1).put(1); pos.put(x).put(y+h).put(z-w); uv.put(0).put(1); + for (int i = 0; i < 4; i++) { nor.put(-1).put(0).put(0); tan.put(0).put(0).put(1).put(1); } idx.put(vi).put(vi+1).put(vi+2); idx.put(vi).put(vi+2).put(vi+3); idx.put(vi+4).put(vi+5).put(vi+6); @@ -362,6 +404,8 @@ public class PlacedObjectState extends BaseAppState { Mesh mesh = new Mesh(); mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, nor); + mesh.setBuffer(VertexBuffer.Type.Tangent, 4, tan); mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); mesh.updateBound(); return mesh; diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index 4cc89f6..6adc5ae 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -28,6 +28,9 @@ import de.blight.common.PlacedModel; import de.blight.editor.SharedInput; import de.blight.editor.object.SceneObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -47,6 +50,8 @@ import java.util.List; */ public class SceneObjectState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(SceneObjectState.class); + private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); private static final Path BLIGHT_ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); @@ -283,6 +288,12 @@ public class SceneObjectState extends BaseAppState { } updatePreview(); + + if (input.reloadPlacedModels) { + input.reloadPlacedModels = false; + try { loadPlacedModels(de.blight.common.PlacedModelIO.load()); } catch (Exception ignored) {} + } + boolean isObjectLayer = input.activeLayer == SharedInput.LAYER_OBJECTS || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT; if (!isObjectLayer) return; @@ -317,11 +328,6 @@ public class SceneObjectState extends BaseAppState { deleteSelected(); } - if (input.reloadPlacedModels) { - input.reloadPlacedModels = false; - try { loadPlacedModels(de.blight.common.PlacedModelIO.load()); } catch (Exception ignored) {} - } - // Zusammenfassen if (input.mergeSelectedRequested) { input.mergeSelectedRequested = false; @@ -605,6 +611,54 @@ public class SceneObjectState extends BaseAppState { selectObject(objects.size() - 1, false); input.objectJustPlaced = true; setStatus("Platziert: " + modelPath); + + // Anhänge aus ModelMeta: Lichter + Emitter mitplatzieren + if (meta != null) { + placeAttachedLights(meta, wx, wy + placementOffY, wz); + placeAttachedEmitters(meta, wx, wy + placementOffY, wz); + } + } + + private void placeAttachedLights(de.blight.common.ModelMeta meta, + float wx, float wy, float wz) { + if (meta.attachedLights().isEmpty()) return; + try { + java.util.List list = + new java.util.ArrayList<>(de.blight.common.LightIO.load()); + for (de.blight.common.ModelMeta.AttachedLight al : meta.attachedLights()) { + list.add(new de.blight.common.PlacedLight( + wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ(), + al.r(), al.g(), al.b(), al.intensity(), al.radius())); + } + de.blight.common.LightIO.save(list); + input.reloadPlacedOther = true; + } catch (java.io.IOException e) { + log.error("[SceneObject] Anhang-Licht speichern fehlgeschlagen: {}", e.getMessage()); + } + } + + private void placeAttachedEmitters(de.blight.common.ModelMeta meta, + float wx, float wy, float wz) { + if (meta.attachedEmitters().isEmpty()) return; + try { + java.util.List list = + new java.util.ArrayList<>(de.blight.common.EmitterIO.load()); + for (de.blight.common.ModelMeta.AttachedEmitter ae : meta.attachedEmitters()) { + float ax = wx + ae.offsetX(); + float ay = wy + ae.offsetY(); + float az = wz + ae.offsetZ(); + de.blight.common.PlacedEmitter pe = switch (ae.preset()) { + case 1 -> de.blight.common.PlacedEmitter.smoke(ax, ay, az); + case 2 -> de.blight.common.PlacedEmitter.sparks(ax, ay, az); + default -> de.blight.common.PlacedEmitter.fire(ax, ay, az); + }; + list.add(pe); + } + de.blight.common.EmitterIO.save(list); + input.reloadPlacedOther = true; + } catch (java.io.IOException e) { + log.error("[SceneObject] Anhang-Emitter speichern fehlgeschlagen: {}", e.getMessage()); + } } private Node loadModelNode(String modelPath, float wx, float wy, float wz) { @@ -1261,7 +1315,7 @@ public class SceneObjectState extends BaseAppState { BinaryExporter.getInstance().save(model, req.destJ3o().toFile()); } } catch (Exception thumbEx) { - System.err.println("[SceneObject] Thumbnail-Fehler: " + thumbEx.getMessage()); + log.warn("[SceneObject] Thumbnail-Fehler: {}", thumbEx.getMessage()); Files.createDirectories(req.destJ3o().getParent()); BinaryExporter.getInstance().save(model, req.destJ3o().toFile()); } @@ -1271,7 +1325,7 @@ public class SceneObjectState extends BaseAppState { input.refreshAssets = true; } catch (Exception e) { setStatus("Konvertierung fehlgeschlagen (" + req.assetPath() + "): " + e.getMessage()); - System.err.println("[SceneObject] Konvertierung fehlgeschlagen: " + e.getMessage()); + log.error("[SceneObject] Konvertierung fehlgeschlagen: {}", e.getMessage()); } } @@ -1291,6 +1345,10 @@ public class SceneObjectState extends BaseAppState { q.fromAngles(prop.rotX(), prop.rotY(), prop.rotZ()); node.setLocalRotation(q); + float sc = Math.max(0.001f, prop.scale()); + so.setScale(sc); + node.setLocalScale(sc); + so.solid = prop.solid(); so.castShadow = prop.castShadow(); so.receiveShadow = prop.receiveShadow(); 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 a26f34f..0d4c9f5 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 @@ -94,7 +94,6 @@ public class TerrainEditorState extends BaseAppState { private TerrainQuad terrain; private float[] cachedHeightMap; // Einmal geladen, danach manuell synchron gehalten private Geometry brushIndicator; - private Geometry livePlayerMarker; private PlacedObjectState placedObjectState; private GrassVertexState grassVertexState; private SceneObjectState sceneObjState; @@ -110,6 +109,7 @@ public class TerrainEditorState extends BaseAppState { private Node axesGizmo; private boolean wireframeMode = false; private boolean topologyMode = false; + private boolean debugNoLight = false; private final java.util.concurrent.ExecutorService saveExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "blight-save"); @@ -130,6 +130,12 @@ public class TerrainEditorState extends BaseAppState { private Image upperSplatImage; private Texture2D upperSplatTex; + // ── Dritte Splatmap (Slots 9-12, AlphaMap_2) ────────────────────────────── + private byte[] thirdSplatR, thirdSplatG, thirdSplatB, thirdSplatA; + private ByteBuffer thirdSplatBuf; + private Image thirdSplatImage; + private Texture2D thirdSplatTex; + // ── Kameraposition ──────────────────────────────────────────────────────── private static final Path EDITOR_PREFS = BlightHome.resolve("config", "editor.prefs"); private static final float DEFAULT_CAM_Y = 50f; @@ -190,8 +196,10 @@ public class TerrainEditorState extends BaseAppState { } private static float parsePref(Properties p, String key, float def) { - try { return Float.parseFloat(p.getProperty(key, String.valueOf(def))); } - catch (NumberFormatException e) { return def; } + try { + float v = Float.parseFloat(p.getProperty(key, String.valueOf(def))); + return Float.isFinite(v) ? v : def; + } catch (NumberFormatException e) { return def; } } @Override @@ -336,9 +344,6 @@ public class TerrainEditorState extends BaseAppState { brushIndicator = buildBrushIndicator(); rootNode.attachChild(brushIndicator); - livePlayerMarker = buildLivePlayerMarker(); - rootNode.attachChild(livePlayerMarker); - axesGizmo = buildAxesGizmo(); rootNode.attachChild(axesGizmo); } @@ -422,6 +427,23 @@ public class TerrainEditorState extends BaseAppState { input.upperTexturePaths, 0, MapData.TEXTURE_SLOTS); System.arraycopy(loadedMapData.upperNormalMaps, 0, input.upperNormalMapPaths, 0, MapData.TEXTURE_SLOTS); + // Dritte Splatmap (Slots 9-12) + thirdSplatR = loadedMapData.thirdSplatR.clone(); + thirdSplatG = loadedMapData.thirdSplatG.clone(); + thirdSplatB = loadedMapData.thirdSplatB.clone(); + thirdSplatA = loadedMapData.thirdSplatA.clone(); + System.arraycopy(loadedMapData.thirdTextures, 0, + input.thirdTexturePaths, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(loadedMapData.thirdNormalMaps, 0, + input.thirdNormalMapPaths, 0, MapData.TEXTURE_SLOTS); + if (loadedMapData.normalMapOrder != null && loadedMapData.normalMapOrder.length == 12) + input.normalMapOrder = loadedMapData.normalMapOrder.clone(); + if (loadedMapData.diffuseScales != null && loadedMapData.diffuseScales.length == 12) + input.diffuseScales = loadedMapData.diffuseScales.clone(); + input.voxelFlatSlot = loadedMapData.voxelFlatSlot; + input.voxelSteepSlot = loadedMapData.voxelSteepSlot; + input.voxelCeilSlot = loadedMapData.voxelCeilSlot; + input.voxelTexturesChanged = true; // Alte Gebirge-Splatmap-Migration: R=255 überall war der Gebirge-Standard. // Im neuen 1-Terrain-System bedeutet das: Slot-5-Textur deckt alles ab → auf 0 setzen. boolean upperRAllMax = true; @@ -437,6 +459,10 @@ public class TerrainEditorState extends BaseAppState { upperSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE]; upperSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE]; upperSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatR = new byte[SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatG = new byte[SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatB = new byte[SPLAT_SIZE * SPLAT_SIZE]; + thirdSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE]; } // A-Kanal aus alten Saves bereinigen (war für Fluss-Textur; nicht mehr verwendet) @@ -463,6 +489,17 @@ public class TerrainEditorState extends BaseAppState { upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp); upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); upperSplatTex.setMagFilter(Texture.MagFilter.Bilinear); + + thirdSplatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4); + for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) { + thirdSplatBuf.put(thirdSplatR[i]).put(thirdSplatG[i]).put(thirdSplatB[i]).put(thirdSplatA[i]); + } + thirdSplatBuf.flip(); + thirdSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, thirdSplatBuf); + thirdSplatTex = new Texture2D(thirdSplatImage); + thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp); + thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear); } // Standard-Texturpfade (Fallback wenn kein benutzerdefinierter Pfad gesetzt) @@ -483,86 +520,193 @@ public class TerrainEditorState extends BaseAppState { new ColorRGBA(0.55f, 0.45f, 0.30f, 1f), new ColorRGBA(0.80f, 0.72f, 0.50f, 1f), }; + /** Zielgröße aller Texture-Array-Layer in Pixeln. Erhöhen für schärfere Nahaufnahmen. */ + private static final int TERRAIN_ARRAY_SIZE = 1024; + private Material buildTerrainMaterial() { - Material mat = new Material(assets, "Common/MatDefs/Terrain/TerrainLighting.j3md"); - mat.setBoolean("useTriPlanarMapping", false); - mat.setFloat("Shininess", 0f); - - String[] paths = input.terrainTexturePaths; - String[] matParams = {"DiffuseMap", "DiffuseMap_1", "DiffuseMap_2", "DiffuseMap_3"}; - String[] scaleParams = {"DiffuseMap_0_scale", "DiffuseMap_1_scale", "DiffuseMap_2_scale", "DiffuseMap_3_scale"}; + Material mat = new Material(assets, "MatDefs/TerrainArray.j3md"); + // ── Diffuse-Array (12 Slots, Slot-Index 0-11) ───────────────────────── + String[] diffPaths = new String[12]; for (int i = 0; i < 4; i++) { - String path = (paths[i] != null && !paths[i].isEmpty()) ? paths[i] : DEFAULT_TERRAIN_TEXTURES[i]; - if (path == null || path.isEmpty()) continue; // Slot 4 ohne Textur überspringen - Texture tex = loadOrFallback(path, DEFAULT_TERRAIN_COLORS[i]); - tex.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(matParams[i], tex); - mat.setFloat(scaleParams[i], 512f); - } - - String[] norms = input.terrainNormalMapPaths; - String[] nmParams = {"NormalMap", "NormalMap_1", "NormalMap_2", "NormalMap_3"}; - for (int i = 0; i < 4; i++) { - if (norms[i] != null && !norms[i].isEmpty()) { - try { - Texture n = assets.loadTexture(norms[i]); - n.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(nmParams[i], n); - } catch (Exception e) { - mat.clearParam(nmParams[i]); - } - } else { - mat.clearParam(nmParams[i]); - } + String p = input.terrainTexturePaths[i]; + diffPaths[i] = (p != null && !p.isEmpty()) ? p + : (i < DEFAULT_TERRAIN_TEXTURES.length ? DEFAULT_TERRAIN_TEXTURES[i] : ""); + } + System.arraycopy(input.upperTexturePaths, 0, diffPaths, 4, 4); + System.arraycopy(input.thirdTexturePaths, 0, diffPaths, 8, 4); + + ColorRGBA[] diffFallbacks = new ColorRGBA[12]; + for (int i = 0; i < 4; i++) diffFallbacks[i] = DEFAULT_TERRAIN_COLORS[i]; + for (int i = 0; i < 4; i++) diffFallbacks[4+i] = DEFAULT_UPPER_COLORS[i]; + for (int i = 0; i < 4; i++) diffFallbacks[8+i] = DEFAULT_UPPER_COLORS[i]; + + com.jme3.texture.TextureArray diffArr = buildTextureArray(diffPaths, diffFallbacks); + mat.setParam("DiffuseArray", com.jme3.shader.VarType.TextureArray, diffArr); + mat.setParam("DiffuseScales", com.jme3.shader.VarType.FloatArray, input.diffuseScales.clone()); + + // ── Normal-Array (alle 12 Slots; nur setzen wenn mindestens eine Normal-Map vorhanden) ── + String[] normPaths = new String[12]; + System.arraycopy(input.terrainNormalMapPaths, 0, normPaths, 0, 4); + System.arraycopy(input.upperNormalMapPaths, 0, normPaths, 4, 4); + System.arraycopy(input.thirdNormalMapPaths, 0, normPaths, 8, 4); + boolean hasNormal = false; + for (String p : normPaths) if (p != null && !p.isEmpty()) { hasNormal = true; break; } + if (hasNormal) { + ColorRGBA[] flatNorm = new ColorRGBA[12]; + Arrays.fill(flatNorm, new ColorRGBA(0.5f, 0.5f, 1f, 1f)); // Tangent-Space "kein Einfluss" + mat.setParam("NormalArray", com.jme3.shader.VarType.TextureArray, + buildTextureArray(normPaths, flatNorm)); + } else { + mat.clearParam("NormalArray"); } + // ── AlphaMaps ───────────────────────────────────────────────────────── mat.setTexture("AlphaMap", splatTex); - if (wireframeMode) mat.getAdditionalRenderState().setWireframe(true); - - // Zweite Splatmap → Slots 5-8 (AlphaMap_1), nur wenn Texturen konfiguriert boolean hasUpperTex = false; for (String s : input.upperTexturePaths) if (s != null && !s.isEmpty()) { hasUpperTex = true; break; } - if (upperSplatTex != null && hasUpperTex) { - mat.setTexture("AlphaMap_1", upperSplatTex); - String[] upperPaths = input.upperTexturePaths; - String[] upperNorms = input.upperNormalMapPaths; - String[] upperMatP = {"DiffuseMap_4","DiffuseMap_5","DiffuseMap_6","DiffuseMap_7"}; - String[] upperScaleP = {"DiffuseMap_4_scale","DiffuseMap_5_scale","DiffuseMap_6_scale","DiffuseMap_7_scale"}; - String[] upperNormP = {"NormalMap_4","NormalMap_5","NormalMap_6","NormalMap_7"}; - for (int i = 0; i < 4; i++) { - String path = upperPaths[i]; - if (path == null || path.isEmpty()) continue; - Texture tex = loadOrFallback(path, DEFAULT_UPPER_COLORS[i]); - tex.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(upperMatP[i], tex); - mat.setFloat(upperScaleP[i], 512f); - if (upperNorms[i] != null && !upperNorms[i].isEmpty()) { - try { - Texture n = assets.loadTexture(upperNorms[i]); - n.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(upperNormP[i], n); - } catch (Exception e) { mat.clearParam(upperNormP[i]); } - } else { - mat.clearParam(upperNormP[i]); - } - } + boolean hasThirdTex = false; + for (String s : input.thirdTexturePaths) if (s != null && !s.isEmpty()) { hasThirdTex = true; break; } + if (hasUpperTex && upperSplatTex != null) mat.setTexture("AlphaMap_1", upperSplatTex); + if (hasThirdTex && thirdSplatTex != null) mat.setTexture("AlphaMap_2", thirdSplatTex); + + // Lichtrichtung und -farben mit DayNightState synchronisieren + if (dayNightState != null && dayNightState.getSunLight() != null) { + mat.setVector3("LightDir", dayNightState.getSunDirection().negate()); + com.jme3.math.ColorRGBA sc = dayNightState.getSunLight().getColor(); + com.jme3.math.ColorRGBA ac = dayNightState.getAmbientLight().getColor(); + mat.setVector3("SunColor", new com.jme3.math.Vector3f(sc.r, sc.g, sc.b)); + mat.setVector3("AmbientColor", new com.jme3.math.Vector3f(ac.r, ac.g, ac.b)); } + + if (wireframeMode) mat.getAdditionalRenderState().setWireframe(true); return mat; } - private Texture loadOrFallback(String path, ColorRGBA color) { - try { - return assets.loadTexture(path); - } catch (Exception e) { - ByteBuffer buf = BufferUtils.createByteBuffer(4); - buf.put((byte)(color.r * 255)); - buf.put((byte)(color.g * 255)); - buf.put((byte)(color.b * 255)); - buf.put((byte)(color.a * 255)); - buf.flip(); - return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf)); + /** + * Baut ein TextureArray mit paths.length Layern aus den angegebenen Asset-Pfaden. + * Leere/fehlende Pfade werden durch die entsprechende Fallback-Farbe ersetzt. + * Alle Layer werden auf TERRAIN_ARRAY_SIZE × TERRAIN_ARRAY_SIZE skaliert. + */ + private com.jme3.texture.TextureArray buildTextureArray(String[] paths, ColorRGBA[] fallbacks) { + int size = TERRAIN_ARRAY_SIZE; + java.util.List images = new java.util.ArrayList<>(paths.length); + for (int i = 0; i < paths.length; i++) { + String path = paths[i]; + ColorRGBA fb = (fallbacks != null && i < fallbacks.length && fallbacks[i] != null) + ? fallbacks[i] : new ColorRGBA(0.5f, 0.5f, 0.5f, 1f); + if (path != null && !path.isEmpty()) { + try { + ByteBuffer buf = loadTextureToRGBA8(path, size); + images.add(new Image(Image.Format.RGBA8, size, size, buf)); + continue; + } catch (Exception e) { + log.warn("[TerrainArray] Slot {} nicht ladbar ({}): {}", i, path, e.getMessage()); + } + } + images.add(new Image(Image.Format.RGBA8, size, size, solidColorLayer(fb, size))); } + + com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images); + texArr.setWrap(Texture.WrapMode.Repeat); + texArr.setMinFilter(Texture.MinFilter.Trilinear); + texArr.setMagFilter(Texture.MagFilter.Bilinear); + return texArr; + } + + /** Lädt eine Textur via AssetManager und konvertiert sie in einen RGBA8-ByteBuffer der Größe size×size. */ + private ByteBuffer loadTextureToRGBA8(String path, int targetSize) throws Exception { + Texture tex = assets.loadTexture(path); + Image src = tex.getImage(); + int sw = src.getWidth(); + int sh = src.getHeight(); + + java.awt.image.BufferedImage bimg = jmeImageToBufferedImage(src, sw, sh); + + java.awt.image.BufferedImage scaled; + if (sw == targetSize && sh == targetSize) { + scaled = bimg; + } else { + scaled = new java.awt.image.BufferedImage(targetSize, targetSize, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g2 = scaled.createGraphics(); + g2.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, + java.awt.RenderingHints.VALUE_RENDER_QUALITY); + g2.drawImage(bimg, 0, 0, targetSize, targetSize, null); + g2.dispose(); + } + + ByteBuffer buf = BufferUtils.createByteBuffer(targetSize * targetSize * 4); + for (int y = 0; y < targetSize; y++) { + for (int x = 0; x < targetSize; x++) { + int argb = scaled.getRGB(x, y); + buf.put((byte)((argb >> 16) & 0xFF)); + buf.put((byte)((argb >> 8) & 0xFF)); + buf.put((byte)( argb & 0xFF)); + buf.put((byte)((argb >> 24) & 0xFF)); + } + } + buf.flip(); + return buf; + } + + /** Konvertiert ein JME3-Image in ein AWT BufferedImage für die häufigsten Formate. */ + private java.awt.image.BufferedImage jmeImageToBufferedImage(Image src, int w, int h) { + java.awt.image.BufferedImage bimg = new java.awt.image.BufferedImage(w, h, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + ByteBuffer data = src.getData(0).duplicate(); + data.rewind(); + switch (src.getFormat()) { + case RGBA8 -> { + for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { + int r = data.get() & 0xFF, g = data.get() & 0xFF, + b = data.get() & 0xFF, a = data.get() & 0xFF; + bimg.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); + } + } + case RGB8 -> { + for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { + int r = data.get() & 0xFF, g = data.get() & 0xFF, b = data.get() & 0xFF; + bimg.setRGB(x, y, (0xFF << 24) | (r << 16) | (g << 8) | b); + } + } + case BGR8 -> { + for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { + int b = data.get() & 0xFF, g = data.get() & 0xFF, r = data.get() & 0xFF; + bimg.setRGB(x, y, (0xFF << 24) | (r << 16) | (g << 8) | b); + } + } + case ABGR8 -> { + for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { + int a = data.get() & 0xFF, b = data.get() & 0xFF, + g = data.get() & 0xFF, r = data.get() & 0xFF; + bimg.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); + } + } + default -> { + // Fallback: ImageRaster (langsamer, aber universell) + com.jme3.texture.image.ImageRaster raster = com.jme3.texture.image.ImageRaster.create(src); + com.jme3.math.ColorRGBA pixel = new com.jme3.math.ColorRGBA(); + for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { + raster.getPixel(x, y, pixel); + bimg.setRGB(x, y, ((int)(pixel.a * 255) << 24) | ((int)(pixel.r * 255) << 16) + | ((int)(pixel.g * 255) << 8) | (int)(pixel.b * 255)); + } + } + } + return bimg; + } + + /** Einfarbiger Fallback-Layer für leere Slots. */ + private ByteBuffer solidColorLayer(ColorRGBA color, int size) { + ByteBuffer buf = BufferUtils.createByteBuffer(size * size * 4); + byte r = (byte)(color.r * 255), g = (byte)(color.g * 255), + b = (byte)(color.b * 255), a = (byte)(color.a * 255); + for (int p = 0; p < size * size; p++) buf.put(r).put(g).put(b).put(a); + buf.flip(); + return buf; } // ── Splatmap malen ──────────────────────────────────────────────────────── @@ -692,6 +836,71 @@ public class TerrainEditorState extends BaseAppState { } } + /** + * Malt dritte Gruppe (Slots 9-12) auf die dritte Splatmap. + * @param textureIndex 0=Slot9(R), 1=Slot10(G), 2=Slot11(B), 3=Slot12(A), -1=Löschen(alle→0) + */ + private void applyThirdTexturePaint(Vector3f contact, float strength, int textureIndex) { + float radius = (float) input.textureTool.brushRadius.getValue(); + + 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(radius / 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 >= radius) continue; + + float t = distWE / radius; + float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f; + int idx = pz * SPLAT_SIZE + px; + float blend = strength * falloff; + + if (textureIndex < 0) { + float curR = (thirdSplatR[idx] & 0xFF) / 255f; + float curG = (thirdSplatG[idx] & 0xFF) / 255f; + float curB = (thirdSplatB[idx] & 0xFF) / 255f; + float curA = (thirdSplatA[idx] & 0xFF) / 255f; + thirdSplatR[idx] = (byte) Math.round(curR * (1f - falloff) * 255f); + thirdSplatG[idx] = (byte) Math.round(curG * (1f - falloff) * 255f); + thirdSplatB[idx] = (byte) Math.round(curB * (1f - falloff) * 255f); + thirdSplatA[idx] = (byte) Math.round(curA * (1f - falloff) * 255f); + } else { + float curR = (thirdSplatR[idx] & 0xFF) / 255f; + float curG = (thirdSplatG[idx] & 0xFF) / 255f; + float curB = (thirdSplatB[idx] & 0xFF) / 255f; + float curA = (thirdSplatA[idx] & 0xFF) / 255f; + float tgR = (textureIndex == 0) ? 1f : 0f; + float tgG = (textureIndex == 1) ? 1f : 0f; + float tgB = (textureIndex == 2) ? 1f : 0f; + float tgA = (textureIndex == 3) ? 1f : 0f; + thirdSplatR[idx] = (byte) Math.round((curR + (tgR - curR) * blend) * 255f); + thirdSplatG[idx] = (byte) Math.round((curG + (tgG - curG) * blend) * 255f); + thirdSplatB[idx] = (byte) Math.round((curB + (tgB - curB) * blend) * 255f); + thirdSplatA[idx] = (byte) Math.round((curA + (tgA - curA) * blend) * 255f); + } + + int bi = idx * 4; + thirdSplatBuf.put(bi, thirdSplatR[idx]); + thirdSplatBuf.put(bi + 1, thirdSplatG[idx]); + thirdSplatBuf.put(bi + 2, thirdSplatB[idx]); + thirdSplatBuf.put(bi + 3, thirdSplatA[idx]); + changed = true; + } + } + if (changed) { + thirdSplatBuf.rewind(); + thirdSplatImage.setUpdateNeeded(); + } + } + // ── Update-Schleife ─────────────────────────────────────────────────────── @Override @@ -717,7 +926,14 @@ public class TerrainEditorState extends BaseAppState { processTextureEdits(); updateBrushIndicator(); updateAxesGizmo(); - updateLivePlayerMarker(); + // Debug: Strg+F8 — Raw-Texture-Modus (kein Lighting) ein/aus + if (input.debugNoLightToggle) { + input.debugNoLightToggle = false; + debugNoLight = !debugNoLight; + if (terrain != null) + terrain.getMaterial().setBoolean("DebugNoLight", debugNoLight); + log.debug("[Debug] DebugNoLight = {}", debugNoLight); + } // Wireframe-Modus setzen int wfReq = input.wireframeRequest; @@ -736,13 +952,21 @@ public class TerrainEditorState extends BaseAppState { setTopologyOverlay(topologyMode); } - // Terrain-Material neu aufbauen wenn Texturen (Slots 1-8) oder Normal-Maps geändert - if (input.terrainTexturesChanged || input.terrainNormalMapsChanged - || input.upperTexturesChanged || input.upperNormalMapsChanged) { + // Terrain-Material neu aufbauen wenn Texturen (Slots 1-12), Normal-Maps + // oder UV-Scales geändert wurden + if (input.terrainTexturesChanged || input.terrainNormalMapsChanged + || input.upperTexturesChanged || input.upperNormalMapsChanged + || input.thirdTexturesChanged || input.thirdNormalMapsChanged + || input.normalMapOrderChanged + || input.diffuseScalesChanged) { input.terrainTexturesChanged = false; input.terrainNormalMapsChanged = false; input.upperTexturesChanged = false; input.upperNormalMapsChanged = false; + input.thirdTexturesChanged = false; + input.thirdNormalMapsChanged = false; + input.normalMapOrderChanged = false; + input.diffuseScalesChanged = false; terrain.setMaterial(buildTerrainMaterial()); } @@ -829,7 +1053,9 @@ public class TerrainEditorState extends BaseAppState { if (edit.action() > 0) { // Linksklick: Slot malen - if (selIdx >= 4) { + 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),... } else { applyTexturePaint(contact, str, selIdx); @@ -838,6 +1064,7 @@ public class TerrainEditorState extends BaseAppState { // 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 } } } @@ -852,6 +1079,19 @@ public class TerrainEditorState extends BaseAppState { return h != null ? h : 0f; } + /** + * Schneller O(1)-Höhenzugriff direkt aus dem gecachten Heightmap-Array. + * Kein Quadtree-Traversal – geeignet für massenweisen Aufruf aus dem Voxel-Tool. + */ + public float getTerrainHeightFast(float worldX, float worldZ) { + if (cachedHeightMap == null) return 0f; + int vx = Math.round(worldX + TERRAIN_SIZE * 0.5f); + int vz = Math.round(worldZ + TERRAIN_SIZE * 0.5f); + vx = Math.max(0, Math.min(TOTAL_SIZE - 1, vx)); + vz = Math.max(0, Math.min(TOTAL_SIZE - 1, vz)); + return cachedHeightMap[vz * TOTAL_SIZE + vx]; + } + // ── Speichern ───────────────────────────────────────────────────────────── @@ -872,10 +1112,20 @@ public class TerrainEditorState extends BaseAppState { final byte[] upG = upperSplatG != null ? upperSplatG.clone() : null; final byte[] upB = upperSplatB != null ? upperSplatB.clone() : null; final byte[] upA = upperSplatA != null ? upperSplatA.clone() : null; + final byte[] thR = thirdSplatR != null ? thirdSplatR.clone() : null; + 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 String[] texPaths = input.terrainTexturePaths.clone(); final String[] normalPaths = input.terrainNormalMapPaths.clone(); final String[] upperPaths = input.upperTexturePaths.clone(); final String[] upperNormPaths = input.upperNormalMapPaths.clone(); + final String[] thirdPaths = input.thirdTexturePaths.clone(); + final String[] thirdNormPaths = input.thirdNormalMapPaths.clone(); + final float[] diffScaleSnap = input.diffuseScales.clone(); + final int voxelFlatSlot = input.voxelFlatSlot; + final int voxelSteepSlot = input.voxelSteepSlot; + final int voxelCeilSlot = input.voxelCeilSlot; final GrassTuftIO.GrassData grassData = placedObjectState != null ? new GrassTuftIO.GrassData(placedObjectState.getSlotPaths(), placedObjectState.getAllTufts()) @@ -930,6 +1180,19 @@ public class TerrainEditorState extends BaseAppState { System.arraycopy(upperPaths, 0, data.upperTextures, 0, MapData.TEXTURE_SLOTS); System.arraycopy(upperNormPaths, 0, data.upperNormalMaps, 0, MapData.TEXTURE_SLOTS); } + if (thR != null) { + System.arraycopy(thR, 0, data.thirdSplatR, 0, data.thirdSplatR.length); + System.arraycopy(thG, 0, data.thirdSplatG, 0, data.thirdSplatG.length); + System.arraycopy(thB, 0, data.thirdSplatB, 0, data.thirdSplatB.length); + System.arraycopy(thA, 0, data.thirdSplatA, 0, data.thirdSplatA.length); + System.arraycopy(thirdPaths, 0, data.thirdTextures, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(thirdNormPaths, 0, data.thirdNormalMaps, 0, MapData.TEXTURE_SLOTS); + } + data.normalMapOrder = input.normalMapOrder.clone(); + data.diffuseScales = diffScaleSnap.clone(); + data.voxelFlatSlot = voxelFlatSlot; + data.voxelSteepSlot = voxelSteepSlot; + data.voxelCeilSlot = voxelCeilSlot; if (grassData != null) { try { GrassTuftIO.save(grassData); } @@ -1064,8 +1327,13 @@ public class TerrainEditorState extends BaseAppState { smoothHeight(contact); } else if (mode == HeightTool.MODE_PLATEAU) { if (edit.action() < 0) { - // Rechtsklick: Terrain-Höhe sampeln und als Plateau-Ziel übernehmen + // Rechtsklick: Terrain- und Voxel-Höhe sampeln, Maximum als Plateau-Ziel float h = sampleTerrainHeight(contact); + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null) { + float vh = ves.columnTopWorldY(contact.x, contact.z); + if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh; + } if (Float.isFinite(h)) { input.heightTool.plateauHeight.setValue(h); input.heightTool.plateauHeightChanged = true; @@ -1189,7 +1457,7 @@ public class TerrainEditorState extends BaseAppState { } /** Liest die Terrain-Höhe am nächstgelegenen Vertex zum Kontaktpunkt. */ - private float sampleTerrainHeight(Vector3f worldContact) { + public float sampleTerrainHeight(Vector3f worldContact) { if (cachedHeightMap == null) return Float.NaN; int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING); int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING); @@ -1488,28 +1756,6 @@ public class TerrainEditorState extends BaseAppState { // ── Live-Spieler-Marker ─────────────────────────────────────────────────── - private Geometry buildLivePlayerMarker() { - com.jme3.scene.shape.Cylinder cyl = - new com.jme3.scene.shape.Cylinder(8, 8, 0.3f, 1.8f, true); - Geometry geo = new Geometry("livePlayerMarker", cyl); - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new ColorRGBA(1f, 0.45f, 0f, 1f)); - geo.setMaterial(mat); - // Cylinder-Achse liegt entlang Y → keine Rotation nötig - geo.setCullHint(Spatial.CullHint.Always); - return geo; - } - - private void updateLivePlayerMarker() { - float x = input.livePlayerX; - if (Float.isNaN(x)) { - livePlayerMarker.setCullHint(Spatial.CullHint.Always); - } else { - livePlayerMarker.setCullHint(Spatial.CullHint.Inherit); - livePlayerMarker.setLocalTranslation(x, input.livePlayerY + 0.9f, input.livePlayerZ); - } - } - // Nearest-Neighbor-Downsampling (src 16385 → dst 4097) private static void downsampleHeights(float[] src, int srcSize, float[] dst, int dstSize) { float step = (float)(srcSize - 1) / (dstSize - 1); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java b/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java index a64934a..68cd4a0 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java @@ -18,6 +18,9 @@ import com.jme3.texture.Image; import com.jme3.texture.Texture2D; import com.jme3.util.BufferUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; @@ -39,6 +42,8 @@ import java.util.Base64; */ public final class ThumbnailRenderer { + private static final Logger log = LoggerFactory.getLogger(ThumbnailRenderer.class); + public static final int SIZE = 128; // Kamerawinkel: yaw = 45° (zwischen X- und Z-Achse), pitch = 45° (von oben) @@ -142,7 +147,7 @@ public final class ThumbnailRenderer { Files.createDirectories(dest.getParent()); Files.write(dest, pngBytes); } catch (IOException e) { - System.err.println("[ThumbnailRenderer] Sidecar-Fehler: " + e.getMessage()); + log.warn("[ThumbnailRenderer] Sidecar-Fehler: {}", e.getMessage()); } } @@ -173,7 +178,7 @@ public final class ThumbnailRenderer { renderer.readFrameBuffer(fb, buf); return pngFromRgba(buf); } catch (Exception e) { - System.err.println("[ThumbnailRenderer] Render-Fehler: " + e.getMessage()); + log.error("[ThumbnailRenderer] Render-Fehler: {}", e.getMessage()); return null; } finally { rm.removePreView(vp); diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java index 5f52fab..2344275 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java @@ -358,6 +358,7 @@ public class TreeGeneratorState extends BaseAppState { // Nur LOD0 initial sichtbar; Control steuert je nach Distanz pendingLdNode.setCullHint(Spatial.CullHint.Always); lod2.setCullHint(Spatial.CullHint.Always); + lod2.setShadowMode(RenderQueue.ShadowMode.Off); root.addControl(new TreeLodControl(app.getCamera(), pendingHdNode, pendingLdNode, lod2, 60f, 200f)); @@ -482,7 +483,7 @@ public class TreeGeneratorState extends BaseAppState { mat.setTexture("LeafMap", assets.loadTexture(p.leafTexture)); mat.setBoolean("HasLeafMap", true); } catch (Exception tex) { - System.err.println("[TreeGenerator] Blatt-Textur nicht gefunden: " + p.leafTexture); + log.warn("[TreeGenerator] Blatt-Textur nicht gefunden: {}", p.leafTexture); } } return mat; @@ -568,6 +569,7 @@ public class TreeGeneratorState extends BaseAppState { private Node cloneForCapture(Node src) { Node copy = new Node(src.getName() + "_cap"); copy.setLocalTranslation(src.getLocalTranslation()); + copy.setLocalScale(src.getLocalScale()); for (Spatial child : src.getChildren()) { if (child instanceof Geometry g) { Geometry gc = new Geometry(g.getName() + "_c", g.getMesh()); @@ -722,7 +724,7 @@ public class TreeGeneratorState extends BaseAppState { private Spatial buildPreviewSky() { // Versuche zuerst SkyFactory mit einer Sphere-Map-Textur - String[] skyPaths = { "Textures/sky.png", "Textures/Sky.png", "Textures/skybox.png" }; + String[] skyPaths = { "Textures/internal/sky.png", "Textures/internal/Sky.png", "Textures/internal/skybox.png" }; for (String path : skyPaths) { try { Texture skyTex = assets.loadTexture(path); diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java index 00ba11b..7ba99cc 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java @@ -17,6 +17,9 @@ import de.blight.editor.SharedInput; import de.blight.editor.tool.HoleTool; import de.blight.editor.tool.UpperHeightTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,6 +30,8 @@ import java.util.Map; */ public class UpperLayerState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(UpperLayerState.class); + // ── Constants ──────────────────────────────────────────────────────────── private static final int CHUNKS_PER_AXIS = 32; // 512 cells / 16 @@ -69,9 +74,9 @@ public class UpperLayerState extends BaseAppState { rock.setWrap(Texture.WrapMode.Repeat); chunkMat.setTexture("DiffuseMap", rock); chunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.32f, 0.25f, 1f)); - System.out.println("[UpperLayer] Vulkangestein-Textur geladen"); + log.info("[UpperLayer] Vulkangestein-Textur geladen"); } catch (Exception e) { - System.out.println("[UpperLayer] Rock-Textur fehlt, Fallback: " + e.getMessage()); + log.info("[UpperLayer] Rock-Textur fehlt, Fallback: {}", e.getMessage()); chunkMat.setBoolean("UseMaterialColors", true); chunkMat.setColor("Diffuse", new ColorRGBA(0.18f, 0.12f, 0.08f, 1f)); } diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index 3bce4b9..c284049 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -32,11 +32,15 @@ import de.blight.game.state.VoxelChunkNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.jme3.export.binary.BinaryExporter; import java.io.IOException; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.*; +import java.util.concurrent.ConcurrentHashMap; /** * Editor-AppState für das Voxel-Werkzeug. @@ -67,15 +71,25 @@ public class VoxelEditorState extends BaseAppState { /** Wird von TerrainEditorState gesetzt; wird als Raycast-Ziel genutzt. */ private Node terrainNode; private TerrainQuad terrainQuad; + private TerrainEditorState terrainEditorState; private final SharedInput input; // ── Chunk-Verwaltung ───────────────────────────────────────────────────── /** key → VoxelChunk (in-memory, ggf. dirty). */ - private final Map chunks = new HashMap<>(); + private final Map chunks = new ConcurrentHashMap<>(); /** key → zugehöriger VoxelChunkNode in der Szene. */ - private final Map nodes = new HashMap<>(); + private final Map nodes = new ConcurrentHashMap<>(); + + // ── Performance: Rebuild-Batching ──────────────────────────────────────── + + /** + * Chunks, die im aktuellen Frame modifiziert wurden. + * MarchingCubes wird am Frameende einmal pro Chunk aufgerufen, + * nicht mehrfach pro Edit-Event. + */ + private final Set dirtyChunksThisFrame = new HashSet<>(); // ── Timers ─────────────────────────────────────────────────────────────── @@ -97,6 +111,7 @@ public class VoxelEditorState extends BaseAppState { // ── Material ───────────────────────────────────────────────────────────── private Material voxelMaterial; + private boolean tessellationEnabled = false; // ── Brush-Indikator ─────────────────────────────────────────────────────── @@ -127,9 +142,9 @@ public class VoxelEditorState extends BaseAppState { app.getRootNode().attachChild(voxelRoot); // TerrainEditorState wurde vor uns attached und ist zu diesem Zeitpunkt initialisiert - TerrainEditorState tes = app.getStateManager().getState(TerrainEditorState.class); - if (tes != null) { - terrainNode = tes.getTerrainNode(); + terrainEditorState = app.getStateManager().getState(TerrainEditorState.class); + if (terrainEditorState != null) { + terrainNode = terrainEditorState.getTerrainNode(); terrainQuad = terrainNode instanceof TerrainQuad tq ? tq : null; } @@ -169,6 +184,19 @@ public class VoxelEditorState extends BaseAppState { // Brush-Indikator immer aktualisieren (zeigen/verstecken je nach Layer) updateBrushIndicator(); + // Bake angefordert? + if (input.bakeVoxelsRequested) { + input.bakeVoxelsRequested = false; + List snapshot = new ArrayList<>(chunks.values()); + executor.submit(() -> bakeAll(snapshot)); + } + + // Voxel-Texturen aktualisiert? + if (input.voxelTexturesChanged) { + input.voxelTexturesChanged = false; + applyTextures(voxelMaterial); + } + // Nur aktiv wenn LAYER_VOXEL gesetzt if (input.activeLayer != SharedInput.LAYER_VOXEL) { idleSinceEdit = 0f; @@ -178,6 +206,7 @@ public class VoxelEditorState extends BaseAppState { } // Edit-Queue verarbeiten (max. MAX_EDITS_PER_FRAME) + // Edits nur akkumulieren; Mesh-Rebuild am Frameende einmal pro Chunk. int processed = 0; SharedInput.VoxelEdit edit; while (processed < MAX_EDITS_PER_FRAME @@ -188,6 +217,41 @@ public class VoxelEditorState extends BaseAppState { idleSinceSave = 0f; } + // Dirty Chunks einmal pro Frame neu bauen (nicht pro Edit-Event) + if (!dirtyChunksThisFrame.isEmpty()) { + Set neighborRebuild = new HashSet<>(); + for (long key : dirtyChunksThisFrame) { + VoxelChunkNode node = nodes.get(key); + if (node == null) continue; + VoxelChunk c = chunks.get(key); + VoxelChunk[] nb = c != null ? getNeighbors(c.cx, c.cy, c.cz) : null; + node.rebuildMesh(0, nb); + node.setActiveLod(0); + if (!lodRebuildQueue.contains(key)) lodRebuildQueue.add(key); + // Benachbarte Chunks brauchen ebenfalls neue Gradienten an der Grenze + if (c != null) { + for (long nk : new long[]{ + chunkKey(c.cx+1,c.cy,c.cz), chunkKey(c.cx-1,c.cy,c.cz), + chunkKey(c.cx,c.cy+1,c.cz), chunkKey(c.cx,c.cy-1,c.cz), + chunkKey(c.cx,c.cy,c.cz+1), chunkKey(c.cx,c.cy,c.cz-1)}) { + if (nodes.containsKey(nk) && !dirtyChunksThisFrame.contains(nk)) + neighborRebuild.add(nk); + } + } + } + for (long key : neighborRebuild) { + VoxelChunkNode node = nodes.get(key); + if (node == null) continue; + VoxelChunk c = chunks.get(key); + VoxelChunk[] nb = c != null ? getNeighbors(c.cx, c.cy, c.cz) : null; + node.rebuildMesh(0, nb); + node.setActiveLod(0); + if (!lodRebuildQueue.contains(key)) lodRebuildQueue.add(key); + } + dirtyChunksThisFrame.clear(); + lodRebuildPending = false; + } + if (processed == 0) { idleSinceEdit += tpf; idleSinceSave += tpf; @@ -231,19 +295,22 @@ public class VoxelEditorState extends BaseAppState { // ── Intern: Edit verarbeiten ─────────────────────────────────────────────── + /** Treffpunkt + Oberflächennormale eines Raycasts. */ + private record Hit(Vector3f pos, Vector3f normal) {} + private void handleEdit(SharedInput.VoxelEdit edit) { float jmeX = edit.screenX() * (float) input.viewportScaleX; float jmeY = cam.getHeight() - edit.screenY() * (float) input.viewportScaleY; - Vector3f worldPos = raycast(jmeX, jmeY); - if (worldPos == null) return; - applyEdit(worldPos); + Hit hit = raycastHit(jmeX, jmeY); + if (hit == null) return; + applyEdit(hit, edit.action()); } /** - * Führt einen Raycast durch und gibt den nächstgelegenen Treffpunkt zurück, - * oder null wenn kein Treffer. + * Raycast gegen Terrain und Voxel-Geometrie. + * Gibt Position + Oberflächennormale des nächstgelegenen Treffers zurück. */ - private Vector3f raycast(float screenX, float screenY) { + private Hit raycastHit(float screenX, float screenY) { Vector2f click2d = new Vector2f(screenX, screenY); Vector3f origin = cam.getWorldCoordinates(click2d, 0f); Vector3f target = cam.getWorldCoordinates(click2d, 1f); @@ -251,144 +318,183 @@ public class VoxelEditorState extends BaseAppState { Ray ray = new Ray(origin, dir); CollisionResults results = new CollisionResults(); - float bestDist = Float.MAX_VALUE; - Vector3f best = null; + float bestDist = Float.MAX_VALUE; + Vector3f bestPos = null; + Vector3f bestNorm = new Vector3f(0, 1, 0); - // Terrain if (terrainNode != null) { terrainNode.collideWith(ray, results); if (results.size() > 0) { CollisionResult cr = results.getClosestCollision(); if (cr.getDistance() < bestDist) { bestDist = cr.getDistance(); - best = cr.getContactPoint(); + bestPos = cr.getContactPoint(); + bestNorm = cr.getContactNormal() != null + ? cr.getContactNormal().normalize() + : new Vector3f(0, 1, 0); } results.clear(); } } - // Voxel-Geometrie voxelRoot.collideWith(ray, results); if (results.size() > 0) { CollisionResult cr = results.getClosestCollision(); if (cr.getDistance() < bestDist) { - best = cr.getContactPoint(); + bestPos = cr.getContactPoint(); + bestNorm = cr.getContactNormal() != null + ? cr.getContactNormal().normalize() + : new Vector3f(0, 1, 0); } } - return best; + return bestPos != null ? new Hit(bestPos, bestNorm) : null; } /** - * Wendet den Pinsel auf alle betroffenen Chunks an. + * Wendet den gewählten Modus an. * - * Modi 0-3: Solid-Voxel hinzufügen (verschiedene Falloff-Kurven), danach alle Voxel - * unterhalb der Terrain-Oberfläche in der Pinselfläche entfernen. - * Modus 4 (Entfernen): Voxel löschen – für Höhlen unterhalb des Terrains. + * Modi 0-3 (Sinus/Spike/Plateau/Smooth): Spalten ab Terrain-Oberfläche nach oben (links) + * bzw. Abtragen nach unten (rechts). Verhalten analog zum Terrain-Tool. + * + * Modus 4 (Klippe): Scheiben-Pinsel entlang der Oberflächennormale – rechts = entfernen. + * + * Modus 5 (Aushöhlen): Kugel-Entfernen mit nach innen versetztem Zentrum. */ - private void applyEdit(Vector3f worldPos) { + private void applyEdit(Hit hit, int action) { float radius = (float) input.voxelTool.brushRadius.getValue(); float strength = (float) input.voxelTool.brushStrength.getValue(); int modeIdx = input.voxelTool.mode.getSelectedIndex(); - int texSlot = input.voxelTool.textureSlot.getSelectedIndex(); - byte matId = (byte)(texSlot & 3); - boolean remove = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE); - boolean addFlat = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_ADD); - float wx = worldPos.x, wy = worldPos.y, wz = worldPos.z; + boolean isSlab = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_ADD); + boolean isCave = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE); + boolean isColumn = !isSlab && !isCave; + boolean lower = action < 0; - int cxMin = VoxelChunk.worldXToCx(wx - radius); - int cxMax = VoxelChunk.worldXToCx(wx + radius); - int czMin = VoxelChunk.worldZToCz(wz - radius); - int czMax = VoxelChunk.worldZToCz(wz + radius); - // Spalten-Modi bauen nach oben bis zu strength Voxel → Y-Range entsprechend erweitern - int cyMin = VoxelChunk.worldYToCy(wy - radius); - int cyMax = (!remove && !addFlat) - ? VoxelChunk.worldYToCy(wy + strength) - : VoxelChunk.worldYToCy(wy + radius); + Vector3f N = hit.normal; + float wx = hit.pos.x, wy = hit.pos.y, wz = hit.pos.z; + + // Plateau-Rechtsklick: Voxel- und Terrain-Höhe sampeln, Maximum als Ziel speichern + if (isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) { + float h = columnTopWorldY(wx, wz); + TerrainEditorState tes = getStateManager().getState(TerrainEditorState.class); + if (tes != null) { + float th = tes.sampleTerrainHeight(new com.jme3.math.Vector3f(wx, wy, wz)); + if (Float.isFinite(th)) h = Float.isFinite(h) ? Math.max(h, th) : th; + } + if (Float.isFinite(h)) { + input.voxelTool.plateauTarget.setValue(h); + input.voxelTool.plateauTargetChanged = true; + } + return; + } + + // Cave-Modus: Mittelpunkt ins Innere versetzen + if (isCave) { + wx -= N.x * radius * 0.6f; + wy -= N.y * radius * 0.6f; + wz -= N.z * radius * 0.6f; + } + + // Spalten-Modi brauchen nur XZ-Radius; Slab/Cave brauchen auch Stärke in Y + float worldExtent = isColumn ? radius + 2f : radius + strength + 2f; + + int cxMin = VoxelChunk.worldXToCx(wx - worldExtent); + int cxMax = VoxelChunk.worldXToCx(wx + worldExtent); + int czMin = VoxelChunk.worldZToCz(wz - worldExtent); + int czMax = VoxelChunk.worldZToCz(wz + worldExtent); + int cyMin = VoxelChunk.worldYToCy(wy - worldExtent); + int cyMax = VoxelChunk.worldYToCy(wy + worldExtent); + + // Smooth-Modus: Slope-Parameter vorab berechnen (für beide Klick-Varianten) + float[] slopeParams = null; + if (isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) { + slopeParams = computeSlopeParams(wx, wz, radius); + } + + float plateauTargetH = (float) input.voxelTool.plateauTarget.getValue(); for (int cz = czMin; cz <= czMax; cz++) { for (int cx = cxMin; cx <= cxMax; cx++) { for (int cy = cyMin; cy <= cyMax; cy++) { - VoxelChunkNode node = getOrCreateNode(cx, cy, cz); - VoxelChunk chunk = node.getChunk(); + VoxelChunk chunk = getOrCreateChunk(cx, cy, cz); float lx = VoxelChunk.worldXToLocal(wx, cx); float ly = VoxelChunk.worldYToLocal(wy, cy); float lz = VoxelChunk.worldZToLocal(wz, cz); - if (remove) { + if (isCave) { chunk.applyBrush(lx, ly, lz, radius, Byte.MIN_VALUE, (byte)0); - } else if (addFlat) { - byte d = (byte) Math.min(127, (int) strength); - chunk.applyBrush(lx, ly, lz, radius, d, matId); + } else if (isSlab) { + applySlabBrush(chunk, cx, cy, cz, wx, wy, wz, N, radius, strength, lower); + } else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU) { + applyPlateauColumn(chunk, cx, cy, cz, wx, wz, radius, plateauTargetH); + } else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) { + if (lower) { + applyCliffColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); + } else { + applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); + } } else { - applyAddBrush(chunk, lx, ly, lz, radius, strength, modeIdx, matId); - clearVoxelsBelowTerrain(chunk, cx, cy, cz, wx, wz, radius); + applyColumnBrush(chunk, cx, cy, cz, wx, wz, radius, strength, modeIdx, lower); } - node.rebuildMesh(0); - node.setActiveLod(0); - long key = chunkKey(cx, cy, cz); - if (!lodRebuildQueue.contains(key)) lodRebuildQueue.add(key); - lodRebuildPending = false; + // Node erst anlegen wenn tatsächlich Daten vorhanden + if (!chunk.isEmpty() && !nodes.containsKey(key)) { + addNodeForChunk(key, chunk); + } + dirtyChunksThisFrame.add(key); } } } } /** - * Spalten-Pinsel: Für jede XZ-Position im Pinselradius wird eine Voxel-Säule - * nach oben aufgebaut. Die Säulenhöhe ist proportional zum mode-spezifischen Falloff: - * Spike → steile Spitze in der Mitte - * Plateau → gleichmäßige flache Erhöhung - * Sinus → sanfte Kuppel - * Smooth → weiche raised-cosine Kuppel + * Scheiben-Pinsel für den Klippe-Modus. + * remove=false: Voxel senkrecht zur Normalen aufbauen. + * remove=true: dieselbe Form entfernen (Rechtsklick). */ - private void applyAddBrush(VoxelChunk chunk, - float lx, float ly, float lz, - float radius, float strength, - int mode, byte matId) { - int x0 = Math.max(0, (int)(lx - radius)); - int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lx + radius)); - int z0 = Math.max(0, (int)(lz - radius)); - int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lz + radius)); - float r2 = radius * radius; - byte d = (byte) Math.min(127, (int) strength); - // Basiszeile innerhalb dieses Chunks (kann negativ sein für höhere Chunks) - int baseY = (int) ly; + private void applySlabBrush(VoxelChunk chunk, int cx, int cy, int cz, + float hitWX, float hitWY, float hitWZ, + Vector3f N, float radius, float strength, + boolean remove) { + float extent = radius + strength + 1f; + float lhX = VoxelChunk.worldXToLocal(hitWX, cx); + float lhY = VoxelChunk.worldYToLocal(hitWY, cy); + float lhZ = VoxelChunk.worldZToLocal(hitWZ, cz); - for (int z = z0; z <= z1; z++) { - float dz = z - lz; - for (int x = x0; x <= x1; x++) { - float dx = x - lx; - float d2 = dx*dx + dz*dz; - if (d2 > r2) continue; + int x0 = Math.max(0, (int)(lhX - extent)); + int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lhX + extent)); + int y0 = Math.max(0, (int)(lhY - extent)); + int y1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lhY + extent)); + int z0 = Math.max(0, (int)(lhZ - extent)); + int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lhZ + extent)); - float t = (float) Math.sqrt(d2) / radius; // 0=Mitte, 1=Rand - float falloff; - switch (mode) { - case de.blight.editor.tool.VoxelTool.MODE_SINUS -> - falloff = (float) Math.cos(t * Math.PI / 2); - case de.blight.editor.tool.VoxelTool.MODE_SPIKE -> - falloff = (1f - t) * (1f - t); - case de.blight.editor.tool.VoxelTool.MODE_SMOOTH -> - falloff = (float)(0.5 * (1 + Math.cos(t * Math.PI))); - default -> // PLATEAU - falloff = 1f; - } + float nx = N.x, ny = N.y, nz = N.z; + float r2 = radius * radius; + float overlap = 1.0f; - int colHeight = (int)(strength * falloff); - if (colHeight < 1) continue; + for (int ly = y0; ly <= y1; ly++) { + float wy = VoxelChunk.toWorldY(cy, ly); + float dy = wy - hitWY; + for (int lz = z0; lz <= z1; lz++) { + float wz = VoxelChunk.toWorldZ(cz, lz); + float dz = wz - hitWZ; + for (int lx = x0; lx <= x1; lx++) { + float wx = VoxelChunk.toWorldX(cx, lx); + float dx = wx - hitWX; - // Säule von baseY bis baseY+colHeight, geclampt auf Chunk-Grenzen - int yBottom = Math.max(0, baseY); - int yTop = Math.min(VoxelChunk.SIZE - 1, baseY + colHeight); - for (int y = yBottom; y <= yTop; y++) { - if (chunk.getDensity(x, y, z) <= 0) { - chunk.setDensity(x, y, z, d); - chunk.setMaterial(x, y, z, matId); + float along = dx*nx + dy*ny + dz*nz; + float slabThick = Math.max(1f, strength / 10f); + if (along < -overlap || along > slabThick) continue; + float perpSq = dx*dx + dy*dy + dz*dz - along*along; + if (perpSq > r2) continue; + + if (remove) { + chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + } else { + chunk.setDensity(lx, ly, lz, (byte) 127); } } } @@ -396,30 +502,73 @@ public class VoxelEditorState extends BaseAppState { } /** - * Entfernt alle Solid-Voxel in der X/Z-Fußfläche des Pinsels, die unterhalb der - * aktuellen Terrain-Oberfläche liegen. + * Säulen-Pinsel (Modi 0-3) – analog zum Terrain-Tool, akkumulierend. + * + * Jedes Edit-Event fügt einen kleinen Schritt (strength/10 * falloff) zur + * Spaltenhöhe hinzu. Die Höhe wird am aktuellen Säulen-Top gemessen, nicht + * fest vorgegeben → langes Drücken = langsames Wachsen. + * + * lower=false: Säule von terrainH nach oben wachsen lassen. + * lower=true: Vom Säulen-Top nach unten abtragen. */ - private void clearVoxelsBelowTerrain(VoxelChunk chunk, int cx, int cy, int cz, - float brushWx, float brushWz, float radius) { - if (terrainQuad == null || chunk.isEmpty()) return; + private void applyColumnBrush(VoxelChunk chunk, int cx, int cy, int cz, + float brushWX, float brushWZ, + float radius, float strength, + int mode, boolean lower) { + float lxC = VoxelChunk.worldXToLocal(brushWX, cx); + float lzC = VoxelChunk.worldZToLocal(brushWZ, cz); - float lxC = VoxelChunk.worldXToLocal(brushWx, cx); - float lzC = VoxelChunk.worldZToLocal(brushWz, cz); int x0 = Math.max(0, (int)(lxC - radius)); int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius)); int z0 = Math.max(0, (int)(lzC - radius)); int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); + float r2 = radius * radius; + + // Schritt pro Event: strength bestimmt die Wachstumsgeschwindigkeit + float stepBase = Math.max(1f, strength / 8f); for (int lz = z0; lz <= z1; lz++) { - float worldZ = VoxelChunk.toWorldZ(cz, lz); + float wz = VoxelChunk.toWorldZ(cz, lz); + float dz = wz - brushWZ; for (int lx = x0; lx <= x1; lx++) { - float worldX = VoxelChunk.toWorldX(cx, lx); - Float h = terrainQuad.getHeight(new Vector2f(worldX, worldZ)); - if (h == null) continue; + float wx = VoxelChunk.toWorldX(cx, lx); + float dx = wx - brushWX; + float d2 = dx*dx + dz*dz; + if (d2 > r2) continue; - for (int ly = 0; ly < VoxelChunk.SIZE; ly++) { - if (chunk.getDensity(lx, ly, lz) <= 0) continue; - if (VoxelChunk.toWorldY(cy, ly) <= h) { + float t = (float) Math.sqrt(d2) / radius; + float falloff = computeFalloff(mode, t); + // Smooth/Sinus: Kanten bekommen weniger Schritt → richtiges Profil + int colStep = (int)(stepBase * falloff); + if (colStep < 1) continue; + + // Terrain-Höhe: ceil stellt sicher, dass Voxel nie unterhalb Terrain beginnen + float terrainH = terrainH(wx, wz); + int terrainLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1, + (int) Math.ceil(VoxelChunk.worldYToLocal(terrainH, cy)))); + // Unterirdische Voxel löschen (kein Überhang > 90°) + for (int ly = 0; ly < terrainLY; ly++) chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + + if (!lower) { + // Aktuellen Säulen-Top finden (höchster Solid-Voxel ≥ terrainLY) + int currentTop = terrainLY; // Fallback: direkt auf Terrain starten + for (int ly = VoxelChunk.SIZE - 1; ly >= terrainLY; ly--) { + if (chunk.getDensity(lx, ly, lz) > 0) { currentTop = ly; break; } + } + // Säule um colStep erhöhen, dabei Basis ab terrainLY immer füllen + int newTop = Math.min(VoxelChunk.SIZE - 1, currentTop + colStep); + for (int ly = terrainLY; ly <= newTop; ly++) { + chunk.setDensity(lx, ly, lz, (byte) 127); + } + } else { + // Höchsten Solid-Voxel finden (egal ob über oder unter terrainLY) + int currentTop = -1; + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + 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++) { chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); } } @@ -427,19 +576,486 @@ public class VoxelEditorState extends BaseAppState { } } - // ── Intern: Chunk/Node-Verwaltung ───────────────────────────────────────── + // ── Terrain-Höhe (schneller O(1)-Zugriff) ───────────────────────────────── + + private float terrainH(float worldX, float worldZ) { + if (terrainEditorState != null) return terrainEditorState.getTerrainHeightFast(worldX, worldZ); + if (terrainQuad != null) { + Float h = terrainQuad.getHeight(new Vector2f(worldX, worldZ)); + return h != null ? h : 0f; + } + return 0f; + } + + // ── Smooth-Modus ────────────────────────────────────────────────────────── /** - * Gibt den VoxelChunkNode für (cx,cy,cz) zurück. - * Legt neuen Chunk und Node an, falls noch nicht vorhanden. + * Berechnet Slope-Parameter im Pinselradius. + * Rückgabe: [highH, lowH, dirX, dirZ, projHigh, projLow] + * dirX/Z = Einheitsvektor vom Tiefpunkt zum Hochpunkt + * projHigh = Projektion des Hochpunkts auf die Achse (relativ zur Brush-Mitte) + * projLow = Projektion des Tiefpunkts auf die Achse (immer ≤ projHigh) + * projHigh - projLow = |maxPos - minPos|, immer ≥ 0. */ - private VoxelChunkNode getOrCreateNode(int cx, int cy, int cz) { - long key = chunkKey(cx, cy, cz); - VoxelChunkNode node = nodes.get(key); - if (node != null) return node; + private float[] computeSlopeParams(float brushWX, float brushWZ, float radius) { + float r2 = radius * radius; + int xMin = (int)(brushWX - radius), xMax = (int) Math.ceil(brushWX + radius); + int zMin = (int)(brushWZ - radius), zMax = (int) Math.ceil(brushWZ + radius); + + float maxH = Float.NEGATIVE_INFINITY, minH = Float.POSITIVE_INFINITY; + float maxX = brushWX, maxZ = brushWZ, minX = brushWX, minZ = brushWZ; + + for (int xi = xMin; xi <= xMax; xi++) { + float dx = xi - brushWX; + for (int zi = zMin; zi <= zMax; zi++) { + float dz = zi - brushWZ; + if (dx*dx + dz*dz > r2) continue; + float h = columnTopWorldY(xi, zi); + if (h > maxH) { maxH = h; maxX = xi; maxZ = zi; } + if (h < minH) { minH = h; minX = xi; minZ = zi; } + } + } + + if (maxH == Float.NEGATIVE_INFINITY) return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0 }; + + // Richtung vom tiefsten zum höchsten Punkt — garantiert projHigh - projLow = |maxPos - minPos| + float ddx = maxX - minX, ddz = maxZ - minZ; + float len = (float) Math.sqrt(ddx*ddx + ddz*ddz); + if (len < 1e-4f) { ddx = 1f; ddz = 0f; } else { ddx /= len; ddz /= len; } + + float projHigh = (maxX - brushWX) * ddx + (maxZ - brushWZ) * ddz; + float projLow = (minX - brushWX) * ddx + (minZ - brushWZ) * ddz; + return new float[]{ maxH, minH, ddx, ddz, projHigh, projLow }; + } + + /** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */ + public float columnTopWorldY(float worldX, float worldZ) { + int cx = VoxelChunk.worldXToCx(worldX); + int cz = VoxelChunk.worldZToCz(worldZ); + int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx))); + int lzl = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldZToLocal(worldZ, cz))); + for (int cy = 10; cy >= -2; cy--) { + VoxelChunk chunk = chunks.get(chunkKey(cx, cy, cz)); + if (chunk == null || chunk.isEmpty()) continue; + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + if (chunk.getDensity(lx, ly, lzl) > 0) { + return VoxelChunk.toWorldY(cy, ly); + } + } + } + return terrainH(worldX, worldZ); + } + + /** + * Gemeinsamer Kern: iteriert über alle Spalten im Radius und bewegt sie + * schrittweise auf die per Lambda berechnete Ziel-Welt-Y zu. + * coord[] = [dx, dz, d2] (relativ zur Brush-Mitte) + */ + private void applyColumnToTarget(VoxelChunk chunk, int cx, int cy, int cz, + float brushWX, float brushWZ, + float radius, float strength, + java.util.function.ToDoubleFunction targetFn) { + float lxC = VoxelChunk.worldXToLocal(brushWX, cx); + float lzC = VoxelChunk.worldZToLocal(brushWZ, cz); + int x0 = Math.max(0, (int)(lxC - radius)); + int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius)); + int z0 = Math.max(0, (int)(lzC - radius)); + int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); + float r2 = radius * radius; + float stepBase = Math.max(1f, strength / 8f); + float[] coord = new float[3]; + + for (int lz = z0; lz <= z1; lz++) { + float wz = VoxelChunk.toWorldZ(cz, lz); + coord[1] = wz - brushWZ; + for (int lx = x0; lx <= x1; lx++) { + float wx = VoxelChunk.toWorldX(cx, lx); + coord[0] = wx - brushWX; + coord[2] = coord[0]*coord[0] + coord[1]*coord[1]; + if (coord[2] > r2) continue; + + float targetH = (float) targetFn.applyAsDouble(coord); + if (!Float.isFinite(targetH)) continue; + + float t = (float) Math.sqrt(coord[2]) / radius; + float falloff = (float)(0.5 * (1 + Math.cos(t * Math.PI))); + int step = Math.max(1, (int)(stepBase * falloff)); + + int currentTopLY = -1; + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + if (chunk.getDensity(lx, ly, lz) > 0) { currentTopLY = ly; break; } + } + float currentTopWY = currentTopLY >= 0 + ? VoxelChunk.toWorldY(cy, currentTopLY) + : terrainH(wx, wz); + + float diff = targetH - currentTopWY; + if (Math.abs(diff) < 0.5f) continue; + + int terrainLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1, + (int) Math.ceil(VoxelChunk.worldYToLocal(terrainH(wx, wz), cy)))); + // Unterirdische Voxel immer leeren (kein Überhang > 90°) + for (int ly = 0; ly < terrainLY; ly++) chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + if (diff > 0) { + int startLY = currentTopLY >= 0 ? Math.max(currentTopLY, terrainLY) : terrainLY; + int newTop = Math.min(VoxelChunk.SIZE - 1, startLY + step); + for (int ly = terrainLY; ly <= newTop; ly++) { + chunk.setDensity(lx, ly, lz, (byte) 127); + } + } else { + if (currentTopLY < 0) continue; + int newTop = Math.max(0, currentTopLY - step); + for (int ly = newTop + 1; ly <= currentTopLY; ly++) { + chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + } + } + } + } + } + + /** + * Smooth-Pinsel (Linksklick): gerichteter Slope von lowH nach highH. + * Ziel interpoliert zwischen tatsächlicher Projektion von Tief- und Hochpunkt — + * der höchste Punkt behält genau highH, der tiefste genau lowH. + */ + private void applySmoothColumn(VoxelChunk chunk, int cx, int cy, int cz, + float brushWX, float brushWZ, + float radius, float strength, float[] sp) { + if (sp == null || Float.isNaN(sp[0])) return; + final float highH = sp[0], lowH = sp[1], dirX = sp[2], dirZ = sp[3]; + final float projHigh = sp[4], projLow = sp[5]; + final float projRange = projHigh - projLow; // = |maxPos − minPos| ≥ 0 + if (projRange < 0.5f) return; + + applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> { + float proj = coord[0] * dirX + coord[1] * dirZ; + float t = (proj - projLow) / projRange; + t = Math.max(0f, Math.min(1f, t)); + return lowH + (highH - lowH) * t; + }); + } + + /** + * Plateau-Pinsel: setzt alle Spalten im Radius sofort auf genau targetH. + * Alles zwischen terrainH und targetH wird solid, darüber wird geleert. + */ + private void applyPlateauColumn(VoxelChunk chunk, int cx, int cy, int cz, + float brushWX, float brushWZ, + float radius, float targetH) { + float lxC = VoxelChunk.worldXToLocal(brushWX, cx); + float lzC = VoxelChunk.worldZToLocal(brushWZ, cz); + int x0 = Math.max(0, (int)(lxC - radius)); + int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius)); + int z0 = Math.max(0, (int)(lzC - radius)); + int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); + float r2 = radius * radius; + + for (int lz = z0; lz <= z1; lz++) { + float wz = VoxelChunk.toWorldZ(cz, lz); + float dz = wz - brushWZ; + for (int lx = x0; lx <= x1; lx++) { + float wx = VoxelChunk.toWorldX(cx, lx); + float dx = wx - brushWX; + if (dx*dx + dz*dz > r2) continue; + + float th = terrainH(wx, wz); + for (int ly = 0; ly < VoxelChunk.SIZE; ly++) { + float wy = VoxelChunk.toWorldY(cy, ly); + if (wy < th) { + // Unterhalb Terrain: immer leeren, kein Überhang möglich + chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + continue; + } + if (wy <= targetH) { + chunk.setDensity(lx, ly, lz, (byte) 127); + } else { + chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + } + } + } + } + } + + /** + * Klippe-Pinsel (Smooth-Rechtsklick): scharfe Terrassenstufe mittig zwischen Hoch- und Tiefpunkt. + */ + private void applyCliffColumn(VoxelChunk chunk, int cx, int cy, int cz, + float brushWX, float brushWZ, + float radius, float strength, float[] sp) { + if (sp == null || Float.isNaN(sp[0])) return; + final float highH = sp[0], lowH = sp[1], dirX = sp[2], dirZ = sp[3]; + final float projHigh = sp[4], projLow = sp[5]; + final float projRange = projHigh - projLow; + if (projRange < 0.5f) return; + + final float projMid = (projHigh + projLow) * 0.5f; + final float blendHalf = Math.max(0.3f, projRange * 0.1f); + + applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> { + float proj = coord[0] * dirX + coord[1] * dirZ; + float rel = proj - projMid; + if (rel > blendHalf) return highH; + if (rel < -blendHalf) return lowH; + float t = (rel / blendHalf + 1f) * 0.5f; + return lowH + (highH - lowH) * t; + }); + } + + // ── Intern: Voxel-Bake ──────────────────────────────────────────────────── + + /** + * Bäckt alle übergebenen Chunks: speichert .blvc, glättet die Dichte mit + * Nachbar-Lookup, erzeugt LOD0/1/2-Meshes und exportiert sie als .j3o. + */ + private void bakeAll(List toProcess) { + saveAll(); + List nonEmpty = new java.util.ArrayList<>(); + for (VoxelChunk c : toProcess) { if (!c.isEmpty()) nonEmpty.add(c); } + + // bakeTotal sofort setzen, damit die Fortschrittsanzeige schon während des Blurs läuft + input.bakeDone = 0; + input.bakeTotal = nonEmpty.size(); + + Map allOriginal = new HashMap<>(); + for (VoxelChunk c : nonEmpty) allOriginal.put(chunkKey(c.cx, c.cy, c.cz), c); + + // Kooperativer Multi-Pass-Blur: jede Iteration liest cross-boundary aus dem + // Ergebnis der vorigen Iteration aller Chunks → blurredA[128] == blurredB[0]. + int blurN = VoxelChunk.SIZE; + Map curBufs = new HashMap<>(); + for (VoxelChunk c : nonEmpty) { + long k = chunkKey(c.cx, c.cy, c.cz); + float[] buf = new float[blurN * blurN * blurN]; + for (int by = 0; by < blurN; by++) + for (int bz = 0; bz < blurN; bz++) + for (int bx = 0; bx < blurN; bx++) + buf[c.idx(bx, by, bz)] = c.getDensity(bx, by, bz); + curBufs.put(k, buf); + } + + // Rand-Synchronisation vor dem ersten Blur-Pass: + // A[CELLS, y, z] und B[0, y, z] repräsentieren denselben Welt-Voxel, können + // aber durch einseitiges Sculpting divergiert sein. Chunk B (höherer Index) + // ist autoritativ (worldXToCx mappt die Grenzposition auf cx+1). + // Ohne diese Sync läuft der Blur auf beiden Seiten mit verschiedenen + // Ausgangswerten → verschiedene Blur-Ergebnisse → MC erzeugt Naht-Vertices + // an unterschiedlichen Positionen → sichtbarer Riss (~50 cm breit). + for (VoxelChunk c : nonEmpty) { + long ck = chunkKey(c.cx, c.cy, c.cz); + float[] cBuf = curBufs.get(ck); + // X-Grenze: Rechter Nachbar ist autoritativ für x=CELLS + float[] rBuf = curBufs.get(chunkKey(c.cx + 1, c.cy, c.cz)); + if (rBuf != null) + for (int by = 0; by < blurN; by++) + for (int bz = 0; bz < blurN; bz++) + cBuf[c.idx(VoxelChunk.CELLS, by, bz)] = rBuf[c.idx(0, by, bz)]; + // Z-Grenze: Vorderer Nachbar ist autoritativ für z=CELLS + float[] fBuf = curBufs.get(chunkKey(c.cx, c.cy, c.cz + 1)); + if (fBuf != null) + for (int by = 0; by < blurN; by++) + for (int bx = 0; bx < blurN; bx++) + cBuf[c.idx(bx, by, VoxelChunk.CELLS)] = fBuf[c.idx(bx, by, 0)]; + // Y-Grenze: Oberer Nachbar ist autoritativ für y=CELLS + float[] tBuf = curBufs.get(chunkKey(c.cx, c.cy + 1, c.cz)); + if (tBuf != null) + for (int bz = 0; bz < blurN; bz++) + for (int bx = 0; bx < blurN; bx++) + cBuf[c.idx(bx, VoxelChunk.CELLS, bz)] = tBuf[c.idx(bx, 0, bz)]; + } + + for (int iter = 0; iter < 7; iter++) { + Map nextBufs = new HashMap<>(); + for (VoxelChunk c : nonEmpty) { + long k = chunkKey(c.cx, c.cy, c.cz); + float[] cur = curBufs.get(k); + float[] next = new float[blurN * blurN * blurN]; + for (int by = 0; by < blurN; by++) { + for (int bz = 0; bz < blurN; bz++) { + for (int bx = 0; bx < blurN; bx++) { + float sum = 0f; + for (int dy = -1; dy <= 1; dy++) + for (int dz = -1; dz <= 1; dz++) + for (int dx = -1; dx <= 1; dx++) { + int sx = bx+dx, sy = by+dy, sz = bz+dz; + if (sx >= 0 && sx < blurN && sy >= 0 && sy < blurN && sz >= 0 && sz < blurN) + sum += cur[c.idx(sx, sy, sz)]; + else + sum += getBlurBuf(curBufs, allOriginal, c, sx, sy, sz); + } + next[c.idx(bx, by, bz)] = sum / 27f; + } + } + } + nextBufs.put(k, next); + } + curBufs = nextBufs; + } + + // Blur-Ergebnisse in VoxelChunks umwandeln + Map blurredMap = new HashMap<>(); + for (VoxelChunk c : nonEmpty) { + long k = chunkKey(c.cx, c.cy, c.cz); + float[] buf = curBufs.get(k); + VoxelChunk result = new VoxelChunk(c.cx, c.cy, c.cz); + for (int by = 0; by < blurN; by++) + for (int bz = 0; bz < blurN; bz++) + for (int bx = 0; bx < blurN; bx++) { + result.setDensity(bx, by, bz, (byte) Math.round(buf[c.idx(bx, by, bz)])); + result.setMaterial(bx, by, bz, c.getMaterial(bx, by, bz)); + } + result.dirty = false; + blurredMap.put(k, result); + } + + // ── Terrain-Übergang: Steigung nach unten extrapolieren ───────────────── + // + // PROBLEM: + // applyColumnBrush() löscht alle Voxel unterhalb der Terrain-Höhe auf + // Byte.MIN_VALUE (Luft). Nach dem Blur bleibt d[yBot] (erster fester + // Voxel von unten) zwar positiv, aber d[yBot-1] springt schlagartig auf + // MIN_VALUE = -128. Dieser steile Dichte-Sprung bewirkt, dass der MC + // am Boden eine nahezu horizontale Fläche erzeugt → sichtbarer Überhang + // an der Terrain-Grenze. + // + // LÖSUNG: + // 1) d[yBot] korrigieren: Steigung aus d[yBot+1] und d[yBot+2] linear + // nach unten extrapolieren (so, als wäre der Blur sauber fortgesetzt). + // 2) Dieselbe Steigung weiter nach UNTEN fortführen (d[yBot-1], d[yBot-2], + // …) bis die Dichte unter -127 fällt. Der MC sieht dadurch eine + // glatt weiter laufende Dichte-Rampe und erzeugt eine geneigte + // Übergangsfläche statt eines flachen Überhangs. + // + // WARUM "daneben" auch gemeint ist: + // Bei einem steilen Voxel-Hang berührt das Terrain die Seite der Struktur. + // Dort übernimmt der XZ-Gradient des Blurs die Richtungsinformation; die + // Y-Extrapolation sorgt lediglich dafür, dass die Dichte unterhalb yBot + // auch im steilen Fall sauber abnimmt statt abrupt auf MIN_VALUE zu fallen. + // + // NICHT ENTFERNEN! Wurde schon einmal versehentlich rückgängig gemacht. + // Ohne diesen Block entsteht am Terrain-Übergang ein hässlicher Überhang. + for (VoxelChunk blurred : blurredMap.values()) { + for (int lz = 0; lz < blurN; lz++) { + for (int lx = 0; lx < blurN; lx++) { + + // Untersten festen Voxel in dieser Spalte finden + int yBot = -1; + for (int ly = 0; ly < blurN; ly++) { + if (blurred.getDensity(lx, ly, lz) > 0) { yBot = ly; break; } + } + if (yBot < 0 || yBot + 2 >= blurN) continue; + + // Steigung aus den zwei Voxeln darüber bestimmen. + // slope < 0: Dichte nimmt nach unten ab (typisch für eine Oberfläche). + float d1 = blurred.getDensity(lx, yBot + 1, lz); + float d2 = blurred.getDensity(lx, yBot + 2, lz); + float slope = d1 - d2; // Δ pro Voxel abwärts + + // Schritt 1: d[yBot] selbst mit der Extrapolation anpassen. + // max(cur, ext) vermeidet, dass ein bereits korrekterer Wert + // durch einen kleineren überschrieben wird. + float extBot = d1 + slope; // = 2*d1 - d2 + byte cur = blurred.getDensity(lx, yBot, lz); + byte nv = (byte) Math.min(127, Math.max(cur, Math.round(extBot))); + blurred.setDensity(lx, yBot, lz, nv); + + // Schritt 2: Steigung nach UNTEN weiter fortführen. + // Ersetzt MIN_VALUE-Sprünge durch eine glatt abnehmende Rampe, + // die der MC als geneigten Übergang interpretiert statt als + // flachen Überhang. Abbruch, sobald die Dichte unter -127 fällt + // (weiter unten kann nichts Sichtbares mehr entstehen). + float extD = nv; + for (int ly = yBot - 1; ly >= 0; ly--) { + extD += slope; // slope ≤ 0 → Dichte nimmt ab + if (extD <= -127f) break; + blurred.setDensity(lx, ly, lz, (byte) Math.max(-128, Math.round(extD))); + } + } + } + } + + int baked = 0; + for (VoxelChunk chunk : nonEmpty) { + bakeChunk(chunk, blurredMap); + baked++; + input.bakeDone = baked; + } + String msg = "Fertig: " + baked + " Chunk" + (baked != 1 ? "s" : "") + " gebacken."; + // bakeTotal/bakeDone werden vom UI-Thread nach Empfang der Statusmeldung zurückgesetzt + input.bakeStatusMsg = msg; + log.info("Voxel-Bake abgeschlossen – {}.", msg); + } + + /** Bäckt einen einzelnen Chunk mit bereits berechneten geblurrten Daten. */ + private void bakeChunk(VoxelChunk original, Map blurredMap) { + try { + VoxelChunk blurred = blurredMap.get(chunkKey(original.cx, original.cy, original.cz)); + if (blurred == null) return; + + // Geblurrte Nachbarn für nahtlose Chunk-Grenzen im MC + VoxelChunk[] nb = getNeighbors(original.cx, original.cy, original.cz, blurredMap); + + Mesh[] meshes = { + MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 4, 0.4f), + MarchingCubes.smooth(MarchingCubes.build(blurred, 4, nb), 3, 0.4f), + MarchingCubes.smooth(MarchingCubes.build(blurred, 16, nb), 2, 0.4f), + }; + + BinaryExporter exp = BinaryExporter.getInstance(); + for (int lod = 0; lod < 3; lod++) { + if (meshes[lod] == null) continue; + Path p = VoxelChunkIO.getBakedPath(original.cx, original.cy, original.cz, lod); + Files.createDirectories(p.getParent()); + exp.save(meshes[lod], p.toFile()); + } + log.debug("Chunk ({},{},{}) gebacken.", original.cx, original.cy, original.cz); + } catch (Exception e) { + log.error("Bake fehlgeschlagen ({},{},{}): {}", + original.cx, original.cy, original.cz, e.getMessage()); + } + } + + /** + * Cross-boundary Lookup für den kooperativen Blur: + * Liest aus dem aktuellen Blur-Buffer des Nachbar-Chunks, oder – falls der + * Nachbar nicht in bufs ist – direkt aus dessen Original-Daten. + * Verwendet {@code src.idx()} da SIZE für alle Chunks identisch ist. + */ + private float getBlurBuf(Map bufs, Map originals, + VoxelChunk src, int x, int y, int z) { + int n = VoxelChunk.SIZE; + int cells = VoxelChunk.CELLS; + int ncx = src.cx, ncy = src.cy, ncz = src.cz; + int lx = x, ly = y, lz = z; + if (x < 0) { ncx--; lx = cells + x; } + else if (x >= n) { ncx++; lx = x - cells; } + if (y < 0) { ncy--; ly = cells + y; } + else if (y >= n) { ncy++; ly = y - cells; } + if (z < 0) { ncz--; lz = cells + z; } + else if (z >= n) { ncz++; lz = z - cells; } + long nk = chunkKey(ncx, ncy, ncz); + float[] buf = bufs.get(nk); + if (buf != null) return buf[src.idx(lx, ly, lz)]; + VoxelChunk nb = originals.get(nk); + return nb != null ? nb.getDensity(lx, ly, lz) : Byte.MIN_VALUE; + } + + private float computeFalloff(int mode, float t) { + return switch (mode) { + case de.blight.editor.tool.VoxelTool.MODE_SINUS -> (float) Math.cos(t * Math.PI / 2); + case de.blight.editor.tool.VoxelTool.MODE_SPIKE -> (1f - t) * (1f - t); + case de.blight.editor.tool.VoxelTool.MODE_SMOOTH -> (float)(0.5 * (1 + Math.cos(t * Math.PI))); + default -> 1f; // PLATEAU + }; + } + + // ── Intern: Chunk/Node-Verwaltung ───────────────────────────────────────── + + /** Gibt den VoxelChunk für (cx,cy,cz) zurück, lädt oder erstellt ihn – ohne Node. */ + private VoxelChunk getOrCreateChunk(int cx, int cy, int cz) { + long key = chunkKey(cx, cy, cz); + VoxelChunk chunk = chunks.get(key); + if (chunk != null) return chunk; - // Ggf. aus Datei laden - VoxelChunk chunk; if (VoxelChunkIO.exists(cx, cy, cz)) { try { chunk = VoxelChunkIO.load(cx, cy, cz); @@ -451,21 +1067,19 @@ public class VoxelEditorState extends BaseAppState { } else { chunk = new VoxelChunk(cx, cy, cz); } - chunks.put(key, chunk); - node = addNodeForChunk(key, chunk); - return node; + return chunk; } /** Erstellt den VoxelChunkNode und hängt ihn in den Szenen-Graph ein. */ private VoxelChunkNode addNodeForChunk(long key, VoxelChunk chunk) { VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial); - // LOD0 bauen - node.rebuildMesh(0); + if (tessellationEnabled) node.enablePatchMode(true); + VoxelChunk[] nb = getNeighbors(chunk.cx, chunk.cy, chunk.cz); + node.rebuildMesh(0, nb); node.setActiveLod(0); voxelRoot.attachChild(node); nodes.put(key, node); - // LOD1/2 für Hintergrund vormerken lodRebuildQueue.add(key); return node; } @@ -478,11 +1092,10 @@ public class VoxelEditorState extends BaseAppState { while ((key = lodRebuildQueue.poll()) != null) { VoxelChunk chunk = chunks.get(key); if (chunk == null) continue; - // LOD1 und LOD2 berechnen (kein JME-Context nötig – nur reine Berechnungen) - var mesh1 = MarchingCubes.build(chunk, 4); - var mesh2 = MarchingCubes.build(chunk, 16); + VoxelChunk[] nb = getNeighbors(chunk.cx, chunk.cy, chunk.cz); + var mesh1 = MarchingCubes.build(chunk, 4, nb); + var mesh2 = MarchingCubes.build(chunk, 16, nb); final Long fKey = key; - // Fertige Meshes an JME-Thread übergeben (kein zweites Build!) lodResultQueue.add(() -> { VoxelChunkNode node = nodes.get(fKey); if (node == null) return; @@ -519,22 +1132,111 @@ public class VoxelEditorState extends BaseAppState { private Material buildMaterial() { Material mat = new Material(assets, "MatDefs/Voxel.j3md"); - mat.setFloat("TexScale", 4f); + applyTextures(mat); + return mat; + } - String[] texPaths = input.terrainTexturePaths; - String[] slotNames = {"Tex0", "Tex1", "Tex2", "Tex3"}; - for (int i = 0; i < slotNames.length; i++) { - String path = (i < texPaths.length && texPaths[i] != null && !texPaths[i].isEmpty()) - ? texPaths[i] : "Common/Textures/MissingTexture.png"; - try { - Texture t = assets.loadTexture(path); - t.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(slotNames[i], t); - } catch (Exception e) { - log.warn("VoxelEditorState: Textur {} ({}) nicht ladbar: {}", slotNames[i], path, e.getMessage()); + private void applyTextures(Material mat) { + mat.setFloat("TexScale", 8f); + int[] slotIdxs = { input.voxelFlatSlot, input.voxelSteepSlot, input.voxelCeilSlot }; + String[] colSlots = { "TexFlat", "TexSteep", "TexCeil" }; + String[] normSlots = { "NormalMapFlat", "NormalMapSteep", "NormalMapCeil" }; + String[] dispSlots = { "DisplacementMapFlat","DisplacementMapSteep","DisplacementMapCeil" }; + int[][] fallbackRgb = { + {100, 130, 60}, + {110, 100, 90}, + { 70, 55, 45}, + }; + boolean anyDisp = false; + for (int i = 0; i < 3; i++) { + String texPath = slotTexPath(slotIdxs[i]); + String normPath = slotNormPath(slotIdxs[i]); + String dispPath = slotDispPath(slotIdxs[i]); + if (!texPath.isEmpty()) { + try { + Texture t = assets.loadTexture(texPath); + t.getImage().setColorSpace(com.jme3.texture.image.ColorSpace.Linear); + t.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(colSlots[i], t); + } catch (Exception e) { + log.warn("Voxel-Textur {} nicht ladbar: {}", texPath, e.getMessage()); + mat.setTexture(colSlots[i], solidColorTexture(fallbackRgb[i])); + } + } else { + mat.setTexture(colSlots[i], solidColorTexture(fallbackRgb[i])); + } + if (!normPath.isEmpty()) { + try { + Texture n = assets.loadTexture(normPath); + n.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(normSlots[i], n); + } catch (Exception e) { + mat.clearParam(normSlots[i]); + } + } else { + mat.clearParam(normSlots[i]); + } + if (!dispPath.isEmpty()) { + try { + Texture d = assets.loadTexture(dispPath); + d.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(dispSlots[i], d); + anyDisp = true; + } catch (Exception e) { + log.warn("Voxel-Displacement {} nicht ladbar: {}", dispPath, e.getMessage()); + mat.clearParam(dispSlots[i]); + } + } else { + mat.clearParam(dispSlots[i]); } } - return mat; + + // Tessellation-Technik ein-/ausschalten wenn sich der Displacement-Zustand ändert + if (anyDisp != tessellationEnabled) { + tessellationEnabled = anyDisp; + if (tessellationEnabled) { + mat.selectTechnique("Tessellation", app.getRenderManager()); + } else { + mat.selectTechnique("Default", app.getRenderManager()); + } + for (VoxelChunkNode node : nodes.values()) { + node.enablePatchMode(tessellationEnabled); + } + } + } + + private String slotTexPath(int slot) { + if (slot < 0) return ""; + if (slot < 4) { String p = input.terrainTexturePaths[slot]; return p != null ? p : ""; } + if (slot < 8) { String p = input.upperTexturePaths[slot - 4]; return p != null ? p : ""; } + if (slot < 12) { String p = input.thirdTexturePaths[slot - 8]; return p != null ? p : ""; } + return ""; + } + + private String slotNormPath(int slot) { + if (slot < 0) return ""; + if (slot < 4) { String p = input.terrainNormalMapPaths[slot]; return p != null ? p : ""; } + if (slot < 8) { String p = input.upperNormalMapPaths[slot - 4]; return p != null ? p : ""; } + if (slot < 12) { String p = input.thirdNormalMapPaths[slot - 8]; return p != null ? p : ""; } + return ""; + } + + private String slotDispPath(int slot) { + if (slot < 0) return ""; + if (slot < 4) { String p = input.terrainDisplacementMapPaths[slot]; return p != null ? p : ""; } + if (slot < 8) { String p = input.upperDisplacementMapPaths[slot - 4]; return p != null ? p : ""; } + if (slot < 12) { String p = input.thirdDisplacementMapPaths[slot - 8]; return p != null ? p : ""; } + return ""; + } + + private Texture solidColorTexture(int[] rgb) { + java.nio.ByteBuffer buf = com.jme3.util.BufferUtils.createByteBuffer(4); + buf.put((byte) rgb[0]).put((byte) rgb[1]).put((byte) rgb[2]).put((byte) 255); + buf.flip(); + com.jme3.texture.Texture2D tex = new com.jme3.texture.Texture2D( + new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf)); + tex.setWrap(Texture.WrapMode.Repeat); + return tex; } // ── Intern: Brush-Indikator ─────────────────────────────────────────────── @@ -553,7 +1255,8 @@ public class VoxelEditorState extends BaseAppState { } float jmeX = mx * (float) input.viewportScaleX; float jmeY = cam.getHeight() - my * (float) input.viewportScaleY; - Vector3f pos = raycast(jmeX, jmeY); + Hit hit = raycastHit(jmeX, jmeY); + Vector3f pos = hit != null ? hit.pos : null; if (pos != null) { float r = (float) input.voxelTool.brushRadius.getValue(); brushIndicator.setLocalTranslation(pos.x, pos.y + 0.3f, pos.z); @@ -597,7 +1300,24 @@ public class VoxelEditorState extends BaseAppState { // ── Hilfsmethode ───────────────────────────────────────────────────────── - private static long chunkKey(int cx, int cy, int cz) { + /** Liefert die 6 face-adjazenten Nachbar-Chunks [+X,-X,+Y,-Y,+Z,-Z] aus der chunks-Map. */ + private VoxelChunk[] getNeighbors(int cx, int cy, int cz) { + return getNeighbors(cx, cy, cz, chunks); + } + + private VoxelChunk[] getNeighbors(int cx, int cy, int cz, Map map) { + return new VoxelChunk[]{ + map.get(chunkKey(cx+1, cy, cz )), + map.get(chunkKey(cx-1, cy, cz )), + map.get(chunkKey(cx, cy+1, cz )), + map.get(chunkKey(cx, cy-1, cz )), + map.get(chunkKey(cx, cy, cz+1)), + map.get(chunkKey(cx, cy, cz-1)), + }; + } + + + public static long chunkKey(int cx, int cy, int cz) { return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32); } } diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java index 18d5b85..54c0f44 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java @@ -40,6 +40,6 @@ public class HeightTool extends EditorTool { @Override public List getParameters() { - return List.of(brushRadius, brushStrength); + return List.of(brushRadius, brushStrength, plateauHeight); } } diff --git a/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java index f1b2090..3a3798c 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java @@ -9,8 +9,8 @@ import java.util.List; */ public class TextureTool extends EditorTool { - // Slots 1-4 = lower splatmap, Slots 5-8 = upper splatmap - public static final String[] TEXTURE_NAMES = {"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"}; + // Slots 1-4 = lower splatmap, Slots 5-8 = upper splatmap, Slots 9-12 = third splatmap + public static final String[] TEXTURE_NAMES = {"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "S11", "S12"}; public final ChoiceToolParameter textureIndex = new ChoiceToolParameter( "Textur", TEXTURE_NAMES, 0 diff --git a/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java b/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java index ca7425c..f935c71 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java @@ -3,11 +3,16 @@ package de.blight.editor.tool; import java.util.List; /** - * Voxel-Klippen/Höhlen-Werkzeug. + * Voxel-Werkzeug für Klippen und Höhlen. * - * Modi 0-3 fügen Solid-Voxel oberhalb des Terrains hinzu (gleiche Pinselformen wie das Höhen-Tool). - * Voxel, die unterhalb der Terrain-Oberfläche landen, werden automatisch entfernt. - * Modus 4 entfernt Voxel – zum Aushöhlen von Höhlen unterhalb des Terrains. + * Modi 0-3 (Sinus/Spike/Plateau/Smooth): Säulen nach oben – für Felstürme, Plateaus. + * Modus 4 (Klippe): Kugel-Pinsel ohne Terrain-Cleanup. + * Modus 5 (Aushöhlen): Entfernt Voxel. + * + * Texturierung erfolgt automatisch anhand der Flächennormale: + * Normal.y > 0.5 → TexFlat (flache Flächen) + * |Normal.y| ≤ 0.5 → TexSteep (steile Wände + Tunneldecke) + * Normal.y < -0.5 → TexCeil (Tunneldecke / Unterseiten) */ public class VoxelTool extends EditorTool { @@ -15,14 +20,13 @@ public class VoxelTool extends EditorTool { public static final int MODE_SPIKE = 1; public static final int MODE_PLATEAU = 2; public static final int MODE_SMOOTH = 3; - public static final int MODE_REMOVE = 4; - /** Einfaches Hinzufügen: gleichmäßige Dichte überall im Pinsel, kein Terrain-Check. */ - public static final int MODE_ADD = 5; + public static final int MODE_ADD = 4; + public static final int MODE_REMOVE = 5; public final ChoiceToolParameter mode = new ChoiceToolParameter( "Modus", - new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Entfernen", "Hinzufügen"}, - MODE_SPIKE, + new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Klippe", "Aushöhlen"}, + MODE_ADD, new String[]{ "img/editor/terraintool_sinus.png", "img/editor/terraintool_spike.png", @@ -33,22 +37,20 @@ public class VoxelTool extends EditorTool { } ); - public final ChoiceToolParameter textureSlot = new ChoiceToolParameter( - "Textur", new String[]{"S1", "S2", "S3", "S4"}, 0 - ); - public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 0.5, 30.0); public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0); + public final ToolParameter plateauTarget = new ToolParameter("Plateau-Ziel", 0.0, -200.0, 500.0); + public volatile boolean plateauTargetChanged = false; @Override public String getName() { return "Voxel"; } @Override public List getChoiceParameters() { - return List.of(mode, textureSlot); + return List.of(mode); } @Override public List getParameters() { - return List.of(brushRadius, brushStrength); + return List.of(brushRadius, brushStrength, plateauTarget); } } diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java index 27d2cca..4140d81 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java @@ -26,8 +26,8 @@ public class PalmOptions { public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f; // Textures - public String barkTexture = "Textures/bark/Bark_Palm.png"; - public String leafTexture = "Textures/leaves/palm.png"; + public String barkTexture = "Textures/internal/bark/Bark_Palm.png"; + public String leafTexture = "Textures/internal/leaves/palm.png"; public float leafTextureAspect = 0f; // width/length-Verhältnis der Textur, 0 = frondWidth nutzen public float gravity = 0.3f; // Durchhang: 0 = gerade, höher = stärker hängend @@ -35,7 +35,7 @@ public class PalmOptions { public float crownHeight = 1.2f; public float crownFlare = 1.6f; // max. Aufweitung = trunkRadiusTop × crownFlare public float crownR = 0.22f, crownG = 0.58f, crownB = 0.14f; - public String crownTexture = "Textures/leaves/palmcrown.png"; + public String crownTexture = "Textures/internal/leaves/palmcrown.png"; public PalmOptions copy() { diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java index 3b3258a..a35c89d 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java @@ -42,8 +42,8 @@ public class TreeParams { public int leafBranchings = 1; // ── Texturen (relative Pfade für den Asset-Manager) ────────────────────── - public String barkTexture = null; // z.B. "Textures/bark/Bark001_Color.jpg" - public String leafTexture = null; // z.B. "Textures/leaves/oak.png" + public String barkTexture = null; // z.B. "Textures/internal/bark/Bark001_Color.jpg" + public String leafTexture = null; // z.B. "Textures/internal/leaves/oak.png" // ── Wind ───────────────────────────────────────────────────────────────── public float trunkFlexibility = 0.05f; @@ -67,8 +67,8 @@ public class TreeParams { p.gnarliness= new float[]{ 0.00f, 0.10f, 0.15f, 0.09f }; p.leafScale = 1.4f; p.leafCount = 6; p.leafAngle = 42f; p.leafBranchings = 2; p.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f; - p.barkTexture = "Textures/bark/Bark001_Color.jpg"; - p.leafTexture = "Textures/leaves/oak.png"; + p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; + p.leafTexture = "Textures/internal/leaves/oak.png"; return p; } @@ -88,8 +88,8 @@ public class TreeParams { p.gnarliness= new float[]{ 0.00f, 0.05f, 0.10f, 0.04f }; p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1; p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f; - p.barkTexture = "Textures/bark/Bark002_Color.jpg"; - p.leafTexture = "Textures/leaves/aspen.png"; + p.barkTexture = "Textures/internal/bark/Bark002_Color.jpg"; + p.leafTexture = "Textures/internal/leaves/aspen.png"; return p; } @@ -109,8 +109,8 @@ public class TreeParams { p.gnarliness= new float[]{ 0.00f, 0.03f, 0.08f, 0.02f }; p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1; p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f; - p.barkTexture = "Textures/bark/Bark003_Color.jpg"; - p.leafTexture = "Textures/leaves/pine.png"; + p.barkTexture = "Textures/internal/bark/Bark003_Color.jpg"; + p.leafTexture = "Textures/internal/leaves/pine.png"; return p; } @@ -130,8 +130,8 @@ public class TreeParams { p.gnarliness= new float[]{ 0.00f, 0.25f, 0.35f, 0.15f }; p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2; p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f; - p.barkTexture = "Textures/bark/Bark001_Color.jpg"; - p.leafTexture = "Textures/leaves/ash.png"; + p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; + p.leafTexture = "Textures/internal/leaves/ash.png"; return p; } @@ -151,8 +151,8 @@ public class TreeParams { p.gnarliness= new float[]{ 0.00f, 0.15f, 0.25f, 0.10f }; p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2; p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f; - p.barkTexture = "Textures/bark/Bark001_Color.jpg"; - p.leafTexture = "Textures/leaves/ash.png"; + p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; + p.leafTexture = "Textures/internal/leaves/ash.png"; return p; } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/TextureChooser.java b/blight-editor/src/main/java/de/blight/editor/ui/TextureChooser.java index 9f22c15..1aee72d 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/TextureChooser.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/TextureChooser.java @@ -8,7 +8,11 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.stage.Modality; +import com.google.gson.Gson; +import de.blight.editor.TextureMapData; + import java.io.InputStream; +import java.io.Reader; import java.nio.file.*; import java.util.*; import java.util.stream.Stream; @@ -145,32 +149,78 @@ public class TextureChooser extends Dialog { // ── User textures ───────────────────────────────────────────────────────── - private void addUserTextures(FlowPane pane, Path texturesDir, Path assetRoot) { - try (Stream walk = Files.walk(texturesDir)) { - walk.filter(Files::isRegularFile) - .filter(p -> { - String low = p.getFileName().toString().toLowerCase(); - return IMAGE_EXTS.stream().anyMatch(low::endsWith); - }) - .sorted() - .forEach(file -> { - String assetPath = assetRoot.relativize(file) - .toString().replace('\\', '/'); - Image img; - try { - img = new Image(file.toUri().toString(), - THUMB_SIZE, THUMB_SIZE, true, true, true); - } catch (Exception e) { - img = null; + private static final Gson GSON = new Gson(); + + private void addUserTextures(FlowPane pane, Path dir, Path assetRoot) { + try { + List entries = Files.list(dir) + .filter(p -> !p.getFileName().toString().startsWith(".")) + .filter(p -> !p.getFileName().toString().equalsIgnoreCase("internal")) + .sorted(Comparator.comparing(p -> p.getFileName().toString().toLowerCase())) + .collect(java.util.stream.Collectors.toList()); + + for (Path entry : entries) { + if (Files.isDirectory(entry)) { + Path setJson = entry.resolve("textureset.json"); + if (Files.exists(setJson)) { + addTextureSetCard(pane, entry, setJson, assetRoot); + } else { + // Rekursiv in reguläre Unterordner absteigen + addUserTextures(pane, entry, assetRoot); } - String name = file.getFileName().toString(); - ToggleButton card = buildCard(name, assetPath, img); - pane.getChildren().add(card); - allCards.add(card); - }); + } else if (Files.isRegularFile(entry) && isImageFile(entry)) { + addSingleCard(pane, entry, entry.getFileName().toString(), assetRoot); + } + } } catch (Exception ignored) {} } + /** Fügt eine Karte für ein Textur-Set hinzu: zeigt die colorMap als Vorschau. */ + private void addTextureSetCard(FlowPane pane, Path setDir, Path setJson, Path assetRoot) { + Path colorFile = null; + try (Reader r = Files.newBufferedReader(setJson)) { + TextureMapData meta = GSON.fromJson(r, TextureMapData.class); + if (meta != null && !meta.colorMap.isEmpty()) { + Path candidate = assetRoot.resolve( + meta.colorMap.replace('/', java.io.File.separatorChar)); + if (Files.exists(candidate)) colorFile = candidate; + } + } catch (Exception ignored) {} + + if (colorFile == null) { + // Fallback: Datei mit _Color-Suffix suchen + try (Stream sub = Files.list(setDir)) { + colorFile = sub.filter(f -> { + String base = f.getFileName().toString().toLowerCase().replaceFirst("\\.[^.]+$", ""); + return isImageFile(f) && (base.endsWith("_color") || base.endsWith("_colour") + || base.endsWith("_albedo") || base.endsWith("_diffuse")); + }).findFirst().orElse(null); + } catch (Exception ignored) {} + } + + if (colorFile != null) { + addSingleCard(pane, colorFile, setDir.getFileName().toString(), assetRoot); + } + } + + private void addSingleCard(FlowPane pane, Path file, String displayName, Path assetRoot) { + String assetPath = assetRoot.relativize(file).toString().replace('\\', '/'); + Image img; + try { + img = new Image(file.toUri().toString(), THUMB_SIZE, THUMB_SIZE, true, true, true); + } catch (Exception e) { + img = null; + } + ToggleButton card = buildCard(displayName, assetPath, img); + pane.getChildren().add(card); + allCards.add(card); + } + + private static boolean isImageFile(Path p) { + String lo = p.getFileName().toString().toLowerCase(); + return IMAGE_EXTS.stream().anyMatch(lo::endsWith); + } + // ── JME textures ────────────────────────────────────────────────────────── private ToggleButton buildJmeCard(String jmePath) { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/TextureDetailDialog.java b/blight-editor/src/main/java/de/blight/editor/ui/TextureDetailDialog.java new file mode 100644 index 0000000..9f1c7ea --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ui/TextureDetailDialog.java @@ -0,0 +1,427 @@ +package de.blight.editor.ui; + +import com.google.gson.GsonBuilder; +import de.blight.editor.TextureMapData; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.file.*; +import java.util.function.Consumer; + +/** + * Detaildialog für Texturen und Textur-Sets im Asset-Baum. + * + * SET-Modus (setDir != null): Zeigt alle 5 Map-Slots des textureset.json, + * erlaubt Ersetzen und Entfernen jeder Map. + * DATEI-Modus (file != null): Zeigt die Einzel-Textur mit Details; + * Zuweisung einer weiteren Map konvertiert + * automatisch in ein Textur-Set. + * + * Gibt {@code true} zurück, wenn sich Dateien/Metadaten geändert haben. + */ +public class TextureDetailDialog extends Dialog { + + private static final Logger log = LoggerFactory.getLogger(TextureDetailDialog.class); + + private static final String[] SLOT_LABELS = {"Farbe", "Normal", "Displacement", "Roughness", "AO"}; + private static final String[] SLOT_SUFFIXES = {"_Color", "_NormalGL", "_Displacement", "_Roughness", "_AmbientOcclusion"}; + + private final Path assetRoot; + private final Path setDir; // null im Datei-Modus + private final Path singleFile; // null im Set-Modus + private final boolean readOnly; + + private TextureMapData meta; + private boolean changed = false; + + // Callback, wenn eine Einzel-Datei in ein Set umgewandelt wurde + // → EditorApp kann den Baum-Eintrag entsprechend aktualisieren + private Consumer onConvertedToSet = null; + + // ── Slot-UI ─────────────────────────────────────────────────────────────── + private final Path[] slotPaths = new Path[5]; + private final ImageView[] thumbs = new ImageView[5]; + private final Label[] nameLabels = new Label[5]; + private final Label[] infoLabels = new Label[5]; + private final Button[] removeBtns = new Button[5]; + + // ── Fabrik-Methoden ─────────────────────────────────────────────────────── + + /** Öffnet den Dialog für ein Textur-Set (Ordner mit textureset.json). */ + public static TextureDetailDialog forSet(Path assetRoot, Path setDir, boolean readOnly) { + return new TextureDetailDialog(assetRoot, setDir, null, readOnly); + } + + /** Öffnet den Dialog für eine einzelne Textur-Datei. */ + public static TextureDetailDialog forFile(Path assetRoot, Path imageFile, boolean readOnly) { + return new TextureDetailDialog(assetRoot, null, imageFile, readOnly); + } + + /** Callback wird aufgerufen wenn eine Einzel-Textur in ein Set gewandelt wird. + * Parameter: der neue Set-Ordner. */ + public void setOnConvertedToSet(Consumer cb) { this.onConvertedToSet = cb; } + + // ── Konstruktor ─────────────────────────────────────────────────────────── + + private TextureDetailDialog(Path assetRoot, Path setDir, Path singleFile, boolean readOnly) { + this.assetRoot = assetRoot; + this.setDir = setDir; + this.singleFile = singleFile; + this.readOnly = readOnly; + + initModality(Modality.APPLICATION_MODAL); + setResizable(true); + + String roSuffix = readOnly ? " (schreibgeschützt)" : ""; + if (setDir != null) { + setTitle("Textur-Set: " + setDir.getFileName() + roSuffix); + meta = loadMeta(setDir); + buildSetContent(); + } else { + setTitle("Textur: " + singleFile.getFileName() + roSuffix); + buildFileContent(); + } + + getDialogPane().getButtonTypes().add(ButtonType.CLOSE); + setResultConverter(btn -> changed); + } + + // ── Set-Modus ───────────────────────────────────────────────────────────── + + private void buildSetContent() { + slotPaths[0] = resolvePath(meta.colorMap); + slotPaths[1] = resolvePath(meta.normalMap); + slotPaths[2] = resolvePath(meta.displacementMap); + slotPaths[3] = resolvePath(meta.roughnessMap); + slotPaths[4] = resolvePath(meta.aoMap); + + GridPane grid = buildSlotGrid(0, 5, false); + ScrollPane scroll = new ScrollPane(grid); + scroll.setFitToWidth(true); + scroll.setPrefHeight(460); + + getDialogPane().setContent(scroll); + getDialogPane().setPrefWidth(720); + } + + // ── Datei-Modus ─────────────────────────────────────────────────────────── + + private void buildFileContent() { + slotPaths[0] = singleFile; + // Weitere Slots leer + for (int i = 1; i < 5; i++) slotPaths[i] = null; + + Label hint = new Label("Durch Zuweisen einer weiteren Map wird diese Textur automatisch\n" + + "in ein Textur-Set (Ordner + textureset.json) umgewandelt."); + hint.setStyle("-fx-font-style:italic; -fx-opacity:0.7;"); + hint.setWrapText(true); + + GridPane grid = buildSlotGrid(0, 1, true); // Farbe-Slot, kein Entfernen + GridPane extraGrid = buildSlotGrid(1, 5, false); // Normal + Rest + extraGrid.setPadding(new Insets(8, 0, 0, 0)); + + Separator sep = new Separator(); + Label extraLbl = new Label("Weitere Maps:"); + extraLbl.setStyle("-fx-font-weight:bold;"); + + VBox root = new VBox(8, grid, new VBox(6, sep, extraLbl, hint, extraGrid)); + root.setPadding(new Insets(10)); + + getDialogPane().setContent(root); + getDialogPane().setPrefWidth(720); + } + + // ── Slot-Grid-Builder ───────────────────────────────────────────────────── + + private GridPane buildSlotGrid(int from, int to, boolean noRemove) { + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(10)); + + ColumnConstraints c0 = new ColumnConstraints(100); + ColumnConstraints c1 = new ColumnConstraints(80); + ColumnConstraints c2 = new ColumnConstraints(); c2.setHgrow(Priority.ALWAYS); + ColumnConstraints c3 = new ColumnConstraints(110); + grid.getColumnConstraints().addAll(c0, c1, c2, c3); + + for (int i = from; i < to; i++) { + final int idx = i; + + Label lbl = new Label(SLOT_LABELS[i] + ":"); + lbl.setStyle("-fx-font-weight:bold;"); + lbl.setAlignment(Pos.CENTER_RIGHT); + lbl.setMaxWidth(Double.MAX_VALUE); + + ImageView iv = new ImageView(); + iv.setFitWidth(72); iv.setFitHeight(72); + iv.setPreserveRatio(true); + thumbs[i] = iv; + + Label nl = new Label(); + nl.setWrapText(true); + nameLabels[i] = nl; + + Label il = new Label(); + il.setStyle("-fx-font-size:10; -fx-text-fill:#666;"); + infoLabels[i] = il; + + VBox info = new VBox(3, nl, il); + info.setAlignment(Pos.CENTER_LEFT); + + Button replaceBtn = new Button("Ersetzen…"); + replaceBtn.setMaxWidth(Double.MAX_VALUE); + replaceBtn.setOnAction(e -> replaceSlot(idx)); + replaceBtn.setDisable(readOnly); + + Button removeBtn = new Button("Entfernen"); + removeBtn.setMaxWidth(Double.MAX_VALUE); + removeBtn.setStyle("-fx-text-fill:#c0392b;"); + removeBtn.setOnAction(e -> removeSlot(idx)); + if (readOnly || noRemove || i == 0) removeBtn.setDisable(true); + removeBtns[i] = removeBtn; + + VBox btns = new VBox(4, replaceBtn, removeBtn); + + int row = i - from; + grid.add(lbl, 0, row); + grid.add(iv, 1, row); + grid.add(info, 2, row); + grid.add(btns, 3, row); + + refreshSlotUI(i); + } + return grid; + } + + // ── Slot-Aktionen ───────────────────────────────────────────────────────── + + private void replaceSlot(int idx) { + FileChooser fc = new FileChooser(); + fc.setTitle(SLOT_LABELS[idx] + " auswählen"); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter( + "Bilder", "*.png", "*.jpg", "*.jpeg", "*.tga", "*.bmp")); + File src = fc.showOpenDialog(getOwner()); + if (src == null) return; + + // Ziel-Ordner bestimmen (Set-Ordner oder ggf. neu erstellen) + Path targetDir = resolveTargetDir(idx); + if (targetDir == null) return; + + String outName = targetDir.getFileName() + SLOT_SUFFIXES[idx] + ".png"; + Path dest = targetDir.resolve(outName); + + try { + convertToPng(src.toPath(), dest); + } catch (Exception ex) { + log.error("Konvertierung fehlgeschlagen: {}", ex.getMessage(), ex); + showError("Konvertierung fehlgeschlagen", ex.getMessage()); + return; + } + + slotPaths[idx] = dest; + String relPath = assetRoot.relativize(dest).toString().replace('\\', '/'); + applyToMeta(idx, relPath); + saveMeta(targetDir); + refreshSlotUI(idx); + changed = true; + } + + private void removeSlot(int idx) { + if (idx == 0) return; + slotPaths[idx] = null; + applyToMeta(idx, ""); + Path dir = setDir != null ? setDir : (singleFile != null ? singleFile.getParent() : null); + if (dir != null) saveMeta(dir); + refreshSlotUI(idx); + changed = true; + } + + /** + * Bestimmt den Zielordner für eine Slot-Ersetzung. + * Im Set-Modus: setDir. + * Im Datei-Modus + Farb-Slot: Datei-Elternordner. + * Im Datei-Modus + Nicht-Farb-Slot: Neuen Set-Ordner anlegen und Farb-Datei hineinverschieben. + */ + private Path resolveTargetDir(int idx) { + if (setDir != null) return setDir; + + if (idx == 0) { + // Farbe ersetzen ohne Set-Konvertierung + return singleFile.getParent(); + } + + // Nicht-Farb-Slot im Datei-Modus → in Set umwandeln + String baseName = singleFile.getFileName().toString().replaceFirst("\\.[^.]+$", ""); + Path newSetDir = singleFile.getParent().resolve(baseName); + try { + Files.createDirectories(newSetDir); + // Farb-Datei verschieben + Path newColor = newSetDir.resolve(singleFile.getFileName()); + if (!Files.exists(newColor)) Files.move(singleFile, newColor); + slotPaths[0] = newColor; + + // Meta initialisieren + meta = new TextureMapData(); + meta.colorMap = assetRoot.relativize(newColor).toString().replace('\\', '/'); + + if (onConvertedToSet != null) onConvertedToSet.accept(newSetDir); + changed = true; + } catch (IOException ex) { + log.error("Set-Ordner konnte nicht erstellt werden: {}", ex.getMessage(), ex); + showError("Fehler", "Set-Ordner konnte nicht erstellt werden:\n" + ex.getMessage()); + return null; + } + return newSetDir; + } + + // ── UI-Refresh ──────────────────────────────────────────────────────────── + + private void refreshSlotUI(int idx) { + Path p = slotPaths[idx]; + if (p == null || !Files.exists(p)) { + thumbs[idx].setImage(null); + nameLabels[idx].setText("–"); + nameLabels[idx].setStyle("-fx-opacity:0.45; -fx-font-style:italic;"); + infoLabels[idx].setText(""); + if (removeBtns[idx] != null && idx != 0) removeBtns[idx].setDisable(true); + } else { + try { thumbs[idx].setImage(new Image(p.toUri().toString(), 72, 72, true, true, true)); } + catch (Exception ignored) {} + nameLabels[idx].setText(p.getFileName().toString()); + nameLabels[idx].setStyle(""); + infoLabels[idx].setText(fileInfo(p)); + if (removeBtns[idx] != null && idx != 0) removeBtns[idx].setDisable(false); + } + } + + // ── Hilfsmethoden ───────────────────────────────────────────────────────── + + private String fileInfo(Path p) { + StringBuilder sb = new StringBuilder(); + try { + long bytes = Files.size(p); + if (bytes < 1024) sb.append(bytes).append(" B"); + else if (bytes < 1024 * 1024) sb.append(String.format("%.1f KB", bytes / 1024.0)); + else sb.append(String.format("%.1f MB", bytes / (1024.0 * 1024))); + } catch (Exception ignored) {} + try { + BufferedImage bi = ImageIO.read(p.toFile()); + if (bi != null) sb.append(" ").append(bi.getWidth()).append(" × ").append(bi.getHeight()).append(" px"); + } catch (Exception ignored) {} + return sb.toString(); + } + + private Path resolvePath(String rel) { + if (rel == null || rel.isBlank()) return null; + Path p = assetRoot.resolve(rel.replace('/', File.separatorChar)); + return Files.exists(p) ? p : null; + } + + private void applyToMeta(int idx, String relPath) { + if (meta == null) meta = new TextureMapData(); + switch (idx) { + case 0 -> meta.colorMap = relPath; + case 1 -> meta.normalMap = relPath; + case 2 -> meta.displacementMap = relPath; + case 3 -> meta.roughnessMap = relPath; + case 4 -> meta.aoMap = relPath; + } + } + + private TextureMapData loadMeta(Path dir) { + Path jsonFile = dir.resolve("textureset.json"); + if (!Files.exists(jsonFile)) return new TextureMapData(); + try (Reader r = Files.newBufferedReader(jsonFile)) { + TextureMapData d = new com.google.gson.Gson().fromJson(r, TextureMapData.class); + return d != null ? d : new TextureMapData(); + } catch (Exception e) { + return new TextureMapData(); + } + } + + private void saveMeta(Path dir) { + if (meta == null) meta = new TextureMapData(); + Path jsonFile = dir.resolve("textureset.json"); + try { + String json = new GsonBuilder().setPrettyPrinting().create().toJson(meta); + Files.writeString(jsonFile, json); + } catch (Exception ex) { + log.error("textureset.json konnte nicht gespeichert werden: {}", ex.getMessage(), ex); + } + } + + private void showError(String header, String msg) { + Alert a = new Alert(Alert.AlertType.ERROR); + a.setTitle("Fehler"); a.setHeaderText(header); a.setContentText(msg); + if (getOwner() != null) a.initOwner(getOwner()); + a.showAndWait(); + } + + // ── PNG-Konvertierung (analog TextureImportDialog) ──────────────────────── + + private static void convertToPng(Path src, Path dst) throws IOException { + String lo = src.getFileName().toString().toLowerCase(); + if (lo.endsWith(".png")) { Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); return; } + BufferedImage img = lo.endsWith(".tga") ? loadTga(src.toFile()) : ImageIO.read(src.toFile()); + if (img == null) throw new IOException("Format nicht lesbar: " + src.getFileName()); + if (img.getType() != BufferedImage.TYPE_INT_ARGB) { + BufferedImage rgba = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = rgba.createGraphics(); + g.drawImage(img, 0, 0, null); g.dispose(); + img = rgba; + } + ImageIO.write(img, "PNG", dst.toFile()); + } + + private static BufferedImage loadTga(File f) throws IOException { + com.jme3.texture.plugins.TGALoader loader = new com.jme3.texture.plugins.TGALoader(); + com.jme3.asset.TextureKey key = new com.jme3.asset.TextureKey(f.getName(), true); + com.jme3.asset.AssetInfo info = new com.jme3.asset.AssetInfo(null, key) { + @Override public InputStream openStream() { + try { return new FileInputStream(f); } + catch (FileNotFoundException ex) { throw new RuntimeException(ex); } + } + }; + com.jme3.texture.Image ji = (com.jme3.texture.Image) loader.load(info); + int w = ji.getWidth(), h = ji.getHeight(); + ByteBuffer buf = ji.getData(0); buf.rewind(); + int[] px = new int[w * h]; + com.jme3.texture.Image.Format fmt = ji.getFormat(); + for (int i = 0; i < w * h; i++) { + int r, g, b, a; + switch (fmt) { + case RGBA8 -> { r=buf.get()&0xFF; g=buf.get()&0xFF; b=buf.get()&0xFF; a=buf.get()&0xFF; } + case RGB8 -> { r=buf.get()&0xFF; g=buf.get()&0xFF; b=buf.get()&0xFF; a=255; } + case BGR8 -> { b=buf.get()&0xFF; g=buf.get()&0xFF; r=buf.get()&0xFF; a=255; } + case BGRA8 -> { b=buf.get()&0xFF; g=buf.get()&0xFF; r=buf.get()&0xFF; a=buf.get()&0xFF; } + default -> { r=g=b=128; a=255; } + } + px[i] = (a<<24)|(r<<16)|(g<<8)|b; + } + // JME3 TGALoader liefert Daten bottom-to-top → vertikal spiegeln + for (int y = 0; y < h / 2; y++) { + int mirror = h - 1 - y; + for (int x = 0; x < w; x++) { + int tmp = px[y * w + x]; + px[y * w + x] = px[mirror * w + x]; + px[mirror * w + x] = tmp; + } + } + BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + bi.setRGB(0, 0, w, h, px, 0, w); + return bi; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/ui/TextureImportDialog.java b/blight-editor/src/main/java/de/blight/editor/ui/TextureImportDialog.java new file mode 100644 index 0000000..e0ecfc4 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ui/TextureImportDialog.java @@ -0,0 +1,510 @@ +package de.blight.editor.ui; + +import com.google.gson.GsonBuilder; +import de.blight.editor.TextureMapData; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.Modality; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.file.*; +import java.util.*; + +/** + * Dialog zum Importieren externer Textur-Sets (TGA, PNG, JPG, BMP). + * + * Scannt einen Ordner, erkennt Map-Typen anhand von Dateinamen-Schlüsselwörtern, + * konvertiert TGA/JPG/BMP nach PNG und schreibt das fertige Set inkl. textureset.json + * in Textures/{subfolder}/{setName}/. + */ +public class TextureImportDialog extends Dialog { + + private static final Logger log = LoggerFactory.getLogger(TextureImportDialog.class); + + // Map-Slot-Definitionen + private static final String[] LABELS = { "Farbe (Diffuse)", "Normal", "Displacement", "Roughness", "AO" }; + private static final String[][] KEYWORDS = { + { "diffus", "diffuse", "color", "colour", "albedo", "col", "base" }, + { "normal", "norm", "nrm", "nor" }, + { "displacement", "disp", "height", "bump" }, + { "roughness", "rough", "rgh", "specular", "spec" }, + { "ao", "ambient", "occlusion" } + }; + private static final String[] SUFFIXES = { + "_Color", "_NormalGL", "_Displacement", "_Roughness", "_AmbientOcclusion" + }; + + private final Path assetRoot; + private final Path[] slots = new Path[5]; + private final ImageView[] thumbs = new ImageView[5]; + private final Label[] fileLabels = new Label[5]; + private final TextField setName = new TextField(); + private final ComboBox subfolder = new ComboBox<>(); + private final Label status = new Label(); + private final CheckBox weitereChk = new CheckBox("Weitere importieren"); + private Path lastImportedDir = null; + + /** Gibt den zuletzt erfolgreich importierten Set-Ordner zurück (null wenn keiner). */ + public Path getLastImportedDir() { return lastImportedDir; } + + /** + * @param assetRoot Pfad zum blight-assets Verzeichnis + */ + public TextureImportDialog(Path assetRoot) { + this.assetRoot = assetRoot; + setTitle("Textur-Set importieren"); + initModality(Modality.APPLICATION_MODAL); + setResizable(true); + + VBox root = new VBox(10); + root.setPadding(new Insets(14)); + + // ── Set-Name + Unterordner ──────────────────────────────────────────── + HBox nameRow = new HBox(10); + nameRow.setAlignment(Pos.CENTER_LEFT); + Label nl = new Label("Set-Name:"); nl.setMinWidth(110); + setName.setPromptText("z.B. gras6"); + setName.setPrefWidth(160); + Label sl = new Label("Unterordner:"); sl.setMinWidth(90); + subfolder.setPrefWidth(130); + subfolder.setEditable(false); + subfolder.setPromptText("Unterordner wählen…"); + loadSubfolders(); + + Button newFolderBtn = new Button("+"); + newFolderBtn.setTooltip(new Tooltip("Neuen Unterordner anlegen")); + newFolderBtn.setOnAction(e -> createSubfolder()); + + nameRow.getChildren().addAll(nl, setName, sl, subfolder, newFolderBtn); + + // ── Ordner-Scan ─────────────────────────────────────────────────────── + Button scanBtn = new Button("Ordner scannen…"); + scanBtn.setOnAction(e -> { + DirectoryChooser dc = new DirectoryChooser(); + dc.setTitle("Textur-Ordner wählen"); + File dir = dc.showDialog(getOwner()); + if (dir != null) scanFolder(dir.toPath()); + }); + scanBtn.setMaxWidth(Double.MAX_VALUE); + + // ── Map-Slots ───────────────────────────────────────────────────────── + GridPane grid = new GridPane(); + grid.setHgap(8); + grid.setVgap(6); + grid.setPadding(new Insets(4, 0, 4, 0)); + ColumnConstraints col0 = new ColumnConstraints(125); + ColumnConstraints col1 = new ColumnConstraints(68); + ColumnConstraints col2 = new ColumnConstraints(); col2.setHgrow(Priority.ALWAYS); + ColumnConstraints col3 = new ColumnConstraints(36); + ColumnConstraints col4 = new ColumnConstraints(28); + grid.getColumnConstraints().addAll(col0, col1, col2, col3, col4); + + for (int i = 0; i < 5; i++) { + final int idx = i; + Label typeLabel = new Label(LABELS[i] + ":"); + + ImageView iv = new ImageView(); + iv.setFitWidth(60); iv.setFitHeight(60); + iv.setPreserveRatio(true); + iv.setStyle("-fx-background-color:#444;"); + thumbs[i] = iv; + + Label fl = new Label("–"); + fl.setStyle("-fx-font-style:italic; -fx-opacity:0.5;"); + fl.setMaxWidth(Double.MAX_VALUE); + fileLabels[i] = fl; + + Button choose = new Button("…"); + choose.setOnAction(e -> pickFile(idx)); + + Button clear = new Button("✕"); + clear.setOnAction(e -> clearSlot(idx)); + + grid.add(typeLabel, 0, i); + grid.add(iv, 1, i); + grid.add(fl, 2, i); + grid.add(choose, 3, i); + grid.add(clear, 4, i); + } + + // ── Status ──────────────────────────────────────────────────────────── + status.setStyle("-fx-font-style:italic;"); + status.setWrapText(true); + + root.getChildren().addAll( + new Label("Import-Einstellungen:"), + nameRow, + new Separator(), + scanBtn, + grid, + new Separator(), + status + ); + + getDialogPane().setContent(root); + getDialogPane().setPrefWidth(720); + getDialogPane().getButtonTypes().addAll(ButtonType.APPLY, ButtonType.CANCEL); + + // APPLY → Import ausführen + Button importBtn = (Button) getDialogPane().lookupButton(ButtonType.APPLY); + importBtn.setText("Importieren"); + importBtn.setDefaultButton(true); + importBtn.addEventFilter(javafx.event.ActionEvent.ACTION, e -> { + e.consume(); + runImport(); + }); + + // Checkbox in die Button-Leiste einfügen (links), sobald die Scene aufgebaut ist + getDialogPane().sceneProperty().addListener((obs, oldScene, newScene) -> { + if (newScene == null) return; + ButtonBar bb = (ButtonBar) getDialogPane().lookup(".button-bar"); + if (bb != null) { + ButtonBar.setButtonData(weitereChk, ButtonBar.ButtonData.LEFT); + bb.getButtons().add(0, weitereChk); + } + }); + + setResultConverter(btn -> lastImportedDir != null); + } + + // ── Ordner-Scan ─────────────────────────────────────────────────────────── + + private void scanFolder(Path dir) { + try { + List files = Files.list(dir) + .filter(Files::isRegularFile) + .filter(this::isImage) + .sorted(Comparator.comparing(p -> p.getFileName().toString().toLowerCase())) + .toList(); + + setName.setText(guessName(files, dir)); + + Arrays.fill(slots, null); + for (int i = 0; i < 5; i++) { thumbs[i].setImage(null); fileLabels[i].setText("–"); fileLabels[i].setStyle("-fx-font-style:italic; -fx-opacity:0.5;"); } + + for (Path file : files) { + String stem = file.getFileName().toString().toLowerCase().replaceFirst("\\.[^.]+$", ""); + for (int i = 0; i < KEYWORDS.length; i++) { + if (slots[i] != null) continue; + for (String kw : KEYWORDS[i]) { + if (stem.contains(kw)) { assignSlot(i, file); break; } + } + } + } + status("Gescannt: " + files.size() + " Dateien, " + countFilled() + " Slots erkannt."); + } catch (Exception ex) { + log.error("Fehler beim Scannen von {}: {}", dir, ex.getMessage(), ex); + } + } + + // ── Slot-Verwaltung ─────────────────────────────────────────────────────── + + private void pickFile(int idx) { + FileChooser fc = new FileChooser(); + fc.setTitle(LABELS[idx] + " wählen"); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter( + "Texturen", "*.png","*.jpg","*.jpeg","*.tga","*.bmp")); + File f = fc.showOpenDialog(getOwner()); + if (f != null) assignSlot(idx, f.toPath()); + } + + private void clearSlot(int idx) { + slots[idx] = null; + thumbs[idx].setImage(null); + fileLabels[idx].setText("–"); + fileLabels[idx].setStyle("-fx-font-style:italic; -fx-opacity:0.5;"); + } + + private void assignSlot(int idx, Path file) { + slots[idx] = file; + fileLabels[idx].setText(file.getFileName().toString()); + fileLabels[idx].setStyle(""); + try { + thumbs[idx].setImage(new Image(file.toUri().toString(), 60, 60, true, true, true)); + } catch (Exception ignored) {} + } + + // ── Import ──────────────────────────────────────────────────────────────── + + private void runImport() { + String name = setName.getText().trim(); + if (name.isBlank()) { status("Bitte Set-Namen eingeben."); return; } + if (slots[0] == null) { status("Mindestens die Farb-Textur (Slot 1) muss gewählt sein."); return; } + if (subfolder.getValue() == null) { status("Bitte einen Unterordner wählen."); return; } + + String sub = subfolder.getValue(); + + Path outDir = assetRoot.resolve("Textures") + .resolve(sub.replace('/', java.io.File.separatorChar)) + .resolve(name); + try { Files.createDirectories(outDir); } + catch (Exception ex) { + log.error("Ordner konnte nicht erstellt werden: {}", outDir, ex); + showError("Import fehlgeschlagen", "Ordner konnte nicht erstellt werden:\n" + outDir + "\n" + ex.getMessage()); + return; + } + + TextureMapData meta = new TextureMapData(); + String relBase = "Textures/" + sub + "/" + name + "/" + name; + int converted = 0; + + for (int i = 0; i < 5; i++) { + if (slots[i] == null) continue; + String outName = name + SUFFIXES[i] + ".png"; + Path outFile = outDir.resolve(outName); + try { + convertToPng(slots[i], outFile); + String assetPath = relBase + SUFFIXES[i] + ".png"; + switch (i) { + case 0 -> meta.colorMap = assetPath; + case 1 -> meta.normalMap = assetPath; + case 2 -> meta.displacementMap = assetPath; + case 3 -> meta.roughnessMap = assetPath; + case 4 -> meta.aoMap = assetPath; + } + converted++; + } catch (Exception ex) { + log.error("Konvertierung fehlgeschlagen: {} → {}: {}", slots[i].getFileName(), outFile.getFileName(), ex.getMessage(), ex); + showError("Konvertierung fehlgeschlagen", + slots[i].getFileName() + " → " + outFile.getFileName() + "\n" + ex.getMessage()); + return; + } + } + + try { + String json = new GsonBuilder().setPrettyPrinting().create().toJson(meta); + Files.writeString(outDir.resolve("textureset.json"), json); + } catch (Exception ex) { + log.error("textureset.json konnte nicht geschrieben werden: {}", ex.getMessage(), ex); + showError("Import fehlgeschlagen", "textureset.json konnte nicht geschrieben werden:\n" + ex.getMessage()); + return; + } + + lastImportedDir = outDir; + if (weitereChk.isSelected()) { + clearForm(); + status("✓ " + converted + " Texturen importiert → " + name + " | Bereit für nächsten Import."); + } else { + close(); + } + } + + private void clearForm() { + setName.clear(); + for (int i = 0; i < 5; i++) clearSlot(i); + status.setText(""); + } + + // ── TGA/JPG/BMP → PNG Konvertierung ────────────────────────────────────── + + private static void convertToPng(Path src, Path dst) throws IOException { + String lo = src.getFileName().toString().toLowerCase(); + if (lo.endsWith(".png")) { + Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); + return; + } + + BufferedImage img; + if (lo.endsWith(".tga")) { + img = loadTga(src.toFile()); + } else { + img = ImageIO.read(src.toFile()); + } + + if (img == null) throw new IOException("Format nicht lesbar: " + src.getFileName()); + + // Sicherstellen, dass wir ARGB haben + if (img.getType() != BufferedImage.TYPE_INT_ARGB) { + BufferedImage rgba = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = rgba.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + img = rgba; + } + ImageIO.write(img, "PNG", dst.toFile()); + } + + /** Lädt eine TGA-Datei über JME3's TGALoader (benötigt keinen GL-Kontext). */ + private static BufferedImage loadTga(File f) throws IOException { + com.jme3.texture.plugins.TGALoader loader = new com.jme3.texture.plugins.TGALoader(); + com.jme3.asset.TextureKey key = new com.jme3.asset.TextureKey(f.getName(), true); + com.jme3.asset.AssetInfo info = new com.jme3.asset.AssetInfo(null, key) { + @Override public InputStream openStream() { + try { return new FileInputStream(f); } + catch (FileNotFoundException ex) { throw new RuntimeException(ex); } + } + }; + com.jme3.texture.Image ji = (com.jme3.texture.Image) loader.load(info); + int w = ji.getWidth(), h = ji.getHeight(); + ByteBuffer buf = ji.getData(0); + buf.rewind(); + int[] px = new int[w * h]; + com.jme3.texture.Image.Format fmt = ji.getFormat(); + for (int i = 0; i < w * h; i++) { + int r, g, b, a; + switch (fmt) { + case RGBA8 -> { r=buf.get()&0xFF; g=buf.get()&0xFF; b=buf.get()&0xFF; a=buf.get()&0xFF; } + case RGB8 -> { r=buf.get()&0xFF; g=buf.get()&0xFF; b=buf.get()&0xFF; a=255; } + case BGR8 -> { b=buf.get()&0xFF; g=buf.get()&0xFF; r=buf.get()&0xFF; a=255; } + case BGRA8 -> { b=buf.get()&0xFF; g=buf.get()&0xFF; r=buf.get()&0xFF; a=buf.get()&0xFF; } + default -> { r=g=b=128; a=255; } + } + px[i] = (a<<24)|(r<<16)|(g<<8)|b; + } + // JME3 TGALoader liefert Daten bottom-to-top → vertikal spiegeln + for (int y = 0; y < h / 2; y++) { + int mirror = h - 1 - y; + for (int x = 0; x < w; x++) { + int tmp = px[y * w + x]; + px[y * w + x] = px[mirror * w + x]; + px[mirror * w + x] = tmp; + } + } + BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + bi.setRGB(0, 0, w, h, px, 0, w); + return bi; + } + + // ── Unterordner-Verwaltung ──────────────────────────────────────────────── + + private void loadSubfolders() { + String previous = subfolder.getValue(); + subfolder.getItems().clear(); + Path texturesDir = assetRoot.resolve("Textures"); + if (Files.isDirectory(texturesDir)) { + List entries = new ArrayList<>(); + collectSubfolders(texturesDir, texturesDir, entries, 0); + entries.sort(String.CASE_INSENSITIVE_ORDER); + subfolder.getItems().addAll(entries); + } + if (previous != null && subfolder.getItems().contains(previous)) { + subfolder.setValue(previous); + } else if (!subfolder.getItems().isEmpty()) { + subfolder.setValue(subfolder.getItems().get(0)); + } + } + + private static void collectSubfolders(Path root, Path dir, List result, int depth) { + if (depth > 4) return; + try (var stream = Files.list(dir)) { + stream.filter(Files::isDirectory) + .filter(p -> !p.getFileName().toString().startsWith(".")) + .filter(p -> !p.getFileName().toString().equalsIgnoreCase("internal")) + .sorted(Comparator.comparing(p -> p.getFileName().toString().toLowerCase())) + .forEach(p -> { + String rel = root.relativize(p).toString().replace(java.io.File.separatorChar, '/'); + result.add(rel); + collectSubfolders(root, p, result, depth + 1); + }); + } catch (Exception ignored) {} + } + + private void createSubfolder() { + TextInputDialog dlg = new TextInputDialog(); + dlg.setTitle("Neuer Unterordner"); + dlg.setHeaderText(null); + dlg.setContentText("Pfad (z.B. terrain/boden):"); + if (getOwner() != null) dlg.initOwner(getOwner()); + dlg.showAndWait().ifPresent(raw -> { + // Jeden Pfad-Abschnitt einzeln bereinigen, dann wieder zusammensetzen + String cleaned = Arrays.stream(raw.trim().split("[/\\\\]+")) + .map(s -> s.replaceAll("[^A-Za-z0-9_-]", "_")) + .filter(s -> !s.isBlank()) + .reduce((a, b) -> a + "/" + b) + .orElse(""); + if (cleaned.isBlank()) return; + Path dir = assetRoot.resolve("Textures") + .resolve(cleaned.replace('/', java.io.File.separatorChar)); + try { + Files.createDirectories(dir); + loadSubfolders(); // Liste neu laden (enthält jetzt alle Ebenen) + subfolder.setValue(cleaned); + } catch (Exception ex) { + log.error("Unterordner '{}' konnte nicht erstellt werden: {}", cleaned, ex.getMessage(), ex); + } + }); + } + + // ── Hilfsmethoden ───────────────────────────────────────────────────────── + + /** Leitet einen Vorschlags-Namen aus den Dateinamen ab (Suffixe wie _Color, _1K … abziehen). */ + private static String guessName(List files, Path dir) { + // Bekannte Suffixe, die iterativ abgezogen werden (Reihenfolge: spezifischste zuerst) + List strips = List.of( + "_ambientocclusion", "_displacement", "_normalgl", + "_diffuse", "_colour", "_albedo", "_color", + "_roughness", "_specular", "_normal", "_height", + "_rough", "_bump", "_disp", "_spec", "_nrm", "_nor", "_diff", + "_ao", "_rgh", + "_4096", "_2048", "_1024", "_512", + "_8k", "_4k", "_2k", "_1k" + ); + + List bases = new ArrayList<>(); + for (Path f : files) { + String stem = f.getFileName().toString().toLowerCase().replaceFirst("\\.[^.]+$", ""); + boolean changed = true; + while (changed) { + changed = false; + for (String s : strips) { + if (stem.endsWith(s)) { stem = stem.substring(0, stem.length() - s.length()); changed = true; } + } + } + stem = stem.replaceAll("[_\\-\\s]+$", ""); + if (!stem.isBlank()) bases.add(stem); + } + + String result = ""; + if (!bases.isEmpty()) { + // Längsten gemeinsamen Präfix aller bereinigten Stems + result = bases.get(0); + for (int i = 1; i < bases.size(); i++) { + String b = bases.get(i); + int len = 0; + while (len < result.length() && len < b.length() && result.charAt(len) == b.charAt(len)) len++; + result = result.substring(0, len); + } + result = result.replaceAll("[_\\-\\s]+$", "").replaceAll("[^A-Za-z0-9_-]", "_"); + } + + if (result.length() < 2) { + result = dir.getFileName().toString().replaceAll("[^A-Za-z0-9_-]", "_").toLowerCase(); + } + return result; + } + + private boolean isImage(Path p) { + String lo = p.getFileName().toString().toLowerCase(); + return lo.endsWith(".png") || lo.endsWith(".jpg") || lo.endsWith(".jpeg") + || lo.endsWith(".tga") || lo.endsWith(".bmp"); + } + + private int countFilled() { + int n = 0; + for (Path p : slots) if (p != null) n++; + return n; + } + + private void status(String msg) { status.setText(msg); } + + private void showError(String header, String detail) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Fehler"); + alert.setHeaderText(header); + alert.setContentText(detail); + if (getOwner() != null) alert.initOwner(getOwner()); + alert.showAndWait(); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/util/MeshUtils.java b/blight-editor/src/main/java/de/blight/editor/util/MeshUtils.java new file mode 100644 index 0000000..17faa6e --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/util/MeshUtils.java @@ -0,0 +1,174 @@ +package de.blight.editor.util; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; + +public class MeshUtils { + + private static final Logger log = LoggerFactory.getLogger(MeshUtils.class); + + /** + * Verschmilzt Vertices, die näher als {@code threshold} beieinander liegen, + * zu einem gemeinsamen Vertex (Merge-by-Distance / Vertex-Welding). + * + * Alle FloatBuffer-Vertex-Attribute (Position, Normal, TexCoord …) werden + * korrekt umgebaut. Index-Buffer können UnsignedShort oder UnsignedInt sein. + * Degenerierte Dreiecke (zwei oder mehr gleiche Index-Werte nach dem Remap) + * werden aus dem Ergebnis entfernt, damit JME's LodGenerator korrekte + * Dreieckszählungen erhält. + */ + public static Mesh mergeByDistance(Mesh src, float threshold) { + FloatBuffer srcPosBuf = src.getFloatBuffer(VertexBuffer.Type.Position); + if (srcPosBuf == null) return src; + + VertexBuffer idxBuf = src.getBuffer(VertexBuffer.Type.Index); + if (idxBuf == null) return src; + + srcPosBuf.rewind(); + int vertCount = srcPosBuf.limit() / 3; + float[] positions = new float[srcPosBuf.limit()]; + srcPosBuf.get(positions); + + // Lese Index-Buffer (Short oder Int) + boolean srcIsShort = (idxBuf.getFormat() == VertexBuffer.Format.UnsignedShort); + int[] origIndices; + if (srcIsShort) { + ShortBuffer sb = (ShortBuffer) idxBuf.getData(); + sb.rewind(); + origIndices = new int[sb.limit()]; + for (int i = 0; i < origIndices.length; i++) origIndices[i] = sb.get() & 0xFFFF; + } else { + IntBuffer ib = (IntBuffer) idxBuf.getData(); + ib.rewind(); + origIndices = new int[ib.limit()]; + for (int i = 0; i < origIndices.length; i++) origIndices[i] = ib.get(); + } + + // Spatial-Hash: Vertices innerhalb des Schwellwertes → gleicher Repräsentant + float inv = 1.0f / threshold; + HashMap> grid = new HashMap<>(); + int[] remap = new int[vertCount]; + List uniqueIdx = new ArrayList<>(); + + for (int i = 0; i < vertCount; i++) { + float x = positions[i * 3], y = positions[i * 3 + 1], z = positions[i * 3 + 2]; + int gx = (int) Math.floor(x * inv); + int gy = (int) Math.floor(y * inv); + int gz = (int) Math.floor(z * inv); + + int merged = -1; + outer: + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + for (int dz = -1; dz <= 1; dz++) { + List cell = grid.get(cellKey(gx + dx, gy + dy, gz + dz)); + if (cell == null) continue; + for (int j : cell) { + float ddx = positions[j * 3] - x; + float ddy = positions[j * 3 + 1] - y; + float ddz = positions[j * 3 + 2] - z; + if (ddx * ddx + ddy * ddy + ddz * ddz <= threshold * threshold) { + merged = j; + break outer; + } + } + } + } + } + + if (merged >= 0) { + remap[i] = remap[merged]; + } else { + remap[i] = uniqueIdx.size(); + uniqueIdx.add(i); + grid.computeIfAbsent(cellKey(gx, gy, gz), k -> new ArrayList<>()).add(i); + } + } + + int newVertCount = uniqueIdx.size(); + if (newVertCount >= vertCount) return src; + + // Vertex-Buffer neu aufbauen (alle FloatBuffer-Attribute) + Mesh newMesh = new Mesh(); + newMesh.setMode(src.getMode()); + for (VertexBuffer.Type type : VertexBuffer.Type.values()) { + if (type == VertexBuffer.Type.Index) continue; + VertexBuffer vb = src.getBuffer(type); + if (vb == null || !(vb.getData() instanceof FloatBuffer)) continue; + int comps = vb.getNumComponents(); + FloatBuffer srcFB = (FloatBuffer) vb.getData(); + srcFB.rewind(); + float[] srcArr = new float[srcFB.limit()]; + srcFB.get(srcArr); + FloatBuffer dstFB = BufferUtils.createFloatBuffer(newVertCount * comps); + for (int ni = 0; ni < newVertCount; ni++) { + int oi = uniqueIdx.get(ni); + for (int c = 0; c < comps; c++) dstFB.put(srcArr[oi * comps + c]); + } + dstFB.flip(); + newMesh.setBuffer(type, comps, dstFB); + } + + // Index-Buffer mit Remap neu aufbauen; degenerierte Dreiecke entfernen. + // JME's LodGenerator zählt degenerierte Dreiecke noch in triangleList.size(), + // was die Ziel-Dreieckszahl aufbläht und zu extremer Über-Reduktion führt. + int rawTriCount = origIndices.length / 3; + int[] remapped = new int[origIndices.length]; + for (int i = 0; i < origIndices.length; i++) remapped[i] = remap[origIndices[i]]; + + int validTris = 0; + for (int t = 0; t < rawTriCount; t++) { + int a = remapped[t * 3], b = remapped[t * 3 + 1], c = remapped[t * 3 + 2]; + if (a != b && b != c && a != c) validTris++; + } + int[] newIndices = new int[validTris * 3]; + int wi = 0; + for (int t = 0; t < rawTriCount; t++) { + int a = remapped[t * 3], b = remapped[t * 3 + 1], c = remapped[t * 3 + 2]; + if (a != b && b != c && a != c) { + newIndices[wi++] = a; + newIndices[wi++] = b; + newIndices[wi++] = c; + } + } + + if (newVertCount <= 65535) { + ShortBuffer dst = BufferUtils.createShortBuffer(newIndices.length); + for (int idx : newIndices) dst.put((short) (idx & 0xFFFF)); + dst.flip(); + newMesh.setBuffer(VertexBuffer.Type.Index, 3, VertexBuffer.Format.UnsignedShort, dst); + } else { + IntBuffer dst = BufferUtils.createIntBuffer(newIndices.length); + for (int idx : newIndices) dst.put(idx); + dst.flip(); + newMesh.setBuffer(VertexBuffer.Type.Index, 3, VertexBuffer.Format.UnsignedInt, dst); + } + + newMesh.updateCounts(); + newMesh.updateBound(); + + int removedTris = rawTriCount - validTris; + if (removedTris > 0) { + log.info("Weld: {} → {} Vertices, {} degenerierte Dreiecke entfernt", + vertCount, newVertCount, removedTris); + } else { + log.info("Weld: {} → {} Vertices", vertCount, newVertCount); + } + return newMesh; + } + + private static long cellKey(int x, int y, int z) { + return ((long) (x & 0xFFFFF) << 40) | ((long) (y & 0xFFFFF) << 20) | (long) (z & 0xFFFFF); + } +} diff --git a/blight-game/src/main/java/de/blight/game/BlightGame.java b/blight-game/src/main/java/de/blight/game/BlightGame.java index aabea57..d2be40e 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -13,6 +13,7 @@ import de.blight.game.state.SaveGameState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.bridge.SLF4JBridgeHandler; +import de.blight.game.console.JmeConsole; import de.blight.game.scene.WorldScene; import javax.imageio.ImageIO; @@ -31,6 +32,7 @@ public class BlightGame extends SimpleApplication { private KeyBindings keyBindings; private GraphicsSettings graphicsSettings; private ScreenshotAppState screenshotState; + private Path screenshotDir; private WorldScene worldScene; private ConfigScreen configScreen; private GraphicsScreen graphicsScreen; @@ -180,9 +182,28 @@ public class BlightGame extends SimpleApplication { stateManager.attach(pauseMenu); pauseMenu.setEnabled(false); + // ── Konsole (^) ────────────────────────────────────────────────────────── + JmeConsole console = new JmeConsole(); + registerGameCommands(console); + console.setOnVisibilityChanged(open -> { + if (open) { + worldScene.setPaused(true); + } else if (!pauseMenu.isEnabled()) { + worldScene.setPaused(false); + } + }); + stateManager.attach(console); + + // ── Debug: Lighting-Toggle (F8) ────────────────────────────────────────── + inputManager.addMapping("DebugNoLight", new KeyTrigger(KeyInput.KEY_F8)); + inputManager.addListener((ActionListener) (name, isPressed, tpf) -> { + if (!isPressed) return; + worldScene.toggleDebugNoLight(); + }, "DebugNoLight"); + // ── Screenshot (F12) ───────────────────────────────────────────────────── try { - Path screenshotDir = BlightHome.resolve("screenshots"); + screenshotDir = BlightHome.resolve("screenshots"); Files.createDirectories(screenshotDir); screenshotState = new ScreenshotAppState(screenshotDir + File.separator, "screenshot"); stateManager.attach(screenshotState); @@ -192,7 +213,10 @@ public class BlightGame extends SimpleApplication { } inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12)); inputManager.addListener((ActionListener) (name, isPressed, tpf) -> { - if (isPressed && screenshotState != null) screenshotState.takeScreenshot(); + if (isPressed && screenshotState != null) { + log.info("[Screenshot] Speichere in: {}", screenshotDir.toAbsolutePath()); + screenshotState.takeScreenshot(); + } }, "Screenshot"); // ── Schnellspeichern (F5, konfigurierbar) ──────────────────────────── @@ -295,4 +319,68 @@ public class BlightGame extends SimpleApplication { status("Bereit"); } } + + // ── Konsolen-Befehle ───────────────────────────────────────────────────── + + private void registerGameCommands(JmeConsole console) { + console.registerCommand("goto", args -> { + WorldScene ws = stateManager.getState(WorldScene.class); + if (ws == null || !ws.isEnabled()) return "Welt nicht geladen"; + try { + if (args.length >= 4) { + float x = Float.parseFloat(args[1]); + float y = Float.parseFloat(args[2]); + float z = Float.parseFloat(args[3]); + return ws.teleportPlayer(x, y, z); + } else if (args.length >= 3) { + float x = Float.parseFloat(args[1]); + float z = Float.parseFloat(args[2]); + return ws.teleportPlayer(x, Float.NaN, z); + } + return "Syntax: goto oder goto "; + } catch (NumberFormatException e) { + return "Fehler: Koordinaten müssen Zahlen sein"; + } + }); + + console.registerCommand("pos", args -> { + WorldScene ws = stateManager.getState(WorldScene.class); + if (ws == null || !ws.isEnabled()) return "Welt nicht geladen"; + com.jme3.math.Vector3f p = ws.getPlayerLocation(); + return String.format("Position: X=%.1f Y=%.1f Z=%.1f", p.x, p.y, p.z); + }); + + console.registerCommand("time", args -> { + if (args.length < 2) return "Syntax: time <0–24> (0=Mitternacht, 12=Mittag)"; + try { + float hours = Float.parseFloat(args[1]); + if (hours < 0 || hours > 24) return "Fehler: Wert zwischen 0 und 24"; + de.blight.game.state.DayNightState dns = + stateManager.getState(de.blight.game.state.DayNightState.class); + if (dns == null) return "Tag/Nacht-System nicht aktiv"; + dns.getDayTime().setTimeOfDay(hours / 24f); + int h = (int) hours, m = (int)((hours - h) * 60f); + return String.format("Zeit gesetzt: %02d:%02d Uhr", h, m); + } catch (NumberFormatException e) { + return "Fehler: Zahl erwartet"; + } + }); + + console.registerCommand("weather", args -> { + de.blight.game.state.WeatherState ws = + stateManager.getState(de.blight.game.state.WeatherState.class); + if (ws == null) return "Wettersystem nicht aktiv"; + if (args.length < 2) + return "Aktuell: " + ws.getActiveWeather() + + " | Syntax: weather "; + try { + de.blight.game.state.WeatherState.Weather w = + de.blight.game.state.WeatherState.Weather.valueOf(args[1].toUpperCase()); + ws.forceWeather(w); + return "Wetter gesetzt: " + w; + } catch (IllegalArgumentException e) { + return "Unbekanntes Wetter. Erlaubt: sunny, cloudy, overcast, storm"; + } + }); + } } diff --git a/blight-game/src/main/java/de/blight/game/LiveBroadcast.java b/blight-game/src/main/java/de/blight/game/LiveBroadcast.java index 8d339c0..971289b 100644 --- a/blight-game/src/main/java/de/blight/game/LiveBroadcast.java +++ b/blight-game/src/main/java/de/blight/game/LiveBroadcast.java @@ -1,19 +1,18 @@ package de.blight.game; -import de.blight.common.MapIO; +import de.blight.common.BlightHome; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; /** - * Schreibt Live-Daten (Spielerposition) in eine Temp-Datei neben der Karte, + * Schreibt Live-Daten (Spielerposition) in eine Temp-Datei im BlightHome-Verzeichnis, * damit der Editor sie live anzeigen kann. */ public final class LiveBroadcast { - public static final Path POS_FILE = - MapIO.getMapPath().resolveSibling("blight_live.pos"); + public static final Path POS_FILE = BlightHome.resolve("blight_live.pos"); private LiveBroadcast() {} diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java index 79a2b3b..98fdcb4 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java @@ -4,11 +4,16 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import de.blight.common.BlightHome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.*; import java.nio.file.*; public class GraphicsStore { + private static final Logger log = LoggerFactory.getLogger(GraphicsStore.class); + private static final Path FILE = BlightHome.resolve("config", "graphics.json"); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -17,7 +22,7 @@ public class GraphicsStore { try (Reader r = Files.newBufferedReader(FILE)) { return GSON.fromJson(r, GraphicsSettings.class); } catch (IOException e) { - System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage()); + log.warn("graphics.json konnte nicht geladen werden: {}", e.getMessage()); } } return new GraphicsSettings(); @@ -30,7 +35,7 @@ public class GraphicsStore { GSON.toJson(gs, w); } } catch (IOException e) { - System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage()); + log.error("graphics.json konnte nicht gespeichert werden: {}", e.getMessage()); } } } diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java index 9c17d8a..adaffd9 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java @@ -4,11 +4,16 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import de.blight.common.BlightHome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.*; import java.nio.file.*; public class KeyBindingStore { + private static final Logger log = LoggerFactory.getLogger(KeyBindingStore.class); + private static final Path FILE = BlightHome.resolve("config", "keybindings.json"); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -17,7 +22,7 @@ public class KeyBindingStore { try (Reader r = Files.newBufferedReader(FILE)) { return GSON.fromJson(r, KeyBindings.class); } catch (IOException e) { - System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage()); + log.warn("keybindings.json konnte nicht geladen werden: {}", e.getMessage()); } } return new KeyBindings(); @@ -30,7 +35,7 @@ public class KeyBindingStore { GSON.toJson(kb, w); } } catch (IOException e) { - System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage()); + log.error("keybindings.json konnte nicht gespeichert werden: {}", e.getMessage()); } } } diff --git a/blight-game/src/main/java/de/blight/game/console/JmeConsole.java b/blight-game/src/main/java/de/blight/game/console/JmeConsole.java index f26a428..ed465cf 100644 --- a/blight-game/src/main/java/de/blight/game/console/JmeConsole.java +++ b/blight-game/src/main/java/de/blight/game/console/JmeConsole.java @@ -74,10 +74,12 @@ public class JmeConsole extends BaseAppState { @Override public void onKeyEvent(KeyInputEvent e) { - if (!e.isPressed()) return; int key = e.getKeyCode(); - if (key == KEY_TOGGLE) { toggle(); return; } + if (key == KEY_TOGGLE && e.isPressed()) { toggle(); e.setConsumed(); return; } if (!open) return; + // Konsole ist offen: alle Events konsumieren damit keine Ingame-Bindings feuern + e.setConsumed(); + if (!e.isPressed()) return; char c = e.getKeyChar(); if (key == KeyInput.KEY_RETURN) feedEnter(); else if (key == KeyInput.KEY_BACK) feedBackspace(); diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 081e149..43f94a0 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -15,10 +15,15 @@ import de.blight.game.animation.AnimationLibrary; import de.blight.game.animation.RetargetingSystem; import de.blight.game.config.KeyBindings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.nio.file.Path; public class PlayerInputControl { + private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class); + private static final float MOVE_SPEED = 0.07f; private static final float SPRINT_MULT = 1.5f; private static final float WALK_MULT = 0.5f; @@ -85,7 +90,7 @@ public class PlayerInputControl { this.currentAnim = null; this.runningClip = null; this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null; - System.out.println("[AnimCtx] AnimComposer gefunden: " + (animComposer != null)); + log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null); if (animSetName != null) { String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.IDLE); if (clip != null && tryPlay(clip)) { @@ -243,13 +248,12 @@ public class PlayerInputControl { if (animLib == null || visual == null || animSetName == null) { if (!animCtxLogged) { animCtxLogged = true; - System.out.println("[Anim] Kein Animations-Kontext:" - + " animLib=" + animLib + " visual=" + visual + " setName=" + animSetName); + log.info("[Anim] Kein Animations-Kontext: animLib={} visual={} setName={}", animLib, visual, animSetName); } return; } String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action); - System.out.println("[Anim] " + action + " → clip='" + clip + "' (set=" + animSetName + ")"); + log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName); if (clip != null && tryPlay(clip)) return; if (action != AnimationAction.DEFAULT) { String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT); @@ -259,11 +263,11 @@ public class PlayerInputControl { private boolean tryPlay(String clip) { if (animComposer == null || !animLib.ensureApplied(clip, visual)) { - System.out.println("[Anim] tryPlay('" + clip + "') → ensureApplied FAILED"); + log.info("[Anim] tryPlay('{}') → ensureApplied FAILED", clip); return false; } com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip); - System.out.println("[Anim] setCurrentAction('" + clip + "') → " + (action != null ? "OK" : "FAILED")); + log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED"); if (action != null) { runningClip = clip; return true; diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 4563427..34009b0 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -21,7 +21,6 @@ import com.jme3.shadow.*; import com.jme3.terrain.geomipmap.*; import com.jme3.texture.*; import com.jme3.util.BufferUtils; -import com.jme3.util.SkyFactory; import java.nio.ByteBuffer; import de.blight.common.MapData; import de.blight.common.MapIO; @@ -41,13 +40,18 @@ import de.blight.game.state.GrassVertexRenderState; import de.blight.game.state.LocationState; import de.blight.game.state.RiverState; import de.blight.game.state.TerrainChunkState; +import de.blight.game.state.VoxelChunkState; import de.blight.game.state.WaterBodyState; +import de.blight.game.state.DayNightState; import de.blight.game.state.WeatherState; import de.blight.game.state.InteractionHudState; import de.blight.game.state.InventoryState; import de.blight.game.state.WorldItemsState; import de.blight.game.state.WorldObjectsState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.FloatBuffer; import java.util.ArrayList; @@ -55,6 +59,8 @@ import java.util.List; public class WorldScene extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(WorldScene.class); + private SimpleApplication app; private Node rootNode; private AssetManager assetManager; @@ -62,6 +68,7 @@ public class WorldScene extends BaseAppState { private MapData loadedMapData; private FilterPostProcessor sharedFPP; private TerrainChunkState terrainChunkState; + private Material terrainMaterial; private final KeyBindings keyBindings; private ThirdPersonCamera thirdPersonCam; @@ -71,7 +78,8 @@ public class WorldScene extends BaseAppState { private Node character; private Spatial characterVisual; private CharacterControl physicsChar; - private boolean animContextReady = false; + private boolean animContextReady = false; + private boolean physicsCharPending = false; private float spawnX = 0f; private float spawnY = 5f; private float spawnZ = 0f; @@ -108,6 +116,10 @@ public class WorldScene extends BaseAppState { animLib = new AnimationLibrary(); app.getStateManager().attach(animLib); + + // Früh starten damit DayNightState bis onEnable() bereits initialisiert ist + dayNight = new DayNightState(false); + app.getStateManager().attach(dayNight); } @Override @@ -148,8 +160,9 @@ public class WorldScene extends BaseAppState { physicsChar.setFallSpeed(35f); physicsChar.setGravity(35f); character.addControl(physicsChar); - bulletAppState.getPhysicsSpace().add(physicsChar); - physicsChar.setPhysicsLocation(new Vector3f(spawnX, spawnY, spawnZ)); + // Physik-Aktivierung wird in update() verzögert bis BulletAppState und + // Terrain-Physics für den Spawn-Bereich bereit sind (verhindert Fall-durch-Terrain). + physicsCharPending = true; playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings); playerInput.setPhysicsCharacter(physicsChar); @@ -178,10 +191,23 @@ public class WorldScene extends BaseAppState { app.getInputManager().setCursorVisible(false); } - private float livePosTimer = 0f; - @Override public void update(float tpf) { + // Physik-Charakter erst aktivieren wenn BulletAppState bereit ist UND + // TerrainChunkState bereits einen Physik-Collider für den Spawn-Chunk hat. + if (physicsCharPending) { + com.jme3.bullet.PhysicsSpace ps = bulletAppState.getPhysicsSpace(); + if (ps == null || !terrainChunkState.hasPhysicsAt(spawnX, spawnZ)) { + return; // Noch nicht bereit – ohne Kamera/Eingabe warten + } + ps.add(physicsChar); + physicsChar.setPhysicsLocation(new Vector3f(spawnX, spawnY, spawnZ)); + physicsCharPending = false; + log.info("[WorldScene] Spieler-Physik aktiviert bei ({}, {}, {})", spawnX, spawnY, spawnZ); + // Kein return – Kamera im selben Frame positionieren damit + // TerrainChunkState.update() danach die korrekte Referenzposition sieht. + } + if (!animContextReady && animLib != null && animLib.isInitialized()) { setupAnimationContext(); animContextReady = true; @@ -189,16 +215,73 @@ public class WorldScene extends BaseAppState { playerInput.update(tpf); thirdPersonCam.update(tpf); - livePosTimer += tpf; - if (livePosTimer >= 0.2f && physicsChar != null) { - livePosTimer = 0f; - com.jme3.math.Vector3f pos = physicsChar.getPhysicsLocation(); - de.blight.game.LiveBroadcast.writePosition(pos.x, pos.y, pos.z); + // Terrain-Shader mit DayNightState-Licht synchronisieren (Richtung + Farben) + if (terrainMaterial != null && dayNight != null + && dayNight.getSunLight() != null) { + terrainMaterial.setVector3("LightDir", + dayNight.getSunDirection().negate()); + ColorRGBA sc = dayNight.getSunLight().getColor(); + ColorRGBA ac = dayNight.getAmbientLight().getColor(); + terrainMaterial.setVector3("SunColor", + new Vector3f(sc.r, sc.g, sc.b)); + terrainMaterial.setVector3("AmbientColor", + new Vector3f(ac.r, ac.g, ac.b)); + } + + } + + /** + * F8: Zyklus durch Debug-Modi + * 0 = normal + * 1 = kein Licht (raw texture, Terrain + Voxel) + * 2 = nur Slot-0 des TextureArrays, kein Blending, kein Licht (nur Terrain) + */ + public void toggleDebugNoLight() { + debugMode = (debugMode + 1) % 4; + VoxelChunkState vcs = getApplication().getStateManager().getState(VoxelChunkState.class); + switch (debugMode) { + case 0 -> { + if (terrainMaterial != null) { + terrainMaterial.setBoolean("DebugNoLight", false); + terrainMaterial.setBoolean("DebugSlot0Only", false); + terrainMaterial.clearParam("DebugDirectTex"); + } + if (vcs != null) vcs.setDebugNoLight(false); + log.info("[Debug] Modus 0: normal"); + } + case 1 -> { + if (terrainMaterial != null) { + terrainMaterial.setBoolean("DebugNoLight", true); + terrainMaterial.setBoolean("DebugSlot0Only", false); + } + if (vcs != null) vcs.setDebugNoLight(true); + log.info("[Debug] Modus 1: kein Licht (Terrain + Voxel)"); + } + case 2 -> { + if (terrainMaterial != null) { + terrainMaterial.setBoolean("DebugNoLight", false); + terrainMaterial.setBoolean("DebugSlot0Only", true); + terrainMaterial.clearParam("DebugDirectTex"); + } + if (vcs != null) vcs.setDebugNoLight(true); + log.info("[Debug] Modus 2: nur Slot-0 (kein Blending, kein Licht)"); + } + case 3 -> { + if (terrainMaterial != null && !debugSlot0Path.isEmpty()) { + terrainMaterial.setBoolean("DebugNoLight", false); + terrainMaterial.setBoolean("DebugSlot0Only", false); + com.jme3.texture.Texture t = getApplication().getAssetManager() + .loadTexture(debugSlot0Path); + t.setWrap(com.jme3.texture.Texture.WrapMode.Repeat); + terrainMaterial.setTexture("DebugDirectTex", t); + } + if (vcs != null) vcs.setDebugNoLight(true); + log.info("[Debug] Modus 3: direkte Texture2D '{}' (bypasses TextureArray)", debugSlot0Path); + } } } @Override protected void cleanup(Application app) { - de.blight.game.LiveBroadcast.clear(); } @Override protected void onDisable() {} @@ -206,18 +289,19 @@ public class WorldScene extends BaseAppState { animLib.applyAllTo(characterVisual != null ? characterVisual : character); MainCharacter mc = findMainCharacter(); String setName = (mc != null) ? mc.getAnimSetPath() : null; - System.out.println("[AnimCtx] MainCharacter: " + (mc != null ? mc.getCharacterId() : "null") - + " animSetPath: " + setName - + " clipCount: " + animLib.getClipKeys().size() - + " clips: " + animLib.getClipKeys()); + log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}", + mc != null ? mc.getCharacterId() : "null", + setName, + animLib.getClipKeys().size(), + animLib.getClipKeys()); // AnimSet-ActionMap ausgeben if (setName != null) { java.nio.file.Path setDir = AnimationLibrary.findAssetRoot().resolve("animations").resolve("sets"); try { de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, setName); - System.out.println("[AnimCtx] AnimSet '" + setName + "' actionMap: " + set.getActionMap()); + log.info("[AnimCtx] AnimSet '{}' actionMap: {}", setName, set.getActionMap()); } catch (Exception e) { - System.out.println("[AnimCtx] AnimSet '" + setName + "' nicht ladbar: " + e.getMessage()); + log.info("[AnimCtx] AnimSet '{}' nicht ladbar: {}", setName, e.getMessage()); } } playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot()); @@ -239,19 +323,17 @@ public class WorldScene extends BaseAppState { // bei Skinned-Meshes, die vor der ersten SkinningControl-Runde falsche Bounds liefern) float[] yRange = vertexYRange(loaded); float modelHeight = yRange[1] - yRange[0]; - System.out.println("[WorldScene] Vertex-Y-Range: min=" + yRange[0] + " max=" + yRange[1] - + " height=" + modelHeight); + log.info("[WorldScene] Vertex-Y-Range: min={} max={} height={}", yRange[0], yRange[1], modelHeight); float offsetY; if (modelHeight > 0.1f) { float scale = 1.8f / modelHeight; loaded.setLocalScale(scale); // Füße des Modells (scale * minY in loaded-Local) auf Kapsel-Unterkante legen offsetY = -(0.9f + scale * yRange[0]); - System.out.println("[WorldScene] Charakter skaliert: " + scale - + "x offsetY=" + offsetY); + log.info("[WorldScene] Charakter skaliert: {}x offsetY={}", scale, offsetY); } else { offsetY = CAPSULE_VISUAL_OFFSET_Y; - System.out.println("[WorldScene] Kein Scale möglich (height=" + modelHeight + "), Fallback-Offset"); + log.info("[WorldScene] Kein Scale möglich (height={}), Fallback-Offset", modelHeight); } // rotationNode als Drehpunkt (CharacterControl überschreibt wrapper-Rotation jeden Frame) @@ -263,11 +345,11 @@ public class WorldScene extends BaseAppState { wrapper.attachChild(rotNode); characterVisual = rotNode; - System.out.println("[WorldScene] Hauptcharakter geladen: " + mc.getModelPath()); + log.info("[WorldScene] Hauptcharakter geladen: {}", mc.getModelPath()); return wrapper; } catch (Exception e) { - System.err.println("[WorldScene] Modell nicht ladbar (" + mc.getModelPath() - + "): " + e.getMessage() + " – Fallback auf Platzhalter"); + log.error("[WorldScene] Modell nicht ladbar ({}): {} – Fallback auf Platzhalter", + mc.getModelPath(), e.getMessage()); } } characterVisual = null; @@ -287,35 +369,25 @@ public class WorldScene extends BaseAppState { // ----------------------------------------------------------------------- private void buildLighting() { - DirectionalLight sun = new DirectionalLight(); - sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal()); - sun.setColor(ColorRGBA.White.mult(1.4f)); - rootNode.addLight(sun); + // Licht, Ambient und Sky kommen von DayNightState (der wurde in initialize() attached). + // Wir erstellen hier nur den Shadow-Filter und übergeben ihn an DayNightState, + // damit dieser Intensität und Lichtbindung dynamisch verwaltet. + DirectionalLightShadowFilter shadowFilter = + new DirectionalLightShadowFilter(assetManager, 2048, 3); + shadowFilter.setShadowIntensity(0.4f); + shadowFilter.setEnabled(true); + dayNight.setShadowFilter(shadowFilter); // bindet sun + übernimmt Intensitäts-Updates - AmbientLight ambient = new AmbientLight(); - ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f)); - rootNode.addLight(ambient); - - DirectionalLightShadowRenderer shadowRenderer = - new DirectionalLightShadowRenderer(assetManager, 2048, 3); - shadowRenderer.setLight(sun); - shadowRenderer.setShadowIntensity(0.4f); - app.getViewPort().addProcessor(shadowRenderer); - - try { - Spatial sky = SkyFactory.createSky(assetManager, - "Textures/Sky/Bright/BrightSky.dds", - SkyFactory.EnvMapType.CubeMap); - rootNode.attachChild(sky); - } catch (Exception ignored) {} - - setupPostProcessing(sun.getDirection()); + setupPostProcessing(dayNight.getSunDirection(), shadowFilter); } - private void setupPostProcessing(Vector3f sunDir) { + private void setupPostProcessing(Vector3f sunDir, DirectionalLightShadowFilter shadowFilter) { sharedFPP = new FilterPostProcessor(assetManager); FilterPostProcessor fpp = sharedFPP; + // Schatten als Filter – vermeidet das Setzen von ShadowMap-Texturen in Scene-Materialien + fpp.addFilter(shadowFilter); + // Globales Wasser bei Y=0 (bedeckt die gesamte Karte unterhalb der Wasserlinie) try { WaterFilter waterFilter = new WaterFilter(rootNode, sunDir); @@ -340,7 +412,7 @@ public class WorldScene extends BaseAppState { weather.setFogFilter(fogFilter); app.getStateManager().attach(weather); } catch (Exception e) { - System.err.println("[WorldScene] Post-Processing nicht verfügbar: " + e.getMessage()); + log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage()); } app.getViewPort().addProcessor(fpp); @@ -360,7 +432,7 @@ public class WorldScene extends BaseAppState { try { loadedMapData = MapIO.load(); } catch (IOException e) { - System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage()); + log.error("[WorldScene] Karte nicht ladbar: {}", e.getMessage()); } } @@ -382,23 +454,35 @@ public class WorldScene extends BaseAppState { spawnZ = loadedMapData.spawnZ; } } - System.out.println("[WorldScene] SpawnXZ: X=" + spawnX + " Z=" + spawnZ); + log.info("[WorldScene] SpawnXZ: X={} Z={}", spawnX, spawnZ); - Material mat = buildTerrainMaterial(loadedMapData); + terrainMaterial = buildTerrainMaterial(loadedMapData); - terrainChunkState = new TerrainChunkState(bulletAppState, mat, loadedMapData); + terrainChunkState = new TerrainChunkState(bulletAppState, terrainMaterial, loadedMapData); + // Höhen vorab laden, damit getHeightAt() bereits hier (vor initialize()) korrekte Werte liefert + terrainChunkState.loadChunkHeights(); app.getStateManager().attach(terrainChunkState); - // Spawn-Höhe: aus gespeicherter Position oder aus Terrain berechnen + // Spawn-Höhe: gespeicherte Y nur verwenden wenn sie über dem Terrain liegt + float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ); de.blight.game.state.SaveGameState _sv = app.getStateManager().getState(de.blight.game.state.SaveGameState.class); boolean hasSavedY = _sv != null && _sv.getSave().character.positionSaved && System.getProperty("blight.temp.spawn.x") == null; - if (!hasSavedY) { - float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ); + if (hasSavedY) { + float savedY = _sv.getSave().character.y; + spawnY = Math.max(savedY, terrainH + 1f); + } else { spawnY = terrainH + 10f; } - System.out.println("[WorldScene] SpawnXYZ=(" + spawnX + ", " + spawnY + ", " + spawnZ + ")"); + log.info("[WorldScene] SpawnXYZ=({}, {}, {}) terrainH={}", spawnX, spawnY, spawnZ, terrainH); + // setSpawnHint sorgt dafür, dass TerrainChunkState.update() beim ersten Frame + // sofort die Physik für die Spawn-Umgebung aufbaut. + terrainChunkState.setSpawnHint(spawnX, spawnZ); + + VoxelChunkState voxelState = new VoxelChunkState(bulletAppState, loadedMapData); + terrainChunkState.addChunkListener(voxelState); + app.getStateManager().attach(voxelState); } // ----------------------------------------------------------------------- @@ -554,39 +638,152 @@ public class WorldScene extends BaseAppState { new ColorRGBA(0.80f, 0.72f, 0.50f, 1f), }; + private static final int TERRAIN_ARRAY_SIZE = 1024; + private DayNightState dayNight; + private int debugMode = 0; // 0=normal, 1=kein Licht, 2=nur Slot0, 3=direkte Tex2D + private String debugSlot0Path = ""; + + /** Baut ein TextureArray mit paths.length Layern. Leere/fehlende Pfade → fallback-Farbe. */ + private com.jme3.texture.TextureArray buildTextureArray( + String[] paths, ColorRGBA[] fallbacks, AssetManager am) { + int size = TERRAIN_ARRAY_SIZE; + java.util.List images = new java.util.ArrayList<>(paths.length); + for (int i = 0; i < paths.length; i++) { + String path = paths[i]; + ColorRGBA fb = (fallbacks != null && i < fallbacks.length && fallbacks[i] != null) + ? fallbacks[i] : new ColorRGBA(0.5f, 0.5f, 0.5f, 1f); + if (path != null && !path.isEmpty()) { + try { + ByteBuffer buf = loadTextureToRGBA8(path, size, am); + images.add(new Image(Image.Format.RGBA8, size, size, buf)); + continue; + } catch (Exception e) { log.warn("[WorldScene-Array] Slot {} nicht ladbar: {}", i, e.getMessage()); } + } + ByteBuffer buf = BufferUtils.createByteBuffer(size * size * 4); + byte r = (byte)(fb.r * 255), g = (byte)(fb.g * 255), + b = (byte)(fb.b * 255), a = (byte)(fb.a * 255); + for (int p = 0; p < size * size; p++) buf.put(r).put(g).put(b).put(a); + buf.flip(); + images.add(new Image(Image.Format.RGBA8, size, size, buf)); + } + com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images); + texArr.setWrap(Texture.WrapMode.Repeat); + texArr.setMinFilter(Texture.MinFilter.Trilinear); + texArr.setMagFilter(Texture.MagFilter.Bilinear); + return texArr; + } + + private ByteBuffer loadTextureToRGBA8(String path, int size, AssetManager am) throws Exception { + Texture tex = am.loadTexture(path); + Image src = tex.getImage(); + int sw = src.getWidth(), sh = src.getHeight(); + log.info("[TextureArray] '{}' → Format={}, Größe={}×{}, Zielgröße={}", + path, src.getFormat(), sw, sh, size); + java.awt.image.BufferedImage bimg = new java.awt.image.BufferedImage(sw, sh, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + ByteBuffer data = src.getData(0).duplicate(); + data.rewind(); + switch (src.getFormat()) { + case RGBA8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) { + int r = data.get()&0xFF, g = data.get()&0xFF, b = data.get()&0xFF, a = data.get()&0xFF; + bimg.setRGB(x,y,(a<<24)|(r<<16)|(g<<8)|b); } } + case RGB8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) { + int r = data.get()&0xFF, g = data.get()&0xFF, b = data.get()&0xFF; + bimg.setRGB(x,y,(0xFF<<24)|(r<<16)|(g<<8)|b); } } + case BGR8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) { + int b = data.get()&0xFF, g = data.get()&0xFF, r = data.get()&0xFF; + bimg.setRGB(x,y,(0xFF<<24)|(r<<16)|(g<<8)|b); } } + case ABGR8 -> { for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) { + int a = data.get()&0xFF, b = data.get()&0xFF, g = data.get()&0xFF, r = data.get()&0xFF; + bimg.setRGB(x,y,(a<<24)|(r<<16)|(g<<8)|b); } } + default -> { + com.jme3.texture.image.ImageRaster raster = com.jme3.texture.image.ImageRaster.create(src); + ColorRGBA pixel = new ColorRGBA(); + for (int y = 0; y < sh; y++) for (int x = 0; x < sw; x++) { + raster.getPixel(x, y, pixel); + bimg.setRGB(x,y,((int)(pixel.a*255)<<24)|((int)(pixel.r*255)<<16) + |((int)(pixel.g*255)<<8)|(int)(pixel.b*255)); + } + } + } + java.awt.image.BufferedImage scaled; + if (sw == size && sh == size) { scaled = bimg; } else { + scaled = new java.awt.image.BufferedImage(size, size, java.awt.image.BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g2 = scaled.createGraphics(); + g2.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, + java.awt.RenderingHints.VALUE_RENDER_QUALITY); + g2.drawImage(bimg, 0, 0, size, size, null); g2.dispose(); + } + ByteBuffer buf = BufferUtils.createByteBuffer(size * size * 4); + for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) { + int argb = scaled.getRGB(x, y); + buf.put((byte)((argb>>16)&0xFF)).put((byte)((argb>>8)&0xFF)) + .put((byte)(argb&0xFF)).put((byte)((argb>>24)&0xFF)); + } + buf.flip(); + return buf; + } + private Material buildTerrainMaterial(MapData map) { if (map != null) { try { - Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md"); - mat.setBoolean("useTriPlanarMapping", false); - mat.setFloat("Shininess", 0f); + Material mat = new Material(assetManager, "MatDefs/TerrainArray.j3md"); - String[] mapTex = map.terrainTextures; - String[] matParams = {"DiffuseMap","DiffuseMap_1","DiffuseMap_2","DiffuseMap_3"}; - String[] scaleP = {"DiffuseMap_0_scale","DiffuseMap_1_scale","DiffuseMap_2_scale","DiffuseMap_3_scale"}; - String[] nmParams = {"NormalMap","NormalMap_1","NormalMap_2","NormalMap_3"}; + boolean hasUpperTex = false; + if (map.upperTextures != null) + for (String s : map.upperTextures) if (s != null && !s.isEmpty()) { hasUpperTex = true; break; } + boolean hasThirdTex = false; + if (map.thirdTextures != null) + for (String s : map.thirdTextures) if (s != null && !s.isEmpty()) { hasThirdTex = true; break; } + + // ── Diffuse-Array ───────────────────────────────────────────── + String[] diffPaths = new String[12]; for (int i = 0; i < 4; i++) { - String path = (mapTex[i] != null && !mapTex[i].isEmpty()) ? mapTex[i] : DEF_TEX[i]; - if (path == null || path.isEmpty()) continue; - Texture tex = loadTexOrFallback(path, DEF_COLOR[i]); - tex.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(matParams[i], tex); - mat.setFloat(scaleP[i], 512f); - String nmp = map.terrainNormalMaps[i]; - System.out.println("[WorldScene] Slot " + i + " NormalMap: '" + nmp + "'"); - if (nmp != null && !nmp.isEmpty()) { - try { - Texture nm = assetManager.loadTexture(nmp); - nm.setWrap(Texture.WrapMode.Repeat); - mat.setTexture(nmParams[i], nm); - System.out.println("[WorldScene] Slot " + i + " NormalMap geladen OK"); - } catch (Exception e) { - System.err.println("[WorldScene] NormalMap nicht ladbar: " + nmp + " – " + e.getMessage()); - } - } + String p = (map.terrainTextures != null) ? map.terrainTextures[i] : ""; + diffPaths[i] = (p != null && !p.isEmpty()) ? p + : (i < DEF_TEX.length ? DEF_TEX[i] : ""); + } + if (map.upperTextures != null) + System.arraycopy(map.upperTextures, 0, diffPaths, 4, 4); + if (map.thirdTextures != null) + System.arraycopy(map.thirdTextures, 0, diffPaths, 8, 4); + ColorRGBA[] diffFb = new ColorRGBA[12]; + for (int i = 0; i < 4; i++) diffFb[i] = DEF_COLOR[i]; + for (int i = 0; i < 4; i++) diffFb[4+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f); + for (int i = 0; i < 4; i++) diffFb[8+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f); + debugSlot0Path = diffPaths[0]; + log.info("[Terrain] Slot-0 Textur = '{}' Slot-1 = '{}'", diffPaths[0], diffPaths[1]); + mat.setParam("DiffuseArray", com.jme3.shader.VarType.TextureArray, + buildTextureArray(diffPaths, diffFb, assetManager)); + + float[] scales = (map.diffuseScales != null && map.diffuseScales.length == 12) + ? map.diffuseScales : new float[]{8,8,8,8, 8,8,8,8, 8,8,8,8}; + log.info("[Terrain] DiffuseScales[0..3] = {}, {}, {}, {} (Voxel TexScale = 8.0)", + scales[0], scales[1], scales[2], scales[3]); + mat.setParam("DiffuseScales", com.jme3.shader.VarType.FloatArray, scales); + + // ── Normal-Array ────────────────────────────────────────────── + String[] normPaths = new String[12]; + if (map.terrainNormalMaps != null) + System.arraycopy(map.terrainNormalMaps, 0, normPaths, 0, 4); + if (map.upperNormalMaps != null) + System.arraycopy(map.upperNormalMaps, 0, normPaths, 4, 4); + if (map.thirdNormalMaps != null) + System.arraycopy(map.thirdNormalMaps, 0, normPaths, 8, 4); + boolean hasNormal = false; + for (String p : normPaths) if (p != null && !p.isEmpty()) { hasNormal = true; break; } + if (hasNormal) { + ColorRGBA[] flatNorm = new ColorRGBA[12]; + java.util.Arrays.fill(flatNorm, new ColorRGBA(0.5f, 0.5f, 1f, 1f)); + mat.setParam("NormalArray", com.jme3.shader.VarType.TextureArray, + buildTextureArray(normPaths, flatNorm, assetManager)); + } else { + mat.clearParam("NormalArray"); } - // Ältere Maps haben splatR=0 → Gras (Slot 0) wäre unsichtbar; auf 255 setzen. + // ── AlphaMap ────────────────────────────────────────────────── byte[] splatR = map.splatR; boolean rAllZero = true; for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } } @@ -594,14 +791,10 @@ public class WorldScene extends BaseAppState { splatR = new byte[splatR.length]; java.util.Arrays.fill(splatR, (byte) 255); } - int sz = MapData.SPLAT_SIZE; ByteBuffer splatBuf = BufferUtils.createByteBuffer(sz * sz * 4); for (int i = 0; i < sz * sz; i++) { - splatBuf.put(splatR[i]); - splatBuf.put(map.splatG[i]); - splatBuf.put(map.splatB[i]); - splatBuf.put(map.splatA[i]); + splatBuf.put(splatR[i]).put(map.splatG[i]).put(map.splatB[i]).put(map.splatA[i]); } splatBuf.flip(); Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf)); @@ -609,16 +802,48 @@ public class WorldScene extends BaseAppState { splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); splatTex.setMagFilter(Texture.MagFilter.Bilinear); mat.setTexture("AlphaMap", splatTex); + + if (hasUpperTex && map.upperSplatR != null) { + ByteBuffer upperBuf = BufferUtils.createByteBuffer(sz * sz * 4); + for (int i = 0; i < sz * sz; i++) { + upperBuf.put(map.upperSplatR[i]).put(map.upperSplatG[i]) + .put(map.upperSplatB[i]).put(map.upperSplatA[i]); + } + upperBuf.flip(); + Texture2D upperSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, upperBuf)); + upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp); + upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + upperSplatTex.setMagFilter(Texture.MagFilter.Bilinear); + mat.setTexture("AlphaMap_1", upperSplatTex); + } + + // ── AlphaMap_2 (dritte Gruppe) ──────────────────────────────── + if (hasThirdTex && map.thirdSplatR != null) { + ByteBuffer thirdBuf = BufferUtils.createByteBuffer(sz * sz * 4); + for (int i = 0; i < sz * sz; i++) { + thirdBuf.put(map.thirdSplatR[i]); + thirdBuf.put(map.thirdSplatG[i]); + thirdBuf.put(map.thirdSplatB[i]); + thirdBuf.put(map.thirdSplatA[i]); + } + thirdBuf.flip(); + Texture2D thirdSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, thirdBuf)); + thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp); + thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear); + mat.setTexture("AlphaMap_2", thirdSplatTex); + } + return mat; } catch (Exception e) { - System.err.println("[WorldScene] Splat-Material fehlgeschlagen: " + e.getMessage()); + log.error("[WorldScene] Splat-Material fehlgeschlagen: {}", e.getMessage()); } } // Fallback: einfaches Gras-Material try { Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md"); - Texture grass = assetManager.loadTexture("Textures/gras.png"); + Texture grass = assetManager.loadTexture("Textures/internal/gras.png"); grass.setWrap(Texture.WrapMode.Repeat); mat.setTexture("DiffuseMap", grass); mat.setFloat("DiffuseMap_0_scale", 32f); @@ -808,4 +1033,21 @@ public class WorldScene extends BaseAppState { return limb; } + /** + * Teleportiert den Spieler zu (x, y, z). Wenn y == Float.NaN wird die + * Terrainhöhe an (x, z) verwendet und ein kleiner Offset nach oben addiert. + */ + public String teleportPlayer(float x, float y, float z) { + if (physicsChar == null) return "Spieler noch nicht initialisiert"; + if (Float.isNaN(y)) { + y = (terrainChunkState != null) ? terrainChunkState.getHeightAt(x, z) + 2f : 5f; + } + physicsChar.warp(new Vector3f(x, y, z)); + return String.format("Teleportiert → X=%.1f Y=%.1f Z=%.1f", x, y, z); + } + + public Vector3f getPlayerLocation() { + return physicsChar != null ? physicsChar.getPhysicsLocation() : Vector3f.ZERO; + } + } diff --git a/blight-game/src/main/java/de/blight/game/state/DayNightState.java b/blight-game/src/main/java/de/blight/game/state/DayNightState.java index 8dc88e9..97bf6c5 100644 --- a/blight-game/src/main/java/de/blight/game/state/DayNightState.java +++ b/blight-game/src/main/java/de/blight/game/state/DayNightState.java @@ -12,7 +12,6 @@ import com.jme3.scene.*; import com.jme3.scene.shape.Sphere; import com.jme3.shadow.DirectionalLightShadowFilter; import com.jme3.shadow.EdgeFilteringMode; -import com.jme3.util.SkyFactory; import de.blight.common.time.DayTime; import de.blight.common.time.TimeListener; import org.slf4j.Logger; @@ -30,7 +29,7 @@ public class DayNightState extends BaseAppState implements TimeListener { private static final ColorRGBA SUN_DAY = new ColorRGBA(1.00f, 0.95f, 0.88f, 1f); private static final ColorRGBA SUN_DAWN = new ColorRGBA(1.00f, 0.55f, 0.20f, 1f); - private static final ColorRGBA AMB_DAY = new ColorRGBA(0.35f, 0.38f, 0.46f, 1f); + private static final ColorRGBA AMB_DAY = new ColorRGBA(0.08f, 0.10f, 0.16f, 1f); private static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.04f, 0.04f, 0.12f, 1f); private static final ColorRGBA BG_DAY = new ColorRGBA(0.35f, 0.55f, 0.85f, 1f); private static final ColorRGBA BG_NIGHT = new ColorRGBA(0.01f, 0.01f, 0.06f, 1f); @@ -72,8 +71,20 @@ public class DayNightState extends BaseAppState implements TimeListener { public void setPaused(boolean paused) { dayTime.setPaused(paused); } /** Aktuelle Sonnenrichtung (normalisiert), oder (0,-1,0) falls noch nicht initialisiert. */ - public com.jme3.math.Vector3f getSunDirection() { - return sun != null ? sun.getDirection() : new com.jme3.math.Vector3f(0f, -1f, 0f); + public Vector3f getSunDirection() { + return sun != null ? sun.getDirection() : new Vector3f(0f, -1f, 0f); + } + + public DirectionalLight getSunLight() { return sun; } + public AmbientLight getAmbientLight() { return ambient; } + + /** + * Übergibt einen Shadow-Filter an DayNightState, damit dieser die Intensität + * mit der Sonnenhöhe synchronisiert. Muss nach initialize() aufgerufen werden. + */ + public void setShadowFilter(DirectionalLightShadowFilter f) { + shadowFilter = f; + if (f != null && sun != null) f.setLight(sun); } // ── Lifecycle ───────────────────────────────────────────────────────────── @@ -83,15 +94,16 @@ public class DayNightState extends BaseAppState implements TimeListener { this.app = (SimpleApplication) app; this.rootNode = this.app.getRootNode(); - // Himmel - try { - sky = SkyFactory.createSky(app.getAssetManager(), - "Textures/Sky/Bright/BrightSky.dds", - SkyFactory.EnvMapType.CubeMap); - rootNode.attachChild(sky); - } catch (Exception e) { - log.warn("Sky-Textur nicht geladen – Viewport-Farbe als Fallback"); - } + // Himmel-Sphere (prozedural, nach innen gerendert) + Sphere skyMesh = new Sphere(32, 32, 450f, true, true); + Geometry skyGeo = new Geometry("sky", skyMesh); + Material skyMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + skyMat.setColor("Color", BG_DAY.clone()); + skyGeo.setMaterial(skyMat); + skyGeo.setQueueBucket(RenderQueue.Bucket.Sky); + skyGeo.setShadowMode(RenderQueue.ShadowMode.Off); + rootNode.attachChild(skyGeo); + sky = skyGeo; // Sonnen-Sphere (sichtbare Sonne am Himmel) Sphere sphereMesh = new Sphere(16, 16, 18f); @@ -112,6 +124,10 @@ public class DayNightState extends BaseAppState implements TimeListener { ambient = new AmbientLight(); rootNode.addLight(ambient); + // Falls WorldScene.buildLighting() schon einen Filter registriert hat (setShadowFilter + // wurde vor initialize() aufgerufen, sun war damals null) — jetzt nachholen. + if (shadowFilter != null) shadowFilter.setLight(sun); + // Schatten (nur im Game) – als Filter, damit WorldScene ihn in den FPP einhängen kann if (withShadows) { try { @@ -157,9 +173,14 @@ public class DayNightState extends BaseAppState implements TimeListener { if (sunSphere != null && sunSphere.getParent() == null) rootNode.attachChild(sunSphere); + Vector3f camPos = app.getCamera().getLocation(); + + // Himmel-Sphere der Kamera folgen lassen + if (sky != null) + sky.setLocalTranslation(camPos); + // Sonnen-Sphere immer relativ zur Kamera positionieren if (sunSphere != null && sunSphere.getCullHint() != Spatial.CullHint.Always) { - Vector3f camPos = app.getCamera().getLocation(); sunSphere.setLocalTranslation(camPos.add(sun.getDirection().negate().mult(480f))); } } @@ -184,7 +205,7 @@ public class DayNightState extends BaseAppState implements TimeListener { // ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) ────────────── float dawnFactor = 1f - FastMath.clamp(elev * 5f, 0f, 1f); ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor); - sun.setColor(sunColor.multLocal(elevC * 1.35f)); + sun.setColor(sunColor.multLocal(elevC * 0.85f)); // ── Sonnen-Sphere ausblenden wenn unter Horizont ──────────────────── if (sunSphere != null) { @@ -209,12 +230,10 @@ public class DayNightState extends BaseAppState implements TimeListener { // ── Himmel & Hintergrundfarbe ──────────────────────────────────────── float skyFactor = FastMath.clamp((elev + 0.05f) / 0.2f, 0f, 1f); - if (sky != null) - sky.setCullHint(skyFactor > 0.01f - ? Spatial.CullHint.Inherit - : Spatial.CullHint.Always); - app.getViewPort().setBackgroundColor( - BG_NIGHT.clone().interpolateLocal(BG_DAY, skyFactor)); + ColorRGBA skyColor = BG_NIGHT.clone().interpolateLocal(BG_DAY, skyFactor); + if (sky instanceof Geometry skyGeo) + skyGeo.getMaterial().setColor("Color", skyColor); + app.getViewPort().setBackgroundColor(skyColor); } // ── Hilfsmethoden ───────────────────────────────────────────────────────── diff --git a/blight-game/src/main/java/de/blight/game/state/GrassState.java b/blight-game/src/main/java/de/blight/game/state/GrassState.java index 26e3254..c9e55b9 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassState.java @@ -20,6 +20,9 @@ import com.jme3.util.BufferUtils; import de.blight.common.GrassTuft; import de.blight.common.GrassTuftIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.FloatBuffer; import java.nio.IntBuffer; @@ -32,6 +35,8 @@ import java.util.*; */ public class GrassState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(GrassState.class); + private static final int TERRAIN_HALF = 2048; private static final int CHUNK_SIZE = 128; private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE; // 32 @@ -77,7 +82,7 @@ public class GrassState extends BaseAppState { } } } catch (IOException e) { - System.err.println("[GrassState] Gras nicht ladbar: " + e.getMessage()); + log.warn("[GrassState] Gras nicht ladbar: {}", e.getMessage()); } if (slotMaterials.isEmpty()) { @@ -101,6 +106,16 @@ public class GrassState extends BaseAppState { nextChunk++; built++; } + + DayNightState dns = getApplication().getStateManager().getState(DayNightState.class); + if (dns != null) { + for (Material mat : slotMaterials.values()) { + if ("Grass".equals(mat.getMaterialDef().getName())) { + mat.setVector3("SunDir", dns.getSunDirection().negate()); + mat.setColor("SunColor", dns.getSunLight().getColor()); + } + } + } } // ── Material ────────────────────────────────────────────────────────────── @@ -119,13 +134,15 @@ public class GrassState extends BaseAppState { Material mat = new Material(assets, "MatDefs/Grass.j3md"); mat.setFloat("WindSpeed", 0.5f); mat.setFloat("WindStrength", 0.14f); + mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f)); + mat.setColor("SunColor", ColorRGBA.White); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); if (texPath != null && !texPath.isEmpty()) { try { mat.setTexture("ColorMap", assets.loadTexture(texPath)); mat.setColor("Color", ColorRGBA.White); } catch (Exception te) { - System.err.println("[GrassState] Gras-Textur nicht ladbar '" + texPath + "': " + te.getMessage()); + log.warn("[GrassState] Gras-Textur nicht ladbar '{}': {}", texPath, te.getMessage()); mat.setColor("Color", new ColorRGBA(0.28f, 0.72f, 0.18f, 1f)); } } else { @@ -133,7 +150,7 @@ public class GrassState extends BaseAppState { } return mat; } catch (Exception e) { - System.err.println("[GrassState] Grass.j3md nicht gefunden, Fallback: " + e.getMessage()); + log.warn("[GrassState] Grass.j3md nicht gefunden, Fallback: {}", e.getMessage()); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", new ColorRGBA(0.25f, 0.65f, 0.15f, 1f)); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); diff --git a/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java b/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java index a0356a3..2832ea0 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java @@ -17,6 +17,9 @@ import com.jme3.util.BufferUtils; import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.List; @@ -28,6 +31,8 @@ import java.util.List; public class GrassVertexRenderState extends BaseAppState implements TerrainChunkState.ChunkListener { + private static final Logger log = LoggerFactory.getLogger(GrassVertexRenderState.class); + // ── Chunks ──────────────────────────────────────────────────────────────── private static final int TERRAIN_HALF = 2048; private static final int CHUNK_SIZE = 128; @@ -79,7 +84,7 @@ public class GrassVertexRenderState extends BaseAppState if (ci >= 0) chunkBlades[ci].add(b); } } catch (Exception e) { - System.err.println("[GrassVertexRenderState] Daten nicht ladbar: " + e.getMessage()); + log.warn("[GrassVertexRenderState] Daten nicht ladbar: {}", e.getMessage()); } material = buildMaterial(); @@ -101,6 +106,15 @@ public class GrassVertexRenderState extends BaseAppState buildChunk(nextChunk++); built++; } + + if (material != null && material.getMaterialDef().getMaterialParam("SunColor") != null) { + DayNightState dns = getApplication().getStateManager().getState(DayNightState.class); + if (dns != null && dns.getSunLight() != null) { + material.setVector3("SunDir", dns.getSunDirection().negate()); + com.jme3.math.ColorRGBA sc = dns.getSunLight().getColor(); + material.setColor("SunColor", sc); + } + } } // ── Material ────────────────────────────────────────────────────────────── @@ -115,7 +129,7 @@ public class GrassVertexRenderState extends BaseAppState mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); return mat; } catch (Exception e) { - System.err.println("[GrassVertexRenderState] Material nicht ladbar: " + e.getMessage()); + log.warn("[GrassVertexRenderState] Material nicht ladbar: {}", e.getMessage()); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f)); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); diff --git a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java index 9e8b82f..c9b2a4b 100644 --- a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java +++ b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java @@ -8,6 +8,7 @@ import de.blight.common.VoxelChunk; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; /** @@ -333,17 +334,17 @@ public final class MarchingCubes { /** * Erzeugt ein JME-Mesh aus den Voxel-Daten mit dem angegebenen LOD-Schritt. * - * @param chunk Quelldaten - * @param lodStep 1=LOD0 (voll), 4=LOD1, 16=LOD2 + * @param chunk Quelldaten + * @param lodStep 1=LOD0 (voll), 4=LOD1, 16=LOD2 + * @param neighbors 6 Nachbar-Chunks [+X,-X,+Y,-Y,+Z,-Z] für nahtlose Grenzen (null-Einträge = Luft) * @return JME-Mesh oder null wenn keine Oberfläche vorhanden */ - public static Mesh build(VoxelChunk chunk, int lodStep) { + public static Mesh build(VoxelChunk chunk, int lodStep, VoxelChunk[] neighbors) { if (chunk.isEmpty()) return null; - // Flache Listen für Positions-, Normal- und Color-Daten + // Flache Listen für Positions- und Normal-Daten List positions = new ArrayList<>(4096); List normals = new ArrayList<>(4096); - List colors = new ArrayList<>(4096); int cells = VoxelChunk.CELLS; // 128 int size = VoxelChunk.SIZE; // 129 @@ -386,22 +387,11 @@ public final class MarchingCubes { if (EDGE_TABLE[cubeIndex] == 0) continue; - // Materialen der 8 Eckpunkte - int m0 = chunk.getMaterial(x0, y0, z0) & 0xFF; - int m1 = chunk.getMaterial(x1, y0, z0) & 0xFF; - int m2 = chunk.getMaterial(x1, y0, z1) & 0xFF; - int m3 = chunk.getMaterial(x0, y0, z1) & 0xFF; - int m4 = chunk.getMaterial(x0, y1, z0) & 0xFF; - int m5 = chunk.getMaterial(x1, y1, z0) & 0xFF; - int m6 = chunk.getMaterial(x1, y1, z1) & 0xFF; - int m7 = chunk.getMaterial(x0, y1, z1) & 0xFF; - // Positionen der 8 Eckpunkte als float[] float[] vx = { x0, x1, x1, x0, x0, x1, x1, x0 }; float[] vy = { y0, y0, y0, y0, y1, y1, y1, y1 }; float[] vz = { z0, z0, z1, z1, z0, z0, z1, z1 }; float[] dd = { d0, d1, d2, d3, d4, d5, d6, d7 }; - int[] mm = { m0, m1, m2, m3, m4, m5, m6, m7 }; // 12 Kantenpunkte berechnen float[] epx = new float[12]; @@ -410,10 +400,6 @@ public final class MarchingCubes { float[] enx = new float[12]; float[] eny = new float[12]; float[] enz = new float[12]; - float[] ecR = new float[12]; - float[] ecG = new float[12]; - float[] ecB = new float[12]; - float[] ecA = new float[12]; // Kante i verbindet Vertex A mit Vertex B int[][] edgeVerts = { @@ -441,21 +427,14 @@ public final class MarchingCubes { epz[e] = vz[a] + t * (vz[b] - vz[a]); // Normalen via Gradient - float[] gA = gradient(chunk, (int)vx[a], (int)vy[a], (int)vz[a]); - float[] gB = gradient(chunk, (int)vx[b], (int)vy[b], (int)vz[b]); + float[] gA = gradient(chunk, neighbors, (int)vx[a], (int)vy[a], (int)vz[a]); + float[] gB = gradient(chunk, neighbors, (int)vx[b], (int)vy[b], (int)vz[b]); enx[e] = gA[0] + t * (gB[0] - gA[0]); eny[e] = gA[1] + t * (gB[1] - gA[1]); enz[e] = gA[2] + t * (gB[2] - gA[2]); float nlen = (float)Math.sqrt(enx[e]*enx[e] + eny[e]*eny[e] + enz[e]*enz[e]); if (nlen > 1e-6f) { enx[e] /= nlen; eny[e] /= nlen; enz[e] /= nlen; } - - // Material-Blend - float[] wA = matWeights(mm[a]); - float[] wB = matWeights(mm[b]); - ecR[e] = wA[0] + t * (wB[0] - wA[0]); - ecG[e] = wA[1] + t * (wB[1] - wA[1]); - ecB[e] = wA[2] + t * (wB[2] - wA[2]); - ecA[e] = wA[3] + t * (wB[3] - wA[3]); + else { enx[e] = 0f; eny[e] = 1f; enz[e] = 0f; } } // Dreiecke ausgeben @@ -466,15 +445,12 @@ public final class MarchingCubes { // Vertex 0 positions.add(epx[e0]); positions.add(epy[e0]); positions.add(epz[e0]); normals.add(enx[e0]); normals.add(eny[e0]); normals.add(enz[e0]); - colors.add(ecR[e0]); colors.add(ecG[e0]); colors.add(ecB[e0]); colors.add(ecA[e0]); // Vertex 1 positions.add(epx[e1]); positions.add(epy[e1]); positions.add(epz[e1]); normals.add(enx[e1]); normals.add(eny[e1]); normals.add(enz[e1]); - colors.add(ecR[e1]); colors.add(ecG[e1]); colors.add(ecB[e1]); colors.add(ecA[e1]); // Vertex 2 positions.add(epx[e2]); positions.add(epy[e2]); positions.add(epz[e2]); normals.add(enx[e2]); normals.add(eny[e2]); normals.add(enz[e2]); - colors.add(ecR[e2]); colors.add(ecG[e2]); colors.add(ecB[e2]); colors.add(ecA[e2]); } } } @@ -486,42 +462,202 @@ public final class MarchingCubes { FloatBuffer posBuf = BufferUtils.createFloatBuffer(positions.size()); FloatBuffer normBuf = BufferUtils.createFloatBuffer(normals.size()); - FloatBuffer colBuf = BufferUtils.createFloatBuffer(colors.size()); IntBuffer idxBuf = BufferUtils.createIntBuffer(vertCount); for (float v : positions) posBuf.put(v); for (float v : normals) normBuf.put(v); - for (float v : colors) colBuf.put(v); for (int i = 0; i < vertCount; i++) idxBuf.put(i); - posBuf.rewind(); normBuf.rewind(); colBuf.rewind(); idxBuf.rewind(); + posBuf.rewind(); normBuf.rewind(); idxBuf.rewind(); Mesh mesh = new Mesh(); mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf); mesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf); - mesh.setBuffer(VertexBuffer.Type.Color, 4, colBuf); mesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf); + mesh.setStatic(); mesh.updateBound(); return mesh; } + /** Erzeugt Mesh ohne Nachbar-Chunks (Chunk-Grenzen werden als Luft behandelt). */ + public static Mesh build(VoxelChunk chunk, int lodStep) { + return build(chunk, lodStep, null); + } + + // ── Laplacian-Glättung ──────────────────────────────────────────────────── + + /** + * Laplacian-Glättung eines MC-Meshes (kein Vertex-Sharing). + * Gruppiert Vertices per Position, mittelt Nachbar-Positionen, + * berechnet danach Normalen aus der geglätteten Geometrie neu. + * + * @param iterations Anzahl Glättungsdurchläufe (3-5 empfohlen) + * @param factor Stärke pro Durchlauf (0.3–0.5; größer = glatter, aber leichtes Schrumpfen) + */ + public static Mesh smooth(Mesh mesh, int iterations, float factor) { + if (mesh == null) return null; + FloatBuffer posF = mesh.getFloatBuffer(VertexBuffer.Type.Position); + if (posF == null) return mesh; + int vertCount = posF.capacity() / 3; + if (vertCount < 3) return mesh; + int triCount = vertCount / 3; + + float[] pos = new float[vertCount * 3]; + posF.rewind(); posF.get(pos); + + // Vertices gleicher Position zu Gruppen zusammenfassen. + // Schlüssel: quantisierte XYZ-Koordinaten (1/2048 Voxel-Auflösung). + HashMap keyToGroup = new HashMap<>(vertCount * 2); + int[] vertGroup = new int[vertCount]; + int[] groupFirst = new int[vertCount]; // ein Repräsentant je Gruppe + int groupCount = 0; + for (int v = 0; v < vertCount; v++) { + long key = posKey(pos, v); + Integer g = keyToGroup.get(key); + if (g == null) { + keyToGroup.put(key, groupCount); + groupFirst[groupCount] = v; + vertGroup[v] = groupCount++; + } else { + vertGroup[v] = g; + } + } + + // Gruppen-Positionen initialisieren + float[] gx = new float[groupCount]; + float[] gy = new float[groupCount]; + float[] gz = new float[groupCount]; + for (int g = 0; g < groupCount; g++) { + int v = groupFirst[g]; + gx[g] = pos[v*3]; gy[g] = pos[v*3+1]; gz[g] = pos[v*3+2]; + } + + // Randvertices einfrieren: Vertices an den 6 Chunk-Grenzflächen (x/y/z = 0 oder CELLS) + // dürfen sich nicht verschieben, damit benachbarte Chunk-Meshes nahtlos bleiben. + float bound = VoxelChunk.CELLS; // = 128.0 + boolean[] pinned = new boolean[groupCount]; + for (int g = 0; g < groupCount; g++) { + float x = gx[g], y = gy[g], z = gz[g]; + if (x < 0.01f || x > bound - 0.01f || + y < 0.01f || y > bound - 0.01f || + z < 0.01f || z > bound - 0.01f) { + pinned[g] = true; + } + } + + float[] ax = new float[groupCount]; + float[] ay = new float[groupCount]; + float[] az = new float[groupCount]; + int[] ac = new int[groupCount]; + + for (int iter = 0; iter < iterations; iter++) { + java.util.Arrays.fill(ax, 0f); java.util.Arrays.fill(ay, 0f); + java.util.Arrays.fill(az, 0f); java.util.Arrays.fill(ac, 0); + + for (int t = 0; t < triCount; t++) { + int g0 = vertGroup[t*3], g1 = vertGroup[t*3+1], g2 = vertGroup[t*3+2]; + ax[g0]+=gx[g1]+gx[g2]; ay[g0]+=gy[g1]+gy[g2]; az[g0]+=gz[g1]+gz[g2]; ac[g0]+=2; + ax[g1]+=gx[g0]+gx[g2]; ay[g1]+=gy[g0]+gy[g2]; az[g1]+=gz[g0]+gz[g2]; ac[g1]+=2; + ax[g2]+=gx[g0]+gx[g1]; ay[g2]+=gy[g0]+gy[g1]; az[g2]+=gz[g0]+gz[g1]; ac[g2]+=2; + } + for (int g = 0; g < groupCount; g++) { + if (!pinned[g] && ac[g] > 0) { + float inv = 1f / ac[g]; + gx[g] += factor * (ax[g]*inv - gx[g]); + gy[g] += factor * (ay[g]*inv - gy[g]); + gz[g] += factor * (az[g]*inv - gz[g]); + } + } + } + + // Vertex-Positionen aktualisieren + for (int v = 0; v < vertCount; v++) { + int g = vertGroup[v]; + pos[v*3] = gx[g]; pos[v*3+1] = gy[g]; pos[v*3+2] = gz[g]; + } + posF.rewind(); posF.put(pos); posF.rewind(); + + // Normalen aus geglätteter Geometrie neu berechnen. + // 1. Flächennormalen pro Dreieck + float[] fn = new float[triCount * 3]; + for (int t = 0; t < triCount; t++) { + float p0x=pos[t*9], p0y=pos[t*9+1], p0z=pos[t*9+2]; + float p1x=pos[t*9+3], p1y=pos[t*9+4], p1z=pos[t*9+5]; + float p2x=pos[t*9+6], p2y=pos[t*9+7], p2z=pos[t*9+8]; + float ex=p1x-p0x, ey=p1y-p0y, ez=p1z-p0z; + float fx=p2x-p0x, fy=p2y-p0y, fz=p2z-p0z; + float nx=ey*fz-ez*fy, ny=ez*fx-ex*fz, nz=ex*fy-ey*fx; + float len=(float)Math.sqrt(nx*nx+ny*ny+nz*nz); + fn[t*3] = len>1e-6f ? nx/len : 0f; + fn[t*3+1] = len>1e-6f ? ny/len : 1f; + fn[t*3+2] = len>1e-6f ? nz/len : 0f; + } + // 2. Gruppen-Normalen: Dreieck-Normalen aller anliegenden Dreiecke mitteln + float[] gnx = new float[groupCount]; + float[] gny = new float[groupCount]; + float[] gnz = new float[groupCount]; + for (int t = 0; t < triCount; t++) { + int g0=vertGroup[t*3], g1=vertGroup[t*3+1], g2=vertGroup[t*3+2]; + gnx[g0]+=fn[t*3]; gny[g0]+=fn[t*3+1]; gnz[g0]+=fn[t*3+2]; + gnx[g1]+=fn[t*3]; gny[g1]+=fn[t*3+1]; gnz[g1]+=fn[t*3+2]; + gnx[g2]+=fn[t*3]; gny[g2]+=fn[t*3+1]; gnz[g2]+=fn[t*3+2]; + } + for (int g = 0; g < groupCount; g++) { + float len=(float)Math.sqrt(gnx[g]*gnx[g]+gny[g]*gny[g]+gnz[g]*gnz[g]); + if (len>1e-6f) { gnx[g]/=len; gny[g]/=len; gnz[g]/=len; } + else { gny[g]=1f; } + } + // 3. Vertex-Normalen schreiben + FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal); + normF.rewind(); + for (int v = 0; v < vertCount; v++) { + int g = vertGroup[v]; + normF.put(gnx[g]).put(gny[g]).put(gnz[g]); + } + normF.rewind(); + + mesh.updateBound(); + return mesh; + } + + private static long posKey(float[] pos, int v) { + // Quantisierung auf 1/2048 Voxel; Position [0,128] → max Wert 262144 < 2^19 → 21 Bits/Achse + int ix = Math.round(pos[v*3] * 2048f) + 131072; + int iy = Math.round(pos[v*3+1] * 2048f) + 131072; + int iz = Math.round(pos[v*3+2] * 2048f) + 131072; + return (long)ix | ((long)iy << 21) | ((long)iz << 42); + } + // ── Hilfsmethoden ───────────────────────────────────────────────────────── /** Berechnet den negierten Gradienten (zeigt von Solid weg) an Position (ix,iy,iz). */ - private static float[] gradient(VoxelChunk chunk, int ix, int iy, int iz) { - float nx = getDensityClamped(chunk, ix+1, iy, iz ) - - getDensityClamped(chunk, ix-1, iy, iz ); - float ny = getDensityClamped(chunk, ix, iy+1, iz ) - - getDensityClamped(chunk, ix, iy-1, iz ); - float nz = getDensityClamped(chunk, ix, iy, iz+1) - - getDensityClamped(chunk, ix, iy, iz-1); + private static float[] gradient(VoxelChunk chunk, VoxelChunk[] nb, int ix, int iy, int iz) { + float nx = getDensityWithNeighbors(chunk, nb, ix+1, iy, iz ) + - getDensityWithNeighbors(chunk, nb, ix-1, iy, iz ); + float ny = getDensityWithNeighbors(chunk, nb, ix, iy+1, iz ) + - getDensityWithNeighbors(chunk, nb, ix, iy-1, iz ); + float nz = getDensityWithNeighbors(chunk, nb, ix, iy, iz+1) + - getDensityWithNeighbors(chunk, nb, ix, iy, iz-1); // negieren: Gradient zeigt vom Solid weg (nach außen) float len = (float)Math.sqrt(nx*nx + ny*ny + nz*nz); if (len < 1e-6f) return new float[]{ 0f, 1f, 0f }; return new float[]{ -nx/len, -ny/len, -nz/len }; } - /** Liest Dichte mit Klemmen an Chunk-Grenzen. */ + /** Liest Dichte: innerhalb des Chunks direkt, außerhalb via Nachbar-Chunk oder Klemmen. */ + private static float getDensityWithNeighbors(VoxelChunk chunk, VoxelChunk[] nb, int x, int y, int z) { + int cells = VoxelChunk.CELLS; // 128 + int size = VoxelChunk.SIZE; // 129 + if (x < 0) return (nb != null && nb[1] != null) ? nb[1].getDensity(cells + x, y, z) : getDensityClamped(chunk, 0, y, z); + if (x >= size) return (nb != null && nb[0] != null) ? nb[0].getDensity(x - cells, y, z) : getDensityClamped(chunk, size-1, y, z); + if (y < 0) return (nb != null && nb[3] != null) ? nb[3].getDensity(x, cells + y, z) : getDensityClamped(chunk, x, 0, z); + if (y >= size) return (nb != null && nb[2] != null) ? nb[2].getDensity(x, y - cells, z) : getDensityClamped(chunk, x, size-1, z); + if (z < 0) return (nb != null && nb[5] != null) ? nb[5].getDensity(x, y, cells + z) : getDensityClamped(chunk, x, y, 0); + if (z >= size) return (nb != null && nb[4] != null) ? nb[4].getDensity(x, y, z - cells) : getDensityClamped(chunk, x, y, size-1); + return chunk.getDensity(x, y, z); + } + + /** Liest Dichte mit Klemmen an Chunk-Grenzen (Fallback wenn kein Nachbar bekannt). */ private static float getDensityClamped(VoxelChunk chunk, int x, int y, int z) { int s = VoxelChunk.SIZE - 1; // 128 x = Math.max(0, Math.min(s, x)); @@ -530,13 +666,4 @@ public final class MarchingCubes { return chunk.getDensity(x, y, z); } - /** Gibt vec4-Gewichte für ein Material zurück: 0→(1,0,0,0), 1→(0,1,0,0), usw. */ - private static float[] matWeights(int matId) { - switch (matId & 3) { - case 0: return new float[]{ 1f, 0f, 0f, 0f }; - case 1: return new float[]{ 0f, 1f, 0f, 0f }; - case 2: return new float[]{ 0f, 0f, 1f, 0f }; - default: return new float[]{ 0f, 0f, 0f, 1f }; - } - } } diff --git a/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java b/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java index 23f9e11..26da3d3 100644 --- a/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java +++ b/blight-game/src/main/java/de/blight/game/state/ModelLodControl.java @@ -1,87 +1,146 @@ package de.blight.game.state; import com.jme3.asset.AssetManager; +import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; -import com.jme3.renderer.Camera; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.AbstractControl; /** - * Wechselt zwischen Main-Model, LOD1, LOD2 und Ausblenden basierend auf Kameradistanz. + * Wechselt zwischen LOD-Stufen und blendet Objekte ab einer Sichtweite aus. * - * Struktur des kontrollierten Node: - * [0] = Haupt-Spatial (LOD0) - * [1] = LOD1-Spatial (wird lazy geladen, wenn lod1Path gesetzt) - * [2] = LOD2-Spatial (wird lazy geladen, wenn lod2Path gesetzt) + * Zwei Modi: + * + * (A) Embedded-LOD-Modus: Das j3o enthält bereits child-Nodes "lod0", "lod1", "lod2" + * (erzeugt vom TreeGenerator). ModelLodControl ist direkt am j3o-Root befestigt + * und steuert deren CullHint. Kein lodRoot-Wrapper nötig. + * + * (B) Externer-LOD-Modus: LOD-Stufen sind separate j3o-Dateien und werden lazy geladen. + * ModelLodControl sitzt an einem lodRoot-Knoten der als [0]=Main, [1]=LOD1, [2]=LOD2 + * strukturiert ist. + * + * controlRender() ist bewusst leer: der Shadow-Renderer ruft controlRender mit seiner + * eigenen Kamera auf — Kameradistanz wird daher nur in controlUpdate() berechnet. */ public class ModelLodControl extends AbstractControl { + private final Camera cam; + private final float lod1DistSq; + private final float lod2DistSq; + private final float cullDistSq; + private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=culled + + // ── Embedded-LOD-Modus ──────────────────────────────────────────────────── + private final Spatial embLod0; + private final Spatial embLod1; + private final Spatial embLod2; + + // ── Externer-LOD-Modus ──────────────────────────────────────────────────── private final AssetManager assets; - private final String lod1Path; - private final String lod2Path; - private final float lod1DistSq; - private final float lod2DistSq; - private final float cullDistSq; + private String lod1Path; + private String lod2Path; + private boolean lod1Loaded; + private boolean lod2Loaded; + private java.util.function.Consumer lodLoadCallback; - private boolean lod1Loaded = false; - private boolean lod2Loaded = false; - private int currentSlot = 0; // 0=main, 1=lod1, 2=lod2, -1=culled + // ── Konstruktoren ───────────────────────────────────────────────────────── - public ModelLodControl(AssetManager assets, + /** + * Embedded-LOD-Modus: Die j3o-Nodes lod0/lod1/lod2 existieren bereits als + * Child-Nodes im Spatial, an dem dieser Control befestigt wird. + * lod1 und lod2 dürfen null sein (dann bleibt lod0 bis zur cullDistance sichtbar). + */ + public ModelLodControl(Camera cam, + Spatial lod0, Spatial lod1, Spatial lod2, + float lod1Distance, float lod2Distance, float cullDistance) { + this.cam = cam; + this.embLod0 = lod0; + this.embLod1 = lod1; + this.embLod2 = lod2; + this.lod1DistSq = lod1 != null ? lod1Distance * lod1Distance : Float.MAX_VALUE; + this.lod2DistSq = lod2 != null ? lod2Distance * lod2Distance : Float.MAX_VALUE; + this.cullDistSq = cullDistance * cullDistance; + this.assets = null; + } + + /** + * Externer-LOD-Modus: LOD-Stufen werden lazy aus separaten j3o-Dateien geladen. + * spatial muss ein lodRoot-Node sein: [0]=Main, [1]=LOD1 (lazy), [2]=LOD2 (lazy). + */ + public ModelLodControl(AssetManager assets, Camera cam, String lod1Path, String lod2Path, float lod1Distance, float lod2Distance, float cullDistance) { - this.assets = assets; - this.lod1Path = (lod1Path != null && !lod1Path.isBlank()) ? lod1Path : null; - this.lod2Path = (lod2Path != null && !lod2Path.isBlank()) ? lod2Path : null; - this.lod1DistSq = lod1Distance * lod1Distance; - this.lod2DistSq = lod2Distance * lod2Distance; - this.cullDistSq = cullDistance * cullDistance; + this.assets = assets; + this.cam = cam; + this.lod1Path = (lod1Path != null && !lod1Path.isBlank()) ? lod1Path : null; + this.lod2Path = (lod2Path != null && !lod2Path.isBlank()) ? lod2Path : null; + this.lod1DistSq = lod1Distance * lod1Distance; + this.lod2DistSq = lod2Distance * lod2Distance; + this.cullDistSq = cullDistance * cullDistance; + this.embLod0 = null; + this.embLod1 = null; + this.embLod2 = null; } + public void setLodLoadCallback(java.util.function.Consumer cb) { + this.lodLoadCallback = cb; + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) {} + @Override protected void controlUpdate(float tpf) { - if (!(spatial instanceof Node node)) return; - Camera cam = null; - // walk up to get the app's camera via the scene - // We use the ViewPort supplied during render; cache camera via controlRender instead. - // Nothing to do here without camera ref — logic moved to controlRender. - } - - @Override - protected void controlRender(RenderManager rm, ViewPort vp) { - if (!(spatial instanceof Node node)) return; - Camera cam = vp.getCamera(); - float dx = cam.getLocation().x - spatial.getWorldTranslation().x; float dy = cam.getLocation().y - spatial.getWorldTranslation().y; float dz = cam.getLocation().z - spatial.getWorldTranslation().z; float distSq = dx*dx + dy*dy + dz*dz; + boolean embedded = embLod0 != null; + int targetSlot; if (distSq >= cullDistSq) { targetSlot = -1; - } else if (lod2Path != null && distSq >= lod2DistSq) { - targetSlot = 2; - } else if (lod1Path != null && distSq >= lod1DistSq) { - targetSlot = 1; + } else if (embedded) { + if (embLod2 != null && distSq >= lod2DistSq) targetSlot = 2; + else if (embLod1 != null && distSq >= lod1DistSq) targetSlot = 1; + else targetSlot = 0; } else { - targetSlot = 0; + if (lod2Path != null && distSq >= lod2DistSq) targetSlot = 2; + else if (lod1Path != null && distSq >= lod1DistSq) targetSlot = 1; + else targetSlot = 0; } if (targetSlot == currentSlot) return; currentSlot = targetSlot; - // Ensure LOD spatials are loaded + if (targetSlot == -1) { + spatial.setCullHint(Spatial.CullHint.Always); + return; + } + spatial.setCullHint(Spatial.CullHint.Dynamic); + + if (embedded) { + if (embLod0 != null) embLod0.setCullHint(targetSlot == 0 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + if (embLod1 != null) embLod1.setCullHint(targetSlot == 1 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + if (embLod2 != null) embLod2.setCullHint(targetSlot == 2 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + return; + } + + // Externer LOD-Modus: lazy Laden + Sichtbarkeit + if (!(spatial instanceof Node node)) return; + if (targetSlot == 1 && !lod1Loaded) { lod1Loaded = true; try { Spatial lod1 = assets.loadModel(lod1Path); lod1.setName("lod1"); node.attachChildAt(lod1, 1); + if (lodLoadCallback != null) lodLoadCallback.accept(lod1); } catch (Exception e) { - // LOD1 load failed — fall back to main + lod1Path = null; currentSlot = 0; targetSlot = 0; } @@ -91,31 +150,21 @@ public class ModelLodControl extends AbstractControl { try { Spatial lod2 = assets.loadModel(lod2Path); lod2.setName("lod2"); - // ensure index 2 exists if (node.getChildren().size() < 2 && lod1Path != null && !lod1Loaded) { - // lod1 slot not yet loaded — add placeholder to maintain index - Node placeholder = new Node("lod1_placeholder"); - node.attachChild(placeholder); + node.attachChild(new Node("lod1_placeholder")); } node.attachChild(lod2); + if (lodLoadCallback != null) lodLoadCallback.accept(lod2); } catch (Exception e) { + lod2Path = null; currentSlot = lod1Path != null ? 1 : 0; targetSlot = currentSlot; } } - // Apply visibility: cull hint on node itself for -1, else show correct child - if (targetSlot == -1) { - spatial.setCullHint(Spatial.CullHint.Always); - return; - } - spatial.setCullHint(Spatial.CullHint.Dynamic); - for (int i = 0; i < node.getChildren().size(); i++) { - Spatial child = node.getChildren().get(i); - child.setCullHint(i == targetSlot - ? Spatial.CullHint.Dynamic - : Spatial.CullHint.Always); + node.getChildren().get(i).setCullHint( + i == targetSlot ? Spatial.CullHint.Dynamic : Spatial.CullHint.Always); } } } diff --git a/blight-game/src/main/java/de/blight/game/state/RiverState.java b/blight-game/src/main/java/de/blight/game/state/RiverState.java index c03cf3e..aa408b3 100644 --- a/blight-game/src/main/java/de/blight/game/state/RiverState.java +++ b/blight-game/src/main/java/de/blight/game/state/RiverState.java @@ -218,8 +218,8 @@ public class RiverState extends BaseAppState { // Normal-Map: erst eigene Texturen versuchen, dann JME-Fallback Texture nm = loadTextureOr( - isWaterfall ? "Textures/water/waterfall_normal.png" - : "Textures/water/river_normal.jpg", + isWaterfall ? "Textures/internal/water/waterfall_normal.png" + : "Textures/internal/water/river_normal.jpg", "Common/MatDefs/Water/Textures/water_normalmap.png"); if (nm != null) { nm.setWrap(Texture.WrapMode.Repeat); @@ -233,8 +233,8 @@ public class RiverState extends BaseAppState { } // Diffuse-Map: river.jpg für Fluss (Farbmodulation), waterfall_diffuse für Gischt - String diffPath = isWaterfall ? "Textures/water/waterfall_diffuse.png" - : "Textures/water/river.jpg"; + String diffPath = isWaterfall ? "Textures/internal/water/waterfall_diffuse.png" + : "Textures/internal/water/river.jpg"; Texture diff = loadTextureOr(diffPath, null); if (diff != null) { diff.setWrap(Texture.WrapMode.Repeat); @@ -327,7 +327,7 @@ public class RiverState extends BaseAppState { "waterfall_particles", ParticleMesh.Type.Triangle, 60); Material pMat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md"); - Texture pTex = loadTextureOr("Textures/Water/spray.png", "Effects/Smoke/Smoke.png"); + Texture pTex = loadTextureOr("Textures/internal/Water/spray.png", "Effects/Smoke/Smoke.png"); if (pTex != null) pMat.setTexture("Texture", pTex); pMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive); diff --git a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java index d3e7436..0edabe0 100644 --- a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java +++ b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java @@ -18,6 +18,9 @@ import com.jme3.util.BufferUtils; import de.blight.common.ChunkTerrainIO; import de.blight.common.MapData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -36,6 +39,8 @@ import java.util.List; */ public class TerrainChunkState extends BaseAppState { + private static final Logger log = LoggerFactory.getLogger(TerrainChunkState.class); + // ── Listener-Interface ──────────────────────────────────────────────────── public interface ChunkListener { @@ -70,6 +75,10 @@ public class TerrainChunkState extends BaseAppState { private int lastPlayerCx = Integer.MIN_VALUE; private int lastPlayerCz = Integer.MIN_VALUE; + /** Beim nächsten update() diese Position statt Kamera-Position verwenden (einmalig). */ + private float spawnHintX = Float.NaN; + private float spawnHintZ = Float.NaN; + // ── Konstruktor ─────────────────────────────────────────────────────────── public TerrainChunkState(BulletAppState bulletAppState, Material terrainMaterial, MapData mapData) { @@ -80,57 +89,89 @@ public class TerrainChunkState extends BaseAppState { // ── Lifecycle ───────────────────────────────────────────────────────────── - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.cam = app.getCamera(); - Arrays.fill(chunkLod, -1); - - terrainRoot = new Node("terrainChunks"); - this.app.getRootNode().attachChild(terrainRoot); - + public void loadChunkHeights() { if (!ChunkTerrainIO.allChunksExist()) { try { if (mapData != null) ChunkTerrainIO.exportFromMapData(mapData); else ChunkTerrainIO.exportBlankChunks(); - System.out.println("[TerrainChunkState] Chunk-Dateien erzeugt."); } catch (IOException e) { - System.err.println("[TerrainChunkState] Chunk-Export fehlgeschlagen: " + e); + log.error("[TerrainChunkState] Chunk-Export fehlgeschlagen: {}", e.toString()); } } - for (int cz = 0; cz < N; cz++) { for (int cx = 0; cx < N; cx++) { int ci = ChunkTerrainIO.chunkIndex(cx, cz); + if (chunkHeights[ci] != null) continue; try { chunkHeights[ci] = ChunkTerrainIO.loadChunk(cx, cz); } catch (IOException e) { chunkHeights[ci] = flatChunk(); - System.err.println("[TerrainChunkState] Chunk " + cx + "," + cz + " nicht ladbar: " + e.getMessage()); } } } - System.out.println("[TerrainChunkState] " + TOTAL + " Chunks geladen."); + } + + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.cam = app.getCamera(); + Arrays.fill(chunkLod, -1); + + ensureTerrainRoot(this.app); + + loadChunkHeights(); // idempotent – überspringt bereits geladene Chunks + log.info("[TerrainChunkState] {} Chunks geladen.", TOTAL); + } + + private void ensureTerrainRoot(SimpleApplication appRef) { + if (terrainRoot == null) { + terrainRoot = new Node("terrainChunks"); + appRef.getRootNode().attachChild(terrainRoot); + } } @Override protected void cleanup(Application app) { - for (int ci = 0; ci < TOTAL; ci++) removePhysics(ci); + // Beim Herunterfahren ist BulletAppState u.U. bereits aufgeräumt und hat die + // PhysicsSpace geleert. removePhysics() würde dann Warnungen "does not exist" loggen. + // Wir null-en nur die Referenzen — BulletAppState kümmert sich um die PhysicsSpace. + java.util.Arrays.fill(physics, null); ((SimpleApplication) app).getRootNode().detachChild(terrainRoot); } @Override protected void onEnable() {} @Override protected void onDisable() {} + /** Erzwingt beim nächsten update() eine Physik-/LOD-Aktualisierung um die Spawn-Position. */ + public void setSpawnHint(float worldX, float worldZ) { + spawnHintX = worldX; + spawnHintZ = worldZ; + lastPlayerCx = Integer.MIN_VALUE; + lastPlayerCz = Integer.MIN_VALUE; + } + + /** Gibt zurück ob für den Chunk an der Weltposition bereits ein Physics-Collider aktiv ist. */ + public boolean hasPhysicsAt(float worldX, float worldZ) { + int cx = Math.max(0, Math.min(N - 1, worldToChunk(worldX))); + int cz = Math.max(0, Math.min(N - 1, worldToChunk(worldZ))); + return physics[ChunkTerrainIO.chunkIndex(cx, cz)] != null; + } + // ── Update: LOD + Physik ────────────────────────────────────────────────── @Override public void update(float tpf) { - Vector3f camPos = cam.getLocation(); - int pcx = worldToChunk(camPos.x); - int pcz = worldToChunk(camPos.z); + float refX, refZ; + if (!Float.isNaN(spawnHintX)) { + refX = spawnHintX; refZ = spawnHintZ; + spawnHintX = Float.NaN; spawnHintZ = Float.NaN; + } else { + Vector3f camPos = cam.getLocation(); + refX = camPos.x; refZ = camPos.z; + } + int pcx = worldToChunk(refX); + int pcz = worldToChunk(refZ); if (pcx == lastPlayerCx && pcz == lastPlayerCz) return; lastPlayerCx = pcx; @@ -243,7 +284,7 @@ public class TerrainChunkState extends BaseAppState { Geometry geom = new Geometry("tc_" + cx + "_" + cz, mesh); geom.setMaterial(terrainMaterial); - geom.setShadowMode(RenderQueue.ShadowMode.Receive); + geom.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); chunkNodes[ci].attachChild(geom); chunkLod[ci] = lod; @@ -360,6 +401,10 @@ public class TerrainChunkState extends BaseAppState { RigidBodyControl rbc = new RigidBodyControl(shape, 0f); chunkNodes[ci].addControl(rbc); bulletAppState.getPhysicsSpace().add(rbc); + // jme3-jbullet's HeightfieldCollisionShape macht die AABB symmetrisch um 0 + // (min = -max), nicht um (min+max)/2. Die Höhenwerte h[i] werden direkt als + // lokale Y-Koordinaten verwendet. Der Body muss deshalb bei Y=0 liegen (Node-Y=0), + // damit Kollisionsfläche = 0 + h[i] = h[i]. Kein setPhysicsLocation nötig. physics[ci] = rbc; } diff --git a/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java b/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java index ca01dc2..5669385 100644 --- a/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java +++ b/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java @@ -4,6 +4,7 @@ import com.jme3.bullet.BulletAppState; import com.jme3.bullet.collision.shapes.MeshCollisionShape; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.material.Material; +import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; import com.jme3.scene.Mesh; import com.jme3.scene.Node; @@ -24,6 +25,7 @@ public class VoxelChunkNode extends Node { private final Geometry[] lodGeos = new Geometry[3]; private int currentLod = -1; + private boolean patchMode = false; private RigidBodyControl physics; private BulletAppState bulletState; @@ -38,17 +40,24 @@ public class VoxelChunkNode extends Node { float wy = chunk.cy * (float) VoxelChunk.CELLS; float wz = chunk.cz * VoxelChunk.CELLS - 2048f; setLocalTranslation(wx, wy, wz); + setShadowMode(RenderQueue.ShadowMode.CastAndReceive); } /** Baut das Mesh für den angegebenen LOD-Level neu. lod: 0/1/2. */ public void rebuildMesh(int lod) { + rebuildMesh(lod, null); + } + + /** Baut das Mesh mit Nachbar-Chunks für nahtlose Grenzen. neighbors: [+X,-X,+Y,-Y,+Z,-Z]. */ + public void rebuildMesh(int lod, VoxelChunk[] neighbors) { if (lod < 0 || lod > 2) return; - Mesh mesh = MarchingCubes.build(chunk, LOD_STEPS[lod]); + Mesh mesh = MarchingCubes.build(chunk, LOD_STEPS[lod], neighbors); if (mesh == null) { // Keine Oberfläche: evtl. vorherige Geo entfernen if (lodGeos[lod] != null) { detachChild(lodGeos[lod]); lodGeos[lod] = null; } return; } + applyPatchMode(mesh); if (lodGeos[lod] == null) { lodGeos[lod] = new Geometry("lod" + lod, mesh); lodGeos[lod].setMaterial(material); @@ -65,6 +74,7 @@ public class VoxelChunkNode extends Node { */ public void setLodMesh(int lod, Mesh mesh) { if (lod < 0 || lod > 2 || mesh == null) return; + applyPatchMode(mesh); if (lodGeos[lod] == null) { lodGeos[lod] = new Geometry("lod" + lod, mesh); lodGeos[lod].setMaterial(material); @@ -73,6 +83,23 @@ public class VoxelChunkNode extends Node { } } + /** Aktiviert / deaktiviert GL_PATCHES-Modus auf allen vorhandenen LOD-Meshes. */ + public void enablePatchMode(boolean enabled) { + this.patchMode = enabled; + for (Geometry geo : lodGeos) { + if (geo != null && geo.getMesh() != null) applyPatchMode(geo.getMesh()); + } + } + + private void applyPatchMode(Mesh mesh) { + if (patchMode) { + mesh.setMode(Mesh.Mode.Patch); + mesh.setPatchVertexCount(3); + } else { + mesh.setMode(Mesh.Mode.Triangles); + } + } + /** Schaltet auf den angegebenen LOD um (blendet andere aus). */ public void setActiveLod(int lod) { if (lod == currentLod) return; diff --git a/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java b/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java index 2320792..12a4937 100644 --- a/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java +++ b/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java @@ -5,17 +5,22 @@ import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; import com.jme3.bullet.BulletAppState; +import com.jme3.export.binary.BinaryImporter; import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Mesh; import com.jme3.scene.Node; -import com.jme3.texture.Image; import com.jme3.texture.Texture; -import com.jme3.texture.TextureArray; +import de.blight.common.MapData; import de.blight.common.VoxelChunk; import de.blight.common.VoxelChunkIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; /** @@ -32,7 +37,7 @@ public class VoxelChunkState extends BaseAppState private static final Logger log = LoggerFactory.getLogger(VoxelChunkState.class); private final BulletAppState bulletState; - private final String[] texturePaths; // 4 Pfade der unteren Terrain-Texturen + private final MapData mapData; private SimpleApplication app; private AssetManager assets; @@ -42,9 +47,9 @@ public class VoxelChunkState extends BaseAppState // key = cx | ((long)cy << 16) | ((long)cz << 32) private final Map nodes = new HashMap<>(); - public VoxelChunkState(BulletAppState bulletState, String[] texturePaths) { - this.bulletState = bulletState; - this.texturePaths = texturePaths; + public VoxelChunkState(BulletAppState bulletState, MapData mapData) { + this.bulletState = bulletState; + this.mapData = mapData; } @Override @@ -65,7 +70,21 @@ public class VoxelChunkState extends BaseAppState @Override protected void onEnable() {} @Override protected void onDisable() {} - @Override public void update(float tpf) {} + + public void setDebugNoLight(boolean enabled) { + if (voxelMaterial != null) voxelMaterial.setBoolean("DebugNoLight", enabled); + } + + @Override + public void update(float tpf) { + DayNightState dns = getApplication().getStateManager().getState(DayNightState.class); + if (dns == null || voxelMaterial == null || dns.getSunLight() == null) return; + voxelMaterial.setVector3("LightDir", dns.getSunDirection().negate()); + ColorRGBA sc = dns.getSunLight().getColor(); + ColorRGBA ac = dns.getAmbientLight().getColor(); + voxelMaterial.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b)); + voxelMaterial.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b)); + } // ── ChunkListener ───────────────────────────────────────────────────────── @@ -104,12 +123,24 @@ public class VoxelChunkState extends BaseAppState // ── Intern ──────────────────────────────────────────────────────────────── private void loadLayersForXZ(int cx, int cz, int lod) { - // Suche alle vorhandenen .blvc-Dateien für diesen cx/cz - // Einfachste Lösung: bekannte cy-Range scannen (-8..+8) for (int cy = -8; cy <= 8; cy++) { - if (!VoxelChunkIO.exists(cx, cy, cz)) continue; long key = chunkKey(cx, cy, cz); - if (nodes.containsKey(key)) continue; // bereits geladen + if (nodes.containsKey(key)) continue; + + // Gebackene J3O-Meshes bevorzugt laden — nur wenn auch .blvc-Quelldaten existieren + // (sonst werden veraltete Meshes von gelöschten Voxeln als Geisterflächen angezeigt) + if (VoxelChunkIO.bakedExists(cx, cy, cz) && VoxelChunkIO.exists(cx, cy, cz)) { + try { + addBakedNode(key, cx, cy, cz, lod); + } catch (Exception e) { + log.warn("Gebackenen Voxel-Chunk laden fehlgeschlagen ({},{},{}): {}", + cx, cy, cz, e.getMessage()); + } + continue; + } + + // Fallback: .blvc laden + Marching Cubes zur Laufzeit + if (!VoxelChunkIO.exists(cx, cy, cz)) continue; try { VoxelChunk chunk = VoxelChunkIO.load(cx, cy, cz); addNode(key, chunk, lod); @@ -119,6 +150,31 @@ public class VoxelChunkState extends BaseAppState } } + /** + * Lädt vorgebackene LOD-Meshes aus den .j3o-Dateien und hängt sie als + * VoxelChunkNode in die Szene ein (kein Marching Cubes zur Laufzeit). + */ + private void addBakedNode(long key, int cx, int cy, int cz, int lod) throws Exception { + BinaryImporter importer = BinaryImporter.getInstance(); + importer.setAssetManager(assets); + + VoxelChunk dummy = new VoxelChunk(cx, cy, cz); + VoxelChunkNode node = new VoxelChunkNode(dummy, voxelMaterial); + + for (int l = 0; l < 3; l++) { + Path p = VoxelChunkIO.getBakedPath(cx, cy, cz, l); + if (Files.exists(p)) { + Mesh m = (Mesh) importer.load(p.toFile()); + node.setLodMesh(l, m); + } + } + + node.setActiveLod(lod); + if (lod == 0) node.updatePhysics(bulletState); + voxelRoot.attachChild(node); + nodes.put(key, node); + } + private void addNode(long key, VoxelChunk chunk, int lod) { VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial); for (int l = 0; l < 3; l++) node.rebuildMesh(l); @@ -150,27 +206,59 @@ public class VoxelChunkState extends BaseAppState private Material buildMaterial() { Material mat = new Material(assets, "MatDefs/Voxel.j3md"); - mat.setFloat("TexScale", 4f); - - // Texture2DArray aus 4 Terrain-Textur-Pfaden aufbauen - try { - List images = new ArrayList<>(); - for (String path : texturePaths) { - Texture t = (path != null && !path.isEmpty()) - ? assets.loadTexture(path) - : assets.loadTexture("Common/Textures/MissingTexture.png"); - images.add(t.getImage()); + mat.setFloat("TexScale", 8f); + int[] slotIdxs = { + mapData != null ? mapData.voxelFlatSlot : -1, + mapData != null ? mapData.voxelSteepSlot : -1, + mapData != null ? mapData.voxelCeilSlot : -1, + }; + log.info("[Voxel] FlatSlot={} → '{}', SteepSlot={} → '{}', TexScale=8.0", + slotIdxs[0], resolveSlotTex(slotIdxs[0]), + slotIdxs[1], resolveSlotTex(slotIdxs[1])); + String[] colSlots = { "TexFlat", "TexSteep", "TexCeil" }; + String[] normSlots = { "NormalMapFlat", "NormalMapSteep", "NormalMapCeil" }; + for (int i = 0; i < 3; i++) { + String tex = resolveSlotTex(slotIdxs[i]); + String norm = resolveSlotNorm(slotIdxs[i]); + if (!tex.isEmpty()) { + try { + Texture t = assets.loadTexture(tex); + t.getImage().setColorSpace(com.jme3.texture.image.ColorSpace.Linear); + t.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(colSlots[i], t); + } catch (Exception e) { + log.warn("Voxel-Textur {} nicht ladbar: {}", tex, e.getMessage()); + } + } + if (!norm.isEmpty()) { + try { + Texture n = assets.loadTexture(norm); + n.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(normSlots[i], n); + } catch (Exception e) { + log.warn("Voxel-NormalMap {} nicht ladbar: {}", norm, e.getMessage()); + } } - TextureArray texArray = new TextureArray(images); - texArray.setMinFilter(Texture.MinFilter.Trilinear); - texArray.setMagFilter(Texture.MagFilter.Bilinear); - mat.setParam("TexArray", com.jme3.shader.VarType.TextureArray, texArray); - } catch (Exception e) { - log.warn("Voxel Texture2DArray Aufbau fehlgeschlagen: {}", e.getMessage()); } return mat; } + private String resolveSlotTex(int slot) { + if (slot < 0 || mapData == null) return ""; + if (slot < 4) { String p = mapData.terrainTextures[slot]; return p != null ? p : ""; } + if (slot < 8) { String p = mapData.upperTextures[slot - 4]; return p != null ? p : ""; } + if (slot < 12) { String p = mapData.thirdTextures[slot - 8]; return p != null ? p : ""; } + return ""; + } + + private String resolveSlotNorm(int slot) { + if (slot < 0 || mapData == null) return ""; + if (slot < 4) { String p = mapData.terrainNormalMaps[slot]; return p != null ? p : ""; } + if (slot < 8) { String p = mapData.upperNormalMaps[slot - 4]; return p != null ? p : ""; } + if (slot < 12) { String p = mapData.thirdNormalMaps[slot - 8]; return p != null ? p : ""; } + return ""; + } + public static long chunkKey(int cx, int cy, int cz) { return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32); } diff --git a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java index 6ce024e..d3bfde8 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java @@ -9,6 +9,8 @@ import com.jme3.bullet.BulletAppState; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.material.Material; +import com.jme3.material.MatParam; +import com.jme3.material.RenderState; import com.jme3.math.*; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; @@ -20,6 +22,7 @@ import de.blight.game.animation.AnimationLibrary; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; public class WorldObjectsState extends BaseAppState { @@ -29,6 +32,7 @@ public class WorldObjectsState extends BaseAppState { private SimpleApplication app; private AssetManager assets; private BulletAppState bulletAppState; + private final List sceneLitMaterials = new ArrayList<>(); @Override protected void initialize(Application app) { @@ -102,6 +106,23 @@ public class WorldObjectsState extends BaseAppState { @Override protected void cleanup(Application app) {} @Override protected void onDisable() {} + @Override + public void update(float tpf) { + if (sceneLitMaterials.isEmpty()) return; + DayNightState dns = getApplication().getStateManager().getState(DayNightState.class); + if (dns == null || dns.getSunLight() == null) return; + Vector3f lightDir = dns.getSunDirection().negate(); + ColorRGBA sc = dns.getSunLight().getColor(); + ColorRGBA ac = dns.getAmbientLight().getColor(); + Vector3f sunVec = new Vector3f(sc.r, sc.g, sc.b); + Vector3f ambVec = new Vector3f(ac.r, ac.g, ac.b); + for (Material mat : sceneLitMaterials) { + mat.setVector3("LightDir", lightDir); + mat.setVector3("SunColor", sunVec); + mat.setVector3("AmbientColor", ambVec); + } + } + private Spatial buildSpatial(PlacedModel m) { // Exportiertes Mesh hat Vorrang vor modelPath String path = (m.meshFile() != null && !m.meshFile().isBlank()) @@ -113,17 +134,42 @@ public class WorldObjectsState extends BaseAppState { applyMaterial(spatial, m); } else { spatial = assets.loadModel(path); + convertUnshadedToLighting(spatial); + collectSceneLitMaterials(spatial); } spatial.setName("obj_" + path); // LOD / Distance-Culling if (m.cullDistance() > 0f) { + // Embedded-LOD: j3o enthält bereits lod0/lod1/lod2 Child-Nodes (TreeGenerator). + // TreeLodControl im j3o ist editor-only und wird im Spiel nicht deserialisiert — + // wir übernehmen die LOD-Steuerung selbst, direkt am j3o-Root-Node. + if (spatial instanceof Node treeNode) { + Spatial lod0 = treeNode.getChild("lod0"); + Spatial lod1 = treeNode.getChild("lod1"); + Spatial lod2 = treeNode.getChild("lod2"); + if (lod0 != null) { + lod0.setCullHint(Spatial.CullHint.Inherit); + if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always); + if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always); + treeNode.addControl(new ModelLodControl( + app.getCamera(), lod0, lod1, lod2, + m.lod1Distance(), m.lod2Distance(), m.cullDistance())); + return treeNode; + } + } + + // Fallback: externe LOD-Dateien (lod1Path / lod2Path) Node lodRoot = new Node("lodRoot_" + path); lodRoot.attachChild(spatial); ModelLodControl ctrl = new ModelLodControl( - assets, + assets, app.getCamera(), m.lod1Path(), m.lod2Path(), m.lod1Distance(), m.lod2Distance(), m.cullDistance()); + ctrl.setLodLoadCallback(lod -> { + convertUnshadedToLighting(lod); + collectSceneLitMaterials(lod); + }); lodRoot.addControl(ctrl); return lodRoot; } @@ -145,6 +191,58 @@ public class WorldObjectsState extends BaseAppState { }; } + /** + * Ersetzt Unshaded-Materialien in geladenen j3o-Modellen durch Lighting.j3md, + * damit diese auf den Tag/Nacht-Zyklus reagieren. + */ + private void convertUnshadedToLighting(Spatial spatial) { + if (spatial instanceof Geometry geo) { + Material mat = geo.getMaterial(); + if (mat == null) return; + if (!"Unshaded".equals(mat.getMaterialDef().getName())) return; + + MatParam colorMap = mat.getParam("ColorMap"); + MatParam color = mat.getParam("Color"); + RenderState origRS = mat.getAdditionalRenderState(); + + Material litMat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md"); + litMat.setBoolean("UseMaterialColors", false); + + if (colorMap != null && colorMap.getValue() instanceof Texture t) { + litMat.setTexture("DiffuseMap", t); + } else if (color != null && color.getValue() instanceof ColorRGBA c) { + litMat.setBoolean("UseMaterialColors", true); + litMat.setColor("Diffuse", new ColorRGBA(c.r, c.g, c.b, 1f)); + litMat.setColor("Ambient", new ColorRGBA(c.r * 0.3f, c.g * 0.3f, c.b * 0.3f, 1f)); + } + + RenderState rs = litMat.getAdditionalRenderState(); + rs.setBlendMode(origRS.getBlendMode()); + rs.setFaceCullMode(origRS.getFaceCullMode()); + + geo.setMaterial(litMat); + } else if (spatial instanceof Node node) { + for (Spatial child : new ArrayList<>(node.getChildren())) { + convertUnshadedToLighting(child); + } + } + } + + private void collectSceneLitMaterials(Spatial spatial) { + if (spatial instanceof Geometry geo) { + Material mat = geo.getMaterial(); + if (mat == null) return; + String name = mat.getMaterialDef().getName(); + if ("Tree".equals(name) || "TreeLeaf".equals(name)) { + sceneLitMaterials.add(mat); + } + } else if (spatial instanceof Node node) { + for (Spatial child : node.getChildren()) { + collectSceneLitMaterials(child); + } + } + } + private void applyMaterial(Spatial s, PlacedModel m) { boolean hasTex = m.texturePath() != null && !m.texturePath().isBlank(); boolean hasNmap = m.normalMapPath() != null && !m.normalMapPath().isBlank(); diff --git a/blight-game/src/main/resources/logback.xml b/blight-game/src/main/resources/logback.xml index d3b7c42..0a9fd60 100644 --- a/blight-game/src/main/resources/logback.xml +++ b/blight-game/src/main/resources/logback.xml @@ -1,6 +1,9 @@ + + ERROR + %d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex diff --git a/blight-map/src/main/map/blight_ferns.blf b/blight-map/src/main/map/blight_ferns.blf deleted file mode 100644 index 35513b9..0000000 Binary files a/blight-map/src/main/map/blight_ferns.blf and /dev/null differ diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index d2f7e9a..4b02cd8 100644 Binary files a/blight-map/src/main/map/blight_grass.blg and b/blight-map/src/main/map/blight_grass.blg differ diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 5655272..0fce0dc 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 9c29247..d16faf4 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_music_areas.bma b/blight-map/src/main/map/blight_music_areas.bma deleted file mode 100644 index 62a18f1..0000000 --- a/blight-map/src/main/map/blight_music_areas.bma +++ /dev/null @@ -1,4 +0,0 @@ -# polygon dayTrack nightTrack combatTrack -17.980,-379.565;17.980,-379.565;17.980,-379.565 -16.540,-386.189;16.540,-386.189;16.540,-386.189 -16.737,-386.357;16.737,-386.357;16.737,-386.357 diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index ce9e369..a2dd243 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -1,45 +1,4 @@ # modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow lod1Path lod2Path lod1Distance lod2Distance cullDistance -Models/Palm_Palme1_20260524_153405.j3o -74.09265 8.19780 -18.47723 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260524_153421.j3o -59.20779 13.84631 -17.90553 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260524_153421.j3o -37.63680 32.63404 -18.65228 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260524_153421.j3o -15.66568 10.46379 -25.60722 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -@box -471.37857 22.27976 -91.47378 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_0.j3o true true 30.00000 80.00000 120.00000 -@box -469.23279 22.49469 -91.51467 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_1.j3o true true 30.00000 80.00000 120.00000 -@group -462.16046 3.59008 -122.32437 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_2.j3o true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1.j3o -245.90610 165.20883 99.17912 -6.43500 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260522_075053.j3o -246.13976 166.24338 89.64748 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260522_075102.j3o -240.90959 166.65724 85.45869 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260522_075134.j3o -254.69368 165.64214 91.69685 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260522_075137.j3o -248.68674 167.64050 76.46214 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1.j3o -205.20802 165.35493 88.46772 -18.77974 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260522_075102.j3o -135.84702 158.69884 84.12026 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -@plane -224.78107 164.15782 108.96712 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_3.j3o true true 30.00000 80.00000 120.00000 -@plane -220.53658 165.75740 108.73174 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_4.j3o true true 30.00000 80.00000 120.00000 -@plane -237.96001 165.08000 113.44000 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_5.j3o true true 30.00000 80.00000 120.00000 -@plane -235.81349 164.75165 114.21590 0.00000 1.00000 0.00000 0.00000 false Models/custom_mesh_6.j3o true true 30.00000 80.00000 120.00000 -Models/Tree/Tree.mesh.j3o -246.86934 164.83850 107.50585 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Boat/boat.j3o -261.74731 164.72397 120.62883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Campfire.j3o -350.02148 164.72334 72.42865 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Campfire.j3o -67.93591 1.71510 -36.36593 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/tree.j3o -28.09666 16.39296 -34.64375 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/tree.j3o -83.04567 -2.69246 -39.34207 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Tree/Tree.mesh.j3o 295.18826 1.00000 189.92809 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/gltf/duck/Duck.gltf 300.54111 1.00000 198.35368 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Buggy/Buggy.j3o 296.60590 1.00000 201.24153 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/Jaime/Jaime.j3o 304.02374 1.00000 199.25398 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/tree.j3o 221.98000 1.00000 130.25000 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 -Models/Palm_Palme1_20260522_075134.j3o 240.61151 1.00000 97.48973 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/FernPlantV2.j3o 336.18240 1.00000 -164.72426 0.00000 0.00200 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165631.j3o 158.98721 0.96388 25.31449 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 159.09314 0.95784 17.07811 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 160.70380 0.93929 19.18060 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 161.39258 0.90349 21.80369 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 163.20784 0.61551 16.58227 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 164.25797 0.72795 11.98990 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 164.15082 0.42406 17.46363 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 163.74956 0.53243 21.45433 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/fern/fern_20260608_165628.j3o 162.27063 0.88450 24.03092 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/misc/heliconia+plant+3d+model.j3o 152.94803 0.96488 8.99865 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/misc/heliconia+plant+3d+model.j3o 155.57500 0.96457 10.04861 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/plants/misc/kaktusfeige.j3o 153.89978 0.98474 38.66549 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 -@plane 102.97000 1.06000 -172.24001 0.00000 1.00000 0.00000 0.00000 false Textures/ground/Rocks015_1K-JPG_Color.jpg Textures/ground/Rocks015_1K-JPG_NormalDX.jpg Common/MatDefs/Light/Lighting.j3md Models/custom_mesh_7.j3o true true 30.00000 80.00000 120.00000 +Models/imported/wooden+cabin+3d+model.j3o 105.61634 5.26108 58.69108 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/pine/medium/pine_medium_20260615_221703.j3o 96.87341 6.23885 51.96483 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/pine/medium/pine_medium_20260615_221707.j3o 110.91007 4.47205 50.56980 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 diff --git a/blight-map/src/main/map/blight_placed_items.bpi b/blight-map/src/main/map/blight_placed_items.bpi deleted file mode 100644 index cf09131..0000000 --- a/blight-map/src/main/map/blight_placed_items.bpi +++ /dev/null @@ -1,9 +0,0 @@ -# uuid itemId x y z -8daba5c4-5a5a-474f-b92f-d537026cae22 blutagave 154.93541 0.96067 31.66673 -452d861c-f1f3-4891-8d7d-bf63426c06f5 blutagave 150.51953 0.97004 29.83217 -ea972904-a271-4ef0-9fee-ee449207941f blutagave 151.96960 0.97972 35.17637 -b458fea6-8394-4315-8874-a35725bffa73 erzmoss 140.69641 0.99656 36.09897 -d9f6c1a8-4b77-4131-b105-0c1373322e12 erzmoss 143.09398 0.98996 30.72424 -fd7d3852-04a1-482b-a00a-641b4de333a4 erzmoss 147.13496 0.97094 24.37807 -c6f52659-2a97-4178-890c-2fb6f2216d2c erzmoss 156.07599 0.96447 26.57461 -b5e3decf-e7a7-4531-8cb3-a55d0a965a54 erzmoss 163.04799 0.93061 29.08921 diff --git a/blight-map/src/main/map/chunks/chunk_12_12.blc b/blight-map/src/main/map/chunks/chunk_12_12.blc index 16b2009..2119abc 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_12.blc and b/blight-map/src/main/map/chunks/chunk_12_12.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_13.blc b/blight-map/src/main/map/chunks/chunk_12_13.blc index 6de02c0..a0e1173 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_13.blc and b/blight-map/src/main/map/chunks/chunk_12_13.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_13.blc b/blight-map/src/main/map/chunks/chunk_13_13.blc index 43a7fb4..f77bef4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_13.blc and b/blight-map/src/main/map/chunks/chunk_13_13.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_11.blc b/blight-map/src/main/map/chunks/chunk_15_11.blc index ef04412..5912ae3 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_11.blc and b/blight-map/src/main/map/chunks/chunk_15_11.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_12.blc b/blight-map/src/main/map/chunks/chunk_15_12.blc index 761f89a..6ed5d9c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_12.blc and b/blight-map/src/main/map/chunks/chunk_15_12.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_14.blc b/blight-map/src/main/map/chunks/chunk_15_14.blc index 36e7b70..fe3dc4e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_14.blc and b/blight-map/src/main/map/chunks/chunk_15_14.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_15.blc b/blight-map/src/main/map/chunks/chunk_15_15.blc index 1c9f183..8129b67 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_15.blc and b/blight-map/src/main/map/chunks/chunk_15_15.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_16.blc b/blight-map/src/main/map/chunks/chunk_15_16.blc index 1cf8e1d..9d6e332 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_16.blc and b/blight-map/src/main/map/chunks/chunk_15_16.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_17.blc b/blight-map/src/main/map/chunks/chunk_15_17.blc index c758f8a..53f6b39 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_17.blc and b/blight-map/src/main/map/chunks/chunk_15_17.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_11.blc b/blight-map/src/main/map/chunks/chunk_16_11.blc index 7d2258c..3ad4529 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_11.blc and b/blight-map/src/main/map/chunks/chunk_16_11.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_12.blc b/blight-map/src/main/map/chunks/chunk_16_12.blc index 77957d0..542f447 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_12.blc and b/blight-map/src/main/map/chunks/chunk_16_12.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_14.blc b/blight-map/src/main/map/chunks/chunk_16_14.blc index edc4f26..567bde6 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_14.blc and b/blight-map/src/main/map/chunks/chunk_16_14.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_15.blc b/blight-map/src/main/map/chunks/chunk_16_15.blc index c37914d..2b4e553 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_15.blc and b/blight-map/src/main/map/chunks/chunk_16_15.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_16.blc b/blight-map/src/main/map/chunks/chunk_16_16.blc index 0da6038..4eebe2c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_16.blc and b/blight-map/src/main/map/chunks/chunk_16_16.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_17.blc b/blight-map/src/main/map/chunks/chunk_16_17.blc index 049b35a..7791b47 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_17.blc and b/blight-map/src/main/map/chunks/chunk_16_17.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_14.blc b/blight-map/src/main/map/chunks/chunk_17_14.blc index 37e3f74..cf5b03c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_14.blc and b/blight-map/src/main/map/chunks/chunk_17_14.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_15.blc b/blight-map/src/main/map/chunks/chunk_17_15.blc index becf35c..75b5299 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_15.blc and b/blight-map/src/main/map/chunks/chunk_17_15.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_16.blc b/blight-map/src/main/map/chunks/chunk_17_16.blc index 70f594f..5666106 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_16.blc and b/blight-map/src/main/map/chunks/chunk_17_16.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_16.blc b/blight-map/src/main/map/chunks/chunk_18_16.blc index 8c916e4..b31a645 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_16.blc and b/blight-map/src/main/map/chunks/chunk_18_16.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_05_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_05_m1_10.blvc deleted file mode 100644 index e0af981..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_05_m1_10.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_06_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_06_m1_10.blvc deleted file mode 100644 index e88d69c..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_06_m1_10.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_08_0_11.blvc b/blight-map/src/main/map/chunks/voxel_08_0_11.blvc deleted file mode 100644 index 681440c..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_08_0_11.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_08_0_12.blvc b/blight-map/src/main/map/chunks/voxel_08_0_12.blvc deleted file mode 100644 index 07c1dc4..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_08_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_08_m1_11.blvc b/blight-map/src/main/map/chunks/voxel_08_m1_11.blvc deleted file mode 100644 index 28b3cb0..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_08_m1_11.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_08_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_08_m1_12.blvc deleted file mode 100644 index 1d841ad..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_08_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_09_0_11.blvc b/blight-map/src/main/map/chunks/voxel_09_0_11.blvc deleted file mode 100644 index f927cc4..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_09_0_11.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_09_0_12.blvc b/blight-map/src/main/map/chunks/voxel_09_0_12.blvc deleted file mode 100644 index 4056663..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_09_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_09_m1_11.blvc b/blight-map/src/main/map/chunks/voxel_09_m1_11.blvc deleted file mode 100644 index 467319d..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_09_m1_11.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_09_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_09_m1_12.blvc deleted file mode 100644 index 3c4da69..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_09_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_10_0_12.blvc b/blight-map/src/main/map/chunks/voxel_10_0_12.blvc deleted file mode 100644 index ce1175b..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_10_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_10_0_13.blvc b/blight-map/src/main/map/chunks/voxel_10_0_13.blvc deleted file mode 100644 index 0087f1e..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_10_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_10_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_10_m1_12.blvc deleted file mode 100644 index 53ed017..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_10_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_10_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_10_m1_13.blvc deleted file mode 100644 index ee1215e..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_10_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_11_0_12.blvc b/blight-map/src/main/map/chunks/voxel_11_0_12.blvc deleted file mode 100644 index e671591..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_11_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_11_0_13.blvc b/blight-map/src/main/map/chunks/voxel_11_0_13.blvc deleted file mode 100644 index 9d6cee3..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_11_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_11_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_11_m1_12.blvc deleted file mode 100644 index 1b87513..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_11_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_11_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_11_m1_13.blvc deleted file mode 100644 index ad9615b..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_11_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_10.blvc b/blight-map/src/main/map/chunks/voxel_12_0_10.blvc deleted file mode 100644 index f6d31bc..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_0_10.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_11.blvc b/blight-map/src/main/map/chunks/voxel_12_0_11.blvc deleted file mode 100644 index e42d9cd..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_0_11.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_13.blvc b/blight-map/src/main/map/chunks/voxel_12_0_13.blvc deleted file mode 100644 index e11476f..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_14.blvc b/blight-map/src/main/map/chunks/voxel_12_0_14.blvc deleted file mode 100644 index d345a1f..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_0_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_10.blvc deleted file mode 100644 index dbf8cdb..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_m1_10.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_11.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_11.blvc deleted file mode 100644 index 74c486f..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_m1_11.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_13.blvc deleted file mode 100644 index 8555ef7..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_14.blvc deleted file mode 100644 index fac2396..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_12_m1_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_12.blvc b/blight-map/src/main/map/chunks/voxel_13_0_12.blvc deleted file mode 100644 index 3b3da86..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_13_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_13.blvc b/blight-map/src/main/map/chunks/voxel_13_0_13.blvc deleted file mode 100644 index 69cef8d..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_13_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_14.blvc b/blight-map/src/main/map/chunks/voxel_13_0_14.blvc deleted file mode 100644 index 2e38026..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_13_0_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_03_0_09.blvc b/blight-map/src/main/map/chunks/voxel_13_0_16.blvc similarity index 96% rename from blight-map/src/main/map/chunks/voxel_03_0_09.blvc rename to blight-map/src/main/map/chunks/voxel_13_0_16.blvc index fd25b59..0a2aa54 100644 Binary files a/blight-map/src/main/map/chunks/voxel_03_0_09.blvc and b/blight-map/src/main/map/chunks/voxel_13_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_12.blvc deleted file mode 100644 index db8c6e8..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_13_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_13.blvc deleted file mode 100644 index db98b95..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_13_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_14.blvc deleted file mode 100644 index f18c8cf..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_13_m1_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_16.blvc new file mode 100644 index 0000000..2f06391 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_12.blvc b/blight-map/src/main/map/chunks/voxel_14_0_12.blvc deleted file mode 100644 index a29f4a4..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_13.blvc b/blight-map/src/main/map/chunks/voxel_14_0_13.blvc deleted file mode 100644 index c50578c..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_14.blvc b/blight-map/src/main/map/chunks/voxel_14_0_14.blvc deleted file mode 100644 index c80fed8..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_05_0_10.blvc b/blight-map/src/main/map/chunks/voxel_14_0_15.blvc similarity index 96% rename from blight-map/src/main/map/chunks/voxel_05_0_10.blvc rename to blight-map/src/main/map/chunks/voxel_14_0_15.blvc index 28ef251..f6c9c26 100644 Binary files a/blight-map/src/main/map/chunks/voxel_05_0_10.blvc and b/blight-map/src/main/map/chunks/voxel_14_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_04_0_10.blvc b/blight-map/src/main/map/chunks/voxel_14_0_16.blvc similarity index 96% rename from blight-map/src/main/map/chunks/voxel_04_0_10.blvc rename to blight-map/src/main/map/chunks/voxel_14_0_16.blvc index 0337620..0690fc9 100644 Binary files a/blight-map/src/main/map/chunks/voxel_04_0_10.blvc and b/blight-map/src/main/map/chunks/voxel_14_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_12.blvc deleted file mode 100644 index 164a60c..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_13.blvc deleted file mode 100644 index d54ec56..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_14.blvc deleted file mode 100644 index 52e5246..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_m1_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_03_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_15.blvc similarity index 95% rename from blight-map/src/main/map/chunks/voxel_03_m1_09.blvc rename to blight-map/src/main/map/chunks/voxel_14_m1_15.blvc index 0317d5d..4378f42 100644 Binary files a/blight-map/src/main/map/chunks/voxel_03_m1_09.blvc and b/blight-map/src/main/map/chunks/voxel_14_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_04_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_16.blvc similarity index 95% rename from blight-map/src/main/map/chunks/voxel_04_m1_10.blvc rename to blight-map/src/main/map/chunks/voxel_14_m1_16.blvc index 1bea1cc..244fcdd 100644 Binary files a/blight-map/src/main/map/chunks/voxel_04_m1_10.blvc and b/blight-map/src/main/map/chunks/voxel_14_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_12.blvc b/blight-map/src/main/map/chunks/voxel_15_0_12.blvc deleted file mode 100644 index 39ffbf6..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_13.blvc b/blight-map/src/main/map/chunks/voxel_15_0_13.blvc deleted file mode 100644 index 7c8273c..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_14.blvc b/blight-map/src/main/map/chunks/voxel_15_0_14.blvc deleted file mode 100644 index 3f3b512..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_15.blvc b/blight-map/src/main/map/chunks/voxel_15_0_15.blvc index d318286..0868d9f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_15.blvc and b/blight-map/src/main/map/chunks/voxel_15_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod0.j3o new file mode 100644 index 0000000..b6a2b6a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod1.j3o new file mode 100644 index 0000000..09a822b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod2.j3o new file mode 100644 index 0000000..d835aa6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_15_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_16.blvc b/blight-map/src/main/map/chunks/voxel_15_0_16.blvc index e0b540f..1acd58d 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_16.blvc and b/blight-map/src/main/map/chunks/voxel_15_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod0.j3o new file mode 100644 index 0000000..1a77cbf Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod1.j3o new file mode 100644 index 0000000..dcac75f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod2.j3o new file mode 100644 index 0000000..984aeeb Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_17.blvc b/blight-map/src/main/map/chunks/voxel_15_0_17.blvc new file mode 100644 index 0000000..d307e85 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_17.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod0.j3o new file mode 100644 index 0000000..6192fc3 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod1.j3o new file mode 100644 index 0000000..dc83802 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod2.j3o new file mode 100644 index 0000000..f0028f4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_17_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_06_0_10.blvc b/blight-map/src/main/map/chunks/voxel_15_0_18.blvc similarity index 96% rename from blight-map/src/main/map/chunks/voxel_06_0_10.blvc rename to blight-map/src/main/map/chunks/voxel_15_0_18.blvc index 8b5f08a..345de80 100644 Binary files a/blight-map/src/main/map/chunks/voxel_06_0_10.blvc and b/blight-map/src/main/map/chunks/voxel_15_0_18.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_12.blvc deleted file mode 100644 index 363642e..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_13.blvc deleted file mode 100644 index 78e2a03..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_14.blvc deleted file mode 100644 index 5ea65cc..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_m1_14.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc index 341ded1..1b1cbb8 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc and b/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc index 28eef63..e048255 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc and b/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod0.j3o new file mode 100644 index 0000000..59f949b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod1.j3o new file mode 100644 index 0000000..9a3a593 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod2.j3o new file mode 100644 index 0000000..1d777b6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_17.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_17.blvc new file mode 100644 index 0000000..1aa0863 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_17.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod0.j3o new file mode 100644 index 0000000..f9136a9 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod1.j3o new file mode 100644 index 0000000..ede5440 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod2.j3o new file mode 100644 index 0000000..7b2bb71 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_17_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_18.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_18.blvc new file mode 100644 index 0000000..ac76838 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_18.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_12.blvc b/blight-map/src/main/map/chunks/voxel_16_0_12.blvc deleted file mode 100644 index be95ae1..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_13.blvc b/blight-map/src/main/map/chunks/voxel_16_0_13.blvc deleted file mode 100644 index 033d616..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_14.blvc b/blight-map/src/main/map/chunks/voxel_16_0_14.blvc index 0a5ad66..fb5eb3d 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_14.blvc and b/blight-map/src/main/map/chunks/voxel_16_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod0.j3o new file mode 100644 index 0000000..9517f37 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod1.j3o new file mode 100644 index 0000000..09a822b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod2.j3o new file mode 100644 index 0000000..d835aa6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_14_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15.blvc b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc index 4b38f16..a96dffa 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_15.blvc and b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod0.j3o new file mode 100644 index 0000000..8077499 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod1.j3o new file mode 100644 index 0000000..fd09c37 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod2.j3o new file mode 100644 index 0000000..d835aa6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16.blvc b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc index 25a74e1..c9db711 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_16.blvc and b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o new file mode 100644 index 0000000..b6b47a2 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o new file mode 100644 index 0000000..f98792b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o new file mode 100644 index 0000000..f9ec1f1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_17.blvc b/blight-map/src/main/map/chunks/voxel_16_0_17.blvc new file mode 100644 index 0000000..08ae4f8 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_17.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod0.j3o new file mode 100644 index 0000000..c11e0ff Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod1.j3o new file mode 100644 index 0000000..221d900 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod2.j3o new file mode 100644 index 0000000..be79db8 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_17_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_18.blvc b/blight-map/src/main/map/chunks/voxel_16_0_18.blvc new file mode 100644 index 0000000..134ccae Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_18.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_12.blvc deleted file mode 100644 index f6ac8c4..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_12.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_13.blvc deleted file mode 100644 index 9c00561..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc index 80cbd8a..2483be0 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc and b/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc index 634fcb0..e8c9c34 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc and b/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc index 43db38f..bfe7c14 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc and b/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod0.j3o new file mode 100644 index 0000000..28e0fb0 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod1.j3o new file mode 100644 index 0000000..fd2cdb2 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod2.j3o new file mode 100644 index 0000000..f808e71 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_17.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_17.blvc new file mode 100644 index 0000000..bf857f6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_17.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod0.j3o new file mode 100644 index 0000000..96a921c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod1.j3o new file mode 100644 index 0000000..91d0bb3 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod2.j3o new file mode 100644 index 0000000..ca35fa1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_17_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_18.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_18.blvc new file mode 100644 index 0000000..bbc53b3 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_18.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_13.blvc b/blight-map/src/main/map/chunks/voxel_17_0_13.blvc deleted file mode 100644 index c017e6b..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_14.blvc b/blight-map/src/main/map/chunks/voxel_17_0_14.blvc index 45e182e..cdace8f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_14.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod0.j3o new file mode 100644 index 0000000..35c80e5 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod1.j3o new file mode 100644 index 0000000..1a56c7f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod2.j3o new file mode 100644 index 0000000..d835aa6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_14_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15.blvc b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc index 33f61b4..d4fa0dd 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_15.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod0.j3o new file mode 100644 index 0000000..f7f705a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod1.j3o new file mode 100644 index 0000000..f484080 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod2.j3o new file mode 100644 index 0000000..aaf6378 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16.blvc b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc index 4480b38..0ecf6c3 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_16.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod0.j3o new file mode 100644 index 0000000..213bf01 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod1.j3o new file mode 100644 index 0000000..d760640 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod2.j3o new file mode 100644 index 0000000..880da6f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_17.blvc b/blight-map/src/main/map/chunks/voxel_17_0_17.blvc new file mode 100644 index 0000000..5819684 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_17.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod0.j3o new file mode 100644 index 0000000..b878971 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod1.j3o new file mode 100644 index 0000000..6a4ae45 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod2.j3o new file mode 100644 index 0000000..f429887 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_17_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_18.blvc b/blight-map/src/main/map/chunks/voxel_17_0_18.blvc new file mode 100644 index 0000000..70b3247 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_18.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_1_14.blvc b/blight-map/src/main/map/chunks/voxel_17_1_14.blvc new file mode 100644 index 0000000..053664b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_1_15.blvc b/blight-map/src/main/map/chunks/voxel_17_1_15.blvc new file mode 100644 index 0000000..8e91299 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_1_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_1_15_baked_lod0.j3o new file mode 100644 index 0000000..829ae6f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_1_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_13.blvc deleted file mode 100644 index 2a98f70..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_13.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc index 93ee8eb..dd77056 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc index 5e8e245..65a337f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod0.j3o new file mode 100644 index 0000000..f21a0f0 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod1.j3o new file mode 100644 index 0000000..5a86e3d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod2.j3o new file mode 100644 index 0000000..ae215b9 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc index 9830c13..6da4af0 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod0.j3o new file mode 100644 index 0000000..9d79c32 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod1.j3o new file mode 100644 index 0000000..0c0081b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod2.j3o new file mode 100644 index 0000000..40f928d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_17.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_17.blvc new file mode 100644 index 0000000..04e0a05 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_17.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod0.j3o new file mode 100644 index 0000000..c10d337 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod1.j3o new file mode 100644 index 0000000..deaa594 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod2.j3o new file mode 100644 index 0000000..28a34ee Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_17_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_18.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_18.blvc new file mode 100644 index 0000000..8883057 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_18.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_14.blvc b/blight-map/src/main/map/chunks/voxel_18_0_14.blvc new file mode 100644 index 0000000..c339eb1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_15.blvc b/blight-map/src/main/map/chunks/voxel_18_0_15.blvc new file mode 100644 index 0000000..bab3a73 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod0.j3o new file mode 100644 index 0000000..9517f37 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod1.j3o new file mode 100644 index 0000000..09a822b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod2.j3o new file mode 100644 index 0000000..d835aa6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_0_15_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_18_1_14.blvc b/blight-map/src/main/map/chunks/voxel_18_1_14.blvc new file mode 100644 index 0000000..43118b6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_18_1_15.blvc b/blight-map/src/main/map/chunks/voxel_18_1_15.blvc new file mode 100644 index 0000000..924218d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_18_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_18_m1_15.blvc new file mode 100644 index 0000000..01c7f21 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_18_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_19_0_09.blvc b/blight-map/src/main/map/chunks/voxel_19_0_09.blvc deleted file mode 100644 index 5f607a3..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_19_0_09.blvc and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc deleted file mode 100644 index 24832a1..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc and /dev/null differ diff --git a/ez-tree-jme/build.gradle b/blight-vegetation-generator/build.gradle similarity index 51% rename from ez-tree-jme/build.gradle rename to blight-vegetation-generator/build.gradle index e71d133..1f2c741 100644 --- a/ez-tree-jme/build.gradle +++ b/blight-vegetation-generator/build.gradle @@ -6,10 +6,3 @@ dependencies { implementation "org.jmonkeyengine:jme3-core:${jmeVersion}" } -sourceSets { - main { - resources { - srcDirs = ['src/main/resources', 'assets'] - } - } -} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/BarkOptions.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/BarkOptions.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/BarkOptions.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/BarkOptions.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/Billboard.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/Billboard.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/Billboard.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/Billboard.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/Branch.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/Branch.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/Branch.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/BranchOptions.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/BranchOptions.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/BranchOptions.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/BranchOptions.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/BranchSection.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/BranchSection.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/ForceOptions.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/ForceOptions.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/ForceOptions.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/ForceOptions.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/LeavesOptions.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/LeavesOptions.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/LeavesOptions.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/LeavesOptions.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/Rng.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/Rng.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/Rng.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/Tree.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/Tree.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/Tree.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreeOptions.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TreeOptions.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreeOptions.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TreeOptions.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java similarity index 98% rename from ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java index e232a1c..af7ddae 100644 --- a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java +++ b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java @@ -16,14 +16,14 @@ public final class TreePresets { private TreePresets() {} - private static final String BARK1 = "Textures/bark/Bark001_Color.jpg"; - private static final String BARK2 = "Textures/bark/Bark002_Color.jpg"; - private static final String BARK3 = "Textures/bark/Bark003_Color.jpg"; + private static final String BARK1 = "Textures/internal/bark/Bark001_Color.jpg"; + private static final String BARK2 = "Textures/internal/bark/Bark002_Color.jpg"; + private static final String BARK3 = "Textures/internal/bark/Bark003_Color.jpg"; - private static final String LEAF_OAK = "Textures/leaves/oak.png"; - private static final String LEAF_ASH = "Textures/leaves/ash.png"; - private static final String LEAF_ASPEN = "Textures/leaves/aspen.png"; - private static final String LEAF_PINE = "Textures/leaves/pine.png"; + private static final String LEAF_OAK = "Textures/internal/leaves/oak.png"; + private static final String LEAF_ASH = "Textures/internal/leaves/ash.png"; + private static final String LEAF_ASPEN = "Textures/internal/leaves/aspen.png"; + private static final String LEAF_PINE = "Textures/internal/leaves/pine.png"; // ════════════════════════════════════════════════════════════════════════ // OAK diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreeType.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TreeType.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreeType.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TreeType.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/Trellis.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/Trellis.java diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class b/blight-vegetation-generator/src/main/java/de/blight/eztree/TrellisOptions.class similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TrellisOptions.class diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/TrellisOptions.java similarity index 100% rename from ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java rename to blight-vegetation-generator/src/main/java/de/blight/eztree/TrellisOptions.java diff --git a/doc/Die vier Freunde.odt b/doc/Die vier Freunde.odt new file mode 100644 index 0000000..8bc97bb Binary files /dev/null and b/doc/Die vier Freunde.odt differ diff --git a/ez-tree-jme/assets/MatDefs/Tree.j3md b/ez-tree-jme/assets/MatDefs/Tree.j3md deleted file mode 100644 index 25e4729..0000000 --- a/ez-tree-jme/assets/MatDefs/Tree.j3md +++ /dev/null @@ -1,21 +0,0 @@ -MaterialDef Tree { - - MaterialParameters { - Color Diffuse (Color) : 0.42 0.26 0.10 1.0 - Float WindStrength : 0.15 - Float WindSpeed : 0.5 - Texture2D BarkMap - Boolean HasBarkMap : false - } - - Technique { - VertexShader GLSL150 : Shaders/Tree.vert - FragmentShader GLSL150 : Shaders/Tree.frag - - WorldParameters { - WorldViewProjectionMatrix - WorldMatrix - Time - } - } -} diff --git a/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md b/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md deleted file mode 100644 index d57e4bd..0000000 --- a/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md +++ /dev/null @@ -1,83 +0,0 @@ -MaterialDef TreeLeaf { - - MaterialParameters { - Color Diffuse (Color) : 0.18 0.60 0.10 1.0 - Float WindStrength : 0.30 - Float WindSpeed : 0.7 - Texture2D LeafMap - Boolean HasLeafMap : false - - // Vom Shadow-Renderer befüllt (PostShadow-Pass) — vollständige Liste aus PostShadow.j3md - Int BoundDrawBuffer - Int FilterMode - Boolean HardwareShadows - Texture2D ShadowMap0 - Texture2D ShadowMap1 - Texture2D ShadowMap2 - Texture2D ShadowMap3 - Texture2D ShadowMap4 - Texture2D ShadowMap5 - Float ShadowIntensity : 1.0 - Vector4 Splits - Vector2 FadeInfo - Matrix4 LightViewProjectionMatrix0 - Matrix4 LightViewProjectionMatrix1 - Matrix4 LightViewProjectionMatrix2 - Matrix4 LightViewProjectionMatrix3 - Matrix4 LightViewProjectionMatrix4 - Matrix4 LightViewProjectionMatrix5 - Vector3 LightPos - Vector3 LightDir - Float PCFEdge - Float ShadowMapSize - Boolean BackfaceShadows : false - } - - Technique { - VertexShader GLSL150 : Shaders/Tree.vert - FragmentShader GLSL150 : Shaders/TreeLeaf.frag - - WorldParameters { - WorldViewProjectionMatrix - WorldMatrix - Time - } - - RenderState { - FaceCull Off - } - } - - Technique PostShadow { - VertexShader GLSL150 : Shaders/LeafPostShadow.vert - FragmentShader GLSL150 : Shaders/LeafPostShadow.frag - - WorldParameters { - WorldViewProjectionMatrix - WorldMatrix - } - - ForcedRenderState { - Blend Modulate - FaceCull Off - DepthWrite Off - } - } - - Technique PreShadow { - VertexShader GLSL150 : Shaders/LeafPreShadow.vert - FragmentShader GLSL150 : Shaders/LeafPreShadow.frag - - WorldParameters { - WorldViewProjectionMatrix - } - - ForcedRenderState { - FaceCull Off - DepthTest On - DepthWrite On - PolyOffset 5 3 - ColorWrite Off - } - } -} diff --git a/ez-tree-jme/assets/Shaders/LeafPostShadow.frag b/ez-tree-jme/assets/Shaders/LeafPostShadow.frag deleted file mode 100644 index f87574b..0000000 --- a/ez-tree-jme/assets/Shaders/LeafPostShadow.frag +++ /dev/null @@ -1,20 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform sampler2D m_ShadowMap0; -uniform float m_ShadowIntensity; -uniform sampler2D m_LeafMap; -uniform bool m_HasLeafMap; - -in vec4 shadowCoord; -in vec2 texCoord; - -void main() { - // Transparente Blattbereiche empfangen keinen Schatten - if (m_HasLeafMap && texture2D(m_LeafMap, texCoord).a < 0.5) discard; - - vec3 coord = shadowCoord.xyz / shadowCoord.w; - float mapDepth = texture2D(m_ShadowMap0, coord.xy).r; - float lit = (coord.z > mapDepth + 0.001) ? (1.0 - m_ShadowIntensity) : 1.0; - - gl_FragColor = vec4(lit, lit, lit, 1.0); -} diff --git a/ez-tree-jme/assets/Shaders/LeafPostShadow.vert b/ez-tree-jme/assets/Shaders/LeafPostShadow.vert deleted file mode 100644 index 5b13bb8..0000000 --- a/ez-tree-jme/assets/Shaders/LeafPostShadow.vert +++ /dev/null @@ -1,18 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform mat4 g_WorldViewProjectionMatrix; -uniform mat4 g_WorldMatrix; -uniform mat4 m_LightViewProjectionMatrix0; - -in vec3 inPosition; -in vec2 inTexCoord; - -out vec4 shadowCoord; -out vec2 texCoord; - -void main() { - gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); - vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); - shadowCoord = m_LightViewProjectionMatrix0 * worldPos; - texCoord = inTexCoord; -} diff --git a/ez-tree-jme/assets/Shaders/LeafPreShadow.frag b/ez-tree-jme/assets/Shaders/LeafPreShadow.frag deleted file mode 100644 index 59a31e7..0000000 --- a/ez-tree-jme/assets/Shaders/LeafPreShadow.frag +++ /dev/null @@ -1,15 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform sampler2D m_LeafMap; -uniform bool m_HasLeafMap; - -in vec2 texCoord; - -void main() { - if (m_HasLeafMap) { - vec4 tex = texture2D(m_LeafMap, texCoord); - if (tex.a < 0.5) discard; - } - // Nur Tiefe schreiben — ColorWrite ist per ForcedRenderState deaktiviert - gl_FragColor = vec4(1.0); -} diff --git a/ez-tree-jme/assets/Shaders/LeafPreShadow.vert b/ez-tree-jme/assets/Shaders/LeafPreShadow.vert deleted file mode 100644 index 8b60506..0000000 --- a/ez-tree-jme/assets/Shaders/LeafPreShadow.vert +++ /dev/null @@ -1,13 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform mat4 g_WorldViewProjectionMatrix; - -in vec3 inPosition; -in vec2 inTexCoord; - -out vec2 texCoord; - -void main() { - gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); - texCoord = inTexCoord; -} diff --git a/ez-tree-jme/assets/Shaders/Tree.frag b/ez-tree-jme/assets/Shaders/Tree.frag deleted file mode 100644 index 4232df7..0000000 --- a/ez-tree-jme/assets/Shaders/Tree.frag +++ /dev/null @@ -1,29 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform vec4 m_Diffuse; -uniform sampler2D m_BarkMap; -uniform bool m_HasBarkMap; - -in vec2 texCoord; -in vec3 worldNormal; - -void main() { - vec3 n = normalize(worldNormal); - - // Sun at ~45° elevation from SE — good contrast on vertical cylinders - vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4)); - vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4)); - vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7)); - - float sun = max(dot(n, sunDir), 0.0); - float fill = max(dot(n, fillDir), 0.0) * 0.22; - float rim = max(dot(n, rimDir), 0.0) * 0.14; - float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; // [0.0, 0.8] - float light = sun * 0.75 + fill + rim + sky * 0.09 + 0.05; - - vec3 baseColor = m_HasBarkMap - ? texture2D(m_BarkMap, texCoord).rgb * m_Diffuse.rgb - : m_Diffuse.rgb; - - gl_FragColor = vec4(baseColor * clamp(light, 0.0, 1.0), m_Diffuse.a); -} diff --git a/ez-tree-jme/assets/Shaders/Tree.vert b/ez-tree-jme/assets/Shaders/Tree.vert deleted file mode 100644 index 583272c..0000000 --- a/ez-tree-jme/assets/Shaders/Tree.vert +++ /dev/null @@ -1,33 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform mat4 g_WorldViewProjectionMatrix; -uniform mat4 g_WorldMatrix; -uniform float g_Time; -uniform float m_WindStrength; -uniform float m_WindSpeed; - -in vec3 inPosition; -in vec3 inNormal; -in vec2 inTexCoord; -in vec4 inColor; // R = Wind-Gewicht (0 = Wurzel, 1 = Spitze) - -out vec2 texCoord; -out vec3 worldNormal; - -void main() { - float windW = inColor.r; - float t = g_Time * m_WindSpeed; - - // Welt-Position für orts-abhängige Phase (verhindert synchrones Schwingen) - vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); - float phase = worldPos.x * 0.08 + worldPos.z * 0.06; - - float swayX = sin(t + phase) * windW * m_WindStrength; - float swayZ = cos(t * 0.73 + phase) * windW * m_WindStrength * 0.55; - - vec3 animPos = inPosition + vec3(swayX, 0.0, swayZ); - - gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0); - texCoord = inTexCoord; - worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz); -} diff --git a/ez-tree-jme/assets/Shaders/TreeLeaf.frag b/ez-tree-jme/assets/Shaders/TreeLeaf.frag deleted file mode 100644 index d805691..0000000 --- a/ez-tree-jme/assets/Shaders/TreeLeaf.frag +++ /dev/null @@ -1,27 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" - -uniform vec4 m_Diffuse; -uniform sampler2D m_LeafMap; -uniform bool m_HasLeafMap; - -in vec2 texCoord; -in vec3 worldNormal; - -void main() { - vec3 baseColor; - - if (m_HasLeafMap) { - vec4 tex = texture2D(m_LeafMap, texCoord); - if (tex.a < 0.5) discard; - baseColor = tex.rgb * m_Diffuse.rgb; - } else { - // Fallback: kreisförmiger Clip - vec2 uv = texCoord * 2.0 - 1.0; - if (dot(uv, uv) > 0.95) discard; - float edge = 1.0 - dot(uv, uv); - baseColor = m_Diffuse.rgb * (0.7 + 0.3 * edge); - } - - // Leaves transmit light — no directional shading, uniform brightness - gl_FragColor = vec4(baseColor, 1.0); -} diff --git a/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg deleted file mode 100644 index 6277b94..0000000 Binary files a/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg deleted file mode 100644 index b2525e9..0000000 Binary files a/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg deleted file mode 100644 index 15ed21b..0000000 Binary files a/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg deleted file mode 100644 index 43ee658..0000000 Binary files a/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/leaves/ash.png b/ez-tree-jme/assets/Textures/leaves/ash.png deleted file mode 100644 index 7f7c844..0000000 Binary files a/ez-tree-jme/assets/Textures/leaves/ash.png and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/leaves/aspen.png b/ez-tree-jme/assets/Textures/leaves/aspen.png deleted file mode 100644 index 89265a9..0000000 Binary files a/ez-tree-jme/assets/Textures/leaves/aspen.png and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/leaves/oak.png b/ez-tree-jme/assets/Textures/leaves/oak.png deleted file mode 100644 index 061fbf6..0000000 Binary files a/ez-tree-jme/assets/Textures/leaves/oak.png and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/leaves/palm.png b/ez-tree-jme/assets/Textures/leaves/palm.png deleted file mode 100644 index 4c8f4b2..0000000 Binary files a/ez-tree-jme/assets/Textures/leaves/palm.png and /dev/null differ diff --git a/ez-tree-jme/assets/Textures/leaves/pine.png b/ez-tree-jme/assets/Textures/leaves/pine.png deleted file mode 100644 index 478dca3..0000000 Binary files a/ez-tree-jme/assets/Textures/leaves/pine.png and /dev/null differ diff --git a/settings.gradle b/settings.gradle index 6534dc1..4e55789 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,4 @@ include 'blight-map' include 'blight-lang' include 'blight-editor' include 'blight-game' -include 'simarboreal' -include 'ez-tree-jme' +include 'blight-vegetation-generator'