Weiter am Wettersystem gearbeitet, ordentliche Wolken und Regen etc ergänzt

This commit is contained in:
2026-07-24 11:41:05 +02:00
parent 5a7d9897e4
commit 919f70bb44
47 changed files with 1665 additions and 1073 deletions

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -6,8 +6,10 @@ MaterialDef Grass {
Texture2D NormalMap
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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);
}

View File

@@ -2,6 +2,7 @@ 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);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -2,6 +2,7 @@ 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 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);
}

View File

@@ -19,6 +19,8 @@ uniform sampler2DArray m_NormalArray;
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;
@@ -110,10 +112,16 @@ void main() {
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
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

View File

@@ -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)

View File

@@ -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,7 +622,8 @@ public class EditorApp extends Application {
drawCompass();
// Spiel-Konsole: gepufferte Zeilen gebündelt ausgeben (max 200 auf einmal)
if (!consoleBuffer.isEmpty() && gameConsoleArea != null) {
if (!consoleBuffer.isEmpty()) {
if (gameConsoleArea != null) {
StringBuilder sb = new StringBuilder();
int count = 0;
String ln;
@@ -631,6 +633,9 @@ public class EditorApp extends Application {
}
gameConsoleArea.appendText(sb.toString());
gameConsoleArea.setScrollTop(Double.MAX_VALUE);
} else {
consoleBuffer.clear();
}
}
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);

View File

@@ -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
}
}

View File

@@ -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 <sunny|cloudy|overcast|storm>";
+ " | 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(", "));
}
});

View File

@@ -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) {

View File

@@ -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;
}
}
}

View File

@@ -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,12 +316,67 @@ 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 ─────────────────────────────────────────────────────────
@@ -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));
}
}
}
}

View File

@@ -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 ──────────────────────────────────────────────────────────────

View File

@@ -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 ──────────────────────────────────────────────────────────────

View File

@@ -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;
}
}

View File

@@ -30,6 +30,7 @@ public class StoneWorldState extends BaseAppState {
private BulletAppState bulletAppState;
private TerrainChunkState terrainChunkState;
private Node stoneRoot;
private final List<Material> 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);

View File

@@ -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 ───────────────────────────────────────────────────────

View File

@@ -6,110 +6,287 @@ 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 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;
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);
// ── 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 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 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 initialize(Application app) {
load();
}
@Override
protected void cleanup(Application app) {
save();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@@ -117,10 +294,15 @@ public class WeatherState extends BaseAppState {
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);
@@ -130,10 +312,16 @@ public class WeatherState extends BaseAppState {
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);
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);
// 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);
@@ -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);
@@ -155,28 +342,115 @@ public class WeatherState extends BaseAppState {
waterFilter.setWindDirection(
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);
}

View File

@@ -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);
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -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.
*
* <p>By default, `SimpleApplication` attaches several essential {@link com.jme3.app.state.AppState} instances:
* <ul>
* <li>{@link com.jme3.app.StatsAppState}: Displays real-time frames-per-second (FPS) and
* detailed performance statistics on-screen.</li>
* <li>{@link com.jme3.app.FlyCamAppState}: Provides a convenient first-person fly-by camera
* controller, allowing easy navigation within the scene.</li>
* <li>{@link com.jme3.audio.AudioListenerState}: Manages the audio listener, essential for 3D sound.</li>
* <li>{@link com.jme3.app.DebugKeysAppState}: Enables debug functionalities like displaying
* camera position and memory usage in the console.</li>
* <li>{@link com.jme3.app.state.ConstantVerifierState}: A utility state for verifying constant
* values, primarily for internal engine debugging.</li>
* </ul>
*
* <p><b>Default Key Bindings:</b></p>
* <ul>
* <li><b>Esc:</b> Closes and exits the application.</li>
* <li><b>F5:</b> Toggles the visibility of the statistics view (FPS and debug stats).</li>
* <li><b>C:</b> Prints the current camera position and rotation to the console.</li>
* <li><b>M:</b> Prints memory usage statistics to the console.</li>
* </ul>
*
* <p>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
}
}

View File

@@ -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<T extends AbstractShadowRenderer> 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 <br>
*
* @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<T> jmeClone() {
try {
return (AbstractShadowFilter<T>) 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);
}
}

View File

@@ -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<DirectionalLightShadowRenderer> {
/**
* 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.
* <p>
* 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.
* <p>
* 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);
}
}