diff --git a/Common/MatDefs/Light/PBRLighting.j3md b/blight-assets/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md similarity index 100% rename from Common/MatDefs/Light/PBRLighting.j3md rename to blight-assets/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md diff --git a/blight-assets/src/main/resources/MatDefs/CloudDome.j3md b/blight-assets/src/main/resources/MatDefs/CloudDome.j3md new file mode 100644 index 0000000..fcdfceb --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/CloudDome.j3md @@ -0,0 +1,23 @@ +MaterialDef CloudDome { + + MaterialParameters { + Vector2 CloudOffset : 0.0 0.0 + Float CloudCover : 0.0 + Color CloudColor : 1.0 1.0 1.0 1.0 + } + + Technique { + VertexShader GLSL150 : Shaders/skies/clouds/CloudDome.vert + FragmentShader GLSL150 : Shaders/skies/clouds/CloudDome.frag + + WorldParameters { + WorldViewProjectionMatrix + } + + RenderState { + Blend Alpha + DepthWrite Off + FaceCull Front + } + } +} diff --git a/blight-assets/src/main/resources/MatDefs/Fern.j3md b/blight-assets/src/main/resources/MatDefs/Fern.j3md index d75dba2..1d252c8 100644 --- a/blight-assets/src/main/resources/MatDefs/Fern.j3md +++ b/blight-assets/src/main/resources/MatDefs/Fern.j3md @@ -5,6 +5,7 @@ MaterialDef Fern { Float WindStrength : 0.15 Float WindSpeed : 0.6 Vector2 WindDir : 0.0 1.0 + Float Wetness : 0.0 Texture2D DiffuseMap Boolean HasDiffuseMap : false Texture2D NormalMap -LINEAR diff --git a/blight-assets/src/main/resources/MatDefs/Grass.j3md b/blight-assets/src/main/resources/MatDefs/Grass.j3md index 24089e6..329c52d 100644 --- a/blight-assets/src/main/resources/MatDefs/Grass.j3md +++ b/blight-assets/src/main/resources/MatDefs/Grass.j3md @@ -4,10 +4,12 @@ MaterialDef Grass { Color Color (Color) : 1.0 1.0 1.0 1.0 Texture2D ColorMap Texture2D NormalMap - Float WindSpeed : 0.5 - Float WindStrength : 0.12 + Float WindSpeed : 0.5 + Float WindStrength : 0.12 + Vector2 WindDir : 0.0 1.0 Vector3 SunDir : 0.55 0.80 0.35 Color SunColor : 1.0 1.0 0.95 1.0 + Float Wetness : 0.0 } Technique { diff --git a/blight-assets/src/main/resources/MatDefs/GrassSeed.j3md b/blight-assets/src/main/resources/MatDefs/GrassSeed.j3md index cccb56e..67ee8e6 100644 --- a/blight-assets/src/main/resources/MatDefs/GrassSeed.j3md +++ b/blight-assets/src/main/resources/MatDefs/GrassSeed.j3md @@ -7,6 +7,7 @@ MaterialDef GrassSeed { Vector2 WindDir : 0.0 1.0 Vector3 SunDir : 0.35 0.8 0.45 Color SunColor : 0.95 0.90 0.75 1.0 + Float Wetness : 0.0 } Technique { diff --git a/blight-assets/src/main/resources/MatDefs/GrassVertex.j3md b/blight-assets/src/main/resources/MatDefs/GrassVertex.j3md index 9ccfe77..da07c5d 100644 --- a/blight-assets/src/main/resources/MatDefs/GrassVertex.j3md +++ b/blight-assets/src/main/resources/MatDefs/GrassVertex.j3md @@ -6,6 +6,7 @@ MaterialDef GrassVertex { Vector2 WindDir : 0.0 1.0 Vector3 SunDir : 0.35 0.8 0.45 Color SunColor : 0.95 0.90 0.75 1.0 + Float Wetness : 0.0 } Technique { diff --git a/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md b/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md index 2e5d0b0..7ac00dd 100644 --- a/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md +++ b/blight-assets/src/main/resources/MatDefs/TerrainArray.j3md @@ -8,6 +8,7 @@ MaterialDef TerrainArray { Vector3 LightDir : 0.6 -0.8 0.4 Vector3 SunColor : 0.68 0.65 0.60 Vector3 AmbientColor : 0.08 0.10 0.16 + Float Wetness : 0.0 Boolean DebugNoLight Boolean DebugSlot0Only Texture2D DebugDirectTex @@ -24,6 +25,7 @@ MaterialDef TerrainArray { WorldViewProjectionMatrix WorldMatrix ViewProjectionMatrix + CameraPosition } Defines { diff --git a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md index 3571ac4..13ac48e 100644 --- a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md +++ b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md @@ -5,6 +5,7 @@ MaterialDef TreeLeaf { Float WindStrength : 0.30 Float WindSpeed : 0.7 Vector2 WindDir : 0.0 1.0 + Float Wetness : 0.0 Texture2D LeafMap Boolean HasLeafMap : false diff --git a/blight-assets/src/main/resources/Shaders/Fern.frag b/blight-assets/src/main/resources/Shaders/Fern.frag index 3893463..73ca928 100644 --- a/blight-assets/src/main/resources/Shaders/Fern.frag +++ b/blight-assets/src/main/resources/Shaders/Fern.frag @@ -5,6 +5,7 @@ uniform sampler2D m_DiffuseMap; uniform bool m_HasDiffuseMap; uniform sampler2D m_NormalMap; uniform bool m_HasNormalMap; +uniform float m_Wetness; in vec2 texCoord; in vec3 vNormal; @@ -38,5 +39,7 @@ void main() { diff = max(diff, max(0.0, dot(-N, sun)) * 0.5); float lit = 0.45 + diff * 0.55; - gl_FragColor = vec4(baseColor * lit, 1.0); + baseColor *= (1.0 - 0.18 * m_Wetness); + float shine = pow(diff, 12.0) * m_Wetness * 0.25; + gl_FragColor = vec4(baseColor * lit + vec3(shine), 1.0); } diff --git a/blight-assets/src/main/resources/Shaders/Grass.frag b/blight-assets/src/main/resources/Shaders/Grass.frag index 3635fd5..ab2cdd8 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.frag +++ b/blight-assets/src/main/resources/Shaders/Grass.frag @@ -1,7 +1,8 @@ -uniform vec4 m_Color; -uniform vec3 m_SunDir; -uniform vec4 m_SunColor; -uniform vec4 g_AmbientLightColor; +uniform vec4 m_Color; +uniform vec3 m_SunDir; +uniform vec4 m_SunColor; +uniform vec4 g_AmbientLightColor; +uniform float m_Wetness; #ifdef HAS_COLORMAP uniform sampler2D m_ColorMap; @@ -35,7 +36,8 @@ void main() { // 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 *= (1.0 - 0.18 * m_Wetness); color.rgb *= lit; - - outFragColor = color; + float shine = pow(diffuse, 12.0) * m_Wetness * 0.25; + outFragColor = vec4(color.rgb + vec3(shine), color.a); } diff --git a/blight-assets/src/main/resources/Shaders/Grass.vert b/blight-assets/src/main/resources/Shaders/Grass.vert index 5179ebb..c3a654b 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.vert +++ b/blight-assets/src/main/resources/Shaders/Grass.vert @@ -4,6 +4,7 @@ uniform float g_Time; uniform float m_WindSpeed; uniform float m_WindStrength; +uniform vec2 m_WindDir; in vec3 inPosition; in vec2 inTexCoord; @@ -26,17 +27,20 @@ void main() { vec2 worldXZ = (g_WorldMatrix * pos).xz; float t = g_Time * m_WindSpeed; + vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0); + float wavePhase = dot(worldXZ, windN); + // Zwei überlagerte Sinuswellen für organische Bewegung - float sway = sin(t * 2.1 + worldXZ.x * 0.08 + worldXZ.y * 0.06) * 0.6 - + sin(t * 1.4 - worldXZ.x * 0.05 + worldXZ.y * 0.09) * 0.4; + float sway = sin(t * 2.1 + wavePhase * 0.08) * 0.6 + + sin(t * 1.4 + wavePhase * 0.06) * 0.4; // Mindest-Stärke damit bei wenig Wind noch Bewegung sichtbar ist float effectiveStrength = max(m_WindStrength, 0.05); // Quadratische Gewichtung: Spitze bewegt sich mehr als Basis float bend = sway * effectiveStrength * inTexCoord.y * inTexCoord.y; - pos.x += bend; - pos.z += bend * 0.3; + pos.x += windN.x * bend; + pos.z += windN.y * bend; // Y-Kompression: Halm neigt sich statt zu strecken → verhindert visuelles Breiterwerden pos.y -= bend * bend * 0.5; } diff --git a/blight-assets/src/main/resources/Shaders/GrassSeed.frag b/blight-assets/src/main/resources/Shaders/GrassSeed.frag index abdead2..bf806e2 100644 --- a/blight-assets/src/main/resources/Shaders/GrassSeed.frag +++ b/blight-assets/src/main/resources/Shaders/GrassSeed.frag @@ -2,6 +2,7 @@ uniform sampler2D m_ColorMap; uniform vec4 g_AmbientLightColor; uniform vec3 m_SunDir; uniform vec4 m_SunColor; +uniform float m_Wetness; in vec2 varUV; @@ -14,6 +15,7 @@ void main() { // Wrapped diffuse – kein Normalvektor nötig für Billboard-Quads float light = 0.5 + 0.5 * max(dot(m_SunDir, vec3(0.0, 1.0, 0.0)), 0.0); vec3 ambient = g_AmbientLightColor.rgb * c.rgb; - vec3 diffuse = m_SunColor.rgb * c.rgb * light; - outFragColor = vec4(min(ambient + diffuse, c.rgb * 1.5), c.a); + vec3 diffuse = m_SunColor.rgb * c.rgb * (1.0 - 0.18 * m_Wetness) * light; + float shine = pow(light, 12.0) * m_Wetness * 0.25; + outFragColor = vec4(min(ambient + diffuse, c.rgb * 1.5) + vec3(shine), c.a); } diff --git a/blight-assets/src/main/resources/Shaders/GrassVertex.frag b/blight-assets/src/main/resources/Shaders/GrassVertex.frag index e69b280..33b583d 100644 --- a/blight-assets/src/main/resources/Shaders/GrassVertex.frag +++ b/blight-assets/src/main/resources/Shaders/GrassVertex.frag @@ -1,7 +1,8 @@ -uniform vec4 g_AmbientLightColor; +uniform vec4 g_AmbientLightColor; -uniform vec3 m_SunDir; // Richtung von der Fläche zur Sonne (world-space, normiert) -uniform vec4 m_SunColor; // Sonnenfarbe RGB +uniform vec3 m_SunDir; // Richtung von der Fläche zur Sonne (world-space, normiert) +uniform vec4 m_SunColor; // Sonnenfarbe RGB +uniform float m_Wetness; in vec4 varColor; in vec3 varNormal; @@ -23,12 +24,13 @@ void main() { float light = nDotL + sss; - vec3 ambient = g_AmbientLightColor.rgb * varColor.rgb; - vec3 diffuse = m_SunColor.rgb * varColor.rgb * light; + vec3 col = varColor.rgb * (1.0 - 0.18 * m_Wetness); + vec3 ambient = g_AmbientLightColor.rgb * col; + vec3 diffuse = m_SunColor.rgb * col * light; // Farbe nicht über die Vertex-Color-Helligkeit hinaus aufhellen vec3 result = ambient + diffuse; - result = min(result, varColor.rgb * 1.5); - - outFragColor = vec4(result, 1.0); + result = min(result, col * 1.5); + float shine = pow(max(nDotL, 0.0), 12.0) * m_Wetness * 0.25; + outFragColor = vec4(result + vec3(shine), 1.0); } diff --git a/blight-assets/src/main/resources/Shaders/TerrainArray.frag b/blight-assets/src/main/resources/Shaders/TerrainArray.frag index e6208fb..96f5f21 100644 --- a/blight-assets/src/main/resources/Shaders/TerrainArray.frag +++ b/blight-assets/src/main/resources/Shaders/TerrainArray.frag @@ -16,9 +16,11 @@ uniform sampler2D m_AlphaMap_2; uniform sampler2DArray m_NormalArray; #endif -uniform vec3 m_LightDir; -uniform vec3 m_SunColor; -uniform vec3 m_AmbientColor; +uniform vec3 m_LightDir; +uniform vec3 m_SunColor; +uniform vec3 m_AmbientColor; +uniform float m_Wetness; +uniform vec3 g_CameraPosition; in vec2 vSplatUV; in vec3 vWorldPos; @@ -106,14 +108,20 @@ void main() { N = normalize(pertN); #endif - vec3 lightDir = normalize(m_LightDir); - float diff = max(dot(N, lightDir), 0.0); - vec3 light = m_AmbientColor + m_SunColor * diff; + 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 + col.rgb *= (1.0 - 0.20 * m_Wetness); + vec3 viewDir = normalize(g_CameraPosition - vWorldPos); + vec3 H = normalize(lightDir + viewDir); + float spec = pow(max(dot(N, H), 0.0), 80.0) * m_Wetness; 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/Tree.frag b/blight-assets/src/main/resources/Shaders/Tree.frag index 22801cc..0ef075d 100644 --- a/blight-assets/src/main/resources/Shaders/Tree.frag +++ b/blight-assets/src/main/resources/Shaders/Tree.frag @@ -6,6 +6,7 @@ uniform bool m_HasBarkMap; uniform vec3 m_LightDir; uniform vec3 m_SunColor; uniform vec3 m_AmbientColor; +uniform float m_Wetness; in vec2 texCoord; in vec3 worldNormal; @@ -20,5 +21,7 @@ void main() { float diff = max(dot(n, normalize(m_LightDir)), 0.0); vec3 light = clamp(m_AmbientColor + m_SunColor * diff, 0.0, 1.0); - gl_FragColor = vec4(baseColor * light, m_Diffuse.a); + baseColor *= (1.0 - 0.18 * m_Wetness); + float shine = pow(diff, 12.0) * m_Wetness * 0.30; + gl_FragColor = vec4(baseColor * light + vec3(shine), m_Diffuse.a); } diff --git a/blight-assets/src/main/resources/Shaders/TreeLeaf.frag b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag index 5782037..52e7c90 100644 --- a/blight-assets/src/main/resources/Shaders/TreeLeaf.frag +++ b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag @@ -6,6 +6,7 @@ uniform bool m_HasLeafMap; uniform vec3 m_LightDir; uniform vec3 m_SunColor; uniform vec3 m_AmbientColor; +uniform float m_Wetness; in vec2 texCoord; in vec3 worldNormal; @@ -27,5 +28,7 @@ void main() { // 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); + baseColor *= (1.0 - 0.18 * m_Wetness); + float shine = pow(diff, 12.0) * m_Wetness * 0.25; + gl_FragColor = vec4(baseColor * light + vec3(shine), 1.0); } diff --git a/blight-assets/src/main/resources/Shaders/skies/clouds/CloudDome.frag b/blight-assets/src/main/resources/Shaders/skies/clouds/CloudDome.frag new file mode 100644 index 0000000..d9f67eb --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/skies/clouds/CloudDome.frag @@ -0,0 +1,53 @@ +uniform vec2 m_CloudOffset; +uniform float m_CloudCover; +uniform vec4 m_CloudColor; + +in vec3 vDir; +out vec4 outColor; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); +} + +float vnoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y); +} + +float fbm(vec2 p) { + float v = 0.0; + float a = 0.5; + for (int i = 0; i < 6; i++) { + v += a * vnoise(p); + p = p * 2.17 + vec2(0.631, 1.137); + a *= 0.5; + } + return v; +} + +void main() { + vec3 dir = normalize(vDir); + + if (dir.y < -0.02) discard; + + // Horizon fade — clouds become transparent near the horizon + float hFade = smoothstep(0.0, 0.15, dir.y); + + // Stereographic projection: maps sphere direction to 2D without pole distortion + vec2 uv = dir.xz / (dir.y + 1.001); + uv = uv * 3.5 + m_CloudOffset; + + float f = fbm(uv); + + // m_CloudCover 0=clear (threshold high → few clouds), 1=overcast (threshold low → all clouds) + float threshold = mix(0.72, 0.22, m_CloudCover); + float softness = 0.09; + float cloud = smoothstep(threshold - softness, threshold + softness, f); + + cloud *= hFade; + + outColor = vec4(m_CloudColor.rgb, cloud * m_CloudColor.a); +} diff --git a/blight-assets/src/main/resources/Shaders/skies/clouds/CloudDome.vert b/blight-assets/src/main/resources/Shaders/skies/clouds/CloudDome.vert new file mode 100644 index 0000000..47f7864 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/skies/clouds/CloudDome.vert @@ -0,0 +1,9 @@ +uniform mat4 g_WorldViewProjectionMatrix; + +in vec3 inPosition; +out vec3 vDir; + +void main() { + vDir = inPosition; + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); +} diff --git a/blight-assets/src/main/resources/Shaders/skies/dome66/dome66.frag b/blight-assets/src/main/resources/Shaders/skies/dome66/dome66.frag new file mode 100644 index 0000000..f2a4b0a --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/skies/dome66/dome66.frag @@ -0,0 +1,238 @@ +/* + Copyright (c) 2014-2022, Stephen Gold + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * fragment shader used by dome66.j3md + */ +#import "Common/ShaderLib/GLSLCompat.glsllib" +uniform vec4 m_ClearColor; +varying vec2 skyTexCoord; + +#ifdef HAS_STARS + uniform sampler2D m_StarsColorMap; +#endif + +#ifdef HAS_OBJECT0 + uniform vec4 m_Object0Color; + uniform sampler2D m_Object0ColorMap; + varying vec2 object0Coord; +#endif + +#ifdef HAS_OBJECT1 + uniform vec4 m_Object1Color; + uniform sampler2D m_Object1ColorMap; + varying vec2 object1Coord; +#endif + +#ifdef HAS_OBJECT2 + uniform vec4 m_Object2Color; + uniform sampler2D m_Object2ColorMap; + varying vec2 object2Coord; +#endif + +#ifdef HAS_OBJECT3 + uniform vec4 m_Object3Color; + uniform sampler2D m_Object3ColorMap; + varying vec2 object3Coord; +#endif + +#ifdef HAS_OBJECT4 + uniform vec4 m_Object4Color; + uniform sampler2D m_Object4ColorMap; + varying vec2 object4Coord; +#endif + +#ifdef HAS_OBJECT5 + uniform vec4 m_Object5Color; + uniform sampler2D m_Object5ColorMap; + varying vec2 object5Coord; +#endif + +#ifdef HAS_HAZE + uniform sampler2D m_HazeAlphaMap; + uniform vec4 m_HazeColor; +#endif + +#ifdef HAS_CLOUDS0 + uniform sampler2D m_Clouds0AlphaMap; + uniform vec4 m_Clouds0Color; + varying vec2 clouds0Coord; +#endif + +#ifdef HAS_CLOUDS1 + uniform sampler2D m_Clouds1AlphaMap; + uniform vec4 m_Clouds1Color; + varying vec2 clouds1Coord; +#endif + +#ifdef HAS_CLOUDS2 + uniform sampler2D m_Clouds2AlphaMap; + uniform vec4 m_Clouds2Color; + varying vec2 clouds2Coord; +#endif + +#ifdef HAS_CLOUDS3 + uniform sampler2D m_Clouds3AlphaMap; + uniform vec4 m_Clouds3Color; + varying vec2 clouds3Coord; +#endif + +#ifdef HAS_CLOUDS4 + uniform sampler2D m_Clouds4AlphaMap; + uniform vec4 m_Clouds4Color; + varying vec2 clouds4Coord; +#endif + +#ifdef HAS_CLOUDS5 + uniform sampler2D m_Clouds5AlphaMap; + uniform vec4 m_Clouds5Color; + varying vec2 clouds5Coord; +#endif + +vec4 mixColors(vec4 color0, vec4 color1) { + vec4 result; + float a0 = color0.a * (1.0 - color1.a); + result.a = a0 + color1.a; + if (result.a > 0.0) { + result.rgb = (a0 * color0.rgb + color1.a * color1.rgb)/result.a; + } else { + result.rgb = vec3(0.0); + } + return result; +} + +void main() { + #ifdef HAS_STARS + vec4 stars = texture2D(m_StarsColorMap, skyTexCoord); + #else + vec4 stars = vec4(0.0); + #endif + + vec4 objects = vec4(0.0); + + #ifdef HAS_OBJECT0 + if (floor(object0Coord.s) == 0.0 && + floor(object0Coord.t) == 0.0) { + objects = m_Object0Color; + objects *= texture2D(m_Object0ColorMap, object0Coord); + } + #endif + + #ifdef HAS_OBJECT1 + if (floor(object1Coord.s) == 0.0 && + floor(object1Coord.t) == 0.0) { + vec4 object1 = m_Object1Color; + object1 *= texture2D(m_Object1ColorMap, object1Coord); + objects = mixColors(objects, object1); + } + #endif + + #ifdef HAS_OBJECT2 + if (floor(object2Coord.s) == 0.0 && + floor(object2Coord.t) == 0.0) { + vec4 object2 = m_Object2Color; + object2 *= texture2D(m_Object2ColorMap, object2Coord); + objects = mixColors(objects, object2); + } + #endif + + #ifdef HAS_OBJECT3 + if (floor(object3Coord.s) == 0.0 && + floor(object3Coord.t) == 0.0) { + vec4 object3 = m_Object3Color; + object3 *= texture2D(m_Object3ColorMap, object3Coord); + objects = mixColors(objects, object3); + } + #endif + + #ifdef HAS_OBJECT4 + if (floor(object4Coord.s) == 0.0 && + floor(object4Coord.t) == 0.0) { + vec4 object4 = m_Object4Color; + object4 *= texture2D(m_Object4ColorMap, object4Coord); + objects = mixColors(objects, object4); + } + #endif + + #ifdef HAS_OBJECT5 + if (floor(object5Coord.s) == 0.0 && + floor(object5Coord.t) == 0.0) { + vec4 object5 = m_Object5Color; + object5 *= texture2D(m_Object5ColorMap, object5Coord); + objects = mixColors(objects, object5); + } + #endif + + vec4 color = mixColors(stars, objects); + + vec4 clear = m_ClearColor; + #ifdef HAS_HAZE + vec4 haze = m_HazeColor; + haze.a *= texture2D(m_HazeAlphaMap, skyTexCoord).r; + clear = mixColors(clear, haze); + #endif + color = mixColors(color, clear); + // Bright parts of objects shine through the clear areas. + color.rgb += objects.rgb * objects.a * (1.0 - clear.rgb) * clear.a; + + #ifdef HAS_CLOUDS0 + vec4 clouds0 = m_Clouds0Color; + clouds0.a *= texture2D(m_Clouds0AlphaMap, clouds0Coord).r; + color = mixColors(color, clouds0); + #endif + + #ifdef HAS_CLOUDS1 + vec4 clouds1 = m_Clouds1Color; + clouds1.a *= texture2D(m_Clouds1AlphaMap, clouds1Coord).r; + color = mixColors(color, clouds1); + #endif + + #ifdef HAS_CLOUDS2 + vec4 clouds2 = m_Clouds2Color; + clouds2.a *= texture2D(m_Clouds2AlphaMap, clouds2Coord).r; + color = mixColors(color, clouds2); + #endif + + #ifdef HAS_CLOUDS3 + vec4 clouds3 = m_Clouds3Color; + clouds3.a *= texture2D(m_Clouds3AlphaMap, clouds3Coord).r; + color = mixColors(color, clouds3); + #endif + + #ifdef HAS_CLOUDS4 + vec4 clouds4 = m_Clouds4Color; + clouds4.a *= texture2D(m_Clouds4AlphaMap, clouds4Coord).r; + color = mixColors(color, clouds4); + #endif + + #ifdef HAS_CLOUDS5 + vec4 clouds5 = m_Clouds5Color; + clouds5.a *= texture2D(m_Clouds5AlphaMap, clouds5Coord).r; + color = mixColors(color, clouds5); + #endif + + gl_FragColor = color; +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Textures/internal/cursor/cursor.png b/blight-assets/src/main/resources/Textures/internal/cursor/cursor.png new file mode 100644 index 0000000..c55ef7c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/cursor/cursor.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/cursor/cursor_pressed.png b/blight-assets/src/main/resources/Textures/internal/cursor/cursor_pressed.png new file mode 100644 index 0000000..eccceb3 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/cursor/cursor_pressed.png differ diff --git a/blight-assets/src/main/resources/Textures/skies/clouds/clear.png b/blight-assets/src/main/resources/Textures/skies/clouds/clear.png new file mode 100644 index 0000000..436d443 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/skies/clouds/clear.png differ diff --git a/blight-assets/src/main/resources/Textures/skies/clouds/overcast.png b/blight-assets/src/main/resources/Textures/skies/clouds/overcast.png new file mode 100644 index 0000000..f683305 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/skies/clouds/overcast.png differ diff --git a/blight-assets/src/main/resources/audio/ambient/weather/donner1.ogg b/blight-assets/src/main/resources/audio/ambient/weather/donner1.ogg new file mode 100644 index 0000000..4d68396 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/weather/donner1.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/weather/donner2.ogg b/blight-assets/src/main/resources/audio/ambient/weather/donner2.ogg new file mode 100644 index 0000000..66895a4 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/weather/donner2.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/weather/donner3.ogg b/blight-assets/src/main/resources/audio/ambient/weather/donner3.ogg new file mode 100644 index 0000000..7daaa9a Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/weather/donner3.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/weather/leichter_regen.ogg b/blight-assets/src/main/resources/audio/ambient/weather/leichter_regen.ogg new file mode 100644 index 0000000..c82e3b0 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/weather/leichter_regen.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/weather/starker_regen.ogg b/blight-assets/src/main/resources/audio/ambient/weather/starker_regen.ogg new file mode 100644 index 0000000..4525cd8 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/weather/starker_regen.ogg differ diff --git a/blight-common/src/main/java/de/blight/common/ChunkTerrainIO.java b/blight-common/src/main/java/de/blight/common/ChunkTerrainIO.java index 184c030..54560d8 100644 --- a/blight-common/src/main/java/de/blight/common/ChunkTerrainIO.java +++ b/blight-common/src/main/java/de/blight/common/ChunkTerrainIO.java @@ -38,7 +38,7 @@ public final class ChunkTerrainIO { public static final int LOD0_RANGE = 1; public static final int LOD1_RANGE = 3; /** Chebyshev-Distanz ≤ PHYSICS_RANGE: Physik-Collider aktiv. */ - public static final int PHYSICS_RANGE = 1; + public static final int PHYSICS_RANGE = 2; // ── Kanten-Konstanten ────────────────────────────────────────────────────── public static final int EDGE_NORTH = 0; // höchste Zeile (row = VERTS-1) 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 7d05f7f..d6b1a6d 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -72,6 +72,7 @@ public class EditorApp extends Application { private Label camCoordsLabel; private boolean launchGameAfterSave = false; private Button gamePlayBtn; // Spielen-Button im Seitenpanel + private CheckBox debugConsoleCB; // Debug-Konsole bei Spielstart anzeigen private VBox toolPanel; private BorderPane root; private Stage gameConsoleStage; @@ -621,16 +622,20 @@ public class EditorApp extends Application { drawCompass(); // 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++; + if (!consoleBuffer.isEmpty()) { + if (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); + } else { + consoleBuffer.clear(); } - gameConsoleArea.appendText(sb.toString()); - gameConsoleArea.setScrollTop(Double.MAX_VALUE); } if (input.soundAreaSelectionChanged) { @@ -7666,7 +7671,7 @@ public class EditorApp extends Application { setStatus(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet"); if (gamePlayBtn != null) gamePlayBtn.setText("🎮 Läuft…"); if (gameNewBtn != null) gameNewBtn.setText("🎮 Läuft…"); - openGameConsole(); + if (debugConsoleCB != null && debugConsoleCB.isSelected()) openGameConsole(); }); // Stdout des Spiels in Puffer schreiben — Flush erfolgt gebündelt im UI-Timer @@ -8501,7 +8506,11 @@ public class EditorApp extends Application { "Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz")); newGameBtn.setOnAction(e -> launchNewGame()); - inner.getChildren().addAll(playBtn, newGameBtn); + debugConsoleCB = new CheckBox("Debug-Konsole anzeigen"); + debugConsoleCB.setTooltip(new javafx.scene.control.Tooltip( + "Öffnet bei Spielstart die Ausgabekonsole mit der Spielausgabe")); + + inner.getChildren().addAll(playBtn, newGameBtn, debugConsoleCB); ScrollPane scroll = new ScrollPane(inner); scroll.setFitToWidth(true); diff --git a/blight-game/src/main/java/de/blight/game/BlightExceptionHandler.java b/blight-game/src/main/java/de/blight/game/BlightExceptionHandler.java new file mode 100644 index 0000000..ef3330c --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/BlightExceptionHandler.java @@ -0,0 +1,139 @@ +package de.blight.game; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Globaler Exception-Handler. + * + * Kritisch (JME-Render-Thread via BlightGame.handleError): + * GLFW-Cursor wird vor dem Dialog freigegeben. Blockierender Dialog → OK → System.exit(1). + * Das JME-Fenster bleibt als eingefrorenem Hintergrund sichtbar, bis der Prozess endet. + * + * Nicht kritisch (Hintergrundthreads via UncaughtExceptionHandler): + * Cursor wird über registrierte Callbacks temporär eingeblendet, nach OK wieder versteckt. + * Das Spiel läuft während des Dialogs weiter. + */ +public final class BlightExceptionHandler implements Thread.UncaughtExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(BlightExceptionHandler.class); + + /** Zeigt den Mauszeiger (auf dem JME-Thread via enqueue auszuführen). */ + private static Runnable showCursorCb; + /** Versteckt den Mauszeiger wieder (auf dem JME-Thread via enqueue auszuführen). */ + private static Runnable hideCursorCb; + + private BlightExceptionHandler() {} + + public static void install() { + Thread.setDefaultUncaughtExceptionHandler(new BlightExceptionHandler()); + } + + /** + * Cursor-Callbacks registrieren. Wird von BlightGame.simpleInitApp() aufgerufen, + * sobald GLFW und InputManager bereit sind. + */ + public static void registerCursorCallbacks(Runnable showCursor, Runnable hideCursor) { + showCursorCb = showCursor; + hideCursorCb = hideCursor; + } + + // ── Hintergrundthread-Fehler (nicht kritisch) ───────────────────────────── + + @Override + public void uncaughtException(Thread t, Throwable e) { + log.error("[Exception] Uncaught in '{}': {}", t.getName(), e.getMessage(), e); + // Cursor einblenden, bevor der Dialog auf dem EDT erscheint + if (showCursorCb != null) showCursorCb.run(); + EventQueue.invokeLater(() -> { + showDialog(t.getName(), e, false); + // Nach Schließen des Dialogs Cursor wieder einfangen + if (hideCursorCb != null) hideCursorCb.run(); + }); + } + + // ── JME-Render-Thread-Fehler (kritisch) ────────────────────────────────── + + /** + * Zeigt einen blockierenden Dialog und beendet die Anwendung nach OK. + * Muss erst NACH dem GLFW-Cursor-Release aufgerufen werden. + */ + public static void showCritical(String threadName, Throwable e) { + log.error("[Exception] Kritisch in '{}': {}", threadName, e.getMessage(), e); + try { + if (EventQueue.isDispatchThread()) { + showDialog(threadName, e, true); + } else { + EventQueue.invokeAndWait(() -> showDialog(threadName, e, true)); + } + } catch (Exception ex) { + log.error("[Exception] Fehlerdialog konnte nicht angezeigt werden", ex); + System.exit(1); + } + } + + // ── Dialog ──────────────────────────────────────────────────────────────── + + private static void showDialog(String threadName, Throwable e, boolean critical) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + + JDialog dialog = new JDialog((Frame) null, + critical ? "Kritischer Fehler" : "Fehler", true); + dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + dialog.setLayout(new BorderLayout(8, 8)); + + JLabel titleLbl = new JLabel(critical + ? "Kritischer Fehler – Anwendung wird beendet" + : "Fehler in Hintergrundthread – Spiel läuft weiter"); + titleLbl.setFont(titleLbl.getFont().deriveFont(Font.BOLD, 14f)); + titleLbl.setForeground(critical ? new Color(0xcc2200) : new Color(0x884400)); + titleLbl.setBorder(BorderFactory.createEmptyBorder(10, 14, 4, 14)); + + String msg = e.getClass().getName() + + (e.getMessage() != null ? ": " + e.getMessage() : "") + + " [Thread: " + threadName + "]"; + JLabel infoLbl = new JLabel(msg); + infoLbl.setFont(infoLbl.getFont().deriveFont(Font.PLAIN, 12f)); + infoLbl.setBorder(BorderFactory.createEmptyBorder(0, 14, 10, 14)); + + JPanel north = new JPanel(new GridLayout(2, 1)); + north.add(titleLbl); + north.add(infoLbl); + + JTextArea ta = new JTextArea(sw.toString(), 22, 90); + ta.setEditable(false); + ta.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); + JScrollPane scroll = new JScrollPane(ta); + scroll.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8)); + + JButton btn = new JButton(critical ? "Anwendung beenden" : "OK"); + btn.setFont(btn.getFont().deriveFont(Font.BOLD, 13f)); + JPanel south = new JPanel(new FlowLayout(FlowLayout.CENTER)); + south.add(btn); + + dialog.add(north, BorderLayout.NORTH); + dialog.add(scroll, BorderLayout.CENTER); + dialog.add(south, BorderLayout.SOUTH); + dialog.setSize(920, 560); + dialog.setLocationRelativeTo(null); + + Runnable close = () -> { + dialog.dispose(); + if (critical) System.exit(1); + }; + btn.addActionListener(ev -> close.run()); + dialog.addWindowListener(new WindowAdapter() { + @Override public void windowClosing(WindowEvent ev) { close.run(); } + }); + + dialog.setVisible(true); // blockiert (modal) bis der Dialog geschlossen wird + } +} 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 c407b38..b41af2f 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -77,6 +77,7 @@ public class BlightGame extends SimpleApplication { public static void main(String[] args) { SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); + BlightExceptionHandler.install(); BlightGame app = new BlightGame(); app.splashWindow = showSplash(); @@ -203,6 +204,17 @@ public class BlightGame extends SimpleApplication { startTimeMs = System.currentTimeMillis(); initialFixDone = false; + + // Cursor-Callbacks für nicht-kritische Exception-Dialoge aus Hintergrundthreads + BlightExceptionHandler.registerCursorCallbacks( + () -> enqueue(() -> { + if (context instanceof LwjglWindow) { + long win = ((LwjglWindow) context).getWindowHandle(); + if (win != 0L) GLFW.glfwSetInputMode(win, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + } + }), + () -> enqueue(() -> inputManager.setCursorVisible(false)) + ); if (graphicsSettings.fullscreen) { log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={} gewünschte Aufl.={}×{}", cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen, @@ -375,6 +387,8 @@ public class BlightGame extends SimpleApplication { worldScene.setPaused(true); }, "ToggleMenu"); + stateManager.attach(new de.blight.game.state.CursorState()); + // ── Startentscheidung: Hauptmenü oder direkt ins Spiel (Editor-Start) ─ boolean autostart = Boolean.getBoolean("blight.autostart"); if (autostart) { @@ -384,6 +398,24 @@ public class BlightGame extends SimpleApplication { } } + @Override + public void handleError(String errMsg, Throwable t) { + // GLFW-Cursor freigeben, damit der Dialog bedienbar ist. + // Das JME-Fenster bleibt als eingefrorener Hintergrund sichtbar bis System.exit(). + try { + if (context instanceof LwjglWindow) { + long win = ((LwjglWindow) context).getWindowHandle(); + if (win != 0L) { + GLFW.glfwSetInputMode(win, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + } + } + } catch (Exception ignored) {} + + Throwable ex = t != null ? t : new RuntimeException(errMsg); + BlightExceptionHandler.showCritical(Thread.currentThread().getName(), ex); + stop(); // Fallback: wird nur erreicht wenn der Dialog nicht angezeigt werden konnte + } + // ── Start-Flows ────────────────────────────────────────────────────────── private void showMainMenu() { @@ -568,14 +600,44 @@ public class BlightGame extends SimpleApplication { if (ws == null) return "Wettersystem nicht aktiv"; if (args.length < 2) return "Aktuell: " + ws.getActiveWeather() - + " | Syntax: weather "; + + " | Syntax: weather current | weather next | weather <" + + java.util.Arrays.stream(de.blight.game.state.WeatherState.Weather.values()) + .map(w -> w.name().toLowerCase()) + .collect(java.util.stream.Collectors.joining("|")) + + ">"; + if ("current".equalsIgnoreCase(args[1])) { + float windDeg = (float) Math.toDegrees(ws.getWindAngle()); + if (windDeg < 0) windDeg += 360f; + String[] compass = {"N","NO","O","SO","S","SW","W","NW"}; + String dir = compass[Math.round(windDeg / 45f) % 8]; + return String.format( + "Wetter: %s%n" + + "Wind: %.1f km/h aus %s (%.0f°)%n" + + "Regen: %.0f%%%n" + + "Wolken: %.0f%%%n" + + "Nebel: %.3f%n" + + "Nächste Ä.: in %.0f s", + ws.getActiveWeather(), + ws.getWindSpeed(), dir, windDeg, + ws.getRainIntensity() * 100f, + ws.getCloudCover() * 100f, + ws.getFogDensity(), + ws.getChangeTimer()); + } + if ("next".equalsIgnoreCase(args[1])) { + de.blight.game.state.WeatherState.Weather next = ws.triggerNext(); + return "Wetter → " + next; + } 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"; + return "Unbekanntes Wetter. Erlaubt: next, " + + java.util.Arrays.stream(de.blight.game.state.WeatherState.Weather.values()) + .map(w -> w.name().toLowerCase()) + .collect(java.util.stream.Collectors.joining(", ")); } }); 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 eaae31a..688d8ff 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 @@ -438,6 +438,8 @@ public class WorldScene extends BaseAppState { ColorRGBA ac = dayNight.getCustomAmbient(); terrainMaterial.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b)); terrainMaterial.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b)); + de.blight.game.state.RainState rs = app.getStateManager().getState(de.blight.game.state.RainState.class); + if (rs != null) terrainMaterial.setFloat("Wetness", rs.getWetness()); // Charakter-PBR: Emissive = customAmbient (IBL-Fallback ohne fertige LightProbe) for (Material mat : characterPbrMaterials) { mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f)); @@ -808,7 +810,9 @@ public class WorldScene extends BaseAppState { weather.setFogFilter(fogFilter); weather.setViewDistanceFactor(graphicsSettings.viewDistance.fogFactor); weather.setSkyControl(dayNight.getSkyControl()); + weather.setDayTime(dayNight.getDayTime()); app.getStateManager().attach(weather); + app.getStateManager().attach(new de.blight.game.state.RainState()); } catch (Exception e) { log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage()); } @@ -828,11 +832,13 @@ public class WorldScene extends BaseAppState { // Initiale Sonnenposition: von Kamera aus weit entfernt entgegen Lichtrichtung Vector3f initSunPos = app.getCamera().getLocation().add(sunDir.negate().mult(10000f)); lightScatterFilter = new com.jme3.post.filters.LightScatteringFilter(initSunPos); - lightScatterFilter.setLightDensity(0.8f); + lightScatterFilter.setLightDensity(0.08f); lightScatterFilter.setBlurStart(0.3f); lightScatterFilter.setBlurWidth(0.4f); lightScatterFilter.setNbSamples(150); fpp.addFilter(lightScatterFilter); + WeatherState ws = app.getStateManager().getState(WeatherState.class); + if (ws != null) ws.setLightScatterFilter(lightScatterFilter); } if (pfx.toneMap) { diff --git a/blight-game/src/main/java/de/blight/game/state/CursorState.java b/blight-game/src/main/java/de/blight/game/state/CursorState.java new file mode 100644 index 0000000..cd9d28b --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/CursorState.java @@ -0,0 +1,126 @@ +package de.blight.game.state; + +import com.jme3.app.Application; +import com.jme3.app.state.BaseAppState; +import com.jme3.input.RawInputListener; +import com.jme3.input.event.*; +import com.jme3.system.lwjgl.LwjglWindow; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWImage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Ersetzt den Betriebssystem-Mauszeiger durch benutzerdefinierte GLFW-Cursor-Images. + * Tauscht automatisch zwischen Normalzustand und gedrücktem Zustand (LMB) aus. + */ +public class CursorState extends BaseAppState implements RawInputListener { + + private static final Logger log = LoggerFactory.getLogger(CursorState.class); + + private long windowHandle; + private long normalCursor; + private long pressedCursor; + + @Override + protected void initialize(Application app) { + if (!(app.getContext() instanceof LwjglWindow)) { + log.warn("[Cursor] Kein LWJGL-Fenster – Custom-Cursor nicht verfügbar."); + return; + } + windowHandle = ((LwjglWindow) app.getContext()).getWindowHandle(); + if (windowHandle == 0L) return; + + normalCursor = loadCursor(app, "Textures/internal/cursor/cursor.png"); + pressedCursor = loadCursor(app, "Textures/internal/cursor/cursor_pressed.png"); + + if (normalCursor != 0L) { + GLFW.glfwSetCursor(windowHandle, normalCursor); + } + + app.getInputManager().addRawInputListener(this); + log.info("[Cursor] Custom-Cursor geladen."); + } + + @Override + protected void cleanup(Application app) { + app.getInputManager().removeRawInputListener(this); + if (windowHandle != 0L) { + GLFW.glfwSetCursor(windowHandle, 0L); // Standard-Cursor wiederherstellen + } + if (normalCursor != 0L) { GLFW.glfwDestroyCursor(normalCursor); normalCursor = 0L; } + if (pressedCursor != 0L) { GLFW.glfwDestroyCursor(pressedCursor); pressedCursor = 0L; } + } + + @Override protected void onEnable() {} + @Override protected void onDisable() {} + + @Override + public void onMouseButtonEvent(MouseButtonEvent evt) { + if (windowHandle == 0L) return; + if (evt.getButtonIndex() == 0) { + long c = evt.isPressed() ? pressedCursor : normalCursor; + if (c != 0L) GLFW.glfwSetCursor(windowHandle, c); + } + } + + // ── RawInputListener-Stubs ──────────────────────────────────────────────── + @Override public void beginInput() {} + @Override public void endInput() {} + @Override public void onJoyAxisEvent(JoyAxisEvent evt) {} + @Override public void onJoyButtonEvent(JoyButtonEvent evt) {} + @Override public void onMouseMotionEvent(MouseMotionEvent evt) {} + @Override public void onKeyEvent(KeyInputEvent evt) {} + @Override public void onTouchEvent(TouchEvent evt) {} + + // ── GLFW-Cursor aus Classpath-PNG laden ──────────────────────────────────── + + private static long loadCursor(Application app, String assetPath) { + try (InputStream is = app.getAssetManager() + .locateAsset(new com.jme3.asset.TextureKey(assetPath)) + .openStream()) { + + BufferedImage src = ImageIO.read(is); + if (src == null) throw new IllegalStateException("ImageIO konnte das Bild nicht lesen"); + + // Auf 32×32 skalieren – GLFW rendert Cursor pixelgenau + int w = 32, h = 32; + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g2 = img.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2.drawImage(src, 0, 0, w, h, null); + g2.dispose(); + + ByteBuffer pixels = ByteBuffer.allocateDirect(w * h * 4); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int argb = img.getRGB(x, y); + pixels.put((byte) ((argb >> 16) & 0xFF)); // R + pixels.put((byte) ((argb >> 8) & 0xFF)); // G + pixels.put((byte) ( argb & 0xFF)); // B + pixels.put((byte) ((argb >> 24) & 0xFF)); // A + } + } + pixels.flip(); + + long cursor; + try (GLFWImage glfwImg = GLFWImage.malloc()) { + glfwImg.set(w, h, pixels); + cursor = GLFW.glfwCreateCursor(glfwImg, 0, 0); + } + if (cursor == 0L) throw new IllegalStateException("glfwCreateCursor schlug fehl"); + return cursor; + + } catch (Exception e) { + log.warn("[Cursor] Cursor nicht ladbar '{}': {}", assetPath, e.getMessage()); + return 0L; + } + } +} 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 7b9abbf..1fe4b70 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 @@ -6,13 +6,17 @@ import com.jme3.app.state.BaseAppState; import com.jme3.bullet.BulletAppState; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; import com.jme3.math.*; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; import com.jme3.scene.Node; +import com.jme3.scene.shape.Sphere; import com.jme3.shadow.DirectionalLightShadowFilter; import com.jme3.shadow.EdgeFilteringMode; import de.blight.common.time.DayTime; import de.blight.common.time.TimeListener; -import jme3utilities.sky.CloudLayer; +import jme3utilities.sky.LunarPhase; import jme3utilities.sky.SkyControl; import jme3utilities.sky.StarsOption; import org.slf4j.Logger; @@ -42,6 +46,17 @@ public class DayNightState extends BaseAppState implements TimeListener { private DirectionalLightShadowFilter shadowFilter; private Node skyNode; private SkyControl skyControl; + private Node cloudDomeNode; + private Material cloudMat; + private float cloudOffsetX = 0f; + private float cloudOffsetZ = 0f; + + // ── Mond-Phase ──────────────────────────────────────────────────────────── + + /** 0 = Neumond, 0.5 = Vollmond, 1 = Neumond. Zyklus: 7 Spieltage = 6300 s Echtzeit. */ + private float moonAge = 0.375f; // Start: zunehmend Gibbous + private LunarPhase currentMoonPhase = null; // zuletzt gesetzte Phase — vermeidet redundante Aufrufe + private static final float LUNAR_CYCLE = 6300f; // ── Sonnenrichtungs-Drosselung ──────────────────────────────────────────── @@ -56,6 +71,10 @@ public class DayNightState extends BaseAppState implements TimeListener { private float caveCheckTimer = 0f; private ColorRGBA sunBaseColor = new ColorRGBA(1, 1, 1, 1); private float shadowBaseIntensity = 0f; + /** Letzter berechneter Wert von sunOcclusionByCloud — für updateCaveLighting(). */ + private float lastSunOcc = 0f; + /** 1.0 = klar, sinkt mit zunehmender Wolkendecke (min 0.20). */ + private float cloudDimFactor = 1f; private ColorRGBA customAmbient = new ColorRGBA(); /** true = Spiel-Beleuchtungswerte, false = Editor-Werte. Standardmäßig = withShadows. */ private boolean gameMode; @@ -144,17 +163,31 @@ public class DayNightState extends BaseAppState implements TimeListener { rootNode.attachChild(skyNode); SkyControl sc = new SkyControl(app.getAssetManager(), app.getCamera(), - 0.9f, StarsOption.Cube, true); + 0f, StarsOption.Cube, true); // SkyControl default: North=+X, but JME3 world: North=-Z, East=+X sc.getSunAndStars().setAxes(new Vector3f(0f, 0f, -1f), Vector3f.UNIT_Y); // Deckel-Kuppel leicht unter den Horizont verlängern → kein sichtbarer Saum beim Blick nach unten sc.setTopVerticalAngle(FastMath.HALF_PI + 0.12f); skyNode.addControl(sc); - CloudLayer clouds = sc.getCloudLayer(0); - clouds.setMotion(0.37f, 0f, 0.2f, 0.001f); - clouds.setTexture("Textures/skies/clouds/fbm.png", 0.3f); - clouds.setOpacity(0f); + // SkyControl-Wolkenlayer alle deaktivieren — CloudDome-Shader übernimmt + sc.setCloudiness(1.0f); + for (int li = 0; li < 6; li++) { + sc.getCloudLayer(li).setTexture("Textures/skies/clouds/clear.png", 1.0f); + } + + // Prozedurale Wolkenkuppel (FBM-Shader, kein Sphere-UV-Drift) + Sphere cloudSphere = new Sphere(32, 64, 900f); + Geometry cloudGeo = new Geometry("cloudDome", cloudSphere); + cloudGeo.setQueueBucket(RenderQueue.Bucket.Sky); + cloudMat = new Material(app.getAssetManager(), "MatDefs/CloudDome.j3md"); + cloudMat.setVector2("CloudOffset", new Vector2f(0f, 0f)); + cloudMat.setFloat("CloudCover", 0f); + cloudMat.setColor("CloudColor", new ColorRGBA(1f, 1f, 1f, 1f)); + cloudGeo.setMaterial(cloudMat); + cloudDomeNode = new Node("cloudDomeNode"); + cloudDomeNode.attachChild(cloudGeo); + rootNode.attachChild(cloudDomeNode); // Updater nur für Viewport-Hintergrundfarbe nutzen (Licht wird selbst gesteuert) sc.getUpdater().addViewPort(this.app.getViewPort()); @@ -185,6 +218,11 @@ public class DayNightState extends BaseAppState implements TimeListener { rootNode.detachChild(skyNode); } skyNode = null; + if (cloudDomeNode != null && cloudDomeNode.getParent() != null) { + rootNode.detachChild(cloudDomeNode); + } + cloudDomeNode = null; + cloudMat = null; } @Override protected void onEnable() {} @@ -199,6 +237,60 @@ public class DayNightState extends BaseAppState implements TimeListener { if (skyNode != null && skyNode.getParent() == null) { rootNode.attachChild(skyNode); } + if (cloudDomeNode != null && cloudDomeNode.getParent() == null) { + rootNode.attachChild(cloudDomeNode); + } + + // Mond-Phase: 7 Spieltage pro Zyklus; 5 Phasen-Texturen über LunarPhase-Enum + moonAge += tpf / LUNAR_CYCLE; + if (moonAge >= 1f) moonAge -= 1f; + if (skyControl != null) { + LunarPhase phase = lunarPhaseFor(moonAge); + if (phase != currentMoonPhase) { + currentMoonPhase = phase; + skyControl.setPhase(phase); + } + } + + // Wolkenkuppel: Kamera folgen, Wind-Scroll und Cover-Uniform setzen + WeatherState ws = app.getStateManager().getState(WeatherState.class); + float cloudCover = 0f; + if (cloudMat != null) { + Vector3f cam = app.getCamera().getLocation(); + cloudDomeNode.setLocalTranslation(cam.x, cam.y, cam.z); + + if (ws != null) { + Vector3f wind = ws.getWindDirection(); + float scrollRate = ws.getWindSpeed() * 0.0002f * tpf; + cloudOffsetX += wind.x * scrollRate; + cloudOffsetZ += wind.z * scrollRate; + cloudCover = ws.getCloudCover(); + cloudMat.setVector2("CloudOffset", new Vector2f(cloudOffsetX, cloudOffsetZ)); + cloudMat.setFloat("CloudCover", cloudCover); + } + // Wolkenfarbe aus sunBaseColor; Minimum-Helligkeit für bewölkte Nächte + ColorRGBA cc = sunBaseColor.clone(); + float lum = Math.max(cc.r, Math.max(cc.g, cc.b)); + if (lum < 0.04f) { + cc.r = Math.max(cc.r, 0.04f); + cc.g = Math.max(cc.g, 0.04f); + cc.b = Math.max(cc.b, 0.05f); + } + cc.a = 1f; + cloudMat.setColor("CloudColor", cc); + } + + // Sonnenlicht + Schatten: Okklusion durch CloudDome bestimmt beides + if (skyControl != null) { + Vector3f sunDir = skyControl.getSunAndStars().sunDirection(null); + lastSunOcc = sunOcclusionByCloud(sunDir, cloudCover); + // Direktlicht: bis 92% gedimmt wenn Sonne verdeckt; min 8% bleibt (diffuses Streulicht) + float sunDirect = FastMath.clamp(1f - lastSunOcc * 0.92f, 0.08f, 1f); + sun.setColor(sunBaseColor.mult(sunDirect * (1f - caveFactor))); + if (shadowFilter != null) { + shadowFilter.setShadowIntensity(shadowBaseIntensity * (1f - lastSunOcc) * (1f - caveFactor)); + } + } updateCaveLighting(tpf, app.getCamera().getLocation()); } @@ -224,14 +316,69 @@ public class DayNightState extends BaseAppState implements TimeListener { } if (caveFactor != prev) { - float scale = 1f - caveFactor; - sun.setColor(sunBaseColor.mult(scale)); - if (shadowFilter != null) { - shadowFilter.setShadowIntensity(shadowBaseIntensity * scale); - } + float sunDirect = FastMath.clamp(1f - lastSunOcc * 0.92f, 0.08f, 1f); + sun.setColor(sunBaseColor.mult(sunDirect * (1f - caveFactor))); } } + // ── Mond-Phasen-Mapping ─────────────────────────────────────────────────── + + /** + * Bildet moonAge (0=Neumond, 0.5=Vollmond) auf die fünf LunarPhase-Texturen ab. + * setPhase(LunarPhase) setzt gleichzeitig den Längenunterschied zur Sonne, + * sodass der Mond sich korrekt relativ zur Sonne positioniert. + */ + private static LunarPhase lunarPhaseFor(float age) { + if (age < 0.20f) return LunarPhase.WAXING_CRESCENT; + if (age < 0.40f) return LunarPhase.WAXING_GIBBOUS; + if (age < 0.60f) return LunarPhase.FULL; + if (age < 0.80f) return LunarPhase.WANING_GIBBOUS; + return LunarPhase.WANING_CRESCENT; + } + + // ── Cloud-Okklusion (CPU-Replikation des CloudDome-Shaders) ────────────── + + /** + * Gibt zurück, wie stark die Sonne durch den CloudDome verdeckt ist (0=frei, 1=verdeckt). + * Gleiche FBM-Formel wie CloudDome.frag, ausgewertet für die Sonnenrichtung. + */ + private float sunOcclusionByCloud(Vector3f sunDir, float cloudCover) { + if (sunDir.y <= 0.02f) return 1f; // Sonne unter Horizont → keine Schatten sowieso + float denom = sunDir.y + 1.001f; + float u = sunDir.x / denom * 3.5f + cloudOffsetX; + float v = sunDir.z / denom * 3.5f + cloudOffsetZ; + float f = fbmCpu(u, v); + float threshold = 0.72f - 0.5f * cloudCover; // mix(0.72, 0.22, cloudCover) + float t = FastMath.clamp((f - (threshold - 0.09f)) / 0.18f, 0f, 1f); + return t * t * (3f - 2f * t); // smoothstep + } + + private static float fbmCpu(float px, float py) { + float v = 0f, a = 0.5f; + for (int i = 0; i < 6; i++) { + v += a * vnoiseCpu(px, py); + px = px * 2.17f + 0.631f; + py = py * 2.17f + 1.137f; + a *= 0.5f; + } + return v; + } + + private static float vnoiseCpu(float px, float py) { + float ix = (float) Math.floor(px), iy = (float) Math.floor(py); + float fx = px - ix, fy = py - iy; + fx = fx * fx * (3f - 2f * fx); + fy = fy * fy * (3f - 2f * fy); + float a = hashCpu(ix, iy) + fx * (hashCpu(ix + 1f, iy) - hashCpu(ix, iy)); + float b = hashCpu(ix, iy + 1f) + fx * (hashCpu(ix + 1f, iy + 1f) - hashCpu(ix, iy + 1f)); + return a + fy * (b - a); + } + + private static float hashCpu(float x, float y) { + double s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; + return (float) (s - Math.floor(s)); + } + // ── Zeit-Callback ───────────────────────────────────────────────────────── @Override @@ -251,12 +398,17 @@ public class DayNightState extends BaseAppState implements TimeListener { ColorRGBA custNight = gameMode ? LightingConfig.CUSTOM_AMB_NIGHT : LightingConfig.EDITOR_CUSTOM_AMB_NIGHT; float sunIntensity = gameMode ? LightingConfig.SUN_INTENSITY : LightingConfig.EDITOR_SUN_INTENSITY; + // ── Wolkendecke dämpft Sonnenlicht und Schatten ────────────────────────── + WeatherState ws = app.getStateManager().getState(WeatherState.class); + float cloudCover = ws != null ? ws.getCloudCover() : 0f; + cloudDimFactor = Math.max(0.20f, 1f - cloudCover * 0.80f); + // ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) — jedes Frame ── float dawnFactor = 1f - FastMath.clamp(elevation * LightingConfig.SUN_DAWN_SLOPE, 0f, 1f); ColorRGBA sunTint = LightingConfig.SUN_COLOR_DAWN.clone() .interpolateLocal(LightingConfig.SUN_COLOR_DAY, 1f - dawnFactor); - sunBaseColor = sunTint.mult(elevC * sunIntensity); - sun.setColor(sunBaseColor.mult(1f - caveFactor)); + sunBaseColor = sunTint.mult(elevC * sunIntensity * cloudDimFactor); + // sun.setColor() wird jedes Frame in update() mit sunOcc-Faktor gesetzt // ── Ambient: Nacht (blau) → Tag (Sonnenfarbe-tinted) — jedes Frame ── // Tages-Ambient bekommt den Farbton der Sonne: orange bei Dämmerung, @@ -268,16 +420,15 @@ public class DayNightState extends BaseAppState implements TimeListener { ambient.setColor(ambNight.clone().interpolateLocal(ambDayTinted, ambFactor)); customAmbient.set(custNight.clone().interpolateLocal(custDayTinted, ambFactor)); + // Basis-Schattenintensität: nur von Sonnenhöhe abhängig. + // Wolken-Okklusion wird jedes Frame separat in update() berechnet. shadowBaseIntensity = FastMath.clamp( elevation * LightingConfig.SHADOW_SLOPE, 0f, LightingConfig.SHADOW_MAX); - // Lichtrichtung und Schatten nur 4×/Sek aktualisieren → kein Shadow-Map-Flackern + // Lichtrichtung nur alle 2 s aktualisieren → kein Shadow-Map-Flackern if (sunDirTimer >= SUN_DIR_INTERVAL) { sunDirTimer = 0f; sun.setDirection(toSun.negateLocal()); - if (shadowFilter != null) { - shadowFilter.setShadowIntensity(shadowBaseIntensity * (1f - caveFactor)); - } } } } 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 019abd4..7008d4a 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 @@ -112,6 +112,28 @@ public class GrassState extends BaseAppState } } } + + WeatherState ws = getApplication().getStateManager().getState(WeatherState.class); + if (ws != null) { + Vector3f wd3 = ws.getWindDirection(); + Vector2f wDir = new Vector2f(wd3.x, wd3.z); + float speed = Math.max(0.05f, ws.getWindSpeed() * 0.04f); + float strength = FastMath.clamp(ws.getWindSpeed() * 0.009f, 0.01f, 0.45f); + for (Material mat : slotMaterials.values()) { + mat.setVector2("WindDir", wDir); + mat.setFloat("WindSpeed", speed); + mat.setFloat("WindStrength", strength); + } + } + + RainState rs = getApplication().getStateManager().getState(RainState.class); + if (rs != null) { + float w = rs.getWetness(); + for (Material mat : slotMaterials.values()) { + if (mat.getMaterialDef().getMaterialParam("Wetness") != null) + mat.setFloat("Wetness", w); + } + } } // ── Material ────────────────────────────────────────────────────────────── 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 69d1e6d..1bbd62d 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 @@ -193,6 +193,20 @@ public class GrassVertexRenderState extends BaseAppState sm.setFloat("WindStrength", strength); } } + + RainState rs = getApplication().getStateManager().getState(RainState.class); + if (rs != null) { + float w = rs.getWetness(); + setWetness(material, w); + setWetness(seedStalkMaterial, w); + for (Material sm : seedMaterials) setWetness(sm, w); + } + } + + private static void setWetness(Material mat, float w) { + if (mat != null && mat.getMaterialDef().getMaterialParam("Wetness") != null) { + mat.setFloat("Wetness", w); + } } // ── Material ────────────────────────────────────────────────────────────── diff --git a/blight-game/src/main/java/de/blight/game/state/RainState.java b/blight-game/src/main/java/de/blight/game/state/RainState.java new file mode 100644 index 0000000..ede5271 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/RainState.java @@ -0,0 +1,310 @@ +package de.blight.game.state; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioNode; +import com.jme3.effect.ParticleEmitter; +import com.jme3.effect.ParticleMesh; +import com.jme3.effect.shapes.EmitterBoxShape; +import com.jme3.light.AmbientLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; +import java.nio.ByteBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.util.Properties; + +/** + * Regen-Partikel, Nass-Effekt, Gewitter-Blitz und Donner-Audio. + * Wetness (0..1) wird von anderen States abgefragt und als Shader-Uniform gesetzt. + */ +public class RainState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(RainState.class); + + // ── Wetness ─────────────────────────────────────────────────────────────── + private static final float WET_RATE_UP = 0.020f; // pro Sekunde + private static final float WET_RATE_DOWN = 0.005f; // langsames Abtrocknen + private float wetness = 0f; + + // ── Regen-Partikel ──────────────────────────────────────────────────────── + private static final float EMITTER_HEIGHT = 26f; + private static final float EMITTER_RADIUS = 45f; + + @SuppressWarnings("deprecation") + private ParticleEmitter rainEmitter; + private Node rainNode; + + // ── Audio ───────────────────────────────────────────────────────────────── + private AudioNode lightRainAudio; + private AudioNode heavyRainAudio; + private final AudioNode[] thunderAudio = new AudioNode[3]; + private float lightRainVol = 0f; + private float heavyRainVol = 0f; + + // ── Blitz / Donner ──────────────────────────────────────────────────────── + private AmbientLight lightningLight; + private float nextLightningTimer = 18f; + private float flashTimer = 0f; + private static final float FLASH_DURATION = 0.13f; + private boolean thunderPending = false; + private float thunderTimer = 0f; + private int thunderIdx = 0; + + // ── Persistenz ──────────────────────────────────────────────────────────── + private float saveTimer = 60f; + + // ───────────────────────────────────────────────────────────────────────── + + /** Nässe-Faktor 0..1 — wird von Terrain/Objekt-States als Shader-Uniform gesetzt. */ + public float getWetness() { return wetness; } + + @Override + @SuppressWarnings("deprecation") + protected void initialize(Application app) { + SimpleApplication sa = (SimpleApplication) app; + Node root = sa.getRootNode(); + AssetManager assets = app.getAssetManager(); + + // Blitz-Licht (startet bei Intensität 0) + lightningLight = new AmbientLight(new ColorRGBA(0, 0, 0, 1)); + root.addLight(lightningLight); + + // Regen-Partikel + rainEmitter = new ParticleEmitter("rain", ParticleMesh.Type.Triangle, 5000); + rainEmitter.setShape(new EmitterBoxShape( + new Vector3f(-EMITTER_RADIUS, -1f, -EMITTER_RADIUS), + new Vector3f( EMITTER_RADIUS, 1f, EMITTER_RADIUS))); + rainEmitter.setGravity(0, 0, 0); + rainEmitter.setParticlesPerSec(0); + rainEmitter.setLowLife(0.9f); + rainEmitter.setHighLife(1.2f); + rainEmitter.setStartSize(0.15f); + rainEmitter.setEndSize(0.06f); + rainEmitter.setStartColor(new ColorRGBA(0.82f, 0.90f, 1.00f, 0.80f)); + rainEmitter.setEndColor( new ColorRGBA(0.82f, 0.90f, 1.00f, 0.00f)); + rainEmitter.setInitialVelocity(new Vector3f(0f, -26f, 0f)); + rainEmitter.setVelocityVariation(0.06f); + com.jme3.material.Material pm = + new com.jme3.material.Material(assets, "Common/MatDefs/Misc/Particle.j3md"); + pm.setTexture("Texture", createRainTexture()); + pm.getAdditionalRenderState() + .setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha); + pm.getAdditionalRenderState().setDepthWrite(false); + rainEmitter.setMaterial(pm); + + rainNode = new Node("rainNode"); + rainNode.attachChild(rainEmitter); + root.attachChild(rainNode); + + // Regen-Audio (gestreamt, da lange Schleifen) + lightRainAudio = streamAudio(assets, "audio/ambient/weather/leichter_regen.ogg"); + heavyRainAudio = streamAudio(assets, "audio/ambient/weather/starker_regen.ogg"); + root.attachChild(lightRainAudio); + root.attachChild(heavyRainAudio); + lightRainAudio.play(); + heavyRainAudio.play(); + + // Donner (gepuffert, kurze Clips) + for (int i = 0; i < 3; i++) { + thunderAudio[i] = bufferAudio(assets, "audio/ambient/weather/donner" + (i + 1) + ".ogg"); + root.attachChild(thunderAudio[i]); + } + + loadWetness(); + log.info("[Rain] initialisiert."); + } + + @Override + @SuppressWarnings("deprecation") + public void update(float tpf) { + WeatherState ws = getApplication().getStateManager().getState(WeatherState.class); + if (ws == null) return; + + float intensity = ws.getRainIntensity(); + WeatherState.Weather weather = ws.getActiveWeather(); + boolean stormy = weather == WeatherState.Weather.THUNDERSTORM; + + saveTimer -= tpf; + if (saveTimer <= 0f) { saveTimer = 60f; saveWetness(); } + + // ── Wetness ─────────────────────────────────────────────────────────── + float wetnessTarget = intensity > 0.01f ? FastMath.clamp(intensity + 0.25f, 0f, 1f) : 0f; + if (wetness < wetnessTarget) { + wetness = Math.min(wetnessTarget, wetness + WET_RATE_UP * tpf); + } else { + wetness = Math.max(wetnessTarget, wetness - WET_RATE_DOWN * tpf); + } + + // ── Partikel ────────────────────────────────────────────────────────── + Vector3f cam = getApplication().getCamera().getLocation(); + rainNode.setLocalTranslation(cam.x, cam.y + EMITTER_HEIGHT, cam.z); + + Vector3f wind = ws.getWindDirection(); + float ws2 = ws.getWindSpeed() * 0.07f; + rainEmitter.getParticleInfluencer() + .setInitialVelocity(new Vector3f(wind.x * ws2, -26f, wind.z * ws2)); + + rainEmitter.setParticlesPerSec(intensity * 2500f); + + // ── Audio ───────────────────────────────────────────────────────────── + float tgtLight = intensity > 0.01f ? FastMath.clamp(1f - intensity * 1.2f, 0f, 0.225f) : 0f; + float tgtHeavy = FastMath.clamp((intensity - 0.20f) * 1.35f, 0f, 0.25f); + + AudioSettingsState audioState = getApplication().getStateManager() + .getState(AudioSettingsState.class); + float ambVol = audioState != null ? audioState.effectiveAmbient() : 1f; + + float alpha = FastMath.clamp(tpf * 2.5f, 0f, 1f); + lightRainVol += (tgtLight - lightRainVol) * alpha; + heavyRainVol += (tgtHeavy - heavyRainVol) * alpha; + lightRainAudio.setVolume(lightRainVol * ambVol); + heavyRainAudio.setVolume(heavyRainVol * ambVol); + + // ── Blitz-Flash ─────────────────────────────────────────────────────── + if (flashTimer > 0f) { + flashTimer = Math.max(0f, flashTimer - tpf); + float phase = 1f - flashTimer / FLASH_DURATION; + float fi = (float) Math.sin(phase * Math.PI); + lightningLight.setColor(new ColorRGBA(fi, fi * 0.95f, fi * 0.85f, 1f)); + } else { + lightningLight.setColor(new ColorRGBA(0f, 0f, 0f, 1f)); + } + + // ── Donner-Verzögerung ──────────────────────────────────────────────── + if (thunderPending) { + thunderTimer -= tpf; + if (thunderTimer <= 0f) { + thunderPending = false; + float vol = FastMath.clamp(1f - (2.5f + thunderTimer) / 2.5f, 0.15f, 1f); + thunderAudio[thunderIdx].setVolume(vol); + thunderAudio[thunderIdx].playInstance(); + } + } + + // ── Nächster Blitz ──────────────────────────────────────────────────── + nextLightningTimer -= tpf; + if (nextLightningTimer <= 0f) { + if (stormy) { + // Gewitter: Blitz + Donner alle 8-28s + triggerLightning(true); + nextLightningTimer = 8f + FastMath.nextRandomFloat() * 20f; + } else if (weather == WeatherState.Weather.RAIN_HEAVY) { + // Starker Regen: ferner Donner ohne Blitz alle 40-80s + triggerLightning(false); + nextLightningTimer = 40f + FastMath.nextRandomFloat() * 40f; + } else { + nextLightningTimer = 30f; + } + } + } + + private void triggerLightning(boolean withFlash) { + if (withFlash) flashTimer = FLASH_DURATION; + thunderIdx = (int)(FastMath.nextRandomFloat() * 3); + float delay = FastMath.nextRandomFloat() * 2.5f + 0.3f; + thunderPending = true; + thunderTimer = delay; + } + + @Override + protected void onDisable() { + if (rainEmitter != null) rainEmitter.setParticlesPerSec(0); + if (lightRainAudio != null) lightRainAudio.setVolume(0); + if (heavyRainAudio != null) heavyRainAudio.setVolume(0); + } + + @Override protected void onEnable() {} + + @Override + protected void cleanup(Application app) { + saveWetness(); + Node root = ((SimpleApplication) app).getRootNode(); + if (lightningLight != null) root.removeLight(lightningLight); + if (rainNode != null) root.detachChild(rainNode); + if (lightRainAudio != null) { lightRainAudio.stop(); root.detachChild(lightRainAudio); } + if (heavyRainAudio != null) { heavyRainAudio.stop(); root.detachChild(heavyRainAudio); } + for (AudioNode tn : thunderAudio) { if (tn != null) root.detachChild(tn); } + } + + // ── Persistenz ──────────────────────────────────────────────────────────── + + private static File wetFile() { + File dir = new File(System.getProperty("user.home"), ".blight"); + dir.mkdirs(); + return new File(dir, "rain.properties"); + } + + private void saveWetness() { + Properties p = new Properties(); + p.setProperty("wetness", String.valueOf(wetness)); + try (FileWriter fw = new FileWriter(wetFile())) { + p.store(fw, null); + } catch (Exception e) { + log.warn("[Rain] Speichern fehlgeschlagen: {}", e.getMessage()); + } + } + + private void loadWetness() { + File f = wetFile(); + if (!f.exists()) return; + Properties p = new Properties(); + try (FileReader fr = new FileReader(f)) { + p.load(fr); + wetness = Float.parseFloat(p.getProperty("wetness", "0")); + } catch (Exception e) { + log.warn("[Rain] Laden fehlgeschlagen: {}", e.getMessage()); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static Texture2D createRainTexture() { + int w = 4, h = 32; + ByteBuffer buf = ByteBuffer.allocateDirect(w * h * 4); + for (int y = 0; y < h; y++) { + float fy = (float) y / (h - 1); + // Oben ausgeblendet (Tropfen erscheint), unten voll sichtbar → Strich-Silhouette + float ay = FastMath.clamp(fy * 5f, 0f, 1f) * FastMath.clamp((1f - fy) * 2f, 0f, 1f); + for (int x = 0; x < w; x++) { + float fx = Math.abs((x - (w - 1) * 0.5f) / ((w - 1) * 0.5f)); + float ax = FastMath.clamp(1f - fx * 1.5f, 0f, 1f); + float alpha = ay * ax; + buf.put((byte) 210); + buf.put((byte) 228); + buf.put((byte) 255); + buf.put((byte) (int) (alpha * 230)); + } + } + buf.flip(); + return new Texture2D(new Image(Image.Format.RGBA8, w, h, buf, ColorSpace.Linear)); + } + + private static AudioNode streamAudio(AssetManager assets, String path) { + AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream); + n.setPositional(false); + n.setLooping(true); + n.setVolume(0f); + return n; + } + + private static AudioNode bufferAudio(AssetManager assets, String path) { + AudioNode n = new AudioNode(assets, path, AudioData.DataType.Buffer); + n.setPositional(false); + n.setLooping(false); + n.setVolume(1f); + return n; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java b/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java index 01e67a2..7d8e5e0 100644 --- a/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java +++ b/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java @@ -30,6 +30,7 @@ public class StoneWorldState extends BaseAppState { private BulletAppState bulletAppState; private TerrainChunkState terrainChunkState; private Node stoneRoot; + private final List stoneMaterials = new ArrayList<>(); @Override protected void initialize(Application app) { @@ -56,6 +57,7 @@ public class StoneWorldState extends BaseAppState { return; } + stoneMaterials.clear(); Material[] slotMat = buildMaterials(data.slotPaths()); Material defMat = buildDefaultMat(); @@ -86,12 +88,27 @@ public class StoneWorldState extends BaseAppState { } } + stoneMaterials.add(mat); stoneRoot.attachChild(geo); count++; } log.info("[StoneWorld] {} Steine geladen.", count); } + @Override + public void update(float tpf) { + if (stoneMaterials.isEmpty()) return; + RainState rs = getApplication().getStateManager().getState(RainState.class); + if (rs == null) return; + float w = rs.getWetness(); + float roughness = 0.85f * (1f - w * 0.80f); + float metallic = w * 0.10f; + for (Material mat : stoneMaterials) { + mat.setFloat("Roughness", roughness); + mat.setFloat("Metallic", metallic); + } + } + @Override protected void cleanup(Application app) { this.app.getRootNode().detachChild(stoneRoot); 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 be87b6e..2e7fff7 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 @@ -219,6 +219,7 @@ public class TerrainChunkState extends BaseAppState { } } + // Loop 1: Meshes für schmutzige Chunks + Nachbarn neu aufbauen for (int cz = 0; cz < N; cz++) { for (int cx = 0; cx < N; cx++) { int ci = ChunkTerrainIO.chunkIndex(cx, cz); @@ -229,12 +230,6 @@ public class TerrainChunkState extends BaseAppState { rebuildChunkMesh(cx, cz, newLod, targetLod); - // Physik: nur für nahe Chunks halten - int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz); - boolean wantsPhysics = dist <= ChunkTerrainIO.PHYSICS_RANGE; - if (wantsPhysics && physics[ci] == null) addPhysics(ci); - if (!wantsPhysics && physics[ci] != null) removePhysics(ci); - // Listener benachrichtigen if (oldLod < 0) { notifyVisible(cx, cz, newLod); @@ -243,6 +238,18 @@ public class TerrainChunkState extends BaseAppState { } } } + + // Loop 2: Physik für alle Chunks aktualisieren – nach dem Mesh-Rebuild, + // damit chunkNodes bereits existieren wenn addPhysics aufgerufen wird. + for (int cz = 0; cz < N; cz++) { + for (int cx = 0; cx < N; cx++) { + int ci = ChunkTerrainIO.chunkIndex(cx, cz); + int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz); + boolean wantsPhysics = dist <= ChunkTerrainIO.PHYSICS_RANGE; + if (wantsPhysics && physics[ci] == null) addPhysics(ci); + if (!wantsPhysics && physics[ci] != null) removePhysics(ci); + } + } } // ── Öffentliche API ─────────────────────────────────────────────────────── diff --git a/blight-game/src/main/java/de/blight/game/state/WeatherState.java b/blight-game/src/main/java/de/blight/game/state/WeatherState.java index 8256b44..e173c82 100644 --- a/blight-game/src/main/java/de/blight/game/state/WeatherState.java +++ b/blight-game/src/main/java/de/blight/game/state/WeatherState.java @@ -6,136 +6,324 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; +import de.blight.common.time.DayTime; import de.blight.game.post.BlightFogFilter; import com.jme3.water.WaterFilter; import jme3utilities.sky.SkyControl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.util.Properties; + public class WeatherState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(WeatherState.class); - public enum Weather { SUNNY, CLOUDY, OVERCAST, STORM, FOG } + /** + * Wetterübergänge folgen meteorologischer Logik: Regen eskaliert stufenweise, + * ein Gewitter setzt immer starken Regen voraus. CALIMA (Sahara-Staub) ist + * kanaren-typisch. + * + * Ordinal-Reihenfolge darf nicht verändert werden — RAIN_INTENSITY und alle + * anderen Arrays sind darauf indiziert. + */ + public enum Weather { + SUNNY, // 0 — strahlend blauer Himmel + PARTLY_CLOUDY, // 1 — einzelne Cumulus-Wolken + CLOUDY, // 2 — bedeckt, aber trocken + OVERCAST, // 3 — dunkle Wolkendecke + FOG, // 4 — Passatwind-Nebel (Bruma) + DRIZZLE, // 5 — leichter Nieselregen + RAIN_LIGHT, // 6 — leichter Regen + RAIN_MEDIUM, // 7 — mäßiger Regen + RAIN_HEAVY, // 8 — starker Regen + THUNDERSTORM, // 9 — Gewitter mit Blitz & Donner + CALIMA, // 10 — Sahara-Staub, gelblicher Dunst + WIND_STORM // 11 — Sturm ohne Regen (Tramontana) + } - // ── Per-weather targets (Reihenfolge: SUNNY, CLOUDY, OVERCAST, STORM, FOG) ─ + // ── Per-Zustand-Zielwerte (Reihenfolge = Weather.ordinal()) ────────────── - private static final float[] FOG_DENSITY = { 0.40f, 0.55f, 0.75f, 0.90f, 0.88f }; - private static final float[] FOG_DISTANCE = { 600f, 350f, 140f, 50f, 80f }; - private static final float[] WIND_SPEED = { 4f, 14f, 26f, 55f, 3f }; - private static final float[] WAVE_SPEED = { 0.5f, 1.0f, 1.5f, 3.2f, 0.4f }; - private static final float[] WAVE_AMP = { 0.3f, 0.5f, 0.8f, 1.8f, 0.2f }; - private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f, 0.009f}; - private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f, 0.12f }; - private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f, 0.0f }; - private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f, 0.95f }; + private static final float[] FOG_DENSITY = { + 0.10f, 0.18f, 0.35f, 0.65f, 0.88f, + 0.55f, 0.50f, 0.72f, 0.85f, 0.92f, + 0.70f, 0.22f + }; + private static final float[] FOG_DISTANCE = { + 800f, 600f, 400f, 180f, 80f, + 220f, 280f, 130f, 70f, 50f, + 120f, 500f + }; + private static final float[] WIND_SPEED = { + 3f, 8f, 16f, 22f, 2f, + 10f, 14f, 28f, 42f, 62f, + 20f, 72f + }; + private static final float[] WAVE_SPEED = { + 0.4f, 0.6f, 0.9f, 1.3f, 0.3f, + 0.7f, 1.0f, 1.8f, 2.5f, 3.5f, + 0.8f, 3.8f + }; + private static final float[] WAVE_AMP = { + 0.20f, 0.30f, 0.50f, 0.75f, 0.15f, + 0.40f, 0.50f, 1.00f, 1.40f, 2.00f, + 0.35f, 2.20f + }; + private static final float[] WAVE_SCALE = { + 0.009f, 0.008f, 0.007f, 0.006f, 0.010f, + 0.007f, 0.007f, 0.006f, 0.005f, 0.005f, + 0.008f, 0.005f + }; + private static final float[] WATER_TRANS = { + 0.18f, 0.14f, 0.10f, 0.07f, 0.13f, + 0.09f, 0.09f, 0.04f, 0.02f, 0.01f, + 0.12f, 0.06f + }; + private static final float[] FOAM_INTENSITY = { + 0.00f, 0.05f, 0.20f, 0.40f, 0.00f, + 0.15f, 0.20f, 0.55f, 0.75f, 0.95f, + 0.10f, 0.80f + }; + private static final float[] CLOUD_OPACITY = { + 0.00f, 0.30f, 0.65f, 0.90f, 0.95f, + 0.75f, 0.85f, 0.95f, 1.00f, 1.00f, + 0.60f, 0.50f + }; + /** Opazität der gleichmäßigen Überdeckungs-Wolkenschicht (Layer 1). */ + private static final float[] OVERCAST_OPACITY = { + 0.00f, 0.08f, 0.35f, 0.60f, 0.50f, + 0.42f, 0.52f, 0.68f, 0.82f, 0.90f, + 0.12f, 0.28f + }; + /** 0 = kein Regen, 1 = Gewitter. */ + private static final float[] RAIN_INTENSITY = { + 0f, 0f, 0f, 0f, 0f, + 0.12f, 0.33f, 0.67f, 0.88f, 1.00f, + 0f, 0f + }; + + /** Minimale Dauer des Zustands in Sekunden. */ + private static final float[] DURATION_MIN = { + 180f, 90f, 60f, 60f, 120f, + 45f, 60f, 45f, 30f, 20f, + 180f, 90f + }; + + /** Maximale Dauer des Zustands in Sekunden. */ + private static final float[] DURATION_MAX = { + 600f, 300f, 240f, 180f, 420f, + 150f, 210f, 150f, 90f, 60f, + 480f, 270f + }; private static final ColorRGBA[] FOG_COLOR = { - new ColorRGBA(0.75f, 0.80f, 0.88f, 1f), - new ColorRGBA(0.62f, 0.65f, 0.70f, 1f), - new ColorRGBA(0.42f, 0.43f, 0.46f, 1f), - new ColorRGBA(0.18f, 0.19f, 0.21f, 1f), - new ColorRGBA(0.70f, 0.72f, 0.75f, 1f), + new ColorRGBA(0.78f, 0.85f, 0.95f, 1f), // SUNNY + new ColorRGBA(0.75f, 0.80f, 0.90f, 1f), // PARTLY_CLOUDY + new ColorRGBA(0.62f, 0.65f, 0.72f, 1f), // CLOUDY + new ColorRGBA(0.42f, 0.44f, 0.48f, 1f), // OVERCAST + new ColorRGBA(0.70f, 0.72f, 0.75f, 1f), // FOG + new ColorRGBA(0.50f, 0.52f, 0.58f, 1f), // DRIZZLE + new ColorRGBA(0.45f, 0.48f, 0.55f, 1f), // RAIN_LIGHT + new ColorRGBA(0.28f, 0.30f, 0.35f, 1f), // RAIN_MEDIUM + new ColorRGBA(0.18f, 0.19f, 0.22f, 1f), // RAIN_HEAVY + new ColorRGBA(0.12f, 0.13f, 0.16f, 1f), // THUNDERSTORM + new ColorRGBA(0.82f, 0.74f, 0.52f, 1f), // CALIMA — Sahara-Gelbton + new ColorRGBA(0.60f, 0.62f, 0.68f, 1f), // WIND_STORM }; private static final ColorRGBA[] WATER_COLOR = { new ColorRGBA(0.05f, 0.25f, 0.55f, 1f), + new ColorRGBA(0.05f, 0.22f, 0.50f, 1f), new ColorRGBA(0.04f, 0.18f, 0.42f, 1f), new ColorRGBA(0.03f, 0.12f, 0.28f, 1f), - new ColorRGBA(0.02f, 0.06f, 0.14f, 1f), new ColorRGBA(0.04f, 0.20f, 0.45f, 1f), + new ColorRGBA(0.03f, 0.15f, 0.36f, 1f), + new ColorRGBA(0.03f, 0.15f, 0.38f, 1f), + new ColorRGBA(0.02f, 0.09f, 0.22f, 1f), + new ColorRGBA(0.01f, 0.05f, 0.14f, 1f), + new ColorRGBA(0.01f, 0.03f, 0.09f, 1f), + new ColorRGBA(0.06f, 0.22f, 0.42f, 1f), // CALIMA — leicht trüb + new ColorRGBA(0.02f, 0.10f, 0.30f, 1f), }; private static final ColorRGBA[] DEEP_WATER_COLOR = { new ColorRGBA(0.02f, 0.12f, 0.30f, 1f), + new ColorRGBA(0.02f, 0.10f, 0.26f, 1f), new ColorRGBA(0.01f, 0.08f, 0.20f, 1f), new ColorRGBA(0.01f, 0.04f, 0.12f, 1f), - new ColorRGBA(0.00f, 0.02f, 0.06f, 1f), new ColorRGBA(0.01f, 0.09f, 0.23f, 1f), + new ColorRGBA(0.01f, 0.07f, 0.18f, 1f), + new ColorRGBA(0.01f, 0.06f, 0.18f, 1f), + new ColorRGBA(0.00f, 0.03f, 0.10f, 1f), + new ColorRGBA(0.00f, 0.02f, 0.06f, 1f), + new ColorRGBA(0.00f, 0.01f, 0.04f, 1f), + new ColorRGBA(0.02f, 0.09f, 0.20f, 1f), + new ColorRGBA(0.01f, 0.04f, 0.14f, 1f), }; - // ── State ──────────────────────────────────────────────────────────────── + /** + * Übergangsmatrix — Wiederholungen erhöhen die Wahrscheinlichkeit. + * Regen darf nur stufenweise eskalieren; direkter Sprung von SUNNY zu + * THUNDERSTORM ist nicht möglich. + */ + private static final Weather[][] TRANSITIONS = { + // SUNNY: fast immer erst PARTLY_CLOUDY, selten Calima oder Sturm + { Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, + Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, + Weather.CALIMA, Weather.CALIMA, Weather.WIND_STORM }, + // PARTLY_CLOUDY: häufig Besserung, manchmal Eintrübung + { Weather.SUNNY, Weather.SUNNY, Weather.SUNNY, + Weather.CLOUDY, Weather.CLOUDY, Weather.WIND_STORM }, + // CLOUDY: kann zu Niesel, Überbedeckung oder Besserung wechseln + { Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, + Weather.OVERCAST, Weather.OVERCAST, Weather.DRIZZLE, Weather.WIND_STORM }, + // OVERCAST: Nebel, Niesel oder leichter Regen, selten Besserung + { Weather.CLOUDY, Weather.CLOUDY, + Weather.FOG, Weather.DRIZZLE, Weather.DRIZZLE, Weather.RAIN_LIGHT }, + // FOG: löst sich langsam auf oder geht in Niesel über + { Weather.OVERCAST, Weather.OVERCAST, Weather.CLOUDY, Weather.DRIZZLE }, + // DRIZZLE: kann intensivieren oder aufhören + { Weather.OVERCAST, Weather.OVERCAST, Weather.RAIN_LIGHT, Weather.CLOUDY }, + // RAIN_LIGHT: eskaliert oder beruhigt sich zu Niesel + { Weather.DRIZZLE, Weather.DRIZZLE, + Weather.RAIN_MEDIUM, Weather.OVERCAST, Weather.OVERCAST }, + // RAIN_MEDIUM: eskaliert zu RAIN_HEAVY oder lässt nach + { Weather.RAIN_LIGHT, Weather.RAIN_LIGHT, Weather.RAIN_HEAVY, Weather.OVERCAST }, + // RAIN_HEAVY: kann zu Gewitter eskalieren oder nachlassen + { Weather.RAIN_MEDIUM, Weather.RAIN_MEDIUM, Weather.THUNDERSTORM, Weather.OVERCAST }, + // THUNDERSTORM: beruhigt sich immer stufenweise + { Weather.RAIN_HEAVY, Weather.RAIN_HEAVY, Weather.RAIN_MEDIUM, Weather.OVERCAST }, + // CALIMA: meist Aufklärung, selten Überbedeckung + { Weather.SUNNY, Weather.SUNNY, Weather.PARTLY_CLOUDY, Weather.OVERCAST }, + // WIND_STORM: klingt über Bewölkung oder Überbedeckung ab + { Weather.CLOUDY, Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, Weather.OVERCAST }, + }; + + // ── Laufzeit-State ──────────────────────────────────────────────────────── private Weather active = Weather.SUNNY; - private float changeTimer = 120f; + private float changeTimer = 180f; + private float saveTimer = 60f; - private float fogDensity = 0.40f; // Startwert = SUNNY-Ziel, kein langsames Fade-in - private float fogDistance = 600f; - private float windSpeed = 4f; - private float waveSpeed = 0.5f; - private float waveAmp = 0.3f; - private float waveScale = 0.008f; - private float waterTrans = 0.15f; - private float foamIntensity = 0f; - private float cloudOpacity = 0f; - private float windAngle = 0f; - private float windAngleTgt = 0.4f; - private final ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f); - private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f); - private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f); + private float fogDensity = FOG_DENSITY[0]; + private float fogDistance = FOG_DISTANCE[0]; + private float windSpeed = WIND_SPEED[0]; + private float waveSpeed = WAVE_SPEED[0]; + private float waveAmp = WAVE_AMP[0]; + private float waveScale = WAVE_SCALE[0]; + private float waterTrans = WATER_TRANS[0]; + private float foamIntensity = FOAM_INTENSITY[0]; + private float cloudOpacity = CLOUD_OPACITY[0]; + private float cloudOvercastOpacity = OVERCAST_OPACITY[0]; + private float cloudOffsetU = 0f; + private float cloudOffsetV = 0f; + private float windAngle = 0f; + private float windAngleTgt = 0.4f; - // ── Sichtweiten-Faktor (aus Grafikeinstellungen) ────────────────────────── + private final ColorRGBA fogColor = FOG_COLOR[0].clone(); + private final ColorRGBA waterColor = WATER_COLOR[0].clone(); + private final ColorRGBA deepWaterColor = DEEP_WATER_COLOR[0].clone(); private float viewDistanceFactor = 1.0f; - public void setViewDistanceFactor(float f) { this.viewDistanceFactor = f; } - // ── Externe Referenzen ──────────────────────────────────────────────────── private BlightFogFilter fogFilter; private WaterFilter waterFilter; private SkyControl skyControl; + private com.jme3.post.filters.LightScatteringFilter lightScatterFilter; + private DayTime dayTime; + public void setViewDistanceFactor(float f) { this.viewDistanceFactor = f; } public void setFogFilter(BlightFogFilter f) { this.fogFilter = f; } - public void setWaterFilter(WaterFilter f) { this.waterFilter = f; } - public void setSkyControl(SkyControl sc) { this.skyControl = sc; } + public void setWaterFilter(WaterFilter f) { this.waterFilter = f; } + public void setSkyControl(SkyControl sc) { this.skyControl = sc; } + public void setLightScatterFilter(com.jme3.post.filters.LightScatteringFilter f) { this.lightScatterFilter = f; } + public void setDayTime(DayTime dt) { this.dayTime = dt; } - public Weather getActiveWeather() { return active; } - public float getWindSpeed() { return windSpeed; } - - /** - * Setzt das Wetter sofort; Werte interpolieren sanft zum neuen Ziel. - * Der automatische Wechsel-Timer wird zurückgesetzt. - */ - public void forceWeather(Weather w) { - active = w; - changeTimer = 90f + FastMath.nextRandomFloat() * 150f; - windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI; - log.info("[Weather] forceWeather → {}", w); - } + public Weather getActiveWeather() { return active; } + public float getWindSpeed() { return windSpeed; } + public float getWindAngle() { return windAngle; } + public float getFogDensity() { return fogDensity; } + public float getChangeTimer() { return changeTimer; } + public float getRainIntensity() { return RAIN_INTENSITY[active.ordinal()]; } + /** Normierte Wolkenbedeckung 0..1. */ + public float getCloudCover() { return FastMath.clamp((cloudOpacity + cloudOvercastOpacity) * 0.5f, 0f, 1f); } public Vector3f getWindDirection() { return new Vector3f(FastMath.sin(windAngle), 0f, FastMath.cos(windAngle)); } + /** + * Nächsten Wetterzustand sofort auslösen (wie automatischer Übergang). + * Gibt den neuen Zustand zurück. + */ + public Weather triggerNext() { + pickNextWeather(); + return active; + } + + /** + * Wetter sofort setzen; Werte interpolieren sanft zum neuen Ziel. + * Automatischer Wechsel-Timer wird zurückgesetzt. + */ + public void forceWeather(Weather w) { + active = w; + changeTimer = randomDuration(w); + windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI; + log.info("[Weather] forceWeather → {}", w); + } + // ── Lifecycle ───────────────────────────────────────────────────────────── - @Override protected void initialize(Application app) {} - @Override protected void cleanup(Application app) {} - @Override protected void onEnable() {} - @Override protected void onDisable() {} + @Override + protected void initialize(Application app) { + load(); + } + + @Override + protected void cleanup(Application app) { + save(); + } + + @Override protected void onEnable() {} + @Override protected void onDisable() {} @Override public void update(float tpf) { changeTimer -= tpf; if (changeTimer <= 0f) { - changeTimer = 90f + FastMath.nextRandomFloat() * 150f; pickNextWeather(); } + saveTimer -= tpf; + if (saveTimer <= 0f) { + saveTimer = 60f; + save(); + } + int i = active.ordinal(); - fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f); - fogDistance = approach(fogDistance, FOG_DISTANCE[i] * viewDistanceFactor, tpf * 0.025f); - windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f); - waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f); - waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f); - waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f); - waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f); - foamIntensity = approach(foamIntensity, FOAM_INTENSITY[i], tpf * 0.020f); - cloudOpacity = approach(cloudOpacity, CLOUD_OPACITY[i], tpf * 0.025f); - windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f); + fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f); + fogDistance = approach(fogDistance, FOG_DISTANCE[i] * viewDistanceFactor, tpf * 0.025f); + windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f); + waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f); + waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f); + waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f); + waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f); + foamIntensity = approach(foamIntensity, FOAM_INTENSITY[i],tpf * 0.020f); + cloudOpacity = approach(cloudOpacity, CLOUD_OPACITY[i], tpf * 0.025f); + cloudOvercastOpacity = approach(cloudOvercastOpacity, OVERCAST_OPACITY[i], tpf * 0.025f); + windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f); - fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f); - waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f); + // UV-Offset in Windrichtung akkumulieren + float scrollRate = windSpeed * 0.0001f; + cloudOffsetU += FastMath.sin(windAngle) * scrollRate * tpf; + cloudOffsetV += FastMath.cos(windAngle) * scrollRate * tpf; + + fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f); + waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f); deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f); if (fogFilter != null) { @@ -143,7 +331,6 @@ public class WeatherState extends BaseAppState { fogFilter.setFogDistance(fogDistance); fogFilter.setFogColor(fogColor.clone()); } - if (waterFilter != null) { waterFilter.setSpeed(waveSpeed); waterFilter.setMaxAmplitude(waveAmp); @@ -153,30 +340,117 @@ public class WeatherState extends BaseAppState { waterFilter.setWaterColor(waterColor.clone()); waterFilter.setDeepWaterColor(deepWaterColor.clone()); waterFilter.setWindDirection( - new Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle))); + new Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle))); } + if (lightScatterFilter != null) { + // Max 0.08 um Gras-Streifen zu vermeiden; Nebel/Wolken reduzieren weiter + float ld = Math.max(0f, 0.08f - fogDensity * 0.3f); + lightScatterFilter.setLightDensity(ld); + } + } - if (skyControl != null) { - skyControl.getCloudLayer(0).setOpacity(cloudOpacity); + // ── Übergänge ───────────────────────────────────────────────────────────── + + private void pickNextWeather() { + Weather[] opts = TRANSITIONS[active.ordinal()]; + Weather next = opts[(int) (FastMath.nextRandomFloat() * opts.length)]; + windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI; + active = next; + changeTimer = randomDuration(next); + log.info("[Weather] → {} ({}s)", active, (int) changeTimer); + } + + private static float randomDuration(Weather w) { + int i = w.ordinal(); + return DURATION_MIN[i] + FastMath.nextRandomFloat() * (DURATION_MAX[i] - DURATION_MIN[i]); + } + + // ── Persistenz ──────────────────────────────────────────────────────────── + + private static File saveFile() { + File dir = new File(System.getProperty("user.home"), ".blight"); + dir.mkdirs(); + return new File(dir, "weather.properties"); + } + + public void save() { + Properties p = new Properties(); + p.setProperty("weather", active.name()); + if (dayTime != null) p.setProperty("timeOfDay", String.valueOf(dayTime.getTimeOfDay())); + p.setProperty("changeTimer", String.valueOf(changeTimer)); + p.setProperty("windAngle", String.valueOf(windAngle)); + p.setProperty("windAngleTgt", String.valueOf(windAngleTgt)); + p.setProperty("fogDensity", String.valueOf(fogDensity)); + p.setProperty("fogDistance", String.valueOf(fogDistance)); + p.setProperty("windSpeed", String.valueOf(windSpeed)); + p.setProperty("waveSpeed", String.valueOf(waveSpeed)); + p.setProperty("waveAmp", String.valueOf(waveAmp)); + p.setProperty("waveScale", String.valueOf(waveScale)); + p.setProperty("waterTrans", String.valueOf(waterTrans)); + p.setProperty("foamIntensity", String.valueOf(foamIntensity)); + p.setProperty("cloudOpacity", String.valueOf(cloudOpacity)); + p.setProperty("fogColorR", String.valueOf(fogColor.r)); + p.setProperty("fogColorG", String.valueOf(fogColor.g)); + p.setProperty("fogColorB", String.valueOf(fogColor.b)); + p.setProperty("waterColorR", String.valueOf(waterColor.r)); + p.setProperty("waterColorG", String.valueOf(waterColor.g)); + p.setProperty("waterColorB", String.valueOf(waterColor.b)); + p.setProperty("deepWaterR", String.valueOf(deepWaterColor.r)); + p.setProperty("deepWaterG", String.valueOf(deepWaterColor.g)); + p.setProperty("deepWaterB", String.valueOf(deepWaterColor.b)); + try (FileWriter fw = new FileWriter(saveFile())) { + p.store(fw, "Blight weather state"); + } catch (Exception e) { + log.warn("[Weather] Speichern fehlgeschlagen: {}", e.getMessage()); + } + } + + private void load() { + File f = saveFile(); + if (!f.exists()) return; + Properties p = new Properties(); + try (FileReader fr = new FileReader(f)) { + p.load(fr); + active = Weather.valueOf(p.getProperty("weather", "SUNNY")); + changeTimer = Float.parseFloat(p.getProperty("changeTimer", "180")); + windAngle = Float.parseFloat(p.getProperty("windAngle", "0")); + windAngleTgt = Float.parseFloat(p.getProperty("windAngleTgt", "0.4")); + fogDensity = Float.parseFloat(p.getProperty("fogDensity", String.valueOf(FOG_DENSITY[0]))); + fogDistance = Float.parseFloat(p.getProperty("fogDistance", String.valueOf(FOG_DISTANCE[0]))); + windSpeed = Float.parseFloat(p.getProperty("windSpeed", String.valueOf(WIND_SPEED[0]))); + waveSpeed = Float.parseFloat(p.getProperty("waveSpeed", String.valueOf(WAVE_SPEED[0]))); + waveAmp = Float.parseFloat(p.getProperty("waveAmp", String.valueOf(WAVE_AMP[0]))); + waveScale = Float.parseFloat(p.getProperty("waveScale", String.valueOf(WAVE_SCALE[0]))); + waterTrans = Float.parseFloat(p.getProperty("waterTrans", String.valueOf(WATER_TRANS[0]))); + foamIntensity = Float.parseFloat(p.getProperty("foamIntensity", String.valueOf(FOAM_INTENSITY[0]))); + cloudOpacity = Float.parseFloat(p.getProperty("cloudOpacity", String.valueOf(CLOUD_OPACITY[0]))); + fogColor.set( + Float.parseFloat(p.getProperty("fogColorR", String.valueOf(FOG_COLOR[0].r))), + Float.parseFloat(p.getProperty("fogColorG", String.valueOf(FOG_COLOR[0].g))), + Float.parseFloat(p.getProperty("fogColorB", String.valueOf(FOG_COLOR[0].b))), + 1f); + waterColor.set( + Float.parseFloat(p.getProperty("waterColorR", String.valueOf(WATER_COLOR[0].r))), + Float.parseFloat(p.getProperty("waterColorG", String.valueOf(WATER_COLOR[0].g))), + Float.parseFloat(p.getProperty("waterColorB", String.valueOf(WATER_COLOR[0].b))), + 1f); + deepWaterColor.set( + Float.parseFloat(p.getProperty("deepWaterR", String.valueOf(DEEP_WATER_COLOR[0].r))), + Float.parseFloat(p.getProperty("deepWaterG", String.valueOf(DEEP_WATER_COLOR[0].g))), + Float.parseFloat(p.getProperty("deepWaterB", String.valueOf(DEEP_WATER_COLOR[0].b))), + 1f); + String savedTime = p.getProperty("timeOfDay"); + if (savedTime != null && dayTime != null) { + dayTime.setTimeOfDay(Float.parseFloat(savedTime)); + } + log.info("[Weather] Geladen: {} (noch {}s)", active, (int) changeTimer); + } catch (Exception e) { + log.warn("[Weather] Laden fehlgeschlagen: {}", e.getMessage()); } } // ── Helpers ─────────────────────────────────────────────────────────────── - private void pickNextWeather() { - float r = FastMath.nextRandomFloat(); - Weather next = r < 0.38f ? Weather.SUNNY - : r < 0.65f ? Weather.CLOUDY - : r < 0.82f ? Weather.OVERCAST - : r < 0.92f ? Weather.STORM - : Weather.FOG; - windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI; - if (next != active) { - active = next; - log.info("[Weather] transitioning to {}", active); - } - } - private static float approach(float cur, float tgt, float alpha) { return cur + (tgt - cur) * FastMath.clamp(alpha, 0f, 1f); } 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 82b4149..229fc54 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 @@ -224,6 +224,13 @@ public class WorldObjectsState extends BaseAppState { mat.setFloat("WindStrength", strength); } } + + RainState rs = getApplication().getStateManager().getState(RainState.class); + if (rs != null) { + float w = rs.getWetness(); + for (Material mat : sceneLitMaterials) setWetnessIfSupported(mat, w); + for (Material mat : windMaterials) setWetnessIfSupported(mat, w); + } } private Spatial buildSpatial(PlacedModel m) { @@ -536,4 +543,10 @@ public class WorldObjectsState extends BaseAppState { // PhysicsSpace kann beim App-Shutdown bereits zerstört sein } } + + private static void setWetnessIfSupported(Material mat, float w) { + if (mat != null && mat.getMaterialDef().getMaterialParam("Wetness") != null) { + mat.setFloat("Wetness", w); + } + } } diff --git a/blight-map/src/main/map/chunks/sculpt_14_0_05.blsm b/blight-map/src/main/map/chunks/sculpt_14_0_05.blsm new file mode 100644 index 0000000..90b9940 Binary files /dev/null and b/blight-map/src/main/map/chunks/sculpt_14_0_05.blsm differ diff --git a/blight-map/src/main/map/chunks/sculpt_14_m1_05.blsm b/blight-map/src/main/map/chunks/sculpt_14_m1_05.blsm new file mode 100644 index 0000000..f58e814 Binary files /dev/null and b/blight-map/src/main/map/chunks/sculpt_14_m1_05.blsm differ diff --git a/com/jme3/app/SimpleApplication.java b/com/jme3/app/SimpleApplication.java deleted file mode 100644 index 704d0eb..0000000 --- a/com/jme3/app/SimpleApplication.java +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Copyright (c) 2009-2025 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.jme3.app; - -import com.jme3.app.state.AppState; -import com.jme3.app.state.ConstantVerifierState; -import com.jme3.audio.AudioListenerState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.FlyByCamera; -import com.jme3.input.KeyInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.KeyTrigger; -import com.jme3.profile.AppStep; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.queue.RenderQueue.Bucket; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial.CullHint; -import com.jme3.scene.threadwarden.SceneGraphThreadWarden; -import com.jme3.system.AppSettings; -import com.jme3.system.JmeContext.Type; -import com.jme3.system.JmeSystem; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * `SimpleApplication` is the foundational base class for all jMonkeyEngine 3 (jME3) applications. - * It provides a streamlined setup for common game development tasks, including scene management, - * camera controls, and performance monitoring. - * - *

By default, `SimpleApplication` attaches several essential {@link com.jme3.app.state.AppState} instances: - *

- * - *

Default Key Bindings:

- * - * - *

Applications extending `SimpleApplication` should implement the - * {@link #simpleInitApp()} method to set up their initial scene and game logic. - */ -public abstract class SimpleApplication extends LegacyApplication { - - protected static final Logger logger = Logger.getLogger(SimpleApplication.class.getName()); - - public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit"; - public static final String INPUT_MAPPING_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS; - public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY; - public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats"; - - protected Node rootNode = new Node("Root Node"); - protected Node guiNode = new Node("Gui Node"); - protected BitmapText fpsText; - protected BitmapFont guiFont; - protected FlyByCamera flyCam; - protected boolean showSettings = true; - private final AppActionListener actionListener = new AppActionListener(); - - private class AppActionListener implements ActionListener { - - @Override - public void onAction(String name, boolean isPressed, float tpf) { - if (!isPressed) { - return; - } - - if (name.equals(INPUT_MAPPING_EXIT)) { - stop(); - } else if (name.equals(INPUT_MAPPING_HIDE_STATS)) { - StatsAppState statsState = stateManager.getState(StatsAppState.class); - if (statsState != null) { - statsState.toggleStats(); - } - } - } - } - - /** - * Constructs a `SimpleApplication` with a predefined set of default - * {@link com.jme3.app.state.AppState} instances. - * These states provide common functionalities like statistics display, - * fly camera control, audio listener, debug keys, and constant verification. - */ - public SimpleApplication() { - this(new StatsAppState(), - new FlyCamAppState(), - new AudioListenerState(), - new DebugKeysAppState(), - new ConstantVerifierState()); - } - - /** - * Constructs a `SimpleApplication` with a custom array of initial - * {@link com.jme3.app.state.AppState} instances. - * - * @param initialStates An array of `AppState` instances to be attached - * to the `stateManager` upon initialization. - */ - public SimpleApplication(AppState... initialStates) { - super(initialStates); - } - - @Override - public void start() { - // set some default settings in-case - // settings dialog is not shown - boolean loadSettings = false; - if (settings == null) { - logger.log(Level.INFO, "AppSettings not set, creating default settings."); - setSettings(new AppSettings(true)); - loadSettings = true; - } - - // show settings dialog - if (showSettings) { - if (!JmeSystem.showSettingsDialog(settings, loadSettings)) { - return; - } - } - //re-setting settings they can have been merged from the registry. - setSettings(settings); - super.start(); - } - - /** - * Returns the current speed multiplier of the application. - * This value affects how quickly the game world updates relative to real time. - * A value of 1.0f means normal speed, 0.5f means half speed, 2.0f means double speed. - * - * @return The current speed of the application. - */ - public float getSpeed() { - return speed; - } - - /** - * Changes the application's speed multiplier. - * A `speed` of 0.0f effectively pauses the application's update cycle. - * - * @param speed The desired speed multiplier. A value of 1.0f is normal speed. - * Must be non-negative. - */ - public void setSpeed(float speed) { - this.speed = speed; - } - - /** - * Retrieves the `FlyByCamera` instance associated with this application. - * This camera allows free-form navigation within the 3D scene. - * - * @return The `FlyByCamera` object, or `null` if `FlyCamAppState` is not attached - * or has not yet initialized the camera. - */ - public FlyByCamera getFlyByCamera() { - return flyCam; - } - - /** - * Retrieves the `Node` dedicated to 2D graphical user interface (GUI) elements. - * Objects attached to this node are rendered on top of the 3D scene, - * typically without perspective effects, suitable for HUDs and UI. - * - * @return The `Node` object representing the GUI root. - */ - public Node getGuiNode() { - return guiNode; - } - - /** - * Retrieves the root `Node` of the 3D scene graph. - * All main 3D spatial objects and models should be attached to this node - * to be part of the rendered scene. - * - * @return The `Node` object representing the 3D scene root. - */ - public Node getRootNode() { - return rootNode; - } - - /** - * Checks whether the settings dialog is configured to be shown at application startup. - * - * @return `true` if the settings dialog will be displayed, `false` otherwise. - */ - public boolean isShowSettings() { - return showSettings; - } - - /** - * Sets whether the jME3 settings dialog should be displayed before the application starts. - * - * @param showSettings `true` to show the settings dialog, `false` to suppress it. - */ - public void setShowSettings(boolean showSettings) { - this.showSettings = showSettings; - } - - /** - * Creates the font that will be set to the guiFont field - * and subsequently set as the font for the stats text. - * - * @return the loaded BitmapFont - */ - protected BitmapFont loadGuiFont() { - return assetManager.loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - public void initialize() { - super.initialize(); - - //noinspection AssertWithSideEffects - assert SceneGraphThreadWarden.setup(rootNode); - //noinspection AssertWithSideEffects - assert SceneGraphThreadWarden.setup(guiNode); - - // Several things rely on having this - guiFont = loadGuiFont(); - - guiNode.setQueueBucket(Bucket.Gui); - guiNode.setCullHint(CullHint.Never); - - viewPort.attachScene(rootNode); - guiViewPort.attachScene(guiNode); - - if (inputManager != null) { - // Special handling for FlyCamAppState: - // Although FlyCamAppState manages the FlyByCamera, SimpleApplication - // historically initializes and configures a default FlyByCamera instance - // and sets its initial speed. This allows subclasses to directly access - // 'flyCam' early in simpleInitApp(). - - FlyCamAppState flyCamState = stateManager.getState(FlyCamAppState.class); - if (flyCamState != null) { - flyCam = new FlyByCamera(cam); - flyCam.setMoveSpeed(1f); // Set a default movement speed for the camera - flyCamState.setCamera(flyCam); // Link the FlyCamAppState to this camera instance - } - - // Register the "Exit" input mapping for the Escape key, but only for Display contexts. - if (context.getType() == Type.Display) { - inputManager.addMapping(INPUT_MAPPING_EXIT, new KeyTrigger(KeyInput.KEY_ESCAPE)); - } - - // Register the "Hide Stats" input mapping for the F5 key, if StatsAppState is active. - StatsAppState statsState = stateManager.getState(StatsAppState.class); - if (statsState != null) { - inputManager.addMapping(INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5)); - inputManager.addListener(actionListener, INPUT_MAPPING_HIDE_STATS); - } - - // Attach the action listener to the "Exit" mapping. - inputManager.addListener(actionListener, INPUT_MAPPING_EXIT); - } - - // Configure the StatsAppState if it exists. - StatsAppState statsState = stateManager.getState(StatsAppState.class); - if (statsState != null) { - statsState.setFont(guiFont); - fpsText = statsState.getFpsText(); - } - - // Call the user's application initialization code. - simpleInitApp(); - } - - @Override - public void stop(boolean waitFor) { - //noinspection AssertWithSideEffects - assert SceneGraphThreadWarden.reset(); - super.stop(waitFor); - } - - @Override - public void update() { - if (prof != null) { - prof.appStep(AppStep.BeginFrame); - } - - // Executes AppTasks from the main thread - super.update(); - - // Skip updates if paused or speed is zero - if (speed == 0 || paused) { - return; - } - - float tpf = timer.getTimePerFrame() * speed; - - // Update AppStates - if (prof != null) { - prof.appStep(AppStep.StateManagerUpdate); - } - stateManager.update(tpf); - - // Call user's per-frame update method - simpleUpdate(tpf); - - // Update scene graph nodes (logical and geometric states) - if (prof != null) { - prof.appStep(AppStep.SpatialUpdate); - } - rootNode.updateLogicalState(tpf); - guiNode.updateLogicalState(tpf); - - rootNode.updateGeometricState(); - guiNode.updateGeometricState(); - - // Render AppStates and the scene - if (prof != null) { - prof.appStep(AppStep.StateManagerRender); - } - stateManager.render(renderManager); - - if (prof != null) { - prof.appStep(AppStep.RenderFrame); - } - renderManager.render(tpf, context.isRenderable()); - // Call user's custom render method - simpleRender(renderManager); - stateManager.postRender(); - - if (prof != null) { - prof.appStep(AppStep.EndFrame); - } - } - - /** - * Controls the visibility of the frames-per-second (FPS) display on the screen. - * - * @param show `true` to display the FPS, `false` to hide it. - */ - public void setDisplayFps(boolean show) { - StatsAppState statsState = stateManager.getState(StatsAppState.class); - if (statsState != null) { - statsState.setDisplayFps(show); - } - } - - /** - * Controls the visibility of the comprehensive statistics view on the screen. - * This view typically includes details about memory, triangles, and other performance metrics. - * - * @param show `true` to display the statistics view, `false` to hide it. - */ - public void setDisplayStatView(boolean show) { - StatsAppState statsState = stateManager.getState(StatsAppState.class); - if (statsState != null) { - statsState.setDisplayStatView(show); - } - } - - public abstract void simpleInitApp(); - - /** - * An optional method that can be overridden by subclasses for per-frame update logic. - * This method is called during the application's update loop, after AppStates are updated - * and before the scene graph's logical state is updated. - * - * @param tpf The time per frame (in seconds), adjusted by the application's speed. - */ - public void simpleUpdate(float tpf) { - // Default empty implementation; subclasses can override - } - - /** - * An optional method that can be overridden by subclasses for custom rendering logic. - * This method is called during the application's render loop, after the main scene - * has been rendered and before post-rendering for states. - * Useful for drawing overlays or specific rendering tasks outside the main scene graph. - * - * @param rm The `RenderManager` instance, which provides access to rendering functionalities. - */ - public void simpleRender(RenderManager rm) { - // Default empty implementation; subclasses can override - } -} diff --git a/com/jme3/shadow/AbstractShadowFilter.java b/com/jme3/shadow/AbstractShadowFilter.java deleted file mode 100644 index 7c76b29..0000000 --- a/com/jme3/shadow/AbstractShadowFilter.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright (c) 2009-2025 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.jme3.shadow; - -import com.jme3.asset.AssetManager; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.Matrix4f; -import com.jme3.math.Vector4f; -import com.jme3.post.Filter; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.ViewPort; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.texture.FrameBuffer; -import com.jme3.util.TempVars; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; - -/** - * Generic abstract filter that holds common implementations for the different - * shadow filters - * - * @author Rémy Bouquet aka Nehon - */ -public abstract class AbstractShadowFilter extends Filter implements JmeCloneable { - - protected T shadowRenderer; - protected ViewPort viewPort; - - private final Vector4f tempVec4 = new Vector4f(); - private final Matrix4f tempMat4 = new Matrix4f(); - - /** - * For serialization only. Do not use. - */ - protected AbstractShadowFilter() { - } - - /** - * Creates an AbstractShadowFilter. Subclasses invoke this constructor. - * - * @param assetManager The application's asset manager. - * @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048). - * @param shadowRenderer The shadowRenderer to use for this Filter - */ - protected AbstractShadowFilter(AssetManager assetManager, int shadowMapSize, T shadowRenderer) { - super("Post Shadow"); - this.shadowRenderer = shadowRenderer; - // this is legacy setting for shadows with backface shadows - this.shadowRenderer.setRenderBackFacesShadows(true); - } - - @Override - protected Material getMaterial() { - return material; - } - - @Override - protected boolean isRequiresDepthTexture() { - return true; - } - - /** - * @deprecated Use {@link #getMaterial()} instead. - * @return The Material used by this filter. - */ - @Deprecated - public Material getShadowMaterial() { - return material; - } - - @Override - protected void preFrame(float tpf) { - shadowRenderer.preFrame(tpf); - Matrix4f m = viewPort.getCamera().getViewProjectionMatrix(); - material.setMatrix4("ViewProjectionMatrixInverse", tempMat4.set(m).invertLocal()); - material.setVector4("ViewProjectionMatrixRow2", tempVec4.set(m.m20, m.m21, m.m22, m.m23)); - } - - @Override - protected void postQueue(RenderQueue queue) { - shadowRenderer.postQueue(queue); - if (shadowRenderer.skipPostPass) { - // removing the shadow map so that the post pass is skipped - material.setTexture("ShadowMap0", null); - } - } - - @Override - protected void postFrame(RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer) { - if (!shadowRenderer.skipPostPass) { - shadowRenderer.setPostShadowParams(); - } - } - - @Override - protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) { - shadowRenderer.needsfallBackMaterial = true; - material = new Material(manager, "Common/MatDefs/Shadow/PostShadowFilter.j3md"); - shadowRenderer.setPostShadowMaterial(material); - shadowRenderer.initialize(renderManager, vp); - this.viewPort = vp; - } - - /** - * How far the shadows are rendered in the view - * - * @return shadowZExtend - * @see #setShadowZExtend(float zFar) - */ - public float getShadowZExtend() { - return shadowRenderer.getShadowZExtend(); - } - - /** - * Set the distance from the eye where the shadows will be rendered default - * value is dynamically computed to the shadow casters/receivers union bound - * zFar, capped to view frustum far value. - * - * @param zFar the zFar values that override the computed one - */ - public void setShadowZExtend(float zFar) { - shadowRenderer.setShadowZExtend(zFar); - } - - /** - * Define the length over which the shadow will fade out when using a - * shadowZextend - * - * @param length the fade length in world units - */ - public void setShadowZFadeLength(float length) { - shadowRenderer.setShadowZFadeLength(length); - } - - /** - * get the length over which the shadow will fade out when using a - * shadowZextend - * - * @return the fade length in world units - */ - public float getShadowZFadeLength() { - return shadowRenderer.getShadowZFadeLength(); - } - - /** - * returns the shadow intensity - * - * @see #setShadowIntensity(float shadowIntensity) - * @return shadowIntensity - */ - public float getShadowIntensity() { - return shadowRenderer.getShadowIntensity(); - } - - /** - * Set the shadowIntensity, the value should be between 0 and 1, a 0 value - * gives a bright and invisible shadow, a 1 value gives a pitch black - * shadow, default is 0.7 - * - * @param shadowIntensity the darkness of the shadow - */ - final public void setShadowIntensity(float shadowIntensity) { - shadowRenderer.setShadowIntensity(shadowIntensity); - } - - /** - * returns the edges thickness
- * - * @see #setEdgesThickness(int edgesThickness) - * @return edgesThickness - */ - public int getEdgesThickness() { - return shadowRenderer.getEdgesThickness(); - } - - /** - * Sets the shadow edges thickness. Default is 10. Setting it to lower values - * can help reduce the jagged effect of shadow edges. - * - * @param edgesThickness the desired thickness (in tenths of a pixel, default=10) - */ - public void setEdgesThickness(int edgesThickness) { - shadowRenderer.setEdgesThickness(edgesThickness); - } - - /** - * isFlushQueues does nothing and is kept only for backward compatibility - * - * @return false - */ - @Deprecated - public boolean isFlushQueues() { - return shadowRenderer.isFlushQueues(); - } - - /** - * sets the shadow compare mode see {@link CompareMode} for more info - * - * @param compareMode the desired mode - */ - final public void setShadowCompareMode(CompareMode compareMode) { - shadowRenderer.setShadowCompareMode(compareMode); - } - - /** - * returns the shadow compare mode - * - * @see CompareMode - * @return the shadowCompareMode - */ - public CompareMode getShadowCompareMode() { - return shadowRenderer.getShadowCompareMode(); - } - - /** - * Sets the filtering mode for shadow edges see {@link EdgeFilteringMode} - * for more info - * - * @param filterMode the desired mode - */ - final public void setEdgeFilteringMode(EdgeFilteringMode filterMode) { - shadowRenderer.setEdgeFilteringMode(filterMode); - } - - /** - * - * !! WARNING !! this parameter is defaulted to true for the ShadowFilter. - * Setting it to true, may produce edges artifacts on shadows. - * - * Set to true if you want back faces shadows on geometries. - * Note that back faces shadows will be blended over dark lighten areas and may produce overly dark lighting. - * - * Setting this parameter will override this parameter for ALL materials in the scene. - * This also will automatically adjust the faceCullMode and the PolyOffset of the pre shadow pass. - * You can modify them by using {@link #getPreShadowForcedRenderState()} - * - * If you want to set it differently for each material in the scene you have to use the ShadowRenderer instead - * of the shadow filter. - * - * @param renderBackFacesShadows true or false. - */ - public void setRenderBackFacesShadows(boolean renderBackFacesShadows) { - shadowRenderer.setRenderBackFacesShadows(renderBackFacesShadows); - } - - /** - * if this filter renders back faces shadows - * @return true if this filter renders back faces shadows - */ - public boolean isRenderBackFacesShadows() { - return shadowRenderer.isRenderBackFacesShadows(); - } - - /** - * returns the pre shadows pass render state. - * use it to adjust the RenderState parameters of the pre shadow pass. - * Note that this will be overridden if the preShadow technique in the material has a ForcedRenderState - * @return the pre shadow render state. - */ - public RenderState getPreShadowForcedRenderState() { - return shadowRenderer.getPreShadowForcedRenderState(); - } - - /** - * returns the edge filtering mode - * - * @see EdgeFilteringMode - * @return the enum value - */ - public EdgeFilteringMode getEdgeFilteringMode() { - return shadowRenderer.getEdgeFilteringMode(); - } - - /** - * Read the number of shadow maps rendered by this filter. - * - * @return count - */ - public int getNumShadowMaps() { - return shadowRenderer.getNumShadowMaps(); - } - - /** - * Read the size of each shadow map rendered by this filter. - * - * @return a map's height (which is also its width, in pixels) - */ - public int getShadowMapSize() { - return shadowRenderer.getShadowMapSize(); - } - - @Override - @SuppressWarnings("unchecked") - public AbstractShadowFilter jmeClone() { - try { - return (AbstractShadowFilter) super.clone(); - } catch (final CloneNotSupportedException e) { - throw new RuntimeException(e); - } - } - - @Override - public void cloneFields(final Cloner cloner, final Object original) { - material = cloner.clone(material); - shadowRenderer = cloner.clone(shadowRenderer); - shadowRenderer.setPostShadowMaterial(material); - } - -} diff --git a/com/jme3/shadow/DirectionalLightShadowFilter.java b/com/jme3/shadow/DirectionalLightShadowFilter.java deleted file mode 100644 index 4438e37..0000000 --- a/com/jme3/shadow/DirectionalLightShadowFilter.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2009-2024 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.jme3.shadow; - -import com.jme3.asset.AssetManager; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; -import com.jme3.light.DirectionalLight; -import java.io.IOException; - -/** - * This Filter does basically the same as a DirectionalLightShadowRenderer - * except it renders the post shadow pass as a fullscreen quad pass instead of a - * geometry pass. It's mostly faster than PssmShadowRenderer as long as you have - * more than about ten shadow receiving objects. The expense is the drawback - * that the shadow Receive mode set on spatial is ignored. So basically all and - * only objects that render depth in the scene receive shadows. - * - * API is basically the same as the PssmShadowRenderer. - * - * @author Rémy Bouquet aka Nehon - */ -public class DirectionalLightShadowFilter extends AbstractShadowFilter { - - /** - * For serialization only. Do not use. - * - * @see #DirectionalLightShadowFilter(AssetManager assetManager, int shadowMapSize, int nbSplits) - */ - public DirectionalLightShadowFilter() { - super(); - } - - /** - * Creates a DirectionalLightShadowFilter. - * - * @param assetManager the application's asset manager - * @param shadowMapSize the size of the rendered shadow maps (512, 1024, 2048, etc...) - * @param nbSplits the number of shadow maps rendered (more shadow maps = better quality, but slower) - * - * @throws IllegalArgumentException if the provided 'nbSplits' is not within the valid range of 1 to 4. - */ - public DirectionalLightShadowFilter(AssetManager assetManager, int shadowMapSize, int nbSplits) { - super(assetManager, shadowMapSize, new DirectionalLightShadowRenderer(assetManager, shadowMapSize, nbSplits)); - } - - /** - * Returns the light used to cast shadows. - * - * @return the DirectionalLight - */ - public DirectionalLight getLight() { - return shadowRenderer.getLight(); - } - - /** - * Sets the light to use to cast shadows. - * - * @param light a DirectionalLight - */ - public void setLight(DirectionalLight light) { - shadowRenderer.setLight(light); - } - - /** - * Returns the lambda parameter. - * - * @see #setLambda(float lambda) - * @return lambda - */ - public float getLambda() { - return shadowRenderer.getLambda(); - } - - /** - * Adjusts the partition of the shadow extend into shadow maps. Lambda is - * usually between 0 and 1. - *

- * A low value gives a more linear partition, resulting in consistent shadow - * quality over the extend, but near shadows could look very jagged. A high - * value gives a more logarithmic partition, resulting in high quality for near - * shadows, but quality decreases rapidly with distance. - *

- * The default value is 0.65 (the theoretical optimum). - * - * @param lambda the lambda value - */ - public void setLambda(float lambda) { - shadowRenderer.setLambda(lambda); - } - - /** - * Returns true if stabilization is enabled. - * - * @return true if stabilization is enabled - */ - public boolean isEnabledStabilization() { - return shadowRenderer.isEnabledStabilization(); - } - - /** - * Enables the stabilization of the shadow's edges. (default is true) - * This prevents shadow edges from flickering when the camera moves. - * However, it can lead to some loss of shadow quality in particular scenes. - * - * @param stabilize true to stabilize, false to disable stabilization - */ - public void setEnabledStabilization(boolean stabilize) { - shadowRenderer.setEnabledStabilization(stabilize); - } - - @Override - public void write(JmeExporter ex) throws IOException { - super.write(ex); - OutputCapsule oc = ex.getCapsule(this); - oc.write(shadowRenderer, "shadowRenderer", null); - } - - @Override - public void read(JmeImporter im) throws IOException { - super.read(im); - InputCapsule ic = im.getCapsule(this); - shadowRenderer = (DirectionalLightShadowRenderer) ic.readSavable("shadowRenderer", null); - } - -}