diff --git a/blight-assets/src/main/resources/Common/ShaderLib/Shadows.glsllib b/blight-assets/src/main/resources/Common/ShaderLib/Shadows.glsllib new file mode 100644 index 0000000..d90d1a4 --- /dev/null +++ b/blight-assets/src/main/resources/Common/ShaderLib/Shadows.glsllib @@ -0,0 +1,307 @@ +// Blight-Override von JME3's Shadows.glsllib +// Änderung: SHADOW_BIAS wird einmal in getDirectionalLightShadows() von +// projCoord.z abgezogen (Empfänger-seitiger Depth-Bias). +// Das behebt Self-Shadowing-Acne bei hohem Sonnenstand, ohne den +// Pre-Shadow-PolyOffset zu erhöhen. +// Wert anpassen: bei verbleibendem Acne erhöhen, bei "Peter Panning" verkleinern. +#define SHADOW_BIAS 0.0002 + +#if __VERSION__ >= 130 + #ifdef GL_ES + #extension GL_OES_gpu_shader5 : enable + #else + #extension GL_ARB_gpu_shader5 : enable + #endif + + #define IVEC2 ivec2 + #ifdef HARDWARE_SHADOWS + #define SHADOWMAP sampler2DShadow + #define SHADOWCOMPAREOFFSET(tex,coord,offset) textureProjOffset(tex, coord, offset) + #define SHADOWCOMPARE(tex,coord) textureProj(tex, coord) + #if defined GL_ES && __VERSION__ <= 300 + #define SHADOWGATHER(tex,coord) step(coord.z, textureGather(tex, coord.xy)) + #else + #define SHADOWGATHER(tex,coord) textureGather(tex, coord.xy, coord.z) + #endif + #else + #define SHADOWMAP sampler2D + #define SHADOWCOMPAREOFFSET(tex,coord,offset) step(coord.z, textureProjOffset(tex, coord, offset).r) + #define SHADOWCOMPARE(tex,coord) step(coord.z, textureProj(tex, coord).r) + #define SHADOWGATHER(tex,coord) step(coord.z, textureGather(tex, coord.xy)) + #endif + + #if FILTER_MODE == 10 + #define GETSHADOW Shadow_Nearest + #define KERNEL 1.0 + #elif FILTER_MODE == 1 + #ifdef HARDWARE_SHADOWS + #define GETSHADOW Shadow_Nearest + #else + #define GETSHADOW Shadow_DoBilinear_2x2 + #endif + #define KERNEL 1.0 + #endif +#else + #define IVEC2 vec2 + #if defined GL_ES + #define SHADOWMAP sampler2D + #define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r) + #elif defined HARDWARE_SHADOWS + #define SHADOWMAP sampler2DShadow + #define SHADOWCOMPARE(tex,coord) shadow2DProj(tex, coord).r + #else + #define SHADOWMAP sampler2D + #define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r) + #endif + + #if FILTER_MODE == 10 + #define GETSHADOW Shadow_DoShadowCompare + #define KERNEL 1.0 + #elif FILTER_MODE == 1 + #ifdef HARDWARE_SHADOWS + #define GETSHADOW Shadow_DoShadowCompare + #else + #define GETSHADOW Shadow_DoBilinear_2x2 + #endif + #define KERNEL 1.0 + #endif + + +#endif + +#if (FILTER_MODE == 2) + #define GETSHADOW Shadow_DoDither_2x2 + #define KERNEL 1.0 +#elif FILTER_MODE == 3 + #define GETSHADOW Shadow_DoPCF + #define KERNEL 4.0 +#elif FILTER_MODE == 4 + #define GETSHADOW Shadow_DoPCFPoisson + #define KERNEL 4.0 +#elif FILTER_MODE == 5 + #define GETSHADOW Shadow_DoPCF + #define KERNEL 8.0 +#endif + +uniform SHADOWMAP m_ShadowMap0; +uniform SHADOWMAP m_ShadowMap1; +uniform SHADOWMAP m_ShadowMap2; +uniform SHADOWMAP m_ShadowMap3; +#ifdef POINTLIGHT + uniform SHADOWMAP m_ShadowMap4; + uniform SHADOWMAP m_ShadowMap5; +#endif + +#ifdef PSSM +uniform vec4 m_Splits; +#endif + +uniform float m_ShadowIntensity; + +const vec2 pixSize2 = vec2(1.0 / SHADOWMAP_SIZE); +float shadowBorderScale = 1.0; + +float Shadow_DoShadowCompare(in SHADOWMAP tex,in vec4 projCoord){ + return SHADOWCOMPARE(tex, projCoord); +} + +float Shadow_BorderCheck(in vec2 coord){ + #ifdef GL_ES + return 0.0; + #else + vec4 t = vec4(coord.xy, 0.0, 1.0); + t = step(t.wwxy, t.xyzz); + return dot(t,t); + #endif +} + +float Shadow_Nearest(in SHADOWMAP tex,in vec4 projCoord){ + float border = Shadow_BorderCheck(projCoord.xy); + if (border > 0.0){ + return 1.0; + } + return SHADOWCOMPARE(tex, projCoord); +} + +float Shadow_DoShadowCompareOffset(in SHADOWMAP tex,in vec4 projCoord,in vec2 offset){ + vec4 coord = vec4(projCoord.xy + offset.xy * pixSize2 * shadowBorderScale, projCoord.zw); + return SHADOWCOMPARE(tex, coord); +} + + +float Shadow_DoDither_2x2(in SHADOWMAP tex, in vec4 projCoord){ + float border = Shadow_BorderCheck(projCoord.xy); + if (border > 0.0) + return 1.0; + + float shadow = 0.0; + vec2 o = vec2(IVEC2(mod(floor(gl_FragCoord.xy), 2.0))); + shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2(-1.5, 1.5)+o)); + shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2( 0.5, 1.5)+o)); + shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2(-1.5, -0.5)+o)); + shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2( 0.5, -0.5)+o)); + shadow *= 0.25; + return shadow; +} + +float Shadow_DoBilinear_2x2(in SHADOWMAP tex, in vec4 projCoord){ + float border = Shadow_BorderCheck(projCoord.xy); + if (border > 0.0){ + return 1.0; + } + + vec4 gather = vec4(0.0); + #if __VERSION__ >= 130 + #if defined GL_ARB_gpu_shader5 || defined GL_OES_gpu_shader5 + vec4 coord = vec4(projCoord.xyz / projCoord.www,0.0); + gather = SHADOWGATHER(tex, coord); + #else + gather.x = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 1)); + gather.y = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 1)); + gather.z = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 0)); + gather.w = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 0)); + #endif + #else + gather.x = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 0.0)); + gather.y = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 0.0)); + gather.z = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 1.0)); + gather.w = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 1.0)); + #endif + + vec2 f = fract( projCoord.xy * SHADOWMAP_SIZE ); + vec2 mx = mix( gather.wx, gather.zy, f.x ); + return mix( mx.x, mx.y, f.y ); +} + +float Shadow_DoPCF(in SHADOWMAP tex,in vec4 projCoord){ + + float shadow = 0.0; + float border = Shadow_BorderCheck(projCoord.xy); + if (border > 0.0) + return 1.0; + + const float bound = (KERNEL * 0.5 - 0.5 ) * PCFEDGE; + for (float y = -bound; y <= bound; y += PCFEDGE){ + for (float x = -bound; x <= bound; x += PCFEDGE){ + #if __VERSION__ < 130 + shadow += clamp(Shadow_DoShadowCompareOffset(tex,projCoord,vec2(x,y)) + border, 0.0, 1.0); + #else + shadow += Shadow_DoShadowCompareOffset(tex, projCoord, vec2(x,y)); + #endif + } + } + + shadow = shadow / (KERNEL * KERNEL); + return shadow; +} + +//12 tap poisson disk +const vec2 poissonDisk0 = vec2(-0.1711046, -0.425016); +const vec2 poissonDisk1 = vec2(-0.7829809, 0.2162201); +const vec2 poissonDisk2 = vec2(-0.2380269, -0.8835521); +const vec2 poissonDisk3 = vec2(0.4198045, 0.1687819); +const vec2 poissonDisk4 = vec2(-0.684418, -0.3186957); +const vec2 poissonDisk5 = vec2(0.6026866, -0.2587841); +const vec2 poissonDisk6 = vec2(-0.2412762, 0.3913516); +const vec2 poissonDisk7 = vec2(0.4720655, -0.7664126); +const vec2 poissonDisk8 = vec2(0.9571564, 0.2680693); +const vec2 poissonDisk9 = vec2(-0.5238616, 0.802707); +const vec2 poissonDisk10 = vec2(0.5653144, 0.60262); +const vec2 poissonDisk11 = vec2(0.0123658, 0.8627419); + + +float Shadow_DoPCFPoisson(in SHADOWMAP tex, in vec4 projCoord){ + float shadow = 0.0; + float border = Shadow_BorderCheck(projCoord.xy); + if (border > 0.0){ + return 1.0; + } + + vec2 texelSize = pixSize2 * 4.0 * PCFEDGE * shadowBorderScale; + + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk0 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk1 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk2 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk3 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk4 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk5 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk6 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk7 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk8 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk9 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk10 * texelSize, projCoord.zw)); + shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk11 * texelSize, projCoord.zw)); + + //this is divided by 12 + return shadow * 0.08333333333; +} + +#ifdef POINTLIGHT + float getPointLightShadows(in vec4 worldPos,in vec3 lightPos, + in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,in SHADOWMAP shadowMap4,in SHADOWMAP shadowMap5, + in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3,in vec4 projCoord4,in vec4 projCoord5){ + float shadow = 1.0; + vec3 vect = worldPos.xyz - lightPos; + vec3 absv= abs(vect); + float maxComp = max(absv.x,max(absv.y,absv.z)); + if(maxComp == absv.y){ + if(vect.y < 0.0){ + shadow = GETSHADOW(shadowMap0, projCoord0 / projCoord0.w); + }else{ + shadow = GETSHADOW(shadowMap1, projCoord1 / projCoord1.w); + } + }else if(maxComp == absv.z){ + if(vect.z < 0.0){ + shadow = GETSHADOW(shadowMap2, projCoord2 / projCoord2.w); + }else{ + shadow = GETSHADOW(shadowMap3, projCoord3 / projCoord3.w); + } + }else if(maxComp == absv.x){ + if(vect.x < 0.0){ + shadow = GETSHADOW(shadowMap4, projCoord4 / projCoord4.w); + }else{ + shadow = GETSHADOW(shadowMap5, projCoord5 / projCoord5.w); + } + } + return shadow; + } +#else + #ifdef PSSM + float getDirectionalLightShadows(in vec4 splits,in float shadowPosition, + in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3, + in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3){ + // Receiver-seitiger Depth-Bias: verhindert Self-Shadowing-Acne bei + // flachen Oberflächen und hohem Sonnenstand (PSSM nutzt orthografische + // Projektion → w=1, daher direkt von z abziehen). + vec4 pc0 = vec4(projCoord0.xy, projCoord0.z - SHADOW_BIAS, projCoord0.w); + vec4 pc1 = vec4(projCoord1.xy, projCoord1.z - SHADOW_BIAS, projCoord1.w); + vec4 pc2 = vec4(projCoord2.xy, projCoord2.z - SHADOW_BIAS, projCoord2.w); + vec4 pc3 = vec4(projCoord3.xy, projCoord3.z - SHADOW_BIAS, projCoord3.w); + + float shadow = 1.0; + if(shadowPosition < splits.x){ + shadow = GETSHADOW(shadowMap0, pc0); + }else if( shadowPosition < splits.y){ + shadowBorderScale = 0.5; + shadow = GETSHADOW(shadowMap1, pc1); + }else if( shadowPosition < splits.z){ + shadowBorderScale = 0.25; + shadow = GETSHADOW(shadowMap2, pc2); + }else if( shadowPosition < splits.w){ + shadowBorderScale = 0.125; + shadow = GETSHADOW(shadowMap3, pc3); + } + return shadow; + } + #else + float getSpotLightShadows(in SHADOWMAP shadowMap,in vec4 projCoord){ + float shadow = 1.0; + projCoord /= projCoord.w; + shadow = GETSHADOW(shadowMap,projCoord); + + projCoord = projCoord * 2.0 - 1.0; + float fallOff = ( length(projCoord.xy) - 0.9 ) / 0.1; + return mix(shadow,1.0,clamp(fallOff,0.0,1.0)); + } + #endif +#endif diff --git a/blight-assets/src/main/resources/MatDefs/BakedTerrain.j3md b/blight-assets/src/main/resources/MatDefs/BakedTerrain.j3md index b179155..3cd9346 100644 --- a/blight-assets/src/main/resources/MatDefs/BakedTerrain.j3md +++ b/blight-assets/src/main/resources/MatDefs/BakedTerrain.j3md @@ -12,6 +12,7 @@ MaterialDef BakedTerrain { Vector3 SunColor : 0.68 0.65 0.60 Vector3 AmbientColor : 0.08 0.10 0.16 Boolean DebugNoLight + Float Wetness } Technique { diff --git a/blight-assets/src/main/resources/Shaders/BakedTerrain.frag b/blight-assets/src/main/resources/Shaders/BakedTerrain.frag index 3c1f520..20350f3 100644 --- a/blight-assets/src/main/resources/Shaders/BakedTerrain.frag +++ b/blight-assets/src/main/resources/Shaders/BakedTerrain.frag @@ -13,10 +13,11 @@ uniform sampler2D m_AlphaMap_2; uniform int m_SteepIndex; #endif -uniform vec3 m_LightDir; -uniform vec3 m_SunColor; -uniform vec3 m_AmbientColor; -uniform vec3 g_CameraPosition; +uniform vec3 m_LightDir; +uniform vec3 m_SunColor; +uniform vec3 m_AmbientColor; +uniform vec3 g_CameraPosition; +uniform float m_Wetness; in vec3 vWorldPos; in vec3 vNormal; @@ -77,15 +78,22 @@ void main() { col = col * flatBlend + steepCol * steepBlend; #endif - vec3 N = normalize(vNormal); + vec3 N = normalize(vNormal); vec3 lightDir = normalize(m_LightDir); float diff = max(dot(N, lightDir), 0.0); vec3 light = m_AmbientColor + m_SunColor * diff; + // Nass-Effekt: Oberfläche abdunkeln + Blinn-Phong Specular Schimmer + float wet = clamp(m_Wetness, 0.0, 1.0); + col.rgb *= (1.0 - 0.20 * wet); + vec3 viewDir = normalize(g_CameraPosition - vWorldPos); + vec3 H = normalize(lightDir + viewDir); + float spec = pow(max(dot(N, H), 0.0), 80.0) * wet; + 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); + outColor = vec4(col.rgb * light * BRIGHTNESS + vec3(spec * 0.35), col.a); #endif } diff --git a/blight-assets/src/main/resources/Shaders/Grass.vert b/blight-assets/src/main/resources/Shaders/Grass.vert index c3a654b..7ff1046 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.vert +++ b/blight-assets/src/main/resources/Shaders/Grass.vert @@ -29,13 +29,15 @@ void main() { vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0); float wavePhase = dot(worldXZ, windN); + // Pro-Halm-Zufallsphase: floor() macht sie für alle Vertices desselben Halms gleich + float randPhase = fract(sin(dot(floor(worldXZ), vec2(127.1, 311.7))) * 43758.5453) * 6.2832; - // Zwei überlagerte Sinuswellen für organische Bewegung - float sway = sin(t * 2.1 + wavePhase * 0.08) * 0.6 - + sin(t * 1.4 + wavePhase * 0.06) * 0.4; + // Zwei überlagerte Sinuswellen + Zufallsphase → kein synchroner Gleichtakt + float sway = sin(t * 2.1 + wavePhase * 0.08 + randPhase) * 0.6 + + sin(t * 1.4 + wavePhase * 0.06 + randPhase * 0.73) * 0.4; - // Mindest-Stärke damit bei wenig Wind noch Bewegung sichtbar ist - float effectiveStrength = max(m_WindStrength, 0.05); + // Halbierte Windstärke für Textur-Gras; Mindest-Stärke für minimale Bewegung + float effectiveStrength = max(m_WindStrength, 0.05) * 0.5; // Quadratische Gewichtung: Spitze bewegt sich mehr als Basis float bend = sway * effectiveStrength * inTexCoord.y * inTexCoord.y; diff --git a/blight-assets/src/main/resources/Textures/hud/compass_needle.png b/blight-assets/src/main/resources/Textures/hud/compass_needle.png new file mode 100644 index 0000000..e84bdf7 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/compass_needle.png differ diff --git a/blight-assets/src/main/resources/Textures/hud/compass_rose.png b/blight-assets/src/main/resources/Textures/hud/compass_rose.png new file mode 100644 index 0000000..145e615 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/compass_rose.png differ 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 a5fcf85..6fe23d3 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 @@ -290,7 +290,8 @@ public class PlacedObjectState extends BaseAppState { } private void paintGrass(float cx, float cz, float radius) { - int n = Math.max(1, (int) input.grassTool.density.getValue()); + int n = (int) Math.round(input.grassTool.density.getValue() * radius * 2.0); + if (n <= 0) return; float baseH = (float) input.grassTool.grassHeight.getValue(); int slot = input.grassActiveSlot; Random rng = new Random(); @@ -423,13 +424,13 @@ public class PlacedObjectState extends BaseAppState { 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 + for (int i = 0; i < 4; i++) { nor.put(0).put(1).put(0); tan.put(1).put(0).put(0).put(1); } + // Quad 2: liegt in ZY-Ebene (konstantes x) – Normal zeigt +Y, 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); } + for (int i = 0; i < 4; i++) { nor.put(0).put(1).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); diff --git a/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java index 99ea6b2..69ff675 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java @@ -7,9 +7,9 @@ import java.util.List; */ public class GrassTool extends EditorTool { - public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 40.0, 1.0, 500.0); - public final ToolParameter grassHeight = new ToolParameter("Grashöhe", 1.5, 0.1, 10.0); - public final ToolParameter density = new ToolParameter("Dichte", 8.0, 1.0, 200.0); + public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0); + public final ToolParameter grassHeight = new ToolParameter("Grashöhe", 0.8, 0.0, 2.0); + public final ToolParameter density = new ToolParameter("Dichte", 0.5, 0.0, 2.0); @Override public String getName() { return "Gras (Textur)"; } 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 0669611..11324de 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -339,12 +339,13 @@ public class BlightGame extends SimpleApplication { super.initialize(sm, a); setDisplayFps(false); setDisplayStatView(false); - int h = a.getCamera().getHeight(); - if (fpsText != null) fpsText.setLocalTranslation(0, h, 0); - if (statsView != null) { - float fpsH = fpsText != null ? fpsText.getLineHeight() : 0f; - statsView.setLocalTranslation(0, h - fpsH, 0); - } + int h = a.getCamera().getHeight(); + float fpsH = fpsText != null ? fpsText.getLineHeight() : 0f; + float statsH = statsView != null ? statsView.getHeight() : 0f; + if (fpsText != null) fpsText.setLocalTranslation(0, h, 0); + if (statsView != null) statsView.setLocalTranslation(0, h - fpsH - statsH, 0); + if (darkenFps != null) darkenFps.setLocalTranslation(0, h - fpsH, -1); + if (darkenStats != null) darkenStats.setLocalTranslation(0, h - fpsH - statsH, -1); } }; stateManager.attach(statsState); diff --git a/blight-game/src/main/java/de/blight/game/LegacyAssetRedirectLocator.java b/blight-game/src/main/java/de/blight/game/LegacyAssetRedirectLocator.java new file mode 100644 index 0000000..5c2178c --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/LegacyAssetRedirectLocator.java @@ -0,0 +1,72 @@ +package de.blight.game; + +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 auf die aktuellen Speicherorte um. + * Behebt Laden von j3o-Dateien, die noch den alten Textur-Pfad + * (z. B. Textures/internal/leaves/) statt des neuen (internal/foliage/) enthalten. + * + * 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/foliage/", + "Textures/internal/leaves/", "Textures/internal/foliage/", + "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 + @SuppressWarnings("rawtypes") + 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; + if (remapped.toLowerCase().endsWith(".tga")) { + info = tryFile(manager, key, remapped.substring(0, remapped.length() - 4) + ".png"); + if (info != null) return info; + } + break; + } + } + return null; + } + + @SuppressWarnings("rawtypes") + 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-game/src/main/java/de/blight/game/config/MenuScreen.java b/blight-game/src/main/java/de/blight/game/config/MenuScreen.java index 5bb9262..235c39d 100644 --- a/blight-game/src/main/java/de/blight/game/config/MenuScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/MenuScreen.java @@ -46,6 +46,10 @@ public abstract class MenuScreen extends BaseAppState { public static final float CONT_W = MenuCanvas.CONT_W; public static final float CONT_H = MenuCanvas.CONT_H; + private static int openCount = 0; + + public static boolean isAnyOpen() { return openCount > 0; } + public enum BtnStyle { DEFAULT, SAVE, QUIT, DISABLED, ARROW } protected SimpleApplication app; @@ -77,6 +81,7 @@ public abstract class MenuScreen extends BaseAppState { @Override protected void onEnable() { + openCount++; uiScale = MenuCanvas.getScale(app.getCamera()); bgLayer = buildBgLayer(); canvasNode = buildCanvas(); @@ -98,6 +103,7 @@ public abstract class MenuScreen extends BaseAppState { @Override protected void onDisable() { + if (openCount > 0) { openCount--; } onDisableExtras(); app.getInputManager().removeListener(clickListener); if (app.getInputManager().hasMapping(MAP_CLICK)) { diff --git a/blight-game/src/main/java/de/blight/game/config/OverlayState.java b/blight-game/src/main/java/de/blight/game/config/OverlayState.java index 6674a61..a57228d 100644 --- a/blight-game/src/main/java/de/blight/game/config/OverlayState.java +++ b/blight-game/src/main/java/de/blight/game/config/OverlayState.java @@ -40,6 +40,8 @@ public abstract class OverlayState extends BaseAppState { private static OverlayState activeOverlay; + public static boolean isAnyOpen() { return activeOverlay != null; } + // ── Panel-Dimensionen (90% des Referenzraums) ───────────────────────────── public static final float PW = MenuCanvas.CONT_W; 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 5bb00cf..66bb1a9 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 @@ -232,9 +232,9 @@ public class WorldScene extends BaseAppState { @Override protected void onEnable() { try { - assetManager.registerLocator( - AnimationLibrary.findAssetRoot().toAbsolutePath().toString(), - com.jme3.asset.plugins.FileLocator.class); + String assetRootStr = AnimationLibrary.findAssetRoot().toAbsolutePath().toString(); + assetManager.registerLocator(assetRootStr, com.jme3.asset.plugins.FileLocator.class); + assetManager.registerLocator(assetRootStr, de.blight.game.LegacyAssetRedirectLocator.class); } catch (Exception ignored) {} app.getCamera().setFrustumPerspective(45f, @@ -271,7 +271,7 @@ public class WorldScene extends BaseAppState { // Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1) CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1); physicsChar = new CharacterControl(capsule, 0.05f); - physicsChar.setJumpSpeed(12f); + physicsChar.setJumpSpeed(7f); physicsChar.setFallSpeed(35f); physicsChar.setGravity(35f); character.addControl(physicsChar); diff --git a/blight-game/src/main/java/de/blight/game/state/CompassHudState.java b/blight-game/src/main/java/de/blight/game/state/CompassHudState.java index ec1c0d4..625426b 100644 --- a/blight-game/src/main/java/de/blight/game/state/CompassHudState.java +++ b/blight-game/src/main/java/de/blight/game/state/CompassHudState.java @@ -4,8 +4,6 @@ import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; @@ -15,51 +13,48 @@ import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; 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.VertexBuffer; -import com.jme3.util.BufferUtils; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Quad; +import com.jme3.texture.Texture; +import de.blight.game.animation.AnimationLibrary; +import de.blight.game.config.MenuScreen; +import de.blight.game.config.OverlayState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; /** - * Kompass unten links – runde rotierende Rose, identisch mit dem Editor-Kompass. + * Textur-basierter Kompass unten links. * - * Die Rose dreht sich mit dem Kamera-Yaw; die Buchstaben konter-rotieren, - * damit sie immer aufrecht bleiben. Ein festes goldenes Dreieck oben zeigt - * die aktuelle Blickrichtung an. + * compass_rose.png (100×100) – dreht sich mit dem Kamera-Yaw. + * compass_needle.png (100×100) – fix, zeigt immer nach oben (= aktuelle Blickrichtung). + * + * Beide Dateien werden beim ersten Start als Platzhalter erzeugt + * und können durch eigene Grafiken ersetzt werden. */ public class CompassHudState extends BaseAppState { - // ── Layout ──────────────────────────────────────────────────────────────── - private static final float SIZE = 100f; // Canvas-Größe (px) - private static final float RADIUS = 47f; // Kreis-Außenradius - private static final float LABEL_R = 30f; // Beschriftungsradius vom Mittelpunkt - private static final float MARGIN = 16f; // Abstand zum Bildschirmrand + private static final Logger log = LoggerFactory.getLogger(CompassHudState.class); - // ── Farben (matching EditorApp.drawCompass) ─────────────────────────────── - private static final ColorRGBA COL_BG = new ColorRGBA(0.047f, 0.047f, 0.094f, 0.82f); - private static final ColorRGBA COL_BORDER = new ColorRGBA(0.392f, 0.392f, 0.627f, 0.85f); - private static final ColorRGBA COL_TICK_MJ = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.65f); - private static final ColorRGBA COL_TICK_MN = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.45f); - private static final ColorRGBA COL_N = new ColorRGBA(1.00f, 0.314f, 0.314f, 1.00f); - private static final ColorRGBA COL_CARD = new ColorRGBA(0.824f, 0.824f, 0.902f, 1.00f); - private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.863f, 0.235f, 0.95f); - private static final ColorRGBA COL_CENTER = new ColorRGBA(0.784f, 0.784f, 0.941f, 0.85f); - - // Kompasswinkel (Grad von Nord CW): N=0, O=90, S=180, W=270 - private static final String[] LABELS = {"N", "O", "S", "W"}; - private static final float[] LABEL_DEG = {0f, 90f, 180f, 270f}; + private static final float SIZE = 100f; + private static final float MARGIN = 16f; private SimpleApplication app; private Camera cam; private AssetManager assets; - private BitmapFont font; - private Node compassNode; - private Node roseNode; + private Node compassNode; + private Node roseNode; - private final Node[] labelNodes = new Node[4]; - private final Quaternion roseRot = new Quaternion(); - private final Quaternion labelRot = new Quaternion(); + private final Quaternion roseRot = new Quaternion(); + private boolean lastMenuOpen = false; // ── Lifecycle ───────────────────────────────────────────────────────────── @@ -68,7 +63,7 @@ public class CompassHudState extends BaseAppState { app = (SimpleApplication) application; cam = app.getCamera(); assets = app.getAssetManager(); - font = assets.loadFont("Interface/Fonts/Default.fnt"); + ensureAssets(); } @Override @@ -94,169 +89,160 @@ public class CompassHudState extends BaseAppState { public void update(float tpf) { if (compassNode == null) { return; } - Vector3f dir = cam.getDirection(); - // yaw=0 = Blick nach -Z (Nord); JME3-Kamera blickt in -Z bei Norden - float yaw = FastMath.atan2(dir.x, -dir.z); + boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen(); + if (menuOpen != lastMenuOpen) { + lastMenuOpen = menuOpen; + compassNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit); + } + if (menuOpen) { return; } - // Rose dreht sich so, dass Nord immer in der echten Nord-Richtung liegt + Vector3f dir = cam.getDirection(); + float yaw = FastMath.atan2(dir.x, -dir.z); roseRot.fromAngleAxis(-yaw, Vector3f.UNIT_Z); roseNode.setLocalRotation(roseRot); - - // Buchstaben konter-rotieren → bleiben immer aufrecht - labelRot.fromAngleAxis(yaw, Vector3f.UNIT_Z); - for (Node ln : labelNodes) { - ln.setLocalRotation(labelRot); - } } // ── Aufbau ──────────────────────────────────────────────────────────────── private void buildCompass() { - float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f; - float cx = MARGIN + SIZE / 2f; - float cy = oy + SIZE / 2f; + // Textur-Größen auslesen → echte Pixelgröße bestimmt Layout und Quad-Größe + float roseW = SIZE, roseH = SIZE; + try { + com.jme3.texture.Texture roseTex = assets.loadTexture("Textures/hud/compass_rose.png"); + roseW = roseTex.getImage().getWidth(); + roseH = roseTex.getImage().getHeight(); + } catch (Exception ignored) {} + + // compassNode-Mittelpunkt so setzen, dass die Rose mit MARGIN Abstand zur Ecke endet + float cx = MARGIN + roseW / 2f; + float cy = MARGIN + roseH / 2f; compassNode = new Node("compass"); compassNode.setLocalTranslation(cx, cy, 0f); - // Rahmen (etwas größerer Kreis) + Hintergrund - compassNode.attachChild(makeCircle(RADIUS + 1.5f, 64, COL_BORDER, 1f)); - compassNode.attachChild(makeCircle(RADIUS, 64, COL_BG, 2f)); - - // Rotierende Rose roseNode = new Node("rose"); compassNode.attachChild(roseNode); + roseNode.attachChild(makeTexQuad("Textures/hud/compass_rose.png", 2f)); - // 8 Tick-Striche (major = Hauptrichtungen, minor = Zwischenrichtungen) - for (int i = 0; i < 8; i++) { - float deg = i * 45f; - boolean maj = (i % 2 == 0); - float innerR = RADIUS - (maj ? 9f : 5f); - roseNode.attachChild(makeTickQuad(deg, innerR, maj ? COL_TICK_MJ : COL_TICK_MN, 3f)); - } - - // Himmelsrichtungs-Beschriftungen - for (int i = 0; i < 4; i++) { - float ca = LABEL_DEG[i] * FastMath.DEG_TO_RAD; - float lx = FastMath.sin(ca) * LABEL_R; - float ly = FastMath.cos(ca) * LABEL_R; - - BitmapText lbl = makeBitmapText(LABELS[i], i == 0 ? 14 : 12, i == 0 ? COL_N : COL_CARD); - // Buchstabe am Label-Ursprung zentrieren - lbl.setLocalTranslation(-lbl.getLineWidth() * 0.5f, lbl.getLineHeight() * 0.4f, 0f); - - Node ln = new Node("lbl_" + LABELS[i]); - ln.setLocalTranslation(lx, ly, 4f); - ln.attachChild(lbl); - labelNodes[i] = ln; - roseNode.attachChild(ln); - } - - // Fixer Richtungs-Zeiger: gelbes Dreieck oben (nicht in roseNode) - compassNode.attachChild(makeTriangle(COL_MARKER, 5f)); - - // Mittelpunkt-Punkt - compassNode.attachChild(makeCircle(2.5f, 16, COL_CENTER, 6f)); + // Nadel zentriert auf dem Kompass-Mittelpunkt, auf natürlicher Textur-Größe + compassNode.attachChild(makeTexQuad("Textures/hud/compass_needle.png", 5f)); app.getGuiNode().attachChild(compassNode); } - // ── Geometry-Helfer ─────────────────────────────────────────────────────── - - /** Gefüllter Kreis (Triangle-Fan) zentriert bei (0,0). */ - private Geometry makeCircle(float r, int segs, ColorRGBA col, float z) { - float[] verts = new float[(segs + 2) * 3]; - // Mittelpunkt - verts[0] = 0f; verts[1] = 0f; verts[2] = 0f; - for (int i = 0; i <= segs; i++) { - float a = FastMath.TWO_PI * i / segs; - int base = (i + 1) * 3; - verts[base] = FastMath.cos(a) * r; - verts[base + 1] = FastMath.sin(a) * r; - verts[base + 2] = 0f; + /** Lädt Textur, erzeugt Quad in natürlicher Textur-Größe, zentriert bei (0,0). */ + private Geometry makeTexQuad(String assetPath, float z) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + float w = SIZE, h = SIZE; + try { + Texture tex = assets.loadTexture(assetPath); + w = tex.getImage().getWidth(); + h = tex.getImage().getHeight(); + mat.setTexture("ColorMap", tex); + } catch (Exception e) { + mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.5f)); } - int[] idx = new int[segs * 3]; - for (int i = 0; i < segs; i++) { - idx[i * 3] = 0; - idx[i * 3 + 1] = i + 1; - idx[i * 3 + 2] = i + 2; - } - Geometry g = new Geometry("circle", buildMesh(verts, idx)); - g.setMaterial(makeMat(col)); - g.setLocalTranslation(0f, 0f, z); - g.setQueueBucket(col.a < 1f ? RenderQueue.Bucket.Transparent : RenderQueue.Bucket.Gui); + String name = "cmp_" + assetPath.substring(assetPath.lastIndexOf('/') + 1); + Geometry g = new Geometry(name, new Quad(w, h)); + g.setLocalTranslation(-w / 2f, -h / 2f, z); + g.setMaterial(mat); + g.setQueueBucket(RenderQueue.Bucket.Gui); return g; } - /** - * Tick-Strich als dünnes Quad entlang der radialen Richtung. - * ca_deg: Kompasswinkel (0=Nord, 90=Ost, CW). - */ - private Geometry makeTickQuad(float ca_deg, float innerR, ColorRGBA col, float z) { - float ca = ca_deg * FastMath.DEG_TO_RAD; - float sx = FastMath.sin(ca); // Richtung entlang des Radius - float cy_ = FastMath.cos(ca); - float px = -cy_ * 0.75f; // Halbe Breite (senkrecht zur Radiale) - float py = sx * 0.75f; + // ── Placeholder-PNGs ────────────────────────────────────────────────────── - float[] v = { - sx * innerR + px, cy_ * innerR + py, z, // v0 links innen - sx * innerR - px, cy_ * innerR - py, z, // v1 rechts innen - sx * RADIUS + px, cy_ * RADIUS + py, z, // v2 links außen - sx * RADIUS - px, cy_ * RADIUS - py, z // v3 rechts außen - }; - int[] idx = {0, 1, 2, 1, 3, 2}; - - Geometry g = new Geometry("tick", buildMesh(v, idx)); - g.setMaterial(makeMat(col)); - g.setQueueBucket(RenderQueue.Bucket.Transparent); - return g; - } - - /** - * Festes gelbes Dreieck – zeigt immer nach oben (Blickrichtung). - * Spitze bei (0, RADIUS−5), Basis bei (±5, RADIUS−16). - */ - private Geometry makeTriangle(ColorRGBA col, float z) { - float tipY = RADIUS - 5f; - float baseY = RADIUS - 16f; - float[] v = { - 0f, tipY, 0f, // Spitze - -5f, baseY, 0f, // Basis links - 5f, baseY, 0f // Basis rechts - }; - // CCW von +Z-Seite: (0,high)→(-5,low)→(5,low) ✓ - int[] idx = {0, 1, 2}; - Geometry g = new Geometry("triangle", buildMesh(v, idx)); - g.setMaterial(makeMat(col)); - g.setLocalTranslation(0f, 0f, z); - g.setQueueBucket(RenderQueue.Bucket.Transparent); - return g; - } - - private static Mesh buildMesh(float[] verts, int[] idx) { - Mesh m = new Mesh(); - m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(verts)); - m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx)); - m.updateBound(); - return m; - } - - private Material makeMat(ColorRGBA col) { - Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - m.setColor("Color", col.clone()); - if (col.a < 1f) { - m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + private void ensureAssets() { + Path root = AnimationLibrary.findAssetRoot(); + Path dir = root.resolve("Textures").resolve("hud"); + try { + Files.createDirectories(dir); + } catch (IOException e) { + log.warn("[Compass] Verzeichnis nicht erstellbar: {}", dir); + return; } - return m; + ensureRose(dir.resolve("compass_rose.png")); + ensureNeedle(dir.resolve("compass_needle.png")); } - private BitmapText makeBitmapText(String text, int size, ColorRGBA col) { - BitmapText t = new BitmapText(font); - t.setSize(size); - t.setColor(col.clone()); - t.setText(text); - t.setQueueBucket(RenderQueue.Bucket.Gui); - return t; + /** Erzeugt Platzhalter-Rose: dunkler Kreis + 8 Striche + N/E/S/W. */ + private static void ensureRose(Path path) { + if (Files.exists(path)) { return; } + try { + int cx = 50, cy = 50, r = 47; + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + // Hintergrund-Kreis + g.setColor(new Color(12, 12, 24, 209)); + g.fillOval(cx - r, cy - r, r * 2, r * 2); + + // Rand-Ring + g.setColor(new Color(100, 100, 160, 216)); + g.setStroke(new BasicStroke(1.5f)); + g.drawOval(cx - r, cy - r, r * 2, r * 2); + + // 8 Tick-Striche; angle −90° → 0° zeigt nach oben (= Norden in BufferedImage-Y) + for (int i = 0; i < 8; i++) { + double angle = Math.toRadians(i * 45.0 - 90.0); + boolean major = (i % 2 == 0); + int innerR = r - (major ? 9 : 5); + int x1 = (int)(cx + Math.cos(angle) * innerR); + int y1 = (int)(cy + Math.sin(angle) * innerR); + int x2 = (int)(cx + Math.cos(angle) * (r - 1)); + int y2 = (int)(cy + Math.sin(angle) * (r - 1)); + g.setColor(new Color(160, 160, 200, 166)); + g.setStroke(new BasicStroke(major ? 1.5f : 1.0f)); + g.drawLine(x1, y1, x2, y2); + } + + // Himmelsrichtungs-Buchstaben + int labelR = 31; + + g.setFont(new Font("SansSerif", Font.BOLD, 13)); + FontMetrics fm = g.getFontMetrics(); + g.setColor(new Color(255, 80, 80, 255)); + drawCentered(g, fm, "N", cx, cy - labelR); // oben in BufferedImage = oben auf dem Bildschirm + + g.setFont(new Font("SansSerif", Font.BOLD, 11)); + fm = g.getFontMetrics(); + g.setColor(new Color(210, 210, 230, 255)); + drawCentered(g, fm, "E", cx + labelR, cy); + drawCentered(g, fm, "S", cx, cy + labelR); + drawCentered(g, fm, "W", cx - labelR, cy); + + g.dispose(); + ImageIO.write(img, "PNG", path.toFile()); + } catch (Exception e) { + log.warn("[Compass] compass_rose.png nicht erstellbar: {}", e.getMessage()); + } + } + + /** Erzeugt Platzhalter-Nadel: gelbes Dreieck, Spitze oben. */ + private static void ensureNeedle(Path path) { + if (Files.exists(path)) { return; } + try { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(new Color(255, 220, 60, 242)); + // Spitze bei y=8, Basis bei y=21; kleines y in BufferedImage = oben auf dem Bildschirm + int[] xp = {50, 43, 57}; + int[] yp = { 8, 21, 21}; + g.fillPolygon(xp, yp, 3); + g.dispose(); + ImageIO.write(img, "PNG", path.toFile()); + } catch (Exception e) { + log.warn("[Compass] compass_needle.png nicht erstellbar: {}", e.getMessage()); + } + } + + private static void drawCentered(Graphics2D g, FontMetrics fm, String text, int cx, int cy) { + int x = cx - fm.stringWidth(text) / 2; + int y = cy + (fm.getAscent() - fm.getDescent()) / 2; + g.drawString(text, x, y); } } 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 7008d4a..2be3720 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 @@ -4,6 +4,8 @@ import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; +import com.jme3.bullet.BulletAppState; +import com.jme3.bullet.collision.PhysicsRayTestResult; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.*; @@ -201,6 +203,8 @@ public class GrassState extends BaseAppState List tufts = chunkTufts[idx]; if (tufts.isEmpty()) return; + BulletAppState bullet = getApplication().getStateManager().getState(BulletAppState.class); + Map> bySlot = new LinkedHashMap<>(); for (GrassTuft t : tufts) { long seed = (long) Float.floatToRawIntBits(t.x()) * 0x9E3779B9L @@ -210,7 +214,7 @@ public class GrassState extends BaseAppState for (int b = 0; b < BLADES_PER_TUFT; b++) { float bx = t.x() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f; float bz = t.z() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f; - float th = terrainChunkState.getHeightAt(bx, bz); + float th = surfaceHeight(bullet, bx, bz); if (Float.isNaN(th)) continue; float h = t.height() * (0.7f + rng.nextFloat() * 0.6f); blades.add(new float[]{bx, th, bz, h}); @@ -259,12 +263,36 @@ public class GrassState extends BaseAppState chunkNodes[ci].setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); } + // ── Oberflächen-Höhe via Physik-Raycast ────────────────────────────────── + + private float surfaceHeight(BulletAppState bullet, float wx, float wz) { + float base = terrainChunkState.getHeightAt(wx, wz); + if (bullet == null || bullet.getPhysicsSpace() == null) { + return base; + } + float refY = Float.isNaN(base) ? 0f : base; + Vector3f from = new Vector3f(wx, refY + 200f, wz); + Vector3f to = new Vector3f(wx, refY - 5f, wz); + List hits = bullet.getPhysicsSpace().rayTest(from, to); + if (hits.isEmpty()) { + return base; + } + float bestFrac = Float.MAX_VALUE; + for (PhysicsRayTestResult hit : hits) { + if (hit.getHitFraction() < bestFrac) { + bestFrac = hit.getHitFraction(); + } + } + return from.y + (to.y - from.y) * bestFrac; + } + // ── Mesh: Kreuz-Quad mit UV ─────────────────────────────────────────────── private static Mesh buildGrassMesh(List blades) { int n = blades.size(); FloatBuffer pos = BufferUtils.createFloatBuffer(n * 8 * 3); FloatBuffer uv = BufferUtils.createFloatBuffer(n * 8 * 2); + FloatBuffer nrm = BufferUtils.createFloatBuffer(n * 8 * 3); IntBuffer idx = BufferUtils.createIntBuffer(n * 12); int vi = 0; @@ -272,15 +300,15 @@ public class GrassState extends BaseAppState float x = blade[0], y = blade[1], z = blade[2], h = blade[3]; float w = Math.max(0.05f, h * BLADE_WIDTH); - 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); + pos.put(x-w).put(y ).put(z); uv.put(0).put(0); nrm.put(0).put(1).put(0); + pos.put(x+w).put(y ).put(z); uv.put(1).put(0); nrm.put(0).put(1).put(0); + pos.put(x+w).put(y+h).put(z); uv.put(1).put(1); nrm.put(0).put(1).put(0); + pos.put(x-w).put(y+h).put(z); uv.put(0).put(1); nrm.put(0).put(1).put(0); - 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); + pos.put(x).put(y ).put(z-w); uv.put(0).put(0); nrm.put(0).put(1).put(0); + pos.put(x).put(y ).put(z+w); uv.put(1).put(0); nrm.put(0).put(1).put(0); + pos.put(x).put(y+h).put(z+w); uv.put(1).put(1); nrm.put(0).put(1).put(0); + pos.put(x).put(y+h).put(z-w); uv.put(0).put(1); nrm.put(0).put(1).put(0); idx.put(vi ).put(vi+1).put(vi+2); idx.put(vi ).put(vi+2).put(vi+3); @@ -292,6 +320,7 @@ public class GrassState 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, nrm); mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); mesh.updateBound(); return mesh; diff --git a/blight-game/src/main/java/de/blight/game/state/HotbarState.java b/blight-game/src/main/java/de/blight/game/state/HotbarState.java index ec810db..fe64d65 100644 --- a/blight-game/src/main/java/de/blight/game/state/HotbarState.java +++ b/blight-game/src/main/java/de/blight/game/state/HotbarState.java @@ -21,6 +21,8 @@ import com.jme3.scene.shape.Quad; import com.jme3.texture.Texture; import de.blight.common.model.Item; import de.blight.common.model.MainCharacter; +import de.blight.game.config.MenuScreen; +import de.blight.game.config.OverlayState; /** * Immer sichtbare Schnellzugriffsleiste (Hotbar) mit 10 Slots (Tasten 1–9, 0). @@ -60,8 +62,9 @@ public class HotbarState extends BaseAppState { private Node hotbarNode; private float originX, originY; - final Item[] slots = new Item[SLOT_COUNT]; - private int activeSlot = 0; + final Item[] slots = new Item[SLOT_COUNT]; + private int activeSlot = 0; + private boolean lastMenuOpen = false; private final Geometry[] borderGeoms = new Geometry[SLOT_COUNT]; private final Node[] thumbNodes = new Node[SLOT_COUNT]; @@ -98,6 +101,17 @@ public class HotbarState extends BaseAppState { @Override protected void cleanup(Application application) {} + @Override + public void update(float tpf) { + boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen(); + if (menuOpen != lastMenuOpen) { + lastMenuOpen = menuOpen; + if (hotbarNode != null) { + hotbarNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit); + } + } + } + // ── Aufbau ─────────────────────────────────────────────────────────────── private void buildHotbar() { float sw = app.getCamera().getWidth(); diff --git a/blight-game/src/main/java/de/blight/game/state/HudState.java b/blight-game/src/main/java/de/blight/game/state/HudState.java index 08077c0..db7fce7 100644 --- a/blight-game/src/main/java/de/blight/game/state/HudState.java +++ b/blight-game/src/main/java/de/blight/game/state/HudState.java @@ -17,6 +17,8 @@ import com.jme3.ui.Picture; import com.jme3.util.BufferUtils; import de.blight.common.model.MainCharacter; import de.blight.game.animation.AnimationLibrary; +import de.blight.game.config.MenuScreen; +import de.blight.game.config.OverlayState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,7 +64,8 @@ public class HudState extends BaseAppState { private SimpleApplication app; private Node hudNode; private final Geometry[] fills = new Geometry[3]; - private final float[] lastRatios = { -1f, -1f, -1f }; + private final float[] lastRatios = { -1f, -1f, -1f }; + private boolean lastMenuOpen = false; public HudState(MainCharacter mc) { this.mc = mc; @@ -96,6 +99,13 @@ public class HudState extends BaseAppState { @Override public void update(float tpf) { + boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen(); + if (menuOpen != lastMenuOpen) { + lastMenuOpen = menuOpen; + if (hudNode != null) { + hudNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit); + } + } if (mc == null) return; int maxHp = mc.getMaxHp(); int maxSt = mc.getMaxStamina(); diff --git a/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java b/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java index 92c3212..0e089c6 100644 --- a/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java +++ b/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java @@ -74,6 +74,8 @@ public class SculptedMeshState extends BaseAppState { ColorRGBA ac = dns.getCustomAmbient(); material.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b)); material.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b)); + RainState rs = app.getStateManager().getState(RainState.class); + if (rs != null) material.setFloat("Wetness", rs.getWetness()); } // ── Laden ──────────────────────────────────────────────────────────────── diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index 07fba3f..090b31d 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 7f99945..12f30bf 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