Weiter an den Dialogen gearbeitet

This commit is contained in:
2026-07-09 21:31:37 +02:00
parent 8bcee4491e
commit f109241e1f
50 changed files with 2413 additions and 413 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,475 @@
#import "Common/ShaderLib/GLSLCompat.glsllib"
#import "Common/ShaderLib/MultiSample.glsllib"
#import "Common/ShaderLib/WaterUtil.glsllib"
// Water pixel shader
// Copyright (C) JMonkeyEngine 3.0
// by Remy Bouquet (nehon) for JMonkeyEngine 3.0
// original HLSL version by Wojciech Toman 2009
uniform COLORTEXTURE m_Texture;
uniform DEPTHTEXTURE m_DepthTexture;
uniform sampler2D m_HeightMap;
uniform sampler2D m_NormalMap;
uniform sampler2D m_FoamMap;
uniform sampler2D m_CausticsMap;
uniform sampler2D m_ReflectionMap;
uniform mat4 g_ViewProjectionMatrixInverse;
uniform mat4 m_TextureProjMatrix;
uniform vec3 m_CameraPosition;
uniform float m_WaterHeight;
uniform float m_Time;
uniform float m_WaterTransparency;
uniform float m_NormalScale;
uniform float m_R0;
uniform float m_MaxAmplitude;
uniform vec3 m_LightDir;
uniform vec4 m_LightColor;
uniform float m_ShoreHardness;
uniform float m_FoamHardness;
uniform float m_RefractionStrength;
uniform vec3 m_FoamExistence;
uniform vec3 m_ColorExtinction;
uniform float m_Shininess;
uniform vec4 m_WaterColor;
uniform vec4 m_DeepWaterColor;
uniform vec2 m_WindDirection;
uniform float m_SunScale;
uniform float m_WaveScale;
uniform float m_UnderWaterFogDistance;
uniform float m_CausticsIntensity;
#ifdef ENABLE_AREA
uniform vec3 m_Center;
uniform float m_Radius;
#endif
#ifdef WAVE_INTERACTION
uniform sampler2D m_WaveMap;
uniform vec2 m_WaveAreaCenter;
uniform float m_WaveAreaInvExtent;
uniform float m_WaveStrength;
#endif
vec2 scale; // = vec2(m_WaveScale, m_WaveScale);
float refractionScale; // = m_WaveScale;
// Modifies 4 sampled normals. Increase first values to have more
// smaller "waves" or last to have more bigger "waves"
const vec4 normalModifier = vec4(3.0, 2.0, 4.0, 10.0);
// Strength of displacement along normal.
uniform float m_ReflectionDisplace;
// Water transparency along eye vector.
const float visibility = 3.0;
// foam intensity
uniform float m_FoamIntensity ;
vec2 m_FrustumNearFar; //=vec2(1.0,m_UnderWaterFogDistance);
const float LOG2 = 1.442695;
varying vec2 texCoord;
void setGlobals(){
scale = vec2(m_WaveScale, m_WaveScale);
refractionScale = m_WaveScale;
m_FrustumNearFar=vec2(1.0,m_UnderWaterFogDistance);
}
mat3 MatrixInverse(in mat3 inMatrix){
float det = dot(cross(inMatrix[0], inMatrix[1]), inMatrix[2]);
mat3 T = transpose(inMatrix);
return mat3(cross(T[1], T[2]),
cross(T[2], T[0]),
cross(T[0], T[1])) / det;
}
mat3 computeTangentFrame(in vec3 N, in vec3 P, in vec2 UV) {
vec3 dp1 = dFdx(P);
vec3 dp2 = dFdy(P);
vec2 duv1 = dFdx(UV);
vec2 duv2 = dFdy(UV);
// solve the linear system
vec3 dp1xdp2 = cross(dp1, dp2);
mat2x3 inverseM = mat2x3(cross(dp2, dp1xdp2), cross(dp1xdp2, dp1));
vec3 T = inverseM * vec2(duv1.x, duv2.x);
vec3 B = inverseM * vec2(duv1.y, duv2.y);
// construct tangent frame
float maxLength = max(length(T), length(B));
T = T / maxLength;
B = B / maxLength;
return mat3(T, B, N);
}
float saturate(in float val){
return clamp(val,0.0,1.0);
}
vec3 saturate(in vec3 val){
return clamp(val,vec3(0.0),vec3(1.0));
}
vec3 getPosition(in float depth, in vec2 uv){
vec4 pos = vec4(uv, depth, 1.0) * 2.0 - 1.0;
pos = g_ViewProjectionMatrixInverse * pos;
return pos.xyz / pos.w;
}
// Function calculating fresnel term.
// - normal - normalized normal vector
// - eyeVec - normalized eye vector
float fresnelTerm(in vec3 normal,in vec3 eyeVec){
float angle = 1.0 - max(0.0, dot(normal, eyeVec));
float fresnel = angle * angle;
fresnel = fresnel * fresnel;
fresnel = fresnel * angle;
return saturate(fresnel * (1.0 - saturate(m_R0)) + m_R0 - m_RefractionStrength);
}
vec4 underWater(int sampleNum){
float sceneDepth = fetchTextureSample(m_DepthTexture, texCoord, sampleNum).r;
vec3 color2 = fetchTextureSample(m_Texture, texCoord, sampleNum).rgb;
vec3 position = getPosition(sceneDepth, texCoord);
float level = m_WaterHeight;
vec3 eyeVec = position - m_CameraPosition;
// Find intersection with water surface
vec3 eyeVecNorm = normalize(eyeVec);
float t = (level - m_CameraPosition.y) / eyeVecNorm.y;
vec3 surfacePoint = m_CameraPosition + eyeVecNorm * t;
vec2 texC = vec2(0.0);
float cameraDepth = length(m_CameraPosition - surfacePoint);
texC = (surfacePoint.xz + eyeVecNorm.xz) * scale + m_Time * 0.03 * m_WindDirection;
float bias = texture2D(m_HeightMap, texC).r;
level += bias * m_MaxAmplitude;
t = (level - m_CameraPosition.y) / eyeVecNorm.y;
surfacePoint = m_CameraPosition + eyeVecNorm * t;
eyeVecNorm = normalize(m_CameraPosition - surfacePoint);
#if __VERSION__ >= 130
// Find normal of water surface
float normal1 = textureOffset(m_HeightMap, texC, ivec2(-1.0, 0.0)).r;
float normal2 = textureOffset(m_HeightMap, texC, ivec2( 1.0, 0.0)).r;
float normal3 = textureOffset(m_HeightMap, texC, ivec2( 0.0, -1.0)).r;
float normal4 = textureOffset(m_HeightMap, texC, ivec2( 0.0, 1.0)).r;
#else
// Find normal of water surface
float normal1 = texture2D(m_HeightMap, (texC + vec2(-1.0, 0.0) / 256.0)).r;
float normal2 = texture2D(m_HeightMap, (texC + vec2(1.0, 0.0) / 256.0)).r;
float normal3 = texture2D(m_HeightMap, (texC + vec2(0.0, -1.0) / 256.0)).r;
float normal4 = texture2D(m_HeightMap, (texC + vec2(0.0, 1.0) / 256.0)).r;
#endif
vec3 myNormal = normalize(vec3((normal1 - normal2) * m_MaxAmplitude,m_NormalScale,(normal3 - normal4) * m_MaxAmplitude));
vec3 normal = myNormal*-1.0;
float fresnel = fresnelTerm(normal, eyeVecNorm);
vec3 refraction = color2;
#ifdef ENABLE_REFRACTION
texC = texCoord.xy *sin (fresnel+1.0);
texC = clamp(texC,0.0,1.0);
refraction = fetchTextureSample(m_Texture, texC, sampleNum).rgb;
#endif
float waterCol = saturate(length(m_LightColor.rgb) / m_SunScale);
refraction = mix(mix(refraction, m_DeepWaterColor.rgb * waterCol, m_WaterTransparency), m_WaterColor.rgb* waterCol,m_WaterTransparency);
vec3 foam = vec3(0.0);
#ifdef ENABLE_FOAM
texC = (surfacePoint.xz + eyeVecNorm.xz * 0.1) * 0.05 + m_Time * 0.05 * m_WindDirection + sin(m_Time * 0.001 + position.x) * 0.005;
vec2 texCoord2 = (surfacePoint.xz + eyeVecNorm.xz * 0.1) * 0.05 + m_Time * 0.1 * m_WindDirection + sin(m_Time * 0.001 + position.z) * 0.005;
if(m_MaxAmplitude - m_FoamExistence.z> 0.0001){
foam += ((texture2D(m_FoamMap, texC) + texture2D(m_FoamMap, texCoord2)) * m_FoamIntensity * m_FoamIntensity * 0.3 *
saturate((level - (m_WaterHeight + m_FoamExistence.z)) / (m_MaxAmplitude - m_FoamExistence.z))).rgb;
}
foam *= m_LightColor.rgb;
#endif
vec3 specular = vec3(0.0);
vec3 color ;
float fogFactor;
if(position.y>level){
#ifdef ENABLE_SPECULAR
if(step(0.9999,sceneDepth)==1.0){
vec3 lightDir=normalize(m_LightDir);
vec3 mirrorEye = (2.0 * dot(eyeVecNorm, normal) * normal - eyeVecNorm);
float dotSpec = saturate(dot(mirrorEye.xyz, -lightDir) * 0.5 + 0.5);
specular = vec3((1.0 - fresnel) * saturate(-lightDir.y) * ((pow(dotSpec, 512.0)) * (m_Shininess * 1.8 + 0.2)));
specular += specular * 25.0 * saturate(m_Shininess - 0.05);
specular=specular * m_LightColor.rgb * 100.0;
}
#endif
float fogIntensity= 8.0 * m_WaterTransparency;
fogFactor = exp2( -fogIntensity * fogIntensity * cameraDepth * 0.03 * LOG2 );
fogFactor = clamp(fogFactor, 0.0, 1.0);
color =mix(m_DeepWaterColor.rgb,refraction,fogFactor);
specular=specular*fogFactor;
color = saturate(color + max(specular, foam ));
}else{
vec3 caustics = vec3(0.0);
#ifdef ENABLE_CAUSTICS
vec2 windDirection=m_WindDirection;
texC = (position.xz + eyeVecNorm.xz * 0.1) * 0.05 + m_Time * 0.05 * windDirection + sin(m_Time + position.x) * 0.01;
vec2 texCoord2 = (position.xz + eyeVecNorm.xz * 0.1) * 0.05 + m_Time * 0.05 * windDirection + sin(m_Time + position.z) * 0.01;
caustics += (texture2D(m_CausticsMap, texC)+ texture2D(m_CausticsMap, texCoord2)).rgb;
caustics=saturate(mix(m_WaterColor.rgb,caustics,m_CausticsIntensity));
color=mix(color2,caustics,m_CausticsIntensity);
#else
color=color2;
#endif
float fogDepth= (2.0 * m_FrustumNearFar.x) / (m_FrustumNearFar.y + m_FrustumNearFar.x - sceneDepth* (m_FrustumNearFar.y-m_FrustumNearFar.x));
float fogIntensity= 18.0 * m_WaterTransparency;
fogFactor = exp2( -fogIntensity * fogIntensity * fogDepth * fogDepth * LOG2 );
fogFactor = clamp(fogFactor, 0.0, 1.0);
color =mix(m_DeepWaterColor.rgb,color,fogFactor);
}
return vec4(color, 1.0);
}
// NOTE: This will be called even for single-sampling
vec4 main_multiSample(int sampleNum){
// If we are underwater let's call the underwater function
if(m_WaterHeight >= m_CameraPosition.y){
#ifdef ENABLE_AREA
if(isOverExtent(m_CameraPosition, m_Center, m_Radius)){
return fetchTextureSample(m_Texture, texCoord, sampleNum);
}
#endif
return underWater(sampleNum);
}
float sceneDepth = fetchTextureSample(m_DepthTexture, texCoord, sampleNum).r;
vec3 color2 = fetchTextureSample(m_Texture, texCoord, sampleNum).rgb;
vec3 color = color2;
vec3 position = getPosition(sceneDepth, texCoord);
#ifdef ENABLE_AREA
if(isOverExtent(position, m_Center, m_Radius)){
return vec4(color2, 1.0);
}
#endif
float level = m_WaterHeight;
float isAtFarPlane = step(0.99998, sceneDepth);
//#ifndef ENABLE_RIPPLES
// This optimization won't work on NVIDIA cards if ripples are enabled
if(position.y > level + m_MaxAmplitude + isAtFarPlane * 100.0){
return vec4(color2, 1.0);
}
//#endif
vec3 eyeVec = position - m_CameraPosition;
float cameraDepth = m_CameraPosition.y - position.y;
// Find intersection with water surface
vec3 eyeVecNorm = normalize(eyeVec);
float t = (level - m_CameraPosition.y) / eyeVecNorm.y;
vec3 surfacePoint = m_CameraPosition + eyeVecNorm * t;
vec2 texC = vec2(0.0);
int samples = 1;
#ifdef ENABLE_HQ_SHORELINE
samples = 10;
#endif
float biasFactor = 1.0 / float(samples);
for (int i = 0; i < samples; i++){
texC = (surfacePoint.xz + eyeVecNorm.xz * biasFactor) * scale + m_Time * 0.03 * m_WindDirection;
float bias = texture2D(m_HeightMap, texC).r;
bias *= biasFactor;
level += bias * m_MaxAmplitude;
t = (level - m_CameraPosition.y) / eyeVecNorm.y;
surfacePoint = m_CameraPosition + eyeVecNorm * t;
}
float depth = length(position - surfacePoint);
float depth2 = surfacePoint.y - position.y;
// XXX: HACK ALERT: Increase water depth to infinity if at far plane
// Prevents "foam on horizon" issue
// For best results, replace the "100.0" below with the
// highest value in the m_ColorExtinction vec3
depth += isAtFarPlane * 100.0;
depth2 += isAtFarPlane * 100.0;
eyeVecNorm = normalize(m_CameraPosition - surfacePoint);
#if __VERSION__ >= 130
// Find normal of water surface
float normal1 = textureOffset(m_HeightMap, texC, ivec2(-1.0, 0.0)).r;
float normal2 = textureOffset(m_HeightMap, texC, ivec2( 1.0, 0.0)).r;
float normal3 = textureOffset(m_HeightMap, texC, ivec2( 0.0, -1.0)).r;
float normal4 = textureOffset(m_HeightMap, texC, ivec2( 0.0, 1.0)).r;
#else
// Find normal of water surface
float normal1 = texture2D(m_HeightMap, (texC + vec2(-1.0, 0.0) / 256.0)).r;
float normal2 = texture2D(m_HeightMap, (texC + vec2(1.0, 0.0) / 256.0)).r;
float normal3 = texture2D(m_HeightMap, (texC + vec2(0.0, -1.0) / 256.0)).r;
float normal4 = texture2D(m_HeightMap, (texC + vec2(0.0, 1.0) / 256.0)).r;
#endif
vec3 myNormal = normalize(vec3((normal1 - normal2) * m_MaxAmplitude,m_NormalScale,(normal3 - normal4) * m_MaxAmplitude));
vec3 normal = vec3(0.0);
#ifdef ENABLE_RIPPLES
texC = surfacePoint.xz * 0.8 + m_WindDirection * m_Time* 1.6;
mat3 tangentFrame = computeTangentFrame(myNormal, eyeVecNorm, texC);
vec3 normal0a = normalize(tangentFrame*(2.0 * texture2D(m_NormalMap, texC).xyz - 1.0));
texC = surfacePoint.xz * 0.4 + m_WindDirection * m_Time* 0.8;
tangentFrame = computeTangentFrame(myNormal, eyeVecNorm, texC);
vec3 normal1a = normalize(tangentFrame*(2.0 * texture2D(m_NormalMap, texC).xyz - 1.0));
texC = surfacePoint.xz * 0.2 + m_WindDirection * m_Time * 0.4;
tangentFrame = computeTangentFrame(myNormal, eyeVecNorm, texC);
vec3 normal2a = normalize(tangentFrame*(2.0 * texture2D(m_NormalMap, texC).xyz - 1.0));
texC = surfacePoint.xz * 0.1 + m_WindDirection * m_Time * 0.2;
tangentFrame = computeTangentFrame(myNormal, eyeVecNorm, texC);
vec3 normal3a = normalize(tangentFrame*(2.0 * texture2D(m_NormalMap, texC).xyz - 1.0));
normal = normalize(normal0a * normalModifier.x + normal1a * normalModifier.y +normal2a * normalModifier.z + normal3a * normalModifier.w);
#if __VERSION__ >= 130 && !defined GL_ES
// XXX: Here's another way to fix the terrain edge issue,
// But it requires GLSL 1.3 and still looks kinda incorrect
// around edges
normal = isnan(normal.x) ? myNormal : normal;
#else
// To make the shader 1.2 compatible we use a trick :
// we clamp the x value of the normal and compare it to it's former value instead of using isnan.
normal = clamp(normal.x,0.0,1.0)!=normal.x ? myNormal : normal;
#endif
#else
normal = myNormal;
#endif
// Overlay dynamic wave normals from CPU simulation (WAVE_INTERACTION define)
#ifdef WAVE_INTERACTION
vec2 waveUV = (surfacePoint.xz - m_WaveAreaCenter) * m_WaveAreaInvExtent + 0.5;
float inWave = step(0.0, waveUV.x) * step(waveUV.x, 1.0)
* step(0.0, waveUV.y) * step(waveUV.y, 1.0);
float ts = 1.0 / 256.0;
float wl = texture2D(m_WaveMap, waveUV + vec2(-ts, 0.0)).r * 2.0 - 1.0;
float wr = texture2D(m_WaveMap, waveUV + vec2( ts, 0.0)).r * 2.0 - 1.0;
float wd = texture2D(m_WaveMap, waveUV + vec2(0.0, -ts)).r * 2.0 - 1.0;
float wu = texture2D(m_WaveMap, waveUV + vec2(0.0, ts)).r * 2.0 - 1.0;
vec3 waveNorm = normalize(vec3((wl - wr) * m_WaveStrength, 1.0, (wd - wu) * m_WaveStrength));
normal = normalize(mix(normal, normalize(normal + waveNorm), inWave));
#endif
vec3 refraction = color2;
#ifdef ENABLE_REFRACTION
// texC = texCoord.xy+ m_ReflectionDisplace * normal.x;
texC = texCoord.xy;
texC += sin(m_Time*1.8 + 3.0 * abs(position.y))* (refractionScale * min(depth2, 1.0));
texC = clamp(texC,vec2(0.0),vec2(0.999));
refraction = fetchTextureSample(m_Texture, texC, sampleNum).rgb;
#endif
vec3 waterPosition = surfacePoint.xyz;
waterPosition.y -= (level - m_WaterHeight);
vec4 texCoordProj = m_TextureProjMatrix * vec4(waterPosition, 1.0);
texCoordProj.x = texCoordProj.x + m_ReflectionDisplace * normal.x;
texCoordProj.z = texCoordProj.z + m_ReflectionDisplace * normal.z;
texCoordProj /= texCoordProj.w;
texCoordProj.y = 1.0 - texCoordProj.y;
vec3 reflection = texture2D(m_ReflectionMap, texCoordProj.xy).rgb;
float fresnel = fresnelTerm(normal, eyeVecNorm);
float depthN = depth * m_WaterTransparency;
float waterCol = saturate(length(m_LightColor.rgb) / m_SunScale);
refraction = mix(mix(refraction, m_WaterColor.rgb * waterCol, saturate(depthN / visibility)),
m_DeepWaterColor.rgb * waterCol, saturate(depth2 / m_ColorExtinction));
vec3 foam = vec3(0.0);
#ifdef ENABLE_FOAM
texC = (surfacePoint.xz + eyeVecNorm.xz * 0.1) * 0.05 + m_Time * 0.05 * m_WindDirection + sin(m_Time * 0.001 + position.x) * 0.005;
vec2 texCoord2 = (surfacePoint.xz + eyeVecNorm.xz * 0.1) * 0.05 + m_Time * 0.1 * m_WindDirection + sin(m_Time * 0.001 + position.z) * 0.005;
vec4 foam1 = texture2D(m_FoamMap, texC);
vec4 foam2 = texture2D(m_FoamMap, texCoord2);
if(depth2 < m_FoamExistence.x){
foam = (foam1.r + foam2).rgb * vec3(m_FoamIntensity);
}else if(depth2 < m_FoamExistence.y){
foam = mix((foam1 + foam2) * m_FoamIntensity , vec4(0.0),
(depth2 - m_FoamExistence.x) / (m_FoamExistence.y - m_FoamExistence.x)).rgb;
}
if(m_MaxAmplitude - m_FoamExistence.z> 0.0001){
foam += ((foam1 + foam2) * m_FoamIntensity * m_FoamIntensity * 0.3 *
saturate((level - (m_WaterHeight + m_FoamExistence.z)) / (m_MaxAmplitude - m_FoamExistence.z))).rgb;
}
foam *= m_LightColor.rgb;
#endif
vec3 specular = vec3(0.0);
#ifdef ENABLE_SPECULAR
vec3 lightDir=normalize(m_LightDir);
vec3 mirrorEye = (2.0 * dot(eyeVecNorm, normal) * normal - eyeVecNorm);
float dotSpec = saturate(dot(mirrorEye.xyz, -lightDir) * 0.5 + 0.5);
specular = vec3((1.0 - fresnel) * saturate(-lightDir.y) * ((pow(dotSpec, 512.0)) * (m_Shininess * 1.8 + 0.2)));
specular += specular * 25.0 * saturate(m_Shininess - 0.05);
//foam does not shine
specular=specular * m_LightColor.rgb - (5.0 * foam);
#endif
color = mix(refraction, reflection, fresnel);
color = mix(refraction, color, saturate(depth * m_ShoreHardness));
color = saturate(color + max(specular, foam ));
color = mix(refraction, color, saturate(depth* m_FoamHardness));
// XXX: HACK ALERT:
// We trick the GeForces to think they have
// to calculate the derivatives for all these pixels by using step()!
// That way we won't get pixels around the edges of the terrain,
// Where the derivatives are undefined
return vec4(mix(color, color2, step(level, position.y)), 1.0);
}
void main(){
setGlobals();
#ifdef RESOLVE_MS
vec4 color = vec4(0.0);
for (int i = 0; i < m_NumSamples; i++){
color += main_multiSample(i);
}
gl_FragColor = color / float(m_NumSamples);
#else
gl_FragColor = main_multiSample(0);
#endif
}

View File

@@ -0,0 +1,84 @@
MaterialDef Advanced Water {
MaterialParameters {
Int BoundDrawBuffer
Int NumSamples
Int NumSamplesDepth
Texture2D FoamMap
Texture2D CausticsMap
Texture2D NormalMap -LINEAR
Texture2D ReflectionMap
Texture2D HeightMap -LINEAR
Texture2D Texture
Texture2D DepthTexture
Vector3 CameraPosition
Float Time
Vector3 frustumCorner
Matrix4 TextureProjMatrix
Float WaterHeight
Vector3 LightDir
Float WaterTransparency
Float NormalScale
Float R0
Float MaxAmplitude
Color LightColor
Float ShoreHardness
Float FoamHardness
Float RefractionStrength
Float WaveScale
Vector3 FoamExistence
Float SunScale
Vector3 ColorExtinction
Float Shininess
Color WaterColor
Color DeepWaterColor
Vector2 WindDirection
Float ReflectionDisplace
Float FoamIntensity
Float CausticsIntensity
Float UnderWaterFogDistance
Boolean UseRipples
Boolean UseHQShoreline
Boolean UseSpecular
Boolean UseFoam
Boolean UseCaustics
Boolean UseRefraction
Float Radius
Vector3 Center
Boolean SquareArea
// Dynamic wave interaction
Texture2D WaveMap -LINEAR
Vector2 WaveAreaCenter
Float WaveAreaInvExtent
Float WaveStrength
Boolean WaveInteraction
}
Technique {
VertexShader GLSL310 GLSL300 GLSL150 GLSL120 : Common/MatDefs/Post/Post.vert
FragmentShader GLSL310 GLSL300 GLSL150 GLSL120: Common/MatDefs/Water/Water.frag
WorldParameters {
ViewProjectionMatrixInverse
}
Defines {
BOUND_DRAW_BUFFER: BoundDrawBuffer
RESOLVE_MS : NumSamples
RESOLVE_DEPTH_MS : NumSamplesDepth
ENABLE_RIPPLES : UseRipples
ENABLE_HQ_SHORELINE : UseHQShoreline
ENABLE_SPECULAR : UseSpecular
ENABLE_FOAM : UseFoam
ENABLE_CAUSTICS : UseCaustics
ENABLE_REFRACTION : UseRefraction
ENABLE_AREA : Center
SQUARE_AREA : SquareArea
WAVE_INTERACTION : WaveInteraction
}
}
}

View File

@@ -4,6 +4,7 @@ MaterialDef Fern {
Color Diffuse (Color) : 0.18 0.60 0.10 1.0 Color Diffuse (Color) : 0.18 0.60 0.10 1.0
Float WindStrength : 0.15 Float WindStrength : 0.15
Float WindSpeed : 0.6 Float WindSpeed : 0.6
Vector2 WindDir : 0.0 1.0
Texture2D DiffuseMap Texture2D DiffuseMap
Boolean HasDiffuseMap : false Boolean HasDiffuseMap : false
Texture2D NormalMap -LINEAR Texture2D NormalMap -LINEAR

View File

@@ -0,0 +1,27 @@
MaterialDef GrassSeed {
MaterialParameters {
Texture2D ColorMap
Float WindSpeed : 1.0
Float WindStrength : 0.15
Vector2 WindDir : 0.0 1.0
Vector3 SunDir : 0.35 0.8 0.45
Color SunColor : 0.95 0.90 0.75 1.0
}
Technique {
VertexShader GLSL150: Shaders/GrassSeed.vert
FragmentShader GLSL150: Shaders/GrassSeed.frag
WorldParameters {
WorldViewProjectionMatrix
WorldMatrix
Time
AmbientLightColor
}
RenderState {
FaceCull Off
}
}
}

View File

@@ -3,6 +3,7 @@ MaterialDef GrassVertex {
MaterialParameters { MaterialParameters {
Float WindSpeed : 1.0 Float WindSpeed : 1.0
Float WindStrength : 0.15 Float WindStrength : 0.15
Vector2 WindDir : 0.0 1.0
Vector3 SunDir : 0.35 0.8 0.45 Vector3 SunDir : 0.35 0.8 0.45
Color SunColor : 0.95 0.90 0.75 1.0 Color SunColor : 0.95 0.90 0.75 1.0
} }

View File

@@ -4,6 +4,7 @@ MaterialDef Tree {
Color Diffuse (Color) : 0.42 0.26 0.10 1.0 Color Diffuse (Color) : 0.42 0.26 0.10 1.0
Float WindStrength : 0.15 Float WindStrength : 0.15
Float WindSpeed : 0.5 Float WindSpeed : 0.5
Vector2 WindDir : 0.0 1.0
Texture2D BarkMap Texture2D BarkMap
Boolean HasBarkMap : false Boolean HasBarkMap : false
Vector3 LightDir Vector3 LightDir

View File

@@ -4,6 +4,7 @@ MaterialDef TreeLeaf {
Color Diffuse (Color) : 0.18 0.60 0.10 1.0 Color Diffuse (Color) : 0.18 0.60 0.10 1.0
Float WindStrength : 0.30 Float WindStrength : 0.30
Float WindSpeed : 0.7 Float WindSpeed : 0.7
Vector2 WindDir : 0.0 1.0
Texture2D LeafMap Texture2D LeafMap
Boolean HasLeafMap : false Boolean HasLeafMap : false

View File

@@ -5,6 +5,7 @@ uniform mat4 g_WorldMatrix;
uniform float g_Time; uniform float g_Time;
uniform float m_WindStrength; uniform float m_WindStrength;
uniform float m_WindSpeed; uniform float m_WindSpeed;
uniform vec2 m_WindDir;
in vec3 inPosition; in vec3 inPosition;
in vec3 inNormal; in vec3 inNormal;
@@ -21,10 +22,21 @@ void main() {
float windW = inColor.r; float windW = inColor.r;
float t = g_Time * m_WindSpeed; float t = g_Time * m_WindSpeed;
vec4 wp = g_WorldMatrix * vec4(inPosition, 1.0); vec4 wp = g_WorldMatrix * vec4(inPosition, 1.0);
float phase = wp.x * 0.08 + wp.z * 0.06; vec2 worldXZ = wp.xz;
float swayX = sin(t + phase) * windW * m_WindStrength;
float swayZ = cos(t*0.73 + phase) * windW * m_WindStrength * 0.55; vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0);
vec3 anim = inPosition + vec3(swayX, 0.0, swayZ); vec2 perpN = vec2(-windN.y, windN.x);
float wavePhase = dot(worldXZ, windN);
float randPhase = fract(sin(dot(worldXZ, vec2(127.1, 311.7))) * 43758.5453) * 6.2832;
float mainSway = sin(t + wavePhase * 0.08 + randPhase) * windW * m_WindStrength;
float crossSway = cos(t * 0.73 + wavePhase * 0.06 + randPhase) * windW * m_WindStrength * 0.25;
vec3 anim = inPosition + vec3(
windN.x * mainSway + perpN.x * crossSway,
0.0,
windN.y * mainSway + perpN.y * crossSway
);
gl_Position = g_WorldViewProjectionMatrix * vec4(anim, 1.0); gl_Position = g_WorldViewProjectionMatrix * vec4(anim, 1.0);
texCoord = inTexCoord; texCoord = inTexCoord;

View File

@@ -0,0 +1,19 @@
uniform sampler2D m_ColorMap;
uniform vec4 g_AmbientLightColor;
uniform vec3 m_SunDir;
uniform vec4 m_SunColor;
in vec2 varUV;
out vec4 outFragColor;
void main() {
vec4 c = texture(m_ColorMap, varUV);
if (c.a < 0.15) discard;
// 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);
}

View File

@@ -0,0 +1,34 @@
uniform mat4 g_WorldViewProjectionMatrix;
uniform mat4 g_WorldMatrix;
uniform float g_Time;
uniform float m_WindSpeed;
uniform float m_WindStrength;
uniform vec2 m_WindDir;
in vec3 inPosition;
in vec2 inTexCoord; // echte UV-Koordinaten der Samen-Textur
out vec2 varUV;
void main() {
vec4 pos = vec4(inPosition, 1.0);
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);
float randPhase = fract(sin(dot(worldXZ, vec2(127.1, 311.7))) * 43758.5453) * 6.2832;
float sway = sin(t * 2.1 + wavePhase * 0.10 + randPhase) * 0.6
+ sin(t * 1.4 + wavePhase * 0.06 + randPhase * 0.73) * 0.4;
// Samen sitzen immer an der Spitze → voller Windfaktor (wf = 1.0)
float bend = sway * m_WindStrength;
pos.x += windN.x * bend;
pos.z += windN.y * bend;
varUV = inTexCoord;
gl_Position = g_WorldViewProjectionMatrix * pos;
}

View File

@@ -4,6 +4,7 @@ uniform float g_Time;
uniform float m_WindSpeed; uniform float m_WindSpeed;
uniform float m_WindStrength; uniform float m_WindStrength;
uniform vec2 m_WindDir; // normierter XZ-Windvektor
in vec3 inPosition; in vec3 inPosition;
in vec3 inNormal; in vec3 inNormal;
@@ -18,22 +19,28 @@ void main() {
float wf = inTexCoord.x; float wf = inTexCoord.x;
if (wf > 0.001) { if (wf > 0.001) {
// Weltposition als Phasenbasis → jeder Halm schwingt anders
vec2 worldXZ = (g_WorldMatrix * pos).xz; vec2 worldXZ = (g_WorldMatrix * pos).xz;
float t = g_Time * m_WindSpeed; float t = g_Time * m_WindSpeed;
float sway = sin(t * 2.1 + worldXZ.x * 0.08 + worldXZ.y * 0.06) * 0.6 // Windrichtung (Fallback: Süd)
+ sin(t * 1.4 - worldXZ.x * 0.05 + worldXZ.y * 0.09) * 0.4; vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0);
// Wellenfront: Position entlang der Windachse → Halme in Windrichtung erreicht der Impuls später
float wavePhase = dot(worldXZ, windN);
// Zufälliger Phasenversatz pro Halm (Spatial-Hash → kein synchrones Schwingen)
float randPhase = fract(sin(dot(worldXZ, vec2(127.1, 311.7))) * 43758.5453) * 6.2832;
float sway = sin(t * 2.1 + wavePhase * 0.10 + randPhase) * 0.6
+ sin(t * 1.4 + wavePhase * 0.06 + randPhase * 0.73) * 0.4;
// Quadratische Gewichtung: Spitze biegt sich mehr als Basis // Quadratische Gewichtung: Spitze biegt sich mehr als Basis
float bend = sway * m_WindStrength * wf * wf; float bend = sway * m_WindStrength * wf * wf;
pos.x += bend; pos.x += windN.x * bend;
pos.z += bend * 0.3; pos.z += windN.y * bend;
} }
varColor = inColor; varColor = inColor;
// Normal in Weltkoordinaten (WorldMatrix ist für Gras typischerweise Identität)
varNormal = normalize(mat3(g_WorldMatrix) * inNormal); varNormal = normalize(mat3(g_WorldMatrix) * inNormal);
gl_Position = g_WorldViewProjectionMatrix * pos; gl_Position = g_WorldViewProjectionMatrix * pos;
} }

View File

@@ -1,11 +1,11 @@
#import "Common/ShaderLib/GLSLCompat.glsllib" #import "Common/ShaderLib/GLSLCompat.glsllib"
uniform mat4 g_WorldViewProjectionMatrix; uniform mat4 g_WorldViewProjectionMatrix;
uniform mat4 g_WorldMatrix; uniform mat4 g_WorldMatrix;
uniform float g_Time; uniform float g_Time;
uniform float m_WindStrength; uniform float m_WindStrength;
uniform float m_WindSpeed; uniform float m_WindSpeed;
uniform vec2 m_WindDir;
in vec3 inPosition; in vec3 inPosition;
in vec3 inNormal; in vec3 inNormal;
@@ -16,18 +16,27 @@ out vec2 texCoord;
out vec3 worldNormal; out vec3 worldNormal;
void main() { void main() {
float windW = inColor.r; float windW = inColor.r;
float t = g_Time * m_WindSpeed; float t = g_Time * m_WindSpeed;
// Welt-Position für orts-abhängige Phase (verhindert synchrones Schwingen) vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); vec2 worldXZ = worldPos.xz;
float phase = worldPos.x * 0.08 + worldPos.z * 0.06;
float swayX = sin(t + phase) * windW * m_WindStrength;
float swayZ = cos(t * 0.73 + phase) * windW * m_WindStrength * 0.55;
vec3 animPos = inPosition + vec3(swayX, 0.0, swayZ); vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0);
vec2 perpN = vec2(-windN.y, windN.x);
float wavePhase = dot(worldXZ, windN);
float randPhase = fract(sin(dot(worldXZ, vec2(127.1, 311.7))) * 43758.5453) * 6.2832;
gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0); float mainSway = sin(t + wavePhase * 0.08 + randPhase) * windW * m_WindStrength;
texCoord = inTexCoord; float crossSway = cos(t * 0.73 + wavePhase * 0.06 + randPhase) * windW * m_WindStrength * 0.25;
worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz);
vec3 animPos = inPosition + vec3(
windN.x * mainSway + perpN.x * crossSway,
0.0,
windN.y * mainSway + perpN.y * crossSway
);
gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0);
texCoord = inTexCoord;
worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

View File

@@ -13,7 +13,8 @@
"stand_up", "stand_up",
"stand_up_bench", "stand_up_bench",
"tpose", "tpose",
"walking" "walking",
"sitting_talking"
], ],
"actionMap": { "actionMap": {
"DEFAULT": "tpose", "DEFAULT": "tpose",
@@ -31,7 +32,22 @@
}, },
"previewModelPath": "Models/Chars/mainchar.j3o", "previewModelPath": "Models/Chars/mainchar.j3o",
"animOffsets": { "animOffsets": {
"sitting": {"tx": 0.0, "ty": 0.0, "tz": -0.5, "rx": 0.0, "ry": 0.0, "rz": 0.0}, "sitting": {
"get_up_sitting": {"tx": 0.0, "ty": 0.0, "tz": -0.5, "rx": 0.0, "ry": 0.0, "rz": 0.0} "tx": 0.0,
"ty": 0.0,
"tz": -0.5,
"rx": 0.0,
"ry": 0.0,
"rz": 0.0
},
"get_up_sitting": {
"tx": 0.0,
"ty": 0.0,
"tz": -0.5,
"rx": 0.0,
"ry": 0.0,
"rz": 0.0
} }
},
"subClips": {}
} }

View File

@@ -1,4 +1,9 @@
package de.blight.common; package de.blight.common;
/** Einzeln platzierter Vertex-Gras-Halm. Y-Position wird beim Platzieren aus dem Terrain gebacken. */ /** Einzeln platzierter Vertex-Gras-Halm. Y-Position wird beim Platzieren aus dem Terrain gebacken. */
public record GrassVertexBlade(float x, float y, float z, float height, float dryness) {} public record GrassVertexBlade(float x, float y, float z, float height, float dryness, int seedIdx) {
/** Rückwärts-kompatibler Konstruktor ohne Samen-Index. */
public GrassVertexBlade(float x, float y, float z, float height, float dryness) {
this(x, y, z, height, dryness, -1);
}
}

View File

@@ -11,11 +11,12 @@ import java.util.zip.*;
* *
* Format v1: int MAGIC, int VERSION, int count, N × (float x, float y, float z, float height) * Format v1: int MAGIC, int VERSION, int count, N × (float x, float y, float z, float height)
* Format v2: wie v1 + float dryness pro Halm (0=grün, 0.51.0=gelb/braun) * Format v2: wie v1 + float dryness pro Halm (0=grün, 0.51.0=gelb/braun)
* Format v3: wie v2 + byte seedIdx pro Halm (1=kein Samen, ≥0=Textur-Index)
*/ */
public final class GrassVertexIO { public final class GrassVertexIO {
private static final int MAGIC = 0x47565458; // "GVTX" private static final int MAGIC = 0x47565458; // "GVTX"
private static final int VERSION = 2; private static final int VERSION = 3;
private GrassVertexIO() {} private GrassVertexIO() {}
@@ -37,6 +38,7 @@ public final class GrassVertexIO {
out.writeFloat(b.z()); out.writeFloat(b.z());
out.writeFloat(b.height()); out.writeFloat(b.height());
out.writeFloat(b.dryness()); out.writeFloat(b.dryness());
out.writeByte(Math.max(-1, Math.min(126, b.seedIdx())));
} }
} }
} }
@@ -58,7 +60,8 @@ public final class GrassVertexIO {
float z = in.readFloat(); float z = in.readFloat();
float h = in.readFloat(); float h = in.readFloat();
float dr = version >= 2 ? in.readFloat() : 0f; float dr = version >= 2 ? in.readFloat() : 0f;
list.add(new GrassVertexBlade(x, y, z, h, dr)); int si = version >= 3 ? in.readByte() : -1;
list.add(new GrassVertexBlade(x, y, z, h, dr, si));
} }
return list; return list;
} }

View File

@@ -24,6 +24,12 @@ public class DialogOption {
private TextReference textNpc; private TextReference textNpc;
private AudioReference audioNpc; private AudioReference audioNpc;
/** Animations-Clip, der beim Start der NPC-Antwort einmalig gespielt wird (z. B. "talk1"). */
private String npcAnimation;
private List<DialogStep> heroSteps = new ArrayList<>();
private List<DialogStep> npcSteps = new ArrayList<>();
private transient List<DialogOption> nextOptions; private transient List<DialogOption> nextOptions;
private transient List<DialogOption> disablesOptions; private transient List<DialogOption> disablesOptions;

View File

@@ -0,0 +1,15 @@
package de.blight.common.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class DialogStep {
private TextReference text;
private AudioReference audio;
}

View File

@@ -10,8 +10,8 @@ import java.util.List;
*/ */
public class DayTime { public class DayTime {
/** Standard-Tagesdauer in Echtzeit-Sekunden (5 Minuten). */ /** Standard-Tagesdauer in Echtzeit-Sekunden (15 Minuten). */
public static final float DEFAULT_DAY_DURATION = 300f; public static final float DEFAULT_DAY_DURATION = 900f;
private float timeOfDay; private float timeOfDay;
private float timeScale; private float timeScale;

View File

@@ -11,12 +11,14 @@ import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray; import com.jme3.math.Ray;
import com.jme3.math.Vector2f; import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh; import com.jme3.scene.Mesh;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.terrain.geomipmap.TerrainQuad; import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexBlade;
import de.blight.common.GrassVertexIO; import de.blight.common.GrassVertexIO;
@@ -26,7 +28,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
/** /**
@@ -47,7 +51,7 @@ public class GrassVertexState extends BaseAppState {
// ── Geometrie ───────────────────────────────────────────────────────────── // ── Geometrie ─────────────────────────────────────────────────────────────
static final int BLADES_PER_TUFT = 3; // Halme pro Büschel static final int BLADES_PER_TUFT = 3; // Halme pro Büschel
static final int SEGMENTS = 5; // Segmente pro Halm (5 → 6 Reihen) static final int SEGMENTS = 5; // Segmente pro Halm (5 → 6 Reihen)
static final float WIDTH_FACTOR = 0.05f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR static final float WIDTH_FACTOR = 0.10f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
static final float BEND_FACTOR = 0.15f; // max. Krümmungsversatz an der Spitze static final float BEND_FACTOR = 0.15f; // max. Krümmungsversatz an der Spitze
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f); static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
@@ -63,6 +67,15 @@ public class GrassVertexState extends BaseAppState {
private static final float CULL_DIST = 150f; private static final float CULL_DIST = 150f;
private static final float CULL_DIST_SQ = CULL_DIST * CULL_DIST; private static final float CULL_DIST_SQ = CULL_DIST * CULL_DIST;
// ── Samen-Texturen ────────────────────────────────────────────────────────
private static final String SEED_TEX_BASE = "Textures/internal/gras/seeds/seeds";
private static final String SEED_TEX_EXT = ".png";
/** Samen-Größe relativ zur Halmhöhe */
private static final float SEED_SIZE_FACTOR = 0.45f;
/** Y-Position der Samen-Unterseite relativ zum Halmfuß (01) */
private static final float SEED_Y_FACTOR = 0.78f;
// ── Zustand ─────────────────────────────────────────────────────────────── // ── Zustand ───────────────────────────────────────────────────────────────
private final SharedInput input; private final SharedInput input;
private AssetManager assetManager; private AssetManager assetManager;
@@ -70,6 +83,8 @@ public class GrassVertexState extends BaseAppState {
private TerrainQuad terrain; private TerrainQuad terrain;
private Node grassNode; private Node grassNode;
private Material material; private Material material;
private Material[] seedMaterials = new Material[0];
private Material seedStalkMaterial;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT]; private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
@@ -98,6 +113,8 @@ public class GrassVertexState extends BaseAppState {
grassNode = new Node("grassVertexNode"); grassNode = new Node("grassVertexNode");
((SimpleApplication) app).getRootNode().attachChild(grassNode); ((SimpleApplication) app).getRootNode().attachChild(grassNode);
material = buildMaterial(); material = buildMaterial();
seedMaterials = loadSeedMaterials();
seedStalkMaterial = buildSeedStalkMaterial();
try { try {
for (GrassVertexBlade b : GrassVertexIO.load()) { for (GrassVertexBlade b : GrassVertexIO.load()) {
@@ -109,6 +126,38 @@ public class GrassVertexState extends BaseAppState {
} }
} }
private Material buildSeedStalkMaterial() {
Material mat = new Material(assetManager, "MatDefs/GrassVertex.j3md");
mat.setFloat("WindSpeed", 1.0f);
mat.setFloat("WindStrength", 0.15f);
mat.setVector3("SunDir", new com.jme3.math.Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
private Material[] loadSeedMaterials() {
List<Material> mats = new ArrayList<>();
for (int i = 1; i <= 99; i++) {
String path = SEED_TEX_BASE + i + SEED_TEX_EXT;
try {
Texture tex = assetManager.loadTexture(path);
Material mat = new Material(assetManager, "MatDefs/GrassSeed.j3md");
mat.setTexture("ColorMap", tex);
mat.setFloat("WindSpeed", 1.0f);
mat.setFloat("WindStrength", 0.15f);
mat.setVector3("SunDir", new com.jme3.math.Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mats.add(mat);
log.info("[GrassVertexState] Samen-Textur geladen: {}", path);
} catch (Exception e) {
break;
}
}
return mats.toArray(new Material[0]);
}
@Override @Override
protected void cleanup(Application app) { protected void cleanup(Application app) {
((SimpleApplication) app).getRootNode().detachChild(grassNode); ((SimpleApplication) app).getRootNode().detachChild(grassNode);
@@ -201,7 +250,9 @@ public class GrassVertexState extends BaseAppState {
float variation = (1f - uniformity) * 0.25f; // max ±25 % bei uniformity=0 float variation = (1f - uniformity) * 0.25f; // max ±25 % bei uniformity=0
float radSq = radius * radius; float radSq = radius * radius;
// Kleinere Rand-Halme im Pinselbereich durch größere ersetzen float seedPct = (float) input.grassVertexTool.seedDensity.getValue() / 100f;
// Kleinere Rand-Halme im Pinselbereich durch größere ersetzen (seedIdx erhalten)
for (int ci = 0; ci < CHUNK_COUNT; ci++) { for (int ci = 0; ci < CHUNK_COUNT; ci++) {
List<GrassVertexBlade> list = chunkBlades[ci]; List<GrassVertexBlade> list = chunkBlades[ci];
boolean changed = false; boolean changed = false;
@@ -215,7 +266,7 @@ public class GrassVertexState extends BaseAppState {
// Halm ist deutlich kleiner als die aktuelle Pinselposition erlaubt → ersetzen // Halm ist deutlich kleiner als die aktuelle Pinselposition erlaubt → ersetzen
if (b.height() < idealH * 0.88f) { if (b.height() < idealH * 0.88f) {
float newH = idealH * (1f + variation * (rng.nextFloat() * 2f - 1f)); float newH = idealH * (1f + variation * (rng.nextFloat() * 2f - 1f));
list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), b.dryness())); list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), b.dryness(), b.seedIdx()));
changed = true; changed = true;
} }
} }
@@ -236,7 +287,11 @@ public class GrassVertexState extends BaseAppState {
h = Math.max(0.05f, h); h = Math.max(0.05f, h);
float bladeDryness = rng.nextFloat() < witherPct float bladeDryness = rng.nextFloat() < witherPct
? 0.5f + rng.nextFloat() * 0.5f : 0f; ? 0.5f + rng.nextFloat() * 0.5f : 0f;
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness); int seedIdx = -1;
if (seedMaterials.length > 0 && rng.nextFloat() < seedPct) {
seedIdx = rng.nextInt(seedMaterials.length);
}
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness, seedIdx);
int ci = chunkIndex(bx, bz); int ci = chunkIndex(bx, bz);
if (ci >= 0) { chunkBlades[ci].add(blade); dirtyChunks[ci] = true; } if (ci >= 0) { chunkBlades[ci].add(blade); dirtyChunks[ci] = true; }
} }
@@ -254,7 +309,7 @@ public class GrassVertexState extends BaseAppState {
if (dx*dx + dz*dz > radSq) continue; if (dx*dx + dz*dz > radSq) continue;
float newY = terrain.getHeight(new Vector2f(b.x(), b.z())); float newY = terrain.getHeight(new Vector2f(b.x(), b.z()));
if (!Float.isNaN(newY)) { if (!Float.isNaN(newY)) {
list.set(i, new GrassVertexBlade(b.x(), newY, b.z(), b.height(), b.dryness())); list.set(i, new GrassVertexBlade(b.x(), newY, b.z(), b.height(), b.dryness(), b.seedIdx()));
changed = true; changed = true;
} }
} }
@@ -333,10 +388,144 @@ public class GrassVertexState extends BaseAppState {
Node node = new Node("gvc_" + ci); Node node = new Node("gvc_" + ci);
node.attachChild(geo); node.attachChild(geo);
// ── Samen-Stiele + Kreuze ─────────────────────────────────────────────
if (seedMaterials.length > 0) {
List<GrassVertexBlade> seededAll = new ArrayList<>();
Map<Integer, List<GrassVertexBlade>> byTex = new HashMap<>();
for (GrassVertexBlade b : blades) {
if (b.seedIdx() >= 0 && b.seedIdx() < seedMaterials.length) {
seededAll.add(b);
byTex.computeIfAbsent(b.seedIdx(), k -> new ArrayList<>()).add(b);
}
}
if (!seededAll.isEmpty()) {
Geometry stalkGeo = buildSeedStalkMesh("stalk_" + ci, seededAll);
stalkGeo.setMaterial(seedStalkMaterial);
node.attachChild(stalkGeo);
}
for (Map.Entry<Integer, List<GrassVertexBlade>> e : byTex.entrySet()) {
Geometry seedGeo = buildSeedCrossMesh("seed_" + ci + "_" + e.getKey(), e.getValue());
seedGeo.setMaterial(seedMaterials[e.getKey()]);
node.attachChild(seedGeo);
}
}
chunkNodes[ci] = node; chunkNodes[ci] = node;
grassNode.attachChild(node); grassNode.attachChild(node);
} }
private static Geometry buildSeedStalkMesh(String name, List<GrassVertexBlade> blades) {
final int SEG = 4;
final float R = 0xbb / 255f, G = 0x90 / 255f, B = 0x59 / 255f;
int n = blades.size();
int vTotal = n * (SEG + 1) * 2;
float[] pos = new float[vTotal * 3];
float[] nrm = new float[vTotal * 3];
float[] col = new float[vTotal * 4];
float[] tex = new float[vTotal * 2];
int[] idx = new int [n * SEG * 6];
int vi = 0, ii = 0;
for (GrassVertexBlade blade : blades) {
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
float hw = h * 0.018f;
float ang = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
float cA = (float) Math.cos(ang), sA = (float) Math.sin(ang);
// Normale senkrecht zur Halmbreite, 30 % Richtung Weltauf gekippt (wie Gras-Shader)
float nx = -sA, ny = 0f, nz = cA;
float blend = 0.30f;
nx *= (1f - blend); ny = blend; nz *= (1f - blend);
float nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
nx /= nLen; ny /= nLen; nz /= nLen;
for (int s = 0; s <= SEG; s++) {
float t = (float) s / SEG;
float curHW = hw * (float) Math.pow(1.0 - t, 1.4);
float py = y + h * t;
int sviL = vi + s * 2;
int sviR = sviL + 1;
// Linkes Vertex
pos[sviL*3] = x - cA*curHW; pos[sviL*3+1] = py; pos[sviL*3+2] = z - sA*curHW;
nrm[sviL*3] = nx; nrm[sviL*3+1] = ny; nrm[sviL*3+2] = nz;
col[sviL*4] = R; col[sviL*4+1] = G; col[sviL*4+2] = B; col[sviL*4+3] = 1f;
tex[sviL*2] = t; tex[sviL*2+1] = 0f;
// Rechtes Vertex
pos[sviR*3] = x + cA*curHW; pos[sviR*3+1] = py; pos[sviR*3+2] = z + sA*curHW;
nrm[sviR*3] = nx; nrm[sviR*3+1] = ny; nrm[sviR*3+2] = nz;
col[sviR*4] = R; col[sviR*4+1] = G; col[sviR*4+2] = B; col[sviR*4+3] = 1f;
tex[sviR*2] = t; tex[sviR*2+1] = 0f;
}
for (int s = 0; s < SEG; s++) {
int b0 = vi + s * 2;
idx[ii] = b0; idx[ii+1] = b0+1; idx[ii+2] = b0+3;
idx[ii+3] = b0; idx[ii+4] = b0+3; idx[ii+5] = b0+2;
ii += 6;
}
vi += (SEG + 1) * 2;
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(nrm));
m.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(col));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return new Geometry(name, m);
}
private static Geometry buildSeedCrossMesh(String name, List<GrassVertexBlade> blades) {
// Pro Halm: 2 Quads (X-Kreuz) × 4 Verts = 8 Verts, 2 × 6 Indices = 12
int n = blades.size();
float[] pos = new float[n * 8 * 3];
float[] tex = new float[n * 8 * 2];
int[] idx = new int [n * 12];
int vi = 0, ii = 0;
for (GrassVertexBlade b : blades) {
float x = b.x();
float yBot = b.y() + b.height() * SEED_Y_FACTOR;
float z = b.z();
float size = b.height() * SEED_SIZE_FACTOR;
float hw = size * 0.5f;
// Quad 1 entlang Welt-X
setSeedV(pos, tex, vi+0, x-hw, yBot, z, 0,0);
setSeedV(pos, tex, vi+1, x+hw, yBot, z, 1,0);
setSeedV(pos, tex, vi+2, x+hw, yBot+size, z, 1,1);
setSeedV(pos, tex, vi+3, x-hw, yBot+size, z, 0,1);
// Quad 2 entlang Welt-Z
setSeedV(pos, tex, vi+4, x, yBot, z-hw, 0,0);
setSeedV(pos, tex, vi+5, x, yBot, z+hw, 1,0);
setSeedV(pos, tex, vi+6, x, yBot+size, z+hw, 1,1);
setSeedV(pos, tex, vi+7, x, yBot+size, z-hw, 0,1);
idx[ii] = vi; idx[ii+1] = vi+1; idx[ii+2] = vi+2;
idx[ii+3] = vi; idx[ii+4] = vi+2; idx[ii+5] = vi+3;
idx[ii+6] = vi+4; idx[ii+7] = vi+5; idx[ii+8] = vi+6;
idx[ii+9] = vi+4; idx[ii+10] = vi+6; idx[ii+11] = vi+7;
vi += 8; ii += 12;
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return new Geometry(name, m);
}
private static void setSeedV(float[] pos, float[] tex, int vi,
float x, float y, float z, float u, float v) {
pos[vi*3] = x; pos[vi*3+1] = y; pos[vi*3+2] = z;
tex[vi*2] = u; tex[vi*2+1] = v;
}
// ── Mesh-Generierung (gemeinsame Logik, auch von GrassVertexRenderState genutzt) ── // ── Mesh-Generierung (gemeinsame Logik, auch von GrassVertexRenderState genutzt) ──
static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx, static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx,

View File

@@ -10,6 +10,8 @@ public class GrassVertexTool extends EditorTool {
public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0); public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0);
/** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */ /** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */
public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0); public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0);
/** Anteil der Halme mit Samenstand (0 = keine, 100 = alle) */
public final ToolParameter seedDensity = new ToolParameter("Samen %", 0.0, 0.0, 100.0);
@Override public String getName() { return "Gras (Vertices)"; } @Override public String getName() { return "Gras (Vertices)"; }
@@ -17,5 +19,5 @@ public class GrassVertexTool extends EditorTool {
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); } public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
@Override @Override
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness, uniformity); } public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness, uniformity, seedDensity); }
} }

View File

@@ -52,8 +52,8 @@ public class DialogEditorView extends BorderPane {
private TextField idField; private TextField idField;
private Label derivedLabelKey; private Label derivedLabelKey;
private Label derivedHeroKey; private ListView<String> heroStepsView;
private Label derivedNpcKey; private ListView<String> npcStepsView;
private CheckBox rootCheck; private CheckBox rootCheck;
private Spinner<Integer> chapterSpinner; private Spinner<Integer> chapterSpinner;
private ComboBox<String> statusCombo; private ComboBox<String> statusCombo;
@@ -70,6 +70,7 @@ public class DialogEditorView extends BorderPane {
private ListView<Trigger> triggersView; private ListView<Trigger> triggersView;
private ListView<String> nextOptionsView; private ListView<String> nextOptionsView;
private ListView<String> disablesOptionsView; private ListView<String> disablesOptionsView;
private ComboBox<String> npcAnimCombo;
// ── Graph-mode ──────────────────────────────────────────────────────────── // ── Graph-mode ────────────────────────────────────────────────────────────
@@ -252,20 +253,16 @@ public class DialogEditorView extends BorderPane {
idField.setPromptText("z. B. begruessung"); idField.setPromptText("z. B. begruessung");
derivedLabelKey = derivedKeyLabel(""); derivedLabelKey = derivedKeyLabel("");
derivedHeroKey = derivedKeyLabel("");
derivedNpcKey = derivedKeyLabel("");
idField.textProperty().addListener((obs, oldVal, newVal) -> { idField.textProperty().addListener((obs, oldVal, newVal) -> {
String id = newVal.trim(); String id = newVal.trim();
if (id.isBlank()) { if (id.isBlank()) {
derivedLabelKey.setText(""); derivedLabelKey.setText("");
derivedHeroKey.setText(""); refreshStepKeys("");
derivedNpcKey.setText("");
} else { } else {
String base = "dialog." + currentNpcId + "." + id; String base = "dialog." + currentNpcId + "." + id;
derivedLabelKey.setText(base + ".description"); derivedLabelKey.setText(base + ".description");
derivedHeroKey.setText(base + ".textmainchar"); refreshStepKeys(base);
derivedNpcKey.setText(base + ".textnpc");
} }
}); });
@@ -310,12 +307,31 @@ public class DialogEditorView extends BorderPane {
new Separator() new Separator()
); );
heroStepsView = stepsListView();
npcStepsView = stepsListView();
Button addHeroBtn = smallBtn("+");
Button delHeroBtn = smallBtn("");
Button addNpcBtn = smallBtn("+");
Button delNpcBtn = smallBtn("");
addHeroBtn.setOnAction(e -> addStep(heroStepsView, ".textmainchar"));
delHeroBtn.setOnAction(e -> removeLastStep(heroStepsView));
addNpcBtn.setOnAction(e -> addStep(npcStepsView, ".textnpc"));
delNpcBtn.setOnAction(e -> removeLastStep(npcStepsView));
npcAnimCombo = new ComboBox<>();
npcAnimCombo.getItems().addAll("— (keine)", "talk1", "talk2", "disappoint", "secret");
npcAnimCombo.setValue("— (keine)");
npcAnimCombo.setMaxWidth(Double.MAX_VALUE);
form.getChildren().addAll( form.getChildren().addAll(
sectionTitle("Texte"), sectionTitle("Texte"),
row("Text Held:", derivedHeroKey), sectionTitle("Held-Steps:"),
row("Text NPC:", derivedNpcKey), heroStepsView,
row("Audio Held:", placeholder("(wird später implementiert)")), new HBox(4, addHeroBtn, delHeroBtn),
row("Audio NPC:", placeholder("(wird später implementiert)")), sectionTitle("NPC-Steps:"),
npcStepsView,
new HBox(4, addNpcBtn, delNpcBtn),
row("NPC-Animation:", npcAnimCombo),
new Separator() new Separator()
); );
@@ -469,7 +485,26 @@ public class DialogEditorView extends BorderPane {
questCompleteField.setText(opt.getRequiresQuestComplete() != null questCompleteField.setText(opt.getRequiresQuestComplete() != null
? safe(opt.getRequiresQuestComplete().getQuestId()) : ""); ? safe(opt.getRequiresQuestComplete().getQuestId()) : "");
// text keys are derived from ID and shown via derivedHeroKey / derivedNpcKey labels // Held- und NPC-Steps laden (mit Migration aus Legacy-Feldern)
String base = (!id.isBlank()) ? "dialog." + currentNpcId + "." + id : "";
heroStepsView.getItems().clear();
java.util.List<de.blight.common.model.DialogStep> hs = opt.getHeroSteps();
if (hs != null && !hs.isEmpty()) {
for (int i = 0; i < hs.size(); i++) {
heroStepsView.getItems().add(stepKey(base, ".textmainchar", i));
}
} else if (opt.getTextHero() != null && !opt.getTextHero().id().isBlank()) {
heroStepsView.getItems().add(stepKey(base, ".textmainchar", 0));
}
npcStepsView.getItems().clear();
java.util.List<de.blight.common.model.DialogStep> ns = opt.getNpcSteps();
if (ns != null && !ns.isEmpty()) {
for (int i = 0; i < ns.size(); i++) {
npcStepsView.getItems().add(stepKey(base, ".textnpc", i));
}
} else if (opt.getTextNpc() != null && !opt.getTextNpc().id().isBlank()) {
npcStepsView.getItems().add(stepKey(base, ".textnpc", 0));
}
if (opt.getRequiredItem() != null && opt.getRequiredItem().getItem() != null) { if (opt.getRequiredItem() != null && opt.getRequiredItem().getItem() != null) {
reqItemIdField.setText(safe(opt.getRequiredItem().getItem().getItemId())); reqItemIdField.setText(safe(opt.getRequiredItem().getItem().getItemId()));
@@ -498,6 +533,9 @@ public class DialogEditorView extends BorderPane {
.map(Quest::getQuestId) .map(Quest::getQuestId)
.forEach(abortsQuestsView.getItems()::add); .forEach(abortsQuestsView.getItems()::add);
String anim = opt.getNpcAnimation();
npcAnimCombo.setValue(anim != null && !anim.isBlank() ? anim : "— (keine)");
enablesTradeCheck.setSelected(opt.isEnablesTrade()); enablesTradeCheck.setSelected(opt.isEnablesTrade());
triggersView.getItems().clear(); triggersView.getItems().clear();
@@ -543,8 +581,24 @@ public class DialogEditorView extends BorderPane {
if (!currentId.isBlank()) { if (!currentId.isBlank()) {
String base = "dialog." + currentNpcId + "." + currentId; String base = "dialog." + currentNpcId + "." + currentId;
opt.setLabel(new TextReference(base + ".description")); opt.setLabel(new TextReference(base + ".description"));
opt.setTextHero(new TextReference(base + ".textmainchar"));
opt.setTextNpc(new TextReference(base + ".textnpc")); List<de.blight.common.model.DialogStep> heroList = new ArrayList<>();
for (int i = 0; i < heroStepsView.getItems().size(); i++) {
heroList.add(new de.blight.common.model.DialogStep(
new TextReference(stepKey(base, ".textmainchar", i)), null));
}
opt.setHeroSteps(heroList);
opt.setTextHero(heroList.isEmpty() ? null
: new TextReference(stepKey(base, ".textmainchar", 0)));
List<de.blight.common.model.DialogStep> npcList = new ArrayList<>();
for (int i = 0; i < npcStepsView.getItems().size(); i++) {
npcList.add(new de.blight.common.model.DialogStep(
new TextReference(stepKey(base, ".textnpc", i)), null));
}
opt.setNpcSteps(npcList);
opt.setTextNpc(npcList.isEmpty() ? null
: new TextReference(stepKey(base, ".textnpc", 0)));
} }
if (rootCheck.isSelected()) { if (rootCheck.isSelected()) {
@@ -578,6 +632,9 @@ public class DialogEditorView extends BorderPane {
} }
opt.setAbortsQuests(aborts); opt.setAbortsQuests(aborts);
String animVal = npcAnimCombo.getValue();
opt.setNpcAnimation(animVal == null || animVal.startsWith("") ? null : animVal);
opt.setEnablesTrade(enablesTradeCheck.isSelected()); opt.setEnablesTrade(enablesTradeCheck.isSelected());
opt.setOnChosenTriggers(new ArrayList<>(triggersView.getItems())); opt.setOnChosenTriggers(new ArrayList<>(triggersView.getItems()));
@@ -594,8 +651,8 @@ public class DialogEditorView extends BorderPane {
private void clearDetailForm() { private void clearDetailForm() {
if (idField != null) idField.clear(); if (idField != null) idField.clear();
if (derivedLabelKey != null) derivedLabelKey.setText(""); if (derivedLabelKey != null) derivedLabelKey.setText("");
if (derivedHeroKey != null) derivedHeroKey.setText(""); if (heroStepsView != null) heroStepsView.getItems().clear();
if (derivedNpcKey != null) derivedNpcKey.setText(""); if (npcStepsView != null) npcStepsView.getItems().clear();
if (rootCheck != null) rootCheck.setSelected(false); if (rootCheck != null) rootCheck.setSelected(false);
if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(0); if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(0);
if (statusCombo != null) statusCombo.setValue("— (keine)"); if (statusCombo != null) statusCombo.setValue("— (keine)");
@@ -608,6 +665,7 @@ public class DialogEditorView extends BorderPane {
if (recvQuestField != null) recvQuestField.clear(); if (recvQuestField != null) recvQuestField.clear();
if (fulfillsQuestField != null) fulfillsQuestField.clear(); if (fulfillsQuestField != null) fulfillsQuestField.clear();
if (abortsQuestsView != null) abortsQuestsView.getItems().clear(); if (abortsQuestsView != null) abortsQuestsView.getItems().clear();
if (npcAnimCombo != null) npcAnimCombo.setValue("— (keine)");
if (enablesTradeCheck != null) enablesTradeCheck.setSelected(false); if (enablesTradeCheck != null) enablesTradeCheck.setSelected(false);
if (triggersView != null) triggersView.getItems().clear(); if (triggersView != null) triggersView.getItems().clear();
if (nextOptionsView != null) nextOptionsView.getItems().clear(); if (nextOptionsView != null) nextOptionsView.getItems().clear();
@@ -649,6 +707,10 @@ public class DialogEditorView extends BorderPane {
opt.setLabel(new TextReference(base + ".description")); opt.setLabel(new TextReference(base + ".description"));
opt.setTextHero(new TextReference(base + ".textmainchar")); opt.setTextHero(new TextReference(base + ".textmainchar"));
opt.setTextNpc(new TextReference(base + ".textnpc")); opt.setTextNpc(new TextReference(base + ".textnpc"));
opt.setHeroSteps(new ArrayList<>(List.of(new de.blight.common.model.DialogStep(
new TextReference(base + ".textmainchar"), null))));
opt.setNpcSteps(new ArrayList<>(List.of(new de.blight.common.model.DialogStep(
new TextReference(base + ".textnpc"), null))));
allOptions.put(id, opt); allOptions.put(id, opt);
if (asRoot) { if (asRoot) {
rootIds.add(id); rootIds.add(id);
@@ -1145,4 +1207,56 @@ public class DialogEditorView extends BorderPane {
}); });
return dlg.showAndWait().orElse(null); return dlg.showAndWait().orElse(null);
} }
// ── Steps helpers ─────────────────────────────────────────────────────────
private static ListView<String> stepsListView() {
ListView<String> lv = new ListView<>();
lv.setPrefHeight(72);
lv.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
lv.setCellFactory(v -> new ListCell<>() {
@Override protected void updateItem(String s, boolean empty) {
super.updateItem(s, empty);
setText(empty || s == null ? null : s);
setStyle("-fx-text-fill: #aaddaa;");
}
});
return lv;
}
private void addStep(ListView<String> lv, String suffix) {
String id = idField.getText().trim();
if (id.isBlank()) {
return;
}
String base = "dialog." + currentNpcId + "." + id;
int idx = lv.getItems().size();
lv.getItems().add(stepKey(base, suffix, idx));
}
private static void removeLastStep(ListView<String> lv) {
if (!lv.getItems().isEmpty()) {
lv.getItems().remove(lv.getItems().size() - 1);
}
}
private static String stepKey(String base, String suffix, int idx) {
return idx == 0 ? base + suffix : base + suffix + "." + idx;
}
private void refreshStepKeys(String base) {
if (heroStepsView == null || npcStepsView == null) {
return;
}
refreshListKeys(heroStepsView, base, ".textmainchar");
refreshListKeys(npcStepsView, base, ".textnpc");
}
private static void refreshListKeys(ListView<String> lv, String base, String suffix) {
int count = lv.getItems().size();
lv.getItems().clear();
for (int i = 0; i < count; i++) {
lv.getItems().add(stepKey(base, suffix, i));
}
}
} }

View File

@@ -26,6 +26,7 @@ dependencies {
implementation "org.jmonkeyengine:jme3-jogg:${jmeVersion}" implementation "org.jmonkeyengine:jme3-jogg:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}" implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}" implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
implementation 'com.github.stephengold:SkyControl:1.1.0'
implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.google.code.gson:gson:2.11.0'
implementation 'org.slf4j:slf4j-api:2.0.17' implementation 'org.slf4j:slf4j-api:2.0.17'
implementation 'org.slf4j:jul-to-slf4j:2.0.17' implementation 'org.slf4j:jul-to-slf4j:2.0.17'

View File

@@ -71,6 +71,7 @@ public class BlightGame extends SimpleApplication {
} catch (IOException | NullPointerException ignored) {} } catch (IOException | NullPointerException ignored) {}
settings.setResolution(gs.width, gs.height); settings.setResolution(gs.width, gs.height);
settings.setFullscreen(gs.fullscreen); settings.setFullscreen(gs.fullscreen);
settings.setBitsPerPixel(32);
settings.setVSync(gs.vsync); settings.setVSync(gs.vsync);
settings.setSamples(gs.samples); settings.setSamples(gs.samples);

View File

@@ -25,7 +25,12 @@ public enum AnimationAction {
SIT_DOWN_FLOOR, SIT_DOWN_FLOOR,
SITTING_FLOOR, SITTING_FLOOR,
GET_UP_FLOOR, GET_UP_FLOOR,
REVIVE; REVIVE,
TALK1,
TALK2,
DISAPPOINT,
SECRET,
TALK_SITTING;
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */ /** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
public String displayName() { public String displayName() {
@@ -50,6 +55,11 @@ public enum AnimationAction {
case SITTING_FLOOR -> TextRegistry.resolve(null, key, "Sitzen (Boden)"); case SITTING_FLOOR -> TextRegistry.resolve(null, key, "Sitzen (Boden)");
case GET_UP_FLOOR -> TextRegistry.resolve(null, key, "Aufstehen (Boden)"); case GET_UP_FLOOR -> TextRegistry.resolve(null, key, "Aufstehen (Boden)");
case REVIVE -> TextRegistry.resolve(null, key, "Wiederbeleben"); case REVIVE -> TextRegistry.resolve(null, key, "Wiederbeleben");
case TALK1 -> TextRegistry.resolve(null, key, "Reden 1");
case TALK2 -> TextRegistry.resolve(null, key, "Reden 2");
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
}; };
} }
} }

View File

@@ -0,0 +1,153 @@
package de.blight.game.audio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* TTS-Fallback für Dialog-Texte via System-TTS (espeak-ng / say / PowerShell SAPI).
* Benötigt keine externe Dependency. Schlägt lautlos fehl wenn kein TTS verfügbar ist.
*
* <p>Wird ersetzt sobald echte Audio-Dateien ({@link de.blight.common.model.AudioReference}) vorhanden sind.
*/
public class DialogTtsService {
private static final Logger log = LoggerFactory.getLogger(DialogTtsService.class);
/** Basiskommando für die aktuelle Plattform, oder null wenn TTS nicht verfügbar. */
private String[] baseCmd;
private ExecutorService executor;
private volatile Process current;
/**
* Erkennt das verfügbare TTS-Programm und startet den Hintergrund-Thread.
* Blockiert den Render-Thread nicht.
*/
public void initialize() {
executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "dialog-tts");
t.setDaemon(true);
return t;
});
String os = System.getProperty("os.name", "").toLowerCase();
if (os.contains("linux")) {
if (commandAvailable("espeak-ng")) {
// -v de = Deutsche Stimme; -s 150 = etwas langsamer als Standard
baseCmd = new String[]{"espeak-ng", "-v", "de", "-s", "150"};
log.info("[TTS] espeak-ng (de) wird verwendet.");
} else if (commandAvailable("espeak")) {
baseCmd = new String[]{"espeak"};
log.info("[TTS] espeak wird verwendet.");
} else if (commandAvailable("festival")) {
baseCmd = new String[]{"festival", "--tts"};
log.info("[TTS] festival wird verwendet.");
}
} else if (os.contains("mac")) {
baseCmd = new String[]{"say"};
log.info("[TTS] macOS say wird verwendet.");
} else if (os.contains("win")) {
// PowerShell SAPI Text wird als letztes Argument angehängt
baseCmd = new String[]{
"powershell", "-NoProfile", "-Command",
"Add-Type -AssemblyName System.Speech;" +
"$s=New-Object System.Speech.Synthesis.SpeechSynthesizer;" +
"$s.Speak("
};
// Sonderbehandlung für Windows im buildCmd()
log.info("[TTS] Windows PowerShell SAPI wird verwendet.");
}
if (baseCmd == null) {
log.warn("[TTS] Kein TTS-Programm gefunden Sprachausgabe deaktiviert.");
}
// Executor-Thread vorab starten; verhindert Verzögerung beim ersten speak()-Aufruf.
// Startet wenn TTS verfügbar ist einmal das Programm mit --version (kein Audio).
executor.submit(() -> {
if (baseCmd == null) return;
try {
new ProcessBuilder(baseCmd[0], "--version")
.redirectErrorStream(true).start().waitFor();
} catch (Exception ignored) {}
});
}
/**
* Spricht den Text asynchron. Läuft als Kindprozess; laufende Ausgabe wird
* zuerst abgebrochen. Tut nichts wenn kein TTS verfügbar.
*/
public void speak(String text) {
if (baseCmd == null || text == null || text.isBlank()) return;
stop();
String[] cmd = buildCmd(text);
executor.submit(() -> {
try {
Process p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
current = p;
p.waitFor();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.debug("[TTS] Prozess-Fehler", e);
} finally {
current = null;
}
});
}
/** Bricht die laufende Sprachausgabe sofort ab. */
public void stop() {
Process p = current;
if (p != null) {
p.destroyForcibly();
current = null;
}
}
/** Gibt alle Ressourcen frei. Nur beim App-Shutdown aufrufen. */
public void shutdown() {
stop();
if (executor != null) executor.shutdownNow();
baseCmd = null;
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private String[] buildCmd(String text) {
// Windows: PowerShell-Kommando braucht Sonderbehandlung
if (isWindows()) {
String escaped = text.replace("\"", "\\\"");
// Letztes Element ist das unvollständige Skript, Text anhängen + schließen
String script = baseCmd[baseCmd.length - 1] + "\"" + escaped + "\")";
String[] cmd = new String[baseCmd.length];
System.arraycopy(baseCmd, 0, cmd, 0, baseCmd.length - 1);
cmd[baseCmd.length - 1] = script;
return cmd;
}
// espeak / say / festival: Text als letztes Argument
String[] cmd = new String[baseCmd.length + 1];
System.arraycopy(baseCmd, 0, cmd, 0, baseCmd.length);
cmd[baseCmd.length] = text;
return cmd;
}
private static boolean commandAvailable(String cmd) {
try {
Process p = new ProcessBuilder("which", cmd)
.redirectErrorStream(true)
.start();
return p.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
private static boolean isWindows() {
return System.getProperty("os.name", "").toLowerCase().contains("win");
}
}

View File

@@ -252,6 +252,7 @@ public class GraphicsScreen extends BaseAppState {
AppSettings s = app.getContext().getSettings(); AppSettings s = app.getContext().getSettings();
s.setResolution(live.width, live.height); s.setResolution(live.width, live.height);
s.setFullscreen(live.fullscreen); s.setFullscreen(live.fullscreen);
s.setBitsPerPixel(32);
s.setVSync(live.vsync); s.setVSync(live.vsync);
s.setSamples(live.samples); s.setSamples(live.samples);
app.setSettings(s); app.setSettings(s);

View File

@@ -75,7 +75,7 @@ public class ThirdPersonCamera {
} }
public void update(float tpf) { public void update(float tpf) {
if (target == null) return; if (target == null || paused) return;
Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0); Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0);

View File

@@ -35,6 +35,8 @@ import de.blight.game.control.ThirdPersonCamera;
import com.jme3.post.FilterPostProcessor; import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FogFilter; import com.jme3.post.filters.FogFilter;
import com.jme3.water.WaterFilter; import com.jme3.water.WaterFilter;
import de.blight.game.state.DynamicWaterFilter;
import de.blight.game.state.WaterInteractionState;
import de.blight.game.state.GrassState; import de.blight.game.state.GrassState;
import de.blight.game.state.GrassVertexRenderState; import de.blight.game.state.GrassVertexRenderState;
import de.blight.game.state.LocationState; import de.blight.game.state.LocationState;
@@ -93,6 +95,7 @@ public class WorldScene extends BaseAppState {
private de.blight.game.state.AmbientSoundSystem ambientSounds; private de.blight.game.state.AmbientSoundSystem ambientSounds;
private de.blight.game.audio.FootstepSystem footstepSystem; private de.blight.game.audio.FootstepSystem footstepSystem;
private de.blight.game.state.DrownState drownState; private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState;
public WorldScene(KeyBindings keyBindings) { public WorldScene(KeyBindings keyBindings) {
this.keyBindings = keyBindings; this.keyBindings = keyBindings;
@@ -221,6 +224,8 @@ public class WorldScene extends BaseAppState {
com.jme3.asset.plugins.FileLocator.class); com.jme3.asset.plugins.FileLocator.class);
} catch (Exception ignored) {} } catch (Exception ignored) {}
app.getCamera().setFrustumFar(4000f);
BlightGame.status("Baue Beleuchtung und Himmel..."); BlightGame.status("Baue Beleuchtung und Himmel...");
buildLighting(); buildLighting();
@@ -256,6 +261,9 @@ public class WorldScene extends BaseAppState {
// Physik-Aktivierung wird in update() verzögert bis BulletAppState und // Physik-Aktivierung wird in update() verzögert bis BulletAppState und
// Terrain-Physics für den Spawn-Bereich bereit sind (verhindert Fall-durch-Terrain). // Terrain-Physics für den Spawn-Bereich bereit sind (verhindert Fall-durch-Terrain).
physicsCharPending = true; physicsCharPending = true;
if (waterInteractionState != null) {
waterInteractionState.setPlayerControl(physicsChar, 0f);
}
playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings); playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings);
playerInput.setPhysicsCharacter(physicsChar); playerInput.setPhysicsCharacter(physicsChar);
@@ -620,7 +628,7 @@ public class WorldScene extends BaseAppState {
// Globales Wasser bei Y=0 (bedeckt die gesamte Karte unterhalb der Wasserlinie) // Globales Wasser bei Y=0 (bedeckt die gesamte Karte unterhalb der Wasserlinie)
try { try {
WaterFilter waterFilter = new WaterFilter(rootNode, sunDir); DynamicWaterFilter waterFilter = new DynamicWaterFilter(rootNode, sunDir);
waterFilter.setWaterHeight(0f); waterFilter.setWaterHeight(0f);
waterFilter.setWaterColor(new ColorRGBA(0.05f, 0.25f, 0.55f, 1f)); waterFilter.setWaterColor(new ColorRGBA(0.05f, 0.25f, 0.55f, 1f));
waterFilter.setDeepWaterColor(new ColorRGBA(0.02f, 0.12f, 0.30f, 1f)); waterFilter.setDeepWaterColor(new ColorRGBA(0.02f, 0.12f, 0.30f, 1f));
@@ -630,6 +638,9 @@ public class WorldScene extends BaseAppState {
waterFilter.setSpeed(0.5f); waterFilter.setSpeed(0.5f);
fpp.addFilter(waterFilter); fpp.addFilter(waterFilter);
waterInteractionState = new WaterInteractionState(waterFilter);
app.getStateManager().attach(waterInteractionState);
WeatherState weather = new WeatherState(); WeatherState weather = new WeatherState();
weather.setWaterFilter(waterFilter); weather.setWaterFilter(waterFilter);
@@ -640,6 +651,7 @@ public class WorldScene extends BaseAppState {
fpp.addFilter(fogFilter); fpp.addFilter(fogFilter);
weather.setFogFilter(fogFilter); weather.setFogFilter(fogFilter);
weather.setSkyControl(dayNight.getSkyControl());
app.getStateManager().attach(weather); app.getStateManager().attach(weather);
} catch (Exception e) { } catch (Exception e) {
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage()); log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());

View File

@@ -1,68 +0,0 @@
package de.blight.game.state;
import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
public class CloudsNode extends Node {
private static final int COUNT = 14;
private static final float Y = 800f;
private static final float WRAP_HALF = 1800f;
private final Geometry[] geoms = new Geometry[COUNT];
private final float[] offsetX = new float[COUNT];
private final float[] offsetZ = new float[COUNT];
private final Material mat;
private static final float[] W = { 350,250,420,180,300,380,220,160,480,260,310,200,440,190 };
private static final float[] D = { 280,160,300,220,350,200,280,180,320,240,180,350,260,300 };
private static final float[] H = { 30, 20, 25, 18, 35, 28, 22, 15, 40, 24, 20, 30, 28, 22 };
private static final float[] IX = {-600,200,-100,500,-800,300,700,-400,0,-200,600,-700,100,-500};
private static final float[] IZ = { 400,-300,700,-500,100,-700,-200,600,-100,800,-400,200,-600,500};
public CloudsNode(AssetManager assetManager) {
super("clouds");
mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.95f, 0.95f, 0.95f, 0.45f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setDepthWrite(false);
for (int i = 0; i < COUNT; i++) {
Box box = new Box(W[i] * 0.5f, H[i] * 0.5f, D[i] * 0.5f);
Geometry g = new Geometry("cloud_" + i, box);
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
g.setShadowMode(RenderQueue.ShadowMode.Off);
offsetX[i] = IX[i];
offsetZ[i] = IZ[i];
attachChild(g);
geoms[i] = g;
}
}
public void setCloudColor(ColorRGBA color) {
mat.setColor("Color", color);
}
public void update(float tpf, Vector3f windDir, float windSpeed, Vector3f camPos) {
float dx = windDir.x * windSpeed * tpf;
float dz = windDir.z * windSpeed * tpf;
for (int i = 0; i < COUNT; i++) {
offsetX[i] += dx;
offsetZ[i] += dz;
if (offsetX[i] > WRAP_HALF) offsetX[i] -= WRAP_HALF * 2f;
if (offsetX[i] < -WRAP_HALF) offsetX[i] += WRAP_HALF * 2f;
if (offsetZ[i] > WRAP_HALF) offsetZ[i] -= WRAP_HALF * 2f;
if (offsetZ[i] < -WRAP_HALF) offsetZ[i] += WRAP_HALF * 2f;
geoms[i].setLocalTranslation(camPos.x + offsetX[i], Y, camPos.z + offsetZ[i]);
}
}
}

View File

@@ -6,20 +6,20 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.BulletAppState; import com.jme3.bullet.BulletAppState;
import com.jme3.light.AmbientLight; import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight; import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Node;
import com.jme3.scene.*;
import com.jme3.scene.shape.Sphere;
import com.jme3.shadow.DirectionalLightShadowFilter; import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.shadow.EdgeFilteringMode; import com.jme3.shadow.EdgeFilteringMode;
import de.blight.common.time.DayTime; import de.blight.common.time.DayTime;
import de.blight.common.time.TimeListener; import de.blight.common.time.TimeListener;
import jme3utilities.sky.CloudLayer;
import jme3utilities.sky.SkyControl;
import jme3utilities.sky.StarsOption;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Tag/Nacht-Zyklus: Sonne, Ambiente, Schatten, Himmelsfarbe. * Tag/Nacht-Zyklus: SkyControl-Himmelskuppel, Sonne, Ambiente, Schatten.
* Wiederverwendbar im Game und Editor (withShadows=false für Editor). * Wiederverwendbar im Game und Editor (withShadows=false für Editor).
*/ */
public class DayNightState extends BaseAppState implements TimeListener { public class DayNightState extends BaseAppState implements TimeListener {
@@ -32,8 +32,6 @@ public class DayNightState extends BaseAppState implements TimeListener {
private static final ColorRGBA SUN_DAWN = new ColorRGBA(1.00f, 0.55f, 0.20f, 1f); private static final ColorRGBA SUN_DAWN = new ColorRGBA(1.00f, 0.55f, 0.20f, 1f);
private static final ColorRGBA AMB_DAY = new ColorRGBA(0.08f, 0.10f, 0.16f, 1f); private static final ColorRGBA AMB_DAY = new ColorRGBA(0.08f, 0.10f, 0.16f, 1f);
private static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.04f, 0.04f, 0.12f, 1f); private static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.04f, 0.04f, 0.12f, 1f);
private static final ColorRGBA BG_DAY = new ColorRGBA(0.35f, 0.55f, 0.85f, 1f);
private static final ColorRGBA BG_NIGHT = new ColorRGBA(0.01f, 0.01f, 0.06f, 1f);
// ── Konfiguration ───────────────────────────────────────────────────────── // ── Konfiguration ─────────────────────────────────────────────────────────
@@ -47,8 +45,13 @@ public class DayNightState extends BaseAppState implements TimeListener {
private DirectionalLight sun; private DirectionalLight sun;
private AmbientLight ambient; private AmbientLight ambient;
private DirectionalLightShadowFilter shadowFilter; private DirectionalLightShadowFilter shadowFilter;
private Spatial sky; private Node skyNode;
private Geometry sunSphere; private SkyControl skyControl;
// ── Sonnenrichtungs-Drosselung ────────────────────────────────────────────
private static final float SUN_DIR_INTERVAL = 0.25f;
private float sunDirTimer = SUN_DIR_INTERVAL; // sofort bereit beim ersten Aufruf
// ── Höhlen-Abdunkelung ──────────────────────────────────────────────────── // ── Höhlen-Abdunkelung ────────────────────────────────────────────────────
@@ -56,16 +59,11 @@ public class DayNightState extends BaseAppState implements TimeListener {
private float caveFactor = 0f; private float caveFactor = 0f;
private float targetCaveFactor = 0f; private float targetCaveFactor = 0f;
private float caveCheckTimer = 0f; private float caveCheckTimer = 0f;
/** Sonnenlicht-Basisfarbe (Tag/Nacht ohne Höhlen-Faktor). */
private ColorRGBA sunBaseColor = new ColorRGBA(1, 1, 1, 1); private ColorRGBA sunBaseColor = new ColorRGBA(1, 1, 1, 1);
/** Schatten-Intensität ohne Höhlen-Faktor. */
private float shadowBaseIntensity = 0f; private float shadowBaseIntensity = 0f;
/** Raycast-Abstand nach oben (m) trifft Decken bis zu dieser Höhe. */
private static final float CAVE_RAY_HEIGHT = 12f; private static final float CAVE_RAY_HEIGHT = 12f;
/** Zeitabstand zwischen Raycasts (Sek.). */
private static final float CAVE_CHECK_INTERVAL = 0.2f; private static final float CAVE_CHECK_INTERVAL = 0.2f;
/** Überblendgeschwindigkeit (Einheit/Sek.) beim Ein- und Ausblenden. */
private static final float CAVE_FADE_SPEED = 1.2f; private static final float CAVE_FADE_SPEED = 1.2f;
// ── Konstruktoren ───────────────────────────────────────────────────────── // ── Konstruktoren ─────────────────────────────────────────────────────────
@@ -86,8 +84,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
} }
public DayTime getDayTime() { return dayTime; } public DayTime getDayTime() { return dayTime; }
public void setPaused(boolean p) { dayTime.setPaused(p); }
public void setPaused(boolean paused) { dayTime.setPaused(paused); }
/** Aktuelle Sonnenrichtung (normalisiert), oder (0,-1,0) falls noch nicht initialisiert. */ /** Aktuelle Sonnenrichtung (normalisiert), oder (0,-1,0) falls noch nicht initialisiert. */
public Vector3f getSunDirection() { public Vector3f getSunDirection() {
@@ -96,6 +93,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
public DirectionalLight getSunLight() { return sun; } public DirectionalLight getSunLight() { return sun; }
public AmbientLight getAmbientLight() { return ambient; } public AmbientLight getAmbientLight() { return ambient; }
public SkyControl getSkyControl() { return skyControl; }
/** /**
* Übergibt einen Shadow-Filter an DayNightState, damit dieser die Intensität * Übergibt einen Shadow-Filter an DayNightState, damit dieser die Intensität
@@ -113,41 +111,14 @@ public class DayNightState extends BaseAppState implements TimeListener {
this.app = (SimpleApplication) app; this.app = (SimpleApplication) app;
this.rootNode = this.app.getRootNode(); this.rootNode = this.app.getRootNode();
// Himmel-Sphere (prozedural, nach innen gerendert)
Sphere skyMesh = new Sphere(32, 32, 450f, true, true);
Geometry skyGeo = new Geometry("sky", skyMesh);
Material skyMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
skyMat.setColor("Color", BG_DAY.clone());
skyGeo.setMaterial(skyMat);
skyGeo.setQueueBucket(RenderQueue.Bucket.Sky);
skyGeo.setShadowMode(RenderQueue.ShadowMode.Off);
rootNode.attachChild(skyGeo);
sky = skyGeo;
// Sonnen-Sphere (sichtbare Sonne am Himmel)
Sphere sphereMesh = new Sphere(16, 16, 18f);
sunSphere = new Geometry("sunSphere", sphereMesh);
Material sunMat = new Material(app.getAssetManager(),
"Common/MatDefs/Misc/Unshaded.j3md");
sunMat.setColor("Color", new ColorRGBA(1f, 0.92f, 0.65f, 1f));
sunSphere.setMaterial(sunMat);
sunSphere.setQueueBucket(RenderQueue.Bucket.Sky);
sunSphere.setShadowMode(RenderQueue.ShadowMode.Off);
rootNode.attachChild(sunSphere);
// Sonne (DirectionalLight)
sun = new DirectionalLight(); sun = new DirectionalLight();
rootNode.addLight(sun); rootNode.addLight(sun);
// Ambient
ambient = new AmbientLight(); ambient = new AmbientLight();
rootNode.addLight(ambient); rootNode.addLight(ambient);
// Falls WorldScene.buildLighting() schon einen Filter registriert hat (setShadowFilter
// wurde vor initialize() aufgerufen, sun war damals null) — jetzt nachholen.
if (shadowFilter != null) shadowFilter.setLight(sun); if (shadowFilter != null) shadowFilter.setLight(sun);
// Schatten (nur im Game) als Filter, damit WorldScene ihn in den FPP einhängen kann
if (withShadows) { if (withShadows) {
try { try {
shadowFilter = new DirectionalLightShadowFilter(app.getAssetManager(), 4096, 4); shadowFilter = new DirectionalLightShadowFilter(app.getAssetManager(), 4096, 4);
@@ -156,12 +127,33 @@ public class DayNightState extends BaseAppState implements TimeListener {
shadowFilter.setShadowZExtend(40f); shadowFilter.setShadowZExtend(40f);
shadowFilter.setShadowZFadeLength(8f); shadowFilter.setShadowZFadeLength(8f);
shadowFilter.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON); shadowFilter.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
// Nicht direkt zum Viewport hinzufügen WorldScene hängt ihn in den FPP ein
} catch (Exception e) { } catch (Exception e) {
log.error("Shadow-Filter konnte nicht erstellt werden", e); log.error("Shadow-Filter konnte nicht erstellt werden", e);
} }
} }
// SkyControl an eigenem Kindknoten damit re-attach nach detachAllChildren() klappt
skyNode = new Node("skyNode");
rootNode.attachChild(skyNode);
SkyControl sc = new SkyControl(app.getAssetManager(), app.getCamera(),
0.9f, 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);
// Updater nur für Viewport-Hintergrundfarbe nutzen (Licht wird selbst gesteuert)
sc.getUpdater().addViewPort(this.app.getViewPort());
sc.setEnabled(true);
skyControl = sc;
dayTime.addListener(this); dayTime.addListener(this);
onTimeChanged(dayTime.getTimeOfDay()); onTimeChanged(dayTime.getTimeOfDay());
} }
@@ -174,9 +166,15 @@ public class DayNightState extends BaseAppState implements TimeListener {
dayTime.removeListener(this); dayTime.removeListener(this);
rootNode.removeLight(sun); rootNode.removeLight(sun);
rootNode.removeLight(ambient); rootNode.removeLight(ambient);
shadowFilter = null; // FPP-Cleanup liegt bei WorldScene shadowFilter = null;
if (sky != null && sky.getParent() != null) rootNode.detachChild(sky); if (skyControl != null) {
if (sunSphere != null && sunSphere.getParent() != null) rootNode.detachChild(sunSphere); skyNode.removeControl(SkyControl.class);
skyControl = null;
}
if (skyNode != null && skyNode.getParent() != null) {
rootNode.detachChild(skyNode);
}
skyNode = null;
} }
@Override protected void onEnable() {} @Override protected void onEnable() {}
@@ -184,31 +182,19 @@ public class DayNightState extends BaseAppState implements TimeListener {
@Override @Override
public void update(float tpf) { public void update(float tpf) {
sunDirTimer += tpf;
dayTime.update(tpf); dayTime.update(tpf);
// Sky/Sonnen-Sphere nach rootNode.detachAllChildren() wiederherstellen // skyNode nach rootNode.detachAllChildren() wiederherstellen
if (sky != null && sky.getParent() == null) if (skyNode != null && skyNode.getParent() == null) {
rootNode.attachChild(sky); rootNode.attachChild(skyNode);
if (sunSphere != null && sunSphere.getParent() == null)
rootNode.attachChild(sunSphere);
Vector3f camPos = app.getCamera().getLocation();
// Himmel-Sphere der Kamera folgen lassen
if (sky != null)
sky.setLocalTranslation(camPos);
// Sonnen-Sphere immer relativ zur Kamera positionieren
if (sunSphere != null && sunSphere.getCullHint() != Spatial.CullHint.Always) {
sunSphere.setLocalTranslation(camPos.add(sun.getDirection().negate().mult(480f)));
} }
updateCaveLighting(tpf, camPos); updateCaveLighting(tpf, app.getCamera().getLocation());
} }
/** Prüft per Physik-Raycast ob die Kamera unter einer Decke ist und blendet das Sonnenlicht aus. */ /** Prüft per Physik-Raycast ob die Kamera unter einer Decke ist und blendet das Sonnenlicht aus. */
private void updateCaveLighting(float tpf, Vector3f camPos) { private void updateCaveLighting(float tpf, Vector3f camPos) {
// Raycast nur alle CAVE_CHECK_INTERVAL Sekunden
caveCheckTimer += tpf; caveCheckTimer += tpf;
if (caveCheckTimer >= CAVE_CHECK_INTERVAL) { if (caveCheckTimer >= CAVE_CHECK_INTERVAL) {
caveCheckTimer = 0f; caveCheckTimer = 0f;
@@ -220,7 +206,6 @@ public class DayNightState extends BaseAppState implements TimeListener {
} }
} }
// Glatte Überblendung des Höhlen-Faktors
float prev = caveFactor; float prev = caveFactor;
if (caveFactor < targetCaveFactor) { if (caveFactor < targetCaveFactor) {
caveFactor = Math.min(targetCaveFactor, caveFactor + CAVE_FADE_SPEED * tpf); caveFactor = Math.min(targetCaveFactor, caveFactor + CAVE_FADE_SPEED * tpf);
@@ -228,72 +213,46 @@ public class DayNightState extends BaseAppState implements TimeListener {
caveFactor = Math.max(targetCaveFactor, caveFactor - CAVE_FADE_SPEED * tpf); caveFactor = Math.max(targetCaveFactor, caveFactor - CAVE_FADE_SPEED * tpf);
} }
// Licht nur aktualisieren wenn sich der Faktor geändert hat
if (caveFactor != prev) { if (caveFactor != prev) {
float scale = 1f - caveFactor; float scale = 1f - caveFactor;
sun.setColor(sunBaseColor.mult(scale)); sun.setColor(sunBaseColor.mult(scale));
if (shadowFilter != null) if (shadowFilter != null) {
shadowFilter.setShadowIntensity(shadowBaseIntensity * scale); shadowFilter.setShadowIntensity(shadowBaseIntensity * scale);
} }
} }
}
// ── Zeit-Callback ───────────────────────────────────────────────────────── // ── Zeit-Callback ─────────────────────────────────────────────────────────
@Override @Override
public void onTimeChanged(float t) { public void onTimeChanged(float t) {
float elev = sunElevation(t); if (skyControl == null) return;
float elevC = FastMath.clamp(elev, 0f, 1f);
// ── Sonnenrichtung ────────────────────────────────────────────────── // Sonne positionieren und Richtung holen
float angle = t * FastMath.TWO_PI; skyControl.getSunAndStars().setHour(t * 24f);
// Sonne bewegt sich von Ost (X+) über Süden nach West (X-) Vector3f toSun = skyControl.getSunAndStars().sunDirection(null);
Vector3f sunPos = new Vector3f( float elevation = toSun.y; // +1 = Zenit, 0 = Horizont, -1 = Nadir
FastMath.sin(angle) * 0.85f, float elevC = FastMath.clamp(elevation, 0f, 1f);
elev,
-0.15f
);
sun.setDirection(sunPos.negate().normalizeLocal());
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) ────────────── // ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) — jedes Frame ──
float dawnFactor = 1f - FastMath.clamp(elev * 5f, 0f, 1f); float dawnFactor = 1f - FastMath.clamp(elevation * 5f, 0f, 1f);
ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor); ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor);
sunBaseColor = sunColor.mult(elevC * 0.68f); sunBaseColor = sunColor.mult(elevC * 0.68f);
sun.setColor(sunBaseColor.mult(1f - caveFactor)); sun.setColor(sunBaseColor.mult(1f - caveFactor));
// ── Sonnen-Sphere ausblenden wenn unter Horizont ──────────────────── // ── Ambient: Nacht → Tag — jedes Frame ─────────────────────────────
if (sunSphere != null) { float ambFactor = FastMath.clamp((elevation + 0.2f) / 0.6f, 0f, 1f);
sunSphere.setCullHint(elev > -0.02f
? Spatial.CullHint.Inherit
: Spatial.CullHint.Always);
// Farbe bei Dämmerung orange, bei Tag weiß-gelb
Material m = sunSphere.getMaterial();
if (m != null) {
ColorRGBA sphereColor = new ColorRGBA(1f, 0.6f + elevC * 0.32f, 0.3f + elevC * 0.35f, 1f);
m.setColor("Color", sphereColor);
}
}
// ── Ambient: Nacht → Tag ────────────────────────────────────────────
float ambFactor = FastMath.clamp((elev + 0.2f) / 0.6f, 0f, 1f);
ambient.setColor(AMB_NIGHT.clone().interpolateLocal(AMB_DAY, ambFactor)); ambient.setColor(AMB_NIGHT.clone().interpolateLocal(AMB_DAY, ambFactor));
// ── Schatten ──────────────────────────────────────────────────────── shadowBaseIntensity = FastMath.clamp(elevation * 0.8f, 0f, 0.5f);
shadowBaseIntensity = FastMath.clamp(elev * 0.8f, 0f, 0.5f);
if (shadowFilter != null) // Lichtrichtung und Schatten nur 4×/Sek aktualisieren → kein Shadow-Map-Flackern
if (sunDirTimer >= SUN_DIR_INTERVAL) {
sunDirTimer = 0f;
sun.setDirection(toSun.negateLocal());
if (shadowFilter != null) {
shadowFilter.setShadowIntensity(shadowBaseIntensity * (1f - caveFactor)); shadowFilter.setShadowIntensity(shadowBaseIntensity * (1f - caveFactor));
// ── Himmel & Hintergrundfarbe ────────────────────────────────────────
float skyFactor = FastMath.clamp((elev + 0.05f) / 0.2f, 0f, 1f);
ColorRGBA skyColor = BG_NIGHT.clone().interpolateLocal(BG_DAY, skyFactor);
if (sky instanceof Geometry skyGeo)
skyGeo.getMaterial().setColor("Color", skyColor);
app.getViewPort().setBackgroundColor(skyColor);
} }
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
/** Sonnenhöhe: 1 = Mitternacht, 0 = Horizont, +1 = Mittag. */
private static float sunElevation(float t) {
return -FastMath.cos(t * FastMath.TWO_PI);
} }
} }

View File

@@ -7,9 +7,16 @@ import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText; import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput; import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput; import com.jme3.input.MouseInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger; import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.input.event.JoyButtonEvent;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.input.event.TouchEvent;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
@@ -20,12 +27,14 @@ import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad; import com.jme3.scene.shape.Quad;
import de.blight.common.model.DialogOption; import de.blight.common.model.DialogOption;
import de.blight.common.model.DialogStep;
import de.blight.common.model.MainCharacter; import de.blight.common.model.MainCharacter;
import de.blight.common.model.NPC; import de.blight.common.model.NPC;
import de.blight.common.model.TextReference; import de.blight.common.model.TextReference;
import de.blight.common.model.trigger.ChangeRoutineTrigger; import de.blight.common.model.trigger.ChangeRoutineTrigger;
import de.blight.common.model.trigger.NpcStatusTrigger; import de.blight.common.model.trigger.NpcStatusTrigger;
import de.blight.common.model.trigger.Trigger; import de.blight.common.model.trigger.Trigger;
import de.blight.game.audio.DialogTtsService;
import de.blight.game.config.MenuCanvas; import de.blight.game.config.MenuCanvas;
import de.blight.game.config.NinePatch; import de.blight.game.config.NinePatch;
import de.blight.lang.TextResolver; import de.blight.lang.TextResolver;
@@ -57,12 +66,12 @@ public class DialogHudState extends BaseAppState {
// ── Layout-Konstanten (virtuelle Koordinaten 1376 × 768) ───────────────── // ── Layout-Konstanten (virtuelle Koordinaten 1376 × 768) ─────────────────
private static final float PNL_X = 63f; private static final float PNL_X = 0f;
private static final float PNL_Y = 20f; private static final float PNL_Y = 0f;
private static final float PNL_W = 1250f; private static final float PNL_W = MenuCanvas.REF_W; // volle Breite
private static final float PNL_H = 255f; private static final float PNL_H = 270f;
private static final float MARGIN_X = 20f; private static final float MARGIN_X = 24f;
private static final float FONT_NAME = 18f; private static final float FONT_NAME = 18f;
private static final float FONT_TEXT = 16f; private static final float FONT_TEXT = 16f;
private static final float FONT_OPT = 15f; private static final float FONT_OPT = 15f;
@@ -70,8 +79,9 @@ public class DialogHudState extends BaseAppState {
private static final float LINE_H_OPT = 26f; private static final float LINE_H_OPT = 26f;
private static final int MAX_TEXT_LINES = 3; private static final int MAX_TEXT_LINES = 3;
private static final int MAX_CHARS_LINE = 82; private static final int MAX_CHARS_LINE = 112; // proportional zur vollen Breite
private static final int MAX_OPTIONS = 5; private static final int MAX_OPTIONS = 5;
private static final int MAX_OPT_CHARS = 90; // Textkürzung bei langen Hero-Sätzen
private static final ColorRGBA COL_NAME = new ColorRGBA(1.00f, 0.90f, 0.55f, 1f); private static final ColorRGBA COL_NAME = new ColorRGBA(1.00f, 0.90f, 0.55f, 1f);
private static final ColorRGBA COL_TEXT = ColorRGBA.White; private static final ColorRGBA COL_TEXT = ColorRGBA.White;
@@ -87,7 +97,6 @@ public class DialogHudState extends BaseAppState {
private static final String ACT_DOWN = "_DlgDown"; private static final String ACT_DOWN = "_DlgDown";
private static final String ACT_CONFIRM = "_DlgConfirm"; private static final String ACT_CONFIRM = "_DlgConfirm";
private static final String ACT_SKIP = "_DlgSkip"; private static final String ACT_SKIP = "_DlgSkip";
private static final String ACT_CLICK = "_DlgClick";
// ── Zustände ───────────────────────────────────────────────────────────── // ── Zustände ─────────────────────────────────────────────────────────────
@@ -113,9 +122,35 @@ public class DialogHudState extends BaseAppState {
/** Verfügbare Optionen (inkl. Exit). */ /** Verfügbare Optionen (inkl. Exit). */
private List<DisplayOption> displayOptions = new ArrayList<>(); private List<DisplayOption> displayOptions = new ArrayList<>();
private int selectedOpt = 0; private int selectedOpt = 0;
/** Erster sichtbarer Options-Index bei mehr Optionen als Slots. */
private int scrollOffset = 0;
/** Panel-Bounds für Maus-Hit-Tests. */ /** Sekunden bis zur automatischen Weiterleitung nach der letzten Textseite. */
private float[][] optBounds = new float[MAX_OPTIONS][4]; // x,y,w,h private static final float AUTO_ADVANCE_DELAY = 3.0f;
private float autoAdvanceTimer = -1f;
// LMB-Klick aus RawInputListener wird in update() verarbeitet.
// Weil RawInputListener VOR Action-Mappings läuft, ist beim dialog-öffnenden LMB
// phase noch HIDDEN → wird nicht gesetzt. Nachfolgende Klicks setzen ihn korrekt.
private boolean pendingClick = false;
private final RawInputListener rawMouseListener = new RawInputListener() {
@Override public void beginInput() {}
@Override public void endInput() {}
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
@Override public void onKeyEvent(KeyInputEvent evt) {}
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
@Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
@Override public void onTouchEvent(TouchEvent evt) {}
@Override
public void onMouseButtonEvent(MouseButtonEvent evt) {
if (evt.getButtonIndex() == MouseInput.BUTTON_LEFT && evt.isPressed()
&& phase == Phase.OPTIONS) {
pendingClick = true;
}
}
};
// ── JME3 UI-Knoten ──────────────────────────────────────────────────────── // ── JME3 UI-Knoten ────────────────────────────────────────────────────────
@@ -129,6 +164,12 @@ public class DialogHudState extends BaseAppState {
private BitmapText[] lineTexts = new BitmapText[MAX_TEXT_LINES]; private BitmapText[] lineTexts = new BitmapText[MAX_TEXT_LINES];
private BitmapText hintText; private BitmapText hintText;
private BitmapText[] optTexts = new BitmapText[MAX_OPTIONS]; private BitmapText[] optTexts = new BitmapText[MAX_OPTIONS];
private BitmapText scrollUpText;
private BitmapText scrollDownText;
// ── TTS ───────────────────────────────────────────────────────────────────
private DialogTtsService tts;
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -137,17 +178,79 @@ public class DialogHudState extends BaseAppState {
this.app = (SimpleApplication) app; this.app = (SimpleApplication) app;
this.guiNode = this.app.getGuiNode(); this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
tts = new DialogTtsService();
tts.initialize();
// Panel einmalig aufbauen, dann Shader sofort vorab kompilieren
buildPanel();
this.app.getRenderManager().preloadScene(canvasNode);
} }
@Override @Override
protected void onEnable() {} protected void onEnable() {}
@Override
public void update(float tpf) {
if (pendingClick) {
pendingClick = false;
onMouseClick(app.getInputManager().getCursorPosition());
}
if (phase == Phase.OPTIONS && !displayOptions.isEmpty()) {
updateHover();
}
if (autoAdvanceTimer > 0f && (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO)) {
autoAdvanceTimer -= tpf;
if (autoAdvanceTimer <= 0f) {
autoAdvanceTimer = -1f;
advanceText();
}
}
}
private void updateHover() {
Vector2f cursor = app.getInputManager().getCursorPosition();
float scale = Math.min(
app.getCamera().getWidth() / MenuCanvas.REF_W,
app.getCamera().getHeight() / MenuCanvas.REF_H);
float oy2 = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
float vy = (cursor.y - oy2) / scale;
if (vy < PNL_Y || vy > PNL_Y + PNL_H) return;
float baseY = PNL_Y + PNL_H - 80f;
int total = displayOptions.size();
for (int slot = 0; slot < MAX_OPTIONS; slot++) {
int optIdx = scrollOffset + slot;
if (optIdx >= total) break;
float ty = baseY - slot * LINE_H_OPT;
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) {
if (selectedOpt != optIdx) {
selectedOpt = optIdx;
renderOptions();
}
return;
}
}
}
@Override @Override
protected void onDisable() { protected void onDisable() {
if (phase != Phase.HIDDEN) closePanel(); if (phase != Phase.HIDDEN) closePanel();
} }
@Override protected void cleanup(Application app) {} @Override
protected void cleanup(Application app) {
if (tts != null) {
tts.shutdown();
tts = null;
}
if (canvasNode != null) {
guiNode.detachChild(canvasNode);
canvasNode = null;
panel = null;
}
}
// ── Public API ──────────────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────────────
@@ -166,31 +269,27 @@ public class DialogHudState extends BaseAppState {
this.onOptionsShown = onOptionsShown; this.onOptionsShown = onOptionsShown;
this.optionsShownFired = false; this.optionsShownFired = false;
buildPanel(); setHudsVisible(false);
canvasNode.setCullHint(Spatial.CullHint.Inherit);
registerInput(); registerInput();
// Erst: Default-Message anzeigen (falls vorhanden), dann Optionen
TextReference greeting = npc.getDefaultMessage();
String greetText = greeting != null ? TextResolver.get().resolve(greeting) : null;
List<DialogOption> available = resolveOptions(npc, mc); List<DialogOption> available = resolveOptions(npc, mc);
if (greetText != null && !greetText.isBlank()) { if (!available.isEmpty()) {
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, () -> { // Optionen sofort anzeigen keine Begrüßungsverzögerung
if (available.isEmpty()) { showOptions(available);
log.info("[DialogHud] NPC '{}' hat keine Optionen nach Begrüßung.", npc.getCharacterId());
closeDialog();
} else { } else {
showOptions(available); // Kein Optionen: Begrüßungstext zeigen und dann schließen
} TextReference greeting = npc.getDefaultMessage();
}); String greetText = greeting != null ? TextResolver.get().resolve(greeting) : null;
} else if (!available.isEmpty()) { if (greetText != null && !greetText.isBlank()) {
showOptions(available); showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, this::closeDialog);
} else { } else {
log.info("[DialogHud] NPC '{}' hat keine Optionen und keine Begrüßung Dialog übersprungen.", npc.getCharacterId()); log.info("[DialogHud] NPC '{}' hat keine Optionen und keine Begrüßung Dialog übersprungen.", npc.getCharacterId());
closeDialog(); closeDialog();
} }
} }
}
public boolean isActive() { public boolean isActive() {
return phase != Phase.HIDDEN; return phase != Phase.HIDDEN;
@@ -200,12 +299,27 @@ public class DialogHudState extends BaseAppState {
private Runnable afterText; private Runnable afterText;
private void playSteps(Phase ph, String speaker, List<DialogStep> steps, int idx, Runnable after) {
if (idx >= steps.size()) {
after.run();
return;
}
DialogStep step = steps.get(idx);
String text = step.getText() != null ? TextResolver.get().resolve(step.getText()) : null;
if (text == null || text.isBlank()) {
playSteps(ph, speaker, steps, idx + 1, after);
return;
}
showText(ph, speaker, text, () -> playSteps(ph, speaker, steps, idx + 1, after));
}
private void showText(Phase textPhase, String speaker, String rawText, Runnable after) { private void showText(Phase textPhase, String speaker, String rawText, Runnable after) {
this.phase = textPhase; this.phase = textPhase;
this.afterText = after; this.afterText = after;
this.textPages = paginate(wrapText(rawText, MAX_CHARS_LINE), MAX_TEXT_LINES); this.textPages = paginate(wrapText(rawText, MAX_CHARS_LINE), MAX_TEXT_LINES);
this.pageIdx = 0; this.pageIdx = 0;
renderCurrentTextPage(speaker); renderCurrentTextPage(speaker);
if (tts != null) tts.speak(rawText);
} }
private void renderCurrentTextPage(String speaker) { private void renderCurrentTextPage(String speaker) {
@@ -219,9 +333,13 @@ public class DialogHudState extends BaseAppState {
} }
boolean more = (pageIdx + 1) < textPages.size(); boolean more = (pageIdx + 1) < textPages.size();
hintText.setText(more if (more) {
? t("dialog.hint.advance") autoAdvanceTimer = -1f;
: t("dialog.hint.continue")); hintText.setText(t("dialog.hint.advance"));
} else {
autoAdvanceTimer = AUTO_ADVANCE_DELAY;
hintText.setText("");
}
hintText.setCullHint(Spatial.CullHint.Inherit); hintText.setCullHint(Spatial.CullHint.Inherit);
for (BitmapText o : optTexts) o.setCullHint(Spatial.CullHint.Always); for (BitmapText o : optTexts) o.setCullHint(Spatial.CullHint.Always);
@@ -233,6 +351,7 @@ public class DialogHudState extends BaseAppState {
phase = Phase.OPTIONS; phase = Phase.OPTIONS;
displayOptions.clear(); displayOptions.clear();
selectedOpt = 0; selectedOpt = 0;
scrollOffset = 0;
if (!optionsShownFired && onOptionsShown != null) { if (!optionsShownFired && onOptionsShown != null) {
optionsShownFired = true; optionsShownFired = true;
@@ -240,11 +359,27 @@ public class DialogHudState extends BaseAppState {
} }
for (DialogOption opt : options) { for (DialogOption opt : options) {
String label = opt.getLabel() != null // Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten)
? TextResolver.get().resolveId(opt.getLabel().id()) : ""; String label = "";
List<DialogStep> hs = opt.getHeroSteps();
if (hs != null && !hs.isEmpty() && hs.get(0).getText() != null) {
label = TextResolver.get().resolve(hs.get(0).getText());
}
if (label.isBlank() && opt.getTextHero() != null) {
label = TextResolver.get().resolve(opt.getTextHero());
}
// Fallback: Label-Key
if (label.isBlank() && opt.getLabel() != null) {
label = TextResolver.get().resolveId(opt.getLabel().id());
}
// Letzter Fallback: Option-ID (gekürzt)
if (label.isBlank()) { if (label.isBlank()) {
label = opt.getId() != null label = opt.getId() != null
? opt.getId().substring(0, Math.min(opt.getId().length(), 20)) : "?"; ? opt.getId().substring(0, Math.min(opt.getId().length(), 40)) : "?";
}
// Lange Texte kürzen damit sie in eine Zeile passen
if (label.length() > MAX_OPT_CHARS) {
label = label.substring(0, MAX_OPT_CHARS - 1) + "";
} }
displayOptions.add(new DisplayOption(label, opt)); displayOptions.add(new DisplayOption(label, opt));
} }
@@ -260,40 +395,51 @@ public class DialogHudState extends BaseAppState {
private void renderOptions() { private void renderOptions() {
float baseY = PNL_Y + PNL_H - 80f; float baseY = PNL_Y + PNL_H - 80f;
int total = displayOptions.size();
for (int i = 0; i < MAX_OPTIONS; i++) { boolean canScrollUp = scrollOffset > 0;
if (i < displayOptions.size()) { boolean canScrollDown = (scrollOffset + MAX_OPTIONS) < total;
DisplayOption do_ = displayOptions.get(i);
boolean sel = (i == selectedOpt);
String prefix = sel ? "" : " ";
optTexts[i].setText(prefix + (i + 1) + ". " + do_.label());
optTexts[i].setColor(sel ? COL_OPT_SEL : COL_OPT);
optTexts[i].setCullHint(Spatial.CullHint.Inherit);
float ty = baseY - i * LINE_H_OPT; scrollUpText .setCullHint(canScrollUp ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
optTexts[i].setLocalTranslation(PNL_X + MARGIN_X, ty, 2f); scrollDownText.setCullHint(canScrollDown ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
optBounds[i][0] = PNL_X + MARGIN_X; for (int slot = 0; slot < MAX_OPTIONS; slot++) {
optBounds[i][1] = ty - FONT_OPT; int optIdx = scrollOffset + slot;
optBounds[i][2] = PNL_W - MARGIN_X * 2; if (optIdx < total) {
optBounds[i][3] = FONT_OPT + 4; DisplayOption do_ = displayOptions.get(optIdx);
boolean sel = (optIdx == selectedOpt);
optTexts[slot].setText((sel ? "" : " ") + do_.label());
optTexts[slot].setColor(sel ? COL_OPT_SEL : COL_OPT);
optTexts[slot].setLocalTranslation(PNL_X + MARGIN_X, baseY - slot * LINE_H_OPT, 2f);
optTexts[slot].setCullHint(Spatial.CullHint.Inherit);
} else { } else {
optTexts[i].setCullHint(Spatial.CullHint.Always); optTexts[slot].setCullHint(Spatial.CullHint.Always);
} }
} }
} }
/** Stellt sicher, dass selectedOpt im sichtbaren Scroll-Fenster liegt. */
private void ensureVisible() {
if (selectedOpt < scrollOffset) {
scrollOffset = selectedOpt;
} else if (selectedOpt >= scrollOffset + MAX_OPTIONS) {
scrollOffset = selectedOpt - MAX_OPTIONS + 1;
}
}
// ── Input-Handler ───────────────────────────────────────────────────────── // ── Input-Handler ─────────────────────────────────────────────────────────
private void onUp() { private void onUp() {
if (phase != Phase.OPTIONS) return; if (phase != Phase.OPTIONS) return;
selectedOpt = Math.max(0, selectedOpt - 1); selectedOpt = Math.max(0, selectedOpt - 1);
ensureVisible();
renderOptions(); renderOptions();
} }
private void onDown() { private void onDown() {
if (phase != Phase.OPTIONS) return; if (phase != Phase.OPTIONS) return;
selectedOpt = Math.min(displayOptions.size() - 1, selectedOpt + 1); selectedOpt = Math.min(displayOptions.size() - 1, selectedOpt + 1);
ensureVisible();
renderOptions(); renderOptions();
} }
@@ -307,42 +453,71 @@ public class DialogHudState extends BaseAppState {
private void onSkip() { private void onSkip() {
if (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO) { if (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO) {
// Alle verbleibenden Seiten überspringen autoAdvanceTimer = -1f;
if (tts != null) tts.stop();
pageIdx = textPages.size(); pageIdx = textPages.size();
if (afterText != null) { Runnable cb = afterText; afterText = null; cb.run(); } if (afterText != null) { Runnable cb = afterText; afterText = null; cb.run(); }
} else if (phase == Phase.OPTIONS) {
confirmOption(selectedOpt);
} }
// RMB only skips text never selects options
} }
private void onMouseClick(Vector2f cursor) { private void onMouseClick(Vector2f cursor) {
if (phase != Phase.OPTIONS) { onConfirm(); return; } if (phase != Phase.OPTIONS) { onConfirm(); return; }
// Kursorenanpassung auf virtuelle Koordinaten
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W) / 2f;
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H) / 2f;
float scale = Math.min( float scale = Math.min(
app.getCamera().getWidth() / MenuCanvas.REF_W, app.getCamera().getWidth() / MenuCanvas.REF_W,
app.getCamera().getHeight() / MenuCanvas.REF_H); app.getCamera().getHeight() / MenuCanvas.REF_H);
float ox2 = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
float oy2 = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f; float oy2 = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
float vx = (cursor.x - ox2) / scale;
float vy = (cursor.y - oy2) / scale; float vy = (cursor.y - oy2) / scale;
for (int i = 0; i < displayOptions.size() && i < MAX_OPTIONS; i++) { log.debug("[DialogHud] Klick vy={} (Panel {}-{})", vy, PNL_Y, PNL_Y + PNL_H);
float bx = optBounds[i][0], by = optBounds[i][1];
float bw = optBounds[i][2], bh = optBounds[i][3]; if (vy < PNL_Y || vy > PNL_Y + PNL_H) return;
if (vx >= bx && vx <= bx + bw && vy >= by && vy <= by + bh) {
selectedOpt = i; float baseY = PNL_Y + PNL_H - 80f;
int total = displayOptions.size();
// Klick auf Scroll-Pfeil ▲
if (scrollOffset > 0) {
float ty = baseY + LINE_H_OPT;
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) {
scrollOffset = Math.max(0, scrollOffset - 1);
renderOptions(); renderOptions();
confirmOption(i);
return; return;
} }
} }
// Klick auf Scroll-Pfeil ▼
if (scrollOffset + MAX_OPTIONS < total) {
float ty = baseY - MAX_OPTIONS * LINE_H_OPT;
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) {
scrollOffset = Math.min(total - MAX_OPTIONS, scrollOffset + 1);
renderOptions();
return;
}
}
// Klick auf Options-Slot
for (int slot = 0; slot < MAX_OPTIONS; slot++) {
int optIdx = scrollOffset + slot;
if (optIdx >= total) break;
float ty = baseY - slot * LINE_H_OPT;
float by = ty - LINE_H_OPT + 4f;
float byEnd = ty + 4f;
if (vy >= by && vy <= byEnd) {
log.debug("[DialogHud] Treffer: Option {} (slot {} ty={})", optIdx, slot, ty);
selectedOpt = optIdx;
renderOptions();
confirmOption(optIdx);
return;
}
}
log.debug("[DialogHud] Klick ohne Treffer: vy={}", vy);
} }
private void advanceText() { private void advanceText() {
autoAdvanceTimer = -1f;
if (pageIdx + 1 < textPages.size()) { if (pageIdx + 1 < textPages.size()) {
pageIdx++; pageIdx++;
renderCurrentTextPage(nameText.getText()); renderCurrentTextPage(nameText.getText());
@@ -365,30 +540,46 @@ public class DialogHudState extends BaseAppState {
DialogOption opt = selected.option(); DialogOption opt = selected.option();
// Held-Text // Steps mit Fallback auf Legacy-Felder
String heroText = opt.getTextHero() != null List<DialogStep> heroSteps = opt.getHeroSteps();
? TextResolver.get().resolve(opt.getTextHero()) : null; List<DialogStep> npcSteps = opt.getNpcSteps();
// NPC-Text if (heroSteps == null || heroSteps.isEmpty()) {
String npcText = opt.getTextNpc() != null if (opt.getTextHero() != null) {
? TextResolver.get().resolve(opt.getTextNpc()) : null; heroSteps = List.of(new DialogStep(opt.getTextHero(), opt.getAudioHero()));
} else {
heroSteps = List.of();
}
}
if (npcSteps == null || npcSteps.isEmpty()) {
if (opt.getTextNpc() != null) {
npcSteps = List.of(new DialogStep(opt.getTextNpc(), opt.getAudioNpc()));
} else {
npcSteps = List.of();
}
}
final List<DialogStep> fHeroSteps = heroSteps;
final List<DialogStep> fNpcSteps = npcSteps;
final String fNpcAnim = opt.getNpcAnimation();
// Option-Effekte anwenden (Optionen aktualisieren, Quest, etc.) // Option-Effekte anwenden (Optionen aktualisieren, Quest, etc.)
applyOption(opt); applyOption(opt);
List<DialogOption> nextOpts = resolveOptions(currentNpc, mainChar); List<DialogOption> nextOpts = resolveOptions(currentNpc, mainChar);
if (heroText != null && !heroText.isBlank()) { if (!fHeroSteps.isEmpty()) {
showText(Phase.TEXT_HERO, t("dialog.speaker.player"), heroText, () -> { playSteps(Phase.TEXT_HERO, t("dialog.speaker.player"), fHeroSteps, 0, () -> {
if (npcText != null && !npcText.isBlank()) { if (!fNpcSteps.isEmpty()) {
showText(Phase.TEXT_NPC, resolveNpcName(currentNpc), npcText, () -> { triggerNpcDialogAnim(fNpcAnim);
playSteps(Phase.TEXT_NPC, resolveNpcName(currentNpc), fNpcSteps, 0, () -> {
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts); if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
}); });
} else { } else {
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts); if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
} }
}); });
} else if (npcText != null && !npcText.isBlank()) { } else if (!fNpcSteps.isEmpty()) {
showText(Phase.TEXT_NPC, resolveNpcName(currentNpc), npcText, () -> { triggerNpcDialogAnim(fNpcAnim);
playSteps(Phase.TEXT_NPC, resolveNpcName(currentNpc), fNpcSteps, 0, () -> {
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts); if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
}); });
} else { } else {
@@ -396,6 +587,16 @@ public class DialogHudState extends BaseAppState {
} }
} }
private void triggerNpcDialogAnim(String anim) {
String effective = (anim != null && !anim.isBlank())
? anim
: (Math.random() < 0.5 ? "talk1" : "talk2");
WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class);
if (npcs != null) {
npcs.playNpcDialogAnim(effective);
}
}
// ── Optionen-Auflösung ──────────────────────────────────────────────────── // ── Optionen-Auflösung ────────────────────────────────────────────────────
private List<DialogOption> resolveOptions(NPC npc, MainCharacter mc) { private List<DialogOption> resolveOptions(NPC npc, MainCharacter mc) {
@@ -454,7 +655,8 @@ public class DialogHudState extends BaseAppState {
// ── UI-Aufbau / -Abbau ──────────────────────────────────────────────────── // ── UI-Aufbau / -Abbau ────────────────────────────────────────────────────
private void buildPanel() { private void buildPanel() {
canvasNode = MenuCanvas.createFixedCanvas(app.getCamera()); canvasNode = MenuCanvas.createCanvas(app.getCamera());
canvasNode.setCullHint(Spatial.CullHint.Always);
guiNode.attachChild(canvasNode); guiNode.attachChild(canvasNode);
panel = new Node("dialog-panel"); panel = new Node("dialog-panel");
@@ -494,16 +696,29 @@ public class DialogHudState extends BaseAppState {
panel.attachChild(optTexts[i]); panel.attachChild(optTexts[i]);
} }
// Scroll-Indikatoren (Positionen relativ zu baseY = PNL_Y + PNL_H - 80)
float baseY0 = PNL_Y + PNL_H - 80f;
scrollUpText = makeTxt("▲ mehr", FONT_OPT - 2f, COL_HINT);
scrollUpText.setLocalTranslation(PNL_X + MARGIN_X, baseY0 + LINE_H_OPT, 2f);
scrollUpText.setCullHint(Spatial.CullHint.Always);
panel.attachChild(scrollUpText);
scrollDownText = makeTxt("▼ mehr", FONT_OPT - 2f, COL_HINT);
scrollDownText.setLocalTranslation(PNL_X + MARGIN_X, baseY0 - MAX_OPTIONS * LINE_H_OPT, 2f);
scrollDownText.setCullHint(Spatial.CullHint.Always);
panel.attachChild(scrollDownText);
canvasNode.attachChild(panel); canvasNode.attachChild(panel);
} }
private void closePanel() { private void closePanel() {
phase = Phase.HIDDEN; phase = Phase.HIDDEN;
autoAdvanceTimer = -1f;
if (tts != null) tts.stop();
setHudsVisible(true);
unregisterInput(); unregisterInput();
if (canvasNode != null) { if (canvasNode != null) {
guiNode.detachChild(canvasNode); canvasNode.setCullHint(Spatial.CullHint.Always);
canvasNode = null;
panel = null;
} }
textPages.clear(); textPages.clear();
displayOptions.clear(); displayOptions.clear();
@@ -522,17 +737,19 @@ public class DialogHudState extends BaseAppState {
im.addMapping(ACT_CONFIRM, new KeyTrigger(KeyInput.KEY_RETURN), im.addMapping(ACT_CONFIRM, new KeyTrigger(KeyInput.KEY_RETURN),
new KeyTrigger(KeyInput.KEY_NUMPADENTER)); new KeyTrigger(KeyInput.KEY_NUMPADENTER));
im.addMapping(ACT_SKIP, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); im.addMapping(ACT_SKIP, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
im.addMapping(ACT_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); im.addListener(inputListener, ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP);
im.addListener(inputListener, ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP, ACT_CLICK); im.addRawInputListener(rawMouseListener);
im.setCursorVisible(true); im.setCursorVisible(true);
} }
private void unregisterInput() { private void unregisterInput() {
var im = app.getInputManager(); var im = app.getInputManager();
try { im.removeListener(inputListener); } catch (Exception ignored) {} try { im.removeListener(inputListener); } catch (Exception ignored) {}
for (String a : new String[]{ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP, ACT_CLICK}) { try { im.removeRawInputListener(rawMouseListener); } catch (Exception ignored) {}
for (String a : new String[]{ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP}) {
try { im.deleteMapping(a); } catch (Exception ignored) {} try { im.deleteMapping(a); } catch (Exception ignored) {}
} }
pendingClick = false;
im.setCursorVisible(false); im.setCursorVisible(false);
} }
@@ -543,7 +760,6 @@ public class DialogHudState extends BaseAppState {
case ACT_DOWN -> onDown(); case ACT_DOWN -> onDown();
case ACT_CONFIRM -> onConfirm(); case ACT_CONFIRM -> onConfirm();
case ACT_SKIP -> onSkip(); case ACT_SKIP -> onSkip();
case ACT_CLICK -> onMouseClick(app.getInputManager().getCursorPosition());
} }
}; };
@@ -580,6 +796,13 @@ public class DialogHudState extends BaseAppState {
private static String t(String id) { return TextResolver.get().resolveId(id); } private static String t(String id) { return TextResolver.get().resolveId(id); }
private void setHudsVisible(boolean visible) {
HudState hud = getStateManager().getState(HudState.class);
if (hud != null) { hud.setEnabled(visible); }
InteractionHudState ihs = getStateManager().getState(InteractionHudState.class);
if (ihs != null) { ihs.setEnabled(visible); }
}
/** Bricht Text an Wortgrenzen auf. */ /** Bricht Text an Wortgrenzen auf. */
private static List<String> wrapText(String text, int maxChars) { private static List<String> wrapText(String text, int maxChars) {
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();

View File

@@ -0,0 +1,66 @@
package de.blight.game.state;
import com.jme3.asset.AssetManager;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.texture.Texture2D;
import com.jme3.water.WaterFilter;
/**
* Extends WaterFilter to support the dynamic wave-interaction texture
* defined in our overridden Water.j3md / Water.frag shaders.
* Handles the case where setWaveMap() is called before the filter is
* initialized by queuing the params and applying them in initFilter().
*/
public class DynamicWaterFilter extends WaterFilter {
private Texture2D pendingMap;
private Vector2f pendingCenter;
private float pendingInvExtent;
private float pendingStrength;
public DynamicWaterFilter(Node scene, Vector3f lightDir) {
super(scene, lightDir);
}
@Override
protected void initFilter(AssetManager manager, RenderManager rm, ViewPort vp, int w, int h) {
super.initFilter(manager, rm, vp, w, h);
if (pendingMap != null) {
applyNow(pendingMap, pendingCenter, pendingInvExtent, pendingStrength);
pendingMap = null;
}
}
/** Called once from WaterInteractionState after the texture is ready. */
public void setWaveMap(Texture2D map, Vector2f center, float invExtent, float strength) {
if (getMaterial() != null) {
applyNow(map, center, invExtent, strength);
} else {
pendingMap = map;
pendingCenter = center;
pendingInvExtent = invExtent;
pendingStrength = strength;
}
}
/** Called every time the simulation area recenters around the player. */
public void updateWaveCenter(Vector2f center) {
if (getMaterial() != null) {
getMaterial().setVector2("WaveAreaCenter", center);
} else {
pendingCenter = center;
}
}
private void applyNow(Texture2D map, Vector2f center, float invExtent, float strength) {
getMaterial().setBoolean("WaveInteraction", true);
getMaterial().setTexture("WaveMap", map);
getMaterial().setVector2("WaveAreaCenter", center);
getMaterial().setFloat("WaveAreaInvExtent", invExtent);
getMaterial().setFloat("WaveStrength", strength);
}
}

View File

@@ -7,12 +7,16 @@ import com.jme3.asset.AssetManager;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh; import com.jme3.scene.Mesh;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexBlade;
import de.blight.common.GrassVertexIO; import de.blight.common.GrassVertexIO;
@@ -21,7 +25,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Rendert Vertex-Gras-Büschel im Spiel: 3 geneigte, verjüngte Halme pro Büschel, * Rendert Vertex-Gras-Büschel im Spiel: 3 geneigte, verjüngte Halme pro Büschel,
@@ -43,7 +49,7 @@ public class GrassVertexRenderState extends BaseAppState
// ── Geometrie (identisch zu GrassVertexState) ───────────────────────────── // ── Geometrie (identisch zu GrassVertexState) ─────────────────────────────
private static final int BLADES_PER_TUFT = 3; private static final int BLADES_PER_TUFT = 3;
private static final int SEGMENTS = 5; private static final int SEGMENTS = 5;
private static final float WIDTH_FACTOR = 0.05f; private static final float WIDTH_FACTOR = 0.10f;
private static final float BEND_FACTOR = 0.15f; private static final float BEND_FACTOR = 0.15f;
private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f); private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f); private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
@@ -54,11 +60,19 @@ public class GrassVertexRenderState extends BaseAppState
private static final ColorRGBA VERY_DRY_ROOT_COLOR = new ColorRGBA(0.18f, 0.09f, 0.02f, 1f); private static final ColorRGBA VERY_DRY_ROOT_COLOR = new ColorRGBA(0.18f, 0.09f, 0.02f, 1f);
private static final ColorRGBA VERY_DRY_TIP_COLOR = new ColorRGBA(0.38f, 0.20f, 0.05f, 1f); private static final ColorRGBA VERY_DRY_TIP_COLOR = new ColorRGBA(0.38f, 0.20f, 0.05f, 1f);
// ── Samen-Texturen ────────────────────────────────────────────────────────
private static final String SEED_TEX_BASE = "Textures/internal/gras/seeds/seeds";
private static final String SEED_TEX_EXT = ".png";
private static final float SEED_SIZE_FACTOR = 0.45f;
private static final float SEED_Y_FACTOR = 0.78f;
// ── Zustand ─────────────────────────────────────────────────────────────── // ── Zustand ───────────────────────────────────────────────────────────────
private final TerrainChunkState terrainChunkState; private final TerrainChunkState terrainChunkState;
private AssetManager assetManager; private AssetManager assetManager;
private Node grassNode; private Node grassNode;
private Material material; private Material material;
private Material[] seedMaterials = new Material[0];
private Material seedStalkMaterial;
private int nextChunk = 0; private int nextChunk = 0;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -88,6 +102,40 @@ public class GrassVertexRenderState extends BaseAppState
} }
material = buildMaterial(); material = buildMaterial();
seedMaterials = loadSeedMaterials();
seedStalkMaterial = buildSeedStalkMaterial();
}
private Material buildSeedStalkMaterial() {
Material mat = new Material(assetManager, "MatDefs/GrassVertex.j3md");
mat.setFloat("WindSpeed", 1.0f);
mat.setFloat("WindStrength", 0.15f);
mat.setVector3("SunDir", new Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
private Material[] loadSeedMaterials() {
List<Material> mats = new ArrayList<>();
for (int i = 1; i <= 99; i++) {
String path = SEED_TEX_BASE + i + SEED_TEX_EXT;
try {
Texture tex = assetManager.loadTexture(path);
Material mat = new Material(assetManager, "MatDefs/GrassSeed.j3md");
mat.setTexture("ColorMap", tex);
mat.setFloat("WindSpeed", 1.0f);
mat.setFloat("WindStrength", 0.15f);
mat.setVector3("SunDir", new Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mats.add(mat);
log.info("[GrassVertexRenderState] Samen-Textur geladen: {}", path);
} catch (Exception e) {
break;
}
}
return mats.toArray(new Material[0]);
} }
@Override @Override
@@ -110,9 +158,39 @@ public class GrassVertexRenderState extends BaseAppState
if (material != null && material.getMaterialDef().getMaterialParam("SunColor") != null) { if (material != null && material.getMaterialDef().getMaterialParam("SunColor") != null) {
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class); DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
if (dns != null && dns.getSunLight() != null) { if (dns != null && dns.getSunLight() != null) {
material.setVector3("SunDir", dns.getSunDirection().negate()); Vector3f sunDir = dns.getSunDirection().negate();
com.jme3.math.ColorRGBA sc = dns.getSunLight().getColor(); ColorRGBA sc = dns.getSunLight().getColor();
material.setVector3("SunDir", sunDir);
material.setColor("SunColor", sc); material.setColor("SunColor", sc);
if (seedStalkMaterial != null) {
seedStalkMaterial.setVector3("SunDir", sunDir);
seedStalkMaterial.setColor("SunColor", sc);
}
for (Material sm : seedMaterials) {
sm.setVector3("SunDir", sunDir);
sm.setColor("SunColor", sc);
}
}
}
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
if (ws != null && material != null) {
Vector3f wd3 = ws.getWindDirection();
Vector2f wDir = new Vector2f(wd3.x, wd3.z);
float speed = Math.max(0.05f, ws.getWindSpeed() * 0.05f);
float strength = FastMath.clamp(ws.getWindSpeed() * 0.009f, 0.01f, 0.45f);
material.setVector2("WindDir", wDir);
material.setFloat("WindSpeed", speed);
material.setFloat("WindStrength", strength);
if (seedStalkMaterial != null) {
seedStalkMaterial.setVector2("WindDir", wDir);
seedStalkMaterial.setFloat("WindSpeed", speed);
seedStalkMaterial.setFloat("WindStrength", strength);
}
for (Material sm : seedMaterials) {
sm.setVector2("WindDir", wDir);
sm.setFloat("WindSpeed", speed);
sm.setFloat("WindStrength", strength);
} }
} }
} }
@@ -180,10 +258,141 @@ public class GrassVertexRenderState extends BaseAppState
Node node = new Node("gvc_" + ci); Node node = new Node("gvc_" + ci);
node.attachChild(geo); node.attachChild(geo);
// ── Samen-Stiele + Kreuze ─────────────────────────────────────────────
if (seedMaterials.length > 0) {
List<GrassVertexBlade> seededAll = new ArrayList<>();
Map<Integer, List<GrassVertexBlade>> byTex = new HashMap<>();
for (GrassVertexBlade b : blades) {
if (b.seedIdx() >= 0 && b.seedIdx() < seedMaterials.length) {
seededAll.add(b);
byTex.computeIfAbsent(b.seedIdx(), k -> new ArrayList<>()).add(b);
}
}
if (!seededAll.isEmpty()) {
Geometry stalkGeo = buildSeedStalkMesh("stalk_" + ci, seededAll);
stalkGeo.setMaterial(seedStalkMaterial);
node.attachChild(stalkGeo);
}
for (Map.Entry<Integer, List<GrassVertexBlade>> e : byTex.entrySet()) {
Geometry seedGeo = buildSeedCrossMesh("seed_" + ci + "_" + e.getKey(), e.getValue());
seedGeo.setMaterial(seedMaterials[e.getKey()]);
node.attachChild(seedGeo);
}
}
chunkNodes[ci] = node; chunkNodes[ci] = node;
grassNode.attachChild(node); grassNode.attachChild(node);
} }
private static Geometry buildSeedStalkMesh(String name, List<GrassVertexBlade> blades) {
final int SEG = 4;
final float R = 0xbb / 255f, G = 0x90 / 255f, B = 0x59 / 255f;
int n = blades.size();
int vTotal = n * (SEG + 1) * 2;
float[] pos = new float[vTotal * 3];
float[] nrm = new float[vTotal * 3];
float[] col = new float[vTotal * 4];
float[] tex = new float[vTotal * 2];
int[] idx = new int [n * SEG * 6];
int vi = 0, ii = 0;
for (GrassVertexBlade blade : blades) {
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
float hw = h * 0.018f;
float ang = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
float cA = (float) Math.cos(ang), sA = (float) Math.sin(ang);
// Normale senkrecht zur Halmbreite, 30 % Richtung Weltauf gekippt (wie Gras-Shader)
float nx = -sA, ny = 0f, nz = cA;
float blend = 0.30f;
nx *= (1f - blend); ny = blend; nz *= (1f - blend);
float nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
nx /= nLen; ny /= nLen; nz /= nLen;
for (int s = 0; s <= SEG; s++) {
float t = (float) s / SEG;
float curHW = hw * (float) Math.pow(1.0 - t, 1.4);
float py = y + h * t;
int sviL = vi + s * 2;
int sviR = sviL + 1;
pos[sviL*3] = x - cA*curHW; pos[sviL*3+1] = py; pos[sviL*3+2] = z - sA*curHW;
nrm[sviL*3] = nx; nrm[sviL*3+1] = ny; nrm[sviL*3+2] = nz;
col[sviL*4] = R; col[sviL*4+1] = G; col[sviL*4+2] = B; col[sviL*4+3] = 1f;
tex[sviL*2] = t; tex[sviL*2+1] = 0f;
pos[sviR*3] = x + cA*curHW; pos[sviR*3+1] = py; pos[sviR*3+2] = z + sA*curHW;
nrm[sviR*3] = nx; nrm[sviR*3+1] = ny; nrm[sviR*3+2] = nz;
col[sviR*4] = R; col[sviR*4+1] = G; col[sviR*4+2] = B; col[sviR*4+3] = 1f;
tex[sviR*2] = t; tex[sviR*2+1] = 0f;
}
for (int s = 0; s < SEG; s++) {
int b0 = vi + s * 2;
idx[ii] = b0; idx[ii+1] = b0+1; idx[ii+2] = b0+3;
idx[ii+3] = b0; idx[ii+4] = b0+3; idx[ii+5] = b0+2;
ii += 6;
}
vi += (SEG + 1) * 2;
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(nrm));
m.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(col));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return new Geometry(name, m);
}
private static Geometry buildSeedCrossMesh(String name, List<GrassVertexBlade> blades) {
int n = blades.size();
float[] pos = new float[n * 8 * 3];
float[] tex = new float[n * 8 * 2];
int[] idx = new int [n * 12];
int vi = 0, ii = 0;
for (GrassVertexBlade b : blades) {
float x = b.x();
float yBot = b.y() + b.height() * SEED_Y_FACTOR;
float z = b.z();
float size = b.height() * SEED_SIZE_FACTOR;
float hw = size * 0.5f;
// Quad 1 entlang Welt-X
setSeedV(pos, tex, vi+0, x-hw, yBot, z, 0,0);
setSeedV(pos, tex, vi+1, x+hw, yBot, z, 1,0);
setSeedV(pos, tex, vi+2, x+hw, yBot+size, z, 1,1);
setSeedV(pos, tex, vi+3, x-hw, yBot+size, z, 0,1);
// Quad 2 entlang Welt-Z
setSeedV(pos, tex, vi+4, x, yBot, z-hw, 0,0);
setSeedV(pos, tex, vi+5, x, yBot, z+hw, 1,0);
setSeedV(pos, tex, vi+6, x, yBot+size, z+hw, 1,1);
setSeedV(pos, tex, vi+7, x, yBot+size, z-hw, 0,1);
idx[ii] = vi; idx[ii+1] = vi+1; idx[ii+2] = vi+2;
idx[ii+3] = vi; idx[ii+4] = vi+2; idx[ii+5] = vi+3;
idx[ii+6] = vi+4; idx[ii+7] = vi+5; idx[ii+8] = vi+6;
idx[ii+9] = vi+4; idx[ii+10] = vi+6; idx[ii+11] = vi+7;
vi += 8; ii += 12;
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return new Geometry(name, m);
}
private static void setSeedV(float[] pos, float[] tex, int vi,
float x, float y, float z, float u, float v) {
pos[vi*3] = x; pos[vi*3+1] = y; pos[vi*3+2] = z;
tex[vi*2] = u; tex[vi*2+1] = v;
}
// ── ChunkListener: Gras ab LOD 1 ausblenden ─────────────────────────────── // ── ChunkListener: Gras ab LOD 1 ausblenden ───────────────────────────────
@Override @Override

View File

@@ -0,0 +1,213 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* CPU wave-equation simulation for dynamic water interaction.
*
* Maintains a 256×256 ping-pong height grid covering a 128m×128m area around the player.
* Each frame:
* 1. Injects a blob at the player's feet if they are in water and moving.
* 2. Advances the discrete wave equation: newH = (2*cur - prev + K*laplacian) * damping
* 3. Uploads the result to a Luminance8 Texture2D that the modified Water.frag reads.
*
* The simulation area recenters on the player when they move more than ~29m from the center;
* the buffers are cleared and DynamicWaterFilter.updateWaveCenter() is called so the shader
* samples the correct world region.
*/
public class WaterInteractionState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WaterInteractionState.class);
private static final int N = 256;
private static final float HALF_EXTENT = 64f; // half of 128m world coverage
private static final float INV_EXTENT = 1.0f / (HALF_EXTENT * 2f);
private static final float WAVE_K = 0.25f; // wave eq. coefficient (stability: ≤ 0.5)
private static final float DAMPING = 0.995f;
private static final float WAVE_STRENGTH = 6.0f; // normal perturbation scale in shader
private static final float BLOB_RADIUS_TEX = 3.0f; // blob radius in texels
private static final float BLOB_COOLDOWN = 0.07f; // seconds between blob injections
private static final float RECENTER_THRESH = HALF_EXTENT * 0.45f; // ~29m
// ── Simulation buffers ───────────────────────────────────────────────────
private float[] waveCur;
private float[] wavePrev;
private float[] waveNext;
private ByteBuffer waveBuf;
private Image waveImg;
private Texture2D waveTex;
// ── Simulation state ─────────────────────────────────────────────────────
private final Vector2f simCenter = new Vector2f();
private boolean paramsApplied = false;
private float blobTimer = 0f;
// ── External references ──────────────────────────────────────────────────
private final DynamicWaterFilter dynFilter;
private CharacterControl playerControl;
private float waterHeight = 0f;
private final Vector3f prevPos = new Vector3f(Float.NaN, 0f, Float.NaN);
public WaterInteractionState(DynamicWaterFilter filter) {
this.dynFilter = filter;
}
/**
* Must be called after CharacterControl is available (typically after buildCharacter() in WorldScene).
* Safe to call multiple times.
*/
public void setPlayerControl(CharacterControl ctrl, float waterLevel) {
this.playerControl = ctrl;
this.waterHeight = waterLevel;
prevPos.set(Float.NaN, 0f, Float.NaN);
}
// ── AppState lifecycle ───────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
waveCur = new float[N * N];
wavePrev = new float[N * N];
waveNext = new float[N * N];
waveBuf = BufferUtils.createByteBuffer(N * N);
for (int i = 0; i < N * N; i++) {
waveBuf.put((byte) 128); // neutral height = 0.5 encoded
}
waveBuf.rewind();
waveImg = new Image(Image.Format.Luminance8, N, N, waveBuf, null, ColorSpace.Linear);
waveTex = new Texture2D(waveImg);
waveTex.setMagFilter(Texture.MagFilter.Bilinear);
waveTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
waveTex.setWrap(Texture.WrapMode.EdgeClamp);
log.debug("[WaterInteraction] Simulation initialized ({}×{}, {}m coverage)", N, N, HALF_EXTENT * 2f);
}
@Override
protected void cleanup(Application app) {
waveCur = wavePrev = waveNext = null;
waveBuf = null;
waveImg = null;
waveTex = null;
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void update(float tpf) {
if (tpf < 0.001f) { return; }
// Register texture with the filter once (handles pre-init via pending queue in DynamicWaterFilter)
if (!paramsApplied) {
dynFilter.setWaveMap(waveTex,
new Vector2f(simCenter.x, simCenter.y), INV_EXTENT, WAVE_STRENGTH);
paramsApplied = true;
}
if (playerControl != null) {
Vector3f pos = playerControl.getPhysicsLocation();
maybeRecenter(pos.x, pos.z);
float feetY = pos.y - 0.9f; // CharacterControl position = capsule center, ~0.9m above feet
if (feetY < waterHeight + 0.4f) { // player in or just above water
float speed = Float.isNaN(prevPos.x) ? 0f : pos.distance(prevPos) / tpf;
if (speed > 0.3f) { // ignore sub-threshold shuffling
blobTimer -= tpf;
if (blobTimer <= 0f) {
float strength = FastMath.clamp(speed / 8.0f, 0.1f, 1.0f);
addBlob(pos.x, pos.z, strength);
blobTimer = BLOB_COOLDOWN;
}
}
}
prevPos.set(pos);
}
runSimulation();
uploadTexture();
}
// ── Simulation ───────────────────────────────────────────────────────────
private void runSimulation() {
for (int y = 1; y < N - 1; y++) {
for (int x = 1; x < N - 1; x++) {
int i = y * N + x;
float cur = waveCur[i];
float prev = wavePrev[i];
float laplacian = waveCur[i - 1] + waveCur[i + 1]
+ waveCur[i - N] + waveCur[i + N]
- 4f * cur;
waveNext[i] = FastMath.clamp((2f * cur - prev + WAVE_K * laplacian) * DAMPING, -1f, 1f);
}
}
// Swap buffers: prev ← cur ← next ← (reuse old prev)
float[] tmp = wavePrev;
wavePrev = waveCur;
waveCur = waveNext;
waveNext = tmp;
}
private void addBlob(float worldX, float worldZ, float strength) {
float left = simCenter.x - HALF_EXTENT;
float top = simCenter.y - HALF_EXTENT;
float span = HALF_EXTENT * 2f;
int cx = (int)((worldX - left) / span * N);
int cz = (int)((worldZ - top) / span * N);
int r = (int)(BLOB_RADIUS_TEX + 1f);
for (int dy = -r; dy <= r; dy++) {
for (int dx = -r; dx <= r; dx++) {
int x = cx + dx;
int z = cz + dy;
if (x < 1 || x >= N - 1 || z < 1 || z >= N - 1) { continue; }
float dist = FastMath.sqrt(dx * dx + dy * dy);
if (dist > BLOB_RADIUS_TEX) { continue; }
float w = (1f - dist / BLOB_RADIUS_TEX) * strength;
waveCur[z * N + x] = FastMath.clamp(waveCur[z * N + x] + w, -1f, 1f);
}
}
}
private void maybeRecenter(float worldX, float worldZ) {
float dx = worldX - simCenter.x;
float dz = worldZ - simCenter.y;
if (FastMath.abs(dx) > RECENTER_THRESH || FastMath.abs(dz) > RECENTER_THRESH) {
simCenter.set(worldX, worldZ);
Arrays.fill(waveCur, 0f);
Arrays.fill(wavePrev, 0f);
Arrays.fill(waveNext, 0f);
dynFilter.updateWaveCenter(new Vector2f(simCenter.x, simCenter.y));
}
}
private void uploadTexture() {
waveBuf.clear();
for (int i = 0; i < N * N; i++) {
waveBuf.put((byte)((waveCur[i] * 0.5f + 0.5f) * 255f));
}
waveBuf.rewind();
waveImg.setUpdateNeeded();
}
}

View File

@@ -1,13 +1,14 @@
package de.blight.game.state; package de.blight.game.state;
import com.jme3.app.Application; import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState; import com.jme3.app.state.BaseAppState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.post.filters.FogFilter; import com.jme3.post.filters.FogFilter;
import com.jme3.water.WaterFilter; import com.jme3.water.WaterFilter;
import jme3utilities.sky.SkyControl;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -27,6 +28,7 @@ public class WeatherState extends BaseAppState {
private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f}; private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f};
private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f }; private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f };
private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f }; private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f };
private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f };
private static final ColorRGBA[] FOG_COLOR = { private static final ColorRGBA[] FOG_COLOR = {
new ColorRGBA(0.75f, 0.80f, 0.88f, 1f), new ColorRGBA(0.75f, 0.80f, 0.88f, 1f),
@@ -34,12 +36,6 @@ public class WeatherState extends BaseAppState {
new ColorRGBA(0.42f, 0.43f, 0.46f, 1f), new ColorRGBA(0.42f, 0.43f, 0.46f, 1f),
new ColorRGBA(0.18f, 0.19f, 0.21f, 1f), new ColorRGBA(0.18f, 0.19f, 0.21f, 1f),
}; };
private static final ColorRGBA[] CLOUD_COLOR = {
new ColorRGBA(0.95f, 0.95f, 0.95f, 0.40f),
new ColorRGBA(0.75f, 0.75f, 0.78f, 0.72f),
new ColorRGBA(0.35f, 0.35f, 0.37f, 0.92f),
new ColorRGBA(0.08f, 0.08f, 0.10f, 0.98f),
};
private static final ColorRGBA[] WATER_COLOR = { private static final ColorRGBA[] WATER_COLOR = {
new ColorRGBA(0.05f, 0.25f, 0.55f, 1f), new ColorRGBA(0.05f, 0.25f, 0.55f, 1f),
new ColorRGBA(0.04f, 0.18f, 0.42f, 1f), new ColorRGBA(0.04f, 0.18f, 0.42f, 1f),
@@ -58,7 +54,6 @@ public class WeatherState extends BaseAppState {
private Weather active = Weather.SUNNY; private Weather active = Weather.SUNNY;
private float changeTimer = 120f; private float changeTimer = 120f;
// Aktuell interpolierte Werte
private float fogDensity = 0f; private float fogDensity = 0f;
private float fogDistance = 600f; private float fogDistance = 600f;
private float windSpeed = 4f; private float windSpeed = 4f;
@@ -67,10 +62,10 @@ public class WeatherState extends BaseAppState {
private float waveScale = 0.008f; private float waveScale = 0.008f;
private float waterTrans = 0.15f; private float waterTrans = 0.15f;
private float foamIntensity = 0f; private float foamIntensity = 0f;
private float cloudOpacity = 0f;
private float windAngle = 0f; private float windAngle = 0f;
private float windAngleTgt = 0.4f; private float windAngleTgt = 0.4f;
private final ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f); private final ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f);
private final ColorRGBA cloudColor = new ColorRGBA(0.95f, 0.95f, 0.95f, 0.40f);
private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f); private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f);
private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f); private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f);
@@ -78,12 +73,11 @@ public class WeatherState extends BaseAppState {
private FogFilter fogFilter; private FogFilter fogFilter;
private WaterFilter waterFilter; private WaterFilter waterFilter;
private CloudsNode cloudsNode; private SkyControl skyControl;
private Application app;
public void setFogFilter(FogFilter f) { this.fogFilter = f; } public void setFogFilter(FogFilter f) { this.fogFilter = f; }
public void setWaterFilter(WaterFilter f) { this.waterFilter = f; } public void setWaterFilter(WaterFilter f) { this.waterFilter = f; }
public void setCloudsNode(CloudsNode n) { this.cloudsNode = n; } public void setSkyControl(SkyControl sc) { this.skyControl = sc; }
public Weather getActiveWeather() { return active; } public Weather getActiveWeather() { return active; }
public float getWindSpeed() { return windSpeed; } public float getWindSpeed() { return windSpeed; }
@@ -105,7 +99,7 @@ public class WeatherState extends BaseAppState {
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@Override protected void initialize(Application app) { this.app = app; } @Override protected void initialize(Application app) {}
@Override protected void cleanup(Application app) {} @Override protected void cleanup(Application app) {}
@Override protected void onEnable() {} @Override protected void onEnable() {}
@Override protected void onDisable() {} @Override protected void onDisable() {}
@@ -128,10 +122,10 @@ public class WeatherState extends BaseAppState {
waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f); waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f);
waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f); 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);
windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f); windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f);
fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f); fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f);
cloudColor.interpolateLocal(CLOUD_COLOR[i], tpf * 0.025f);
waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f); waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f);
deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f); deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f);
@@ -150,13 +144,11 @@ public class WeatherState extends BaseAppState {
waterFilter.setWaterColor(waterColor.clone()); waterFilter.setWaterColor(waterColor.clone());
waterFilter.setDeepWaterColor(deepWaterColor.clone()); waterFilter.setDeepWaterColor(deepWaterColor.clone());
waterFilter.setWindDirection( waterFilter.setWindDirection(
new com.jme3.math.Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle))); new Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle)));
} }
if (cloudsNode != null) { if (skyControl != null) {
cloudsNode.setCloudColor(cloudColor.clone()); skyControl.getCloudLayer(0).setOpacity(cloudOpacity);
Vector3f camPos = ((SimpleApplication) app).getCamera().getLocation();
cloudsNode.update(tpf, getWindDirection(), windSpeed * 0.5f, camPos);
} }
} }

View File

@@ -17,8 +17,16 @@ import com.jme3.math.*;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.scene.*; import com.jme3.scene.*;
import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Box;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.Armature;
import com.jme3.anim.ArmatureMask;
import com.jme3.anim.Joint;
import com.jme3.anim.SkinningControl;
import com.jme3.anim.tween.Tween;
import com.jme3.anim.tween.action.BaseAction;
import de.blight.common.PlacedModel; import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO; import de.blight.common.PlacedModelIO;
import de.blight.common.SaveGameIO;
import de.blight.common.model.*; import de.blight.common.model.*;
import de.blight.common.model.trigger.ChangeRoutineTrigger; import de.blight.common.model.trigger.ChangeRoutineTrigger;
import de.blight.common.model.trigger.NpcStatusTrigger; import de.blight.common.model.trigger.NpcStatusTrigger;
@@ -106,8 +114,17 @@ public class WorldNpcsState extends BaseAppState {
private int hoveredIdx = -1; private int hoveredIdx = -1;
private boolean dialogActive = false; private boolean dialogActive = false;
private SpawnedNpc dialogNpc = null;
private ThirdPersonCamera thirdPersonCam; private ThirdPersonCamera thirdPersonCam;
// ── NPC Head-Look während Dialog ──────────────────────────────────────────
private static final String HEAD_LAYER = "__dlg_headlook__";
private HeadLookTween dialogHeadTween = null;
// Einmaliger Warmup: BVH-Baum des rootNode beim ersten NPC-Spawn aufbauen,
// damit der erste Dialog-Raycast nicht stottert.
private boolean sceneBvhWarmedUp = false;
// ── NPC-Rotations-Animation ─────────────────────────────────────────────── // ── NPC-Rotations-Animation ───────────────────────────────────────────────
private static final float ROT_DURATION = 0.5f; private static final float ROT_DURATION = 0.5f;
@@ -214,6 +231,9 @@ public class WorldNpcsState extends BaseAppState {
} }
updateNpcRotation(tpf); updateNpcRotation(tpf);
updateNpcRevert(tpf); updateNpcRevert(tpf);
if (dialogActive && dialogHeadTween != null) {
dialogHeadTween.targetPos = physicsChar.getPhysicsLocation();
}
} }
// ── Spawn-Logik ─────────────────────────────────────────────────────────── // ── Spawn-Logik ───────────────────────────────────────────────────────────
@@ -260,6 +280,13 @@ public class WorldNpcsState extends BaseAppState {
spawned.add(s); spawned.add(s);
playNpcAction(s, activityActionFor(npc, currentHour)); playNpcAction(s, activityActionFor(npc, currentHour));
log.debug("[WorldNpcs] NPC '{}' gespawnt bei ({}, {})", npc.getCharacterId(), pos.x, pos.z); log.debug("[WorldNpcs] NPC '{}' gespawnt bei ({}, {})", npc.getCharacterId(), pos.x, pos.z);
// Beim ersten Spawn: BVH-Baum der Szene aufbauen, damit der erste Dialog-Raycast nicht stottert
if (!sceneBvhWarmedUp) {
sceneBvhWarmedUp = true;
com.jme3.collision.CollisionResults warmup = new com.jme3.collision.CollisionResults();
rootNode.collideWith(new com.jme3.math.Ray(pos.add(0f, 5f, 0f), new com.jme3.math.Vector3f(0f, -1f, 0f)), warmup);
}
} }
} }
@@ -346,8 +373,26 @@ public class WorldNpcsState extends BaseAppState {
startDialog(hoveredIdx); startDialog(hoveredIdx);
}; };
/**
* Spielt einen Dialog-Animations-Clip einmalig auf dem aktuellen Dialog-NPC.
* Falls der NPC sitzt, wird stattdessen "talk_sitting" gespielt.
* Tut nichts wenn kein Dialog aktiv ist oder clipName leer.
*/
public void playNpcDialogAnim(String clipName) {
if (dialogNpc == null || animLib == null) return;
if (clipName == null || clipName.isBlank()) return;
String effective = (dialogNpc.currentAction == AnimationAction.SITTING) ? "talk_sitting" : clipName;
if (animLib.playOn(effective, dialogNpc.visual())) {
dialogNpc.currentAction = null;
}
}
private void startDialog(int idx) { private void startDialog(int idx) {
SpawnedNpc entry = spawned.get(idx); SpawnedNpc entry = spawned.get(idx);
dialogNpc = entry;
if (entry.currentAction == AnimationAction.SITTING) {
startDialogHeadLook(entry);
}
DialogHudState dialog = getApplication().getStateManager().getState(DialogHudState.class); DialogHudState dialog = getApplication().getStateManager().getState(DialogHudState.class);
if (dialog == null) return; if (dialog == null) return;
@@ -370,12 +415,11 @@ public class WorldNpcsState extends BaseAppState {
Quaternion facingRot = computeFacingQuat(npcPos, playerPos); Quaternion facingRot = computeFacingQuat(npcPos, playerPos);
beginSmoothRotation(entry, origRot, facingRot); beginSmoothRotation(entry, origRot, facingRot);
// Kamera wird erst gesetzt wenn Optionen erscheinen // Dialog-Kamera sofort aktivieren (nicht erst bei Optionen)
boolean[] optionsWereShown = {false};
Runnable onOptions = () -> {
optionsWereShown[0] = true;
enterDialogCamera(npcPos, playerPos); enterDialogCamera(npcPos, playerPos);
};
boolean[] optionsWereShown = {false};
Runnable onOptions = () -> { optionsWereShown[0] = true; };
dialog.startDialog(entry.npc(), mainCharacter, onOptions, () -> { dialog.startDialog(entry.npc(), mainCharacter, onOptions, () -> {
exitDialogCamera(); exitDialogCamera();
@@ -402,6 +446,12 @@ public class WorldNpcsState extends BaseAppState {
} }
private void scheduleRevert(SpawnedNpc npc, Quaternion from, Quaternion to, float delay) { private void scheduleRevert(SpawnedNpc npc, Quaternion from, Quaternion to, float delay) {
// Falls die NPC-Rotation noch läuft, sofort auf Zielrotation snappen,
// damit updateNpcRotation und updateNpcRevert nicht gegeneinander arbeiten.
if (rotNpc == npc) {
npc.visual().setLocalRotation(rotTo);
rotNpc = null;
}
revertNpc = npc; revertNpc = npc;
revertFrom = from.clone(); revertFrom = from.clone();
revertTo = to.clone(); revertTo = to.clone();
@@ -429,49 +479,80 @@ public class WorldNpcsState extends BaseAppState {
} }
private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) { private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) {
// Mittelpunkt auf Hüfthöhe // NPC-Spatial auf Bodenniveau; playerPos = Physik-Kapsel-Mitte → auf NPC-Boden projizieren
Vector3f mid = npcPos.add(playerPos).multLocal(0.5f); float groundY = npcPos.y;
mid.y += 0.9f; Vector3f npcFeet = new Vector3f(npcPos.x, groundY, npcPos.z);
Vector3f playerFeet = new Vector3f(playerPos.x, groundY, playerPos.z);
float charDist = Math.max(1.0f, npcFeet.distance(playerFeet));
// Zwei mögliche Seiten (je 90° zur Blicklinie) // Lookat: Brust-/Schulterhöhe zwischen beiden Figuren
Vector3f axis = npcPos.subtract(playerPos).normalizeLocal(); Vector3f lookAt = npcFeet.add(playerFeet).multLocal(0.5f);
Vector3f sideA = axis.cross(Vector3f.UNIT_Y).normalizeLocal(); lookAt.y += 1.2f;
Vector3f sideB = sideA.negate();
float camSide = 3.5f; // Senkrechte Seite zur NPC↔Spieler-Achse
float camUp = 1.2f; Vector3f axis = npcFeet.subtract(playerFeet);
if (axis.lengthSquared() < 0.001f) { axis.set(1f, 0f, 0f); }
axis.normalizeLocal();
Vector3f side = axis.cross(Vector3f.UNIT_Y).normalizeLocal();
Vector3f posA = mid.add(sideA.mult(camSide)).add(0, camUp, 0); // FOV 45° → tan(22.5°) ≈ 0.41 → für charDist/2 sichtbar: dist = (charDist/2)/0.41 × Puffer
Vector3f posB = mid.add(sideB.mult(camSide)).add(0, camUp, 0); // Faktor 2.5 gibt genug Rand dass Figuren + Umgebung im Bild sind; Minimum 5 m
float dist = Math.max(5.0f, charDist * 2.5f);
float camUp = 1.6f;
posA = avoidClipping(mid, posA); // Seite wählen: erhöhter Horizontalstrahl (2 m über lookAt), um Terrain-Clipping zu vermeiden.
posB = avoidClipping(mid, posB); // avoidClipping() wird NICHT für die Distanz genutzt, weil Bodenstrahlen bei großen
// Distanzen häufig auf Geländekanten treffen und die Kamera fälschlich nah platzieren.
Vector3f elevated = lookAt.add(0f, 2f, 0f);
float freeA = openDistance(elevated, side);
float freeB = openDistance(elevated, side.negate());
Vector3f chosen = freeA >= freeB ? side : side.negate();
// Seite mit mehr Abstand ist weniger verdeckt Vector3f camPos = lookAt.add(chosen.mult(dist)).add(0f, camUp, 0f);
Vector3f camPos = posA.distance(mid) >= posB.distance(mid) ? posA : posB;
if (thirdPersonCam != null) { if (thirdPersonCam != null) { thirdPersonCam.setPaused(true); }
thirdPersonCam.setPaused(true);
}
cam.setLocation(camPos); cam.setLocation(camPos);
cam.lookAt(mid, Vector3f.UNIT_Y); cam.lookAt(lookAt, Vector3f.UNIT_Y);
}
/** Freier Abstand in {@code dir} ab {@code origin} bis zur ersten Kollision (max Float.MAX_VALUE). */
private float openDistance(Vector3f origin, Vector3f dir) {
CollisionResults cr = new CollisionResults();
rootNode.collideWith(new Ray(origin, dir), cr);
return cr.size() == 0 ? Float.MAX_VALUE : cr.getClosestCollision().getDistance();
} }
private void exitDialogCamera() { private void exitDialogCamera() {
if (thirdPersonCam != null) { stopDialogHeadLook();
thirdPersonCam.setPaused(false); if (thirdPersonCam != null) { thirdPersonCam.setPaused(false); }
if (dialogNpc != null) {
int hour = dayNight != null ? dayNight.getDayTime().getHour() : 12;
playNpcAction(dialogNpc, activityActionFor(dialogNpc.npc(), hour));
dialogNpc = null;
} }
} }
/** Prüft ob zwischen {@code from} und {@code to} kein Hindernis im rootNode liegt. */
private boolean hasLineOfSight(Vector3f from, Vector3f to) {
Vector3f dir = to.subtract(from);
float dist = dir.length();
if (dist < 0.001f) { return true; }
dir.normalizeLocal();
CollisionResults results = new CollisionResults();
rootNode.collideWith(new Ray(from, dir), results);
if (results.size() == 0) { return true; }
return results.getClosestCollision().getDistance() >= dist - 0.2f;
}
private Vector3f avoidClipping(Vector3f from, Vector3f to) { private Vector3f avoidClipping(Vector3f from, Vector3f to) {
Vector3f dir = to.subtract(from); Vector3f dir = to.subtract(from);
float maxDist = dir.length(); float maxDist = dir.length();
dir.normalizeLocal(); dir.normalizeLocal();
CollisionResults results = new CollisionResults(); CollisionResults results = new CollisionResults();
rootNode.collideWith(new Ray(from, dir), results); rootNode.collideWith(new Ray(from, dir), results);
if (results.size() == 0) return to; if (results.size() == 0) { return to; }
CollisionResult nearest = results.getClosestCollision(); CollisionResult nearest = results.getClosestCollision();
if (nearest.getDistance() >= maxDist) return to; if (nearest.getDistance() >= maxDist) { return to; }
float safeDist = Math.max(1.2f, nearest.getDistance() - 0.3f); float safeDist = Math.max(1.2f, nearest.getDistance() - 0.3f);
return from.add(dir.mult(safeDist)); return from.add(dir.mult(safeDist));
} }
@@ -516,17 +597,24 @@ public class WorldNpcsState extends BaseAppState {
private void loadAllNpcs() { private void loadAllNpcs() {
allNpcs.clear(); allNpcs.clear();
SaveGameState saveState = getStateManager().getState(SaveGameState.class); SaveGameState saveState = getStateManager().getState(SaveGameState.class);
// blight.new.game=true → Editor startet neues Spiel; autostart überspringt resetForNewGame()
boolean isNewGame = "true".equals(System.getProperty("blight.new.game"));
boolean useSaved = !isNewGame
&& saveState != null
&& saveState.getSave().character.positionSaved
&& SaveGameIO.exists();
try { try {
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character"); java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
for (GameCharacter gc : CharacterIO.loadAll(charDir)) { for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
if (gc instanceof NPC npc) { if (gc instanceof NPC npc) {
java.util.List<String> savedState = saveState != null java.util.List<String> savedState = useSaved
? saveState.getDialogState(npc.getCharacterId()) : null; ? saveState.getDialogState(npc.getCharacterId()) : null;
npc.initRuntimeTree(savedState); npc.initRuntimeTree(savedState);
allNpcs.add(npc); allNpcs.add(npc);
} }
} }
log.info("[WorldNpcs] {} NPCs geladen.", allNpcs.size()); log.info("[WorldNpcs] {} NPCs geladen (Dialogzustand: {}).",
allNpcs.size(), useSaved ? "gespeichert" : "neu");
} catch (Exception e) { } catch (Exception e) {
log.warn("[WorldNpcs] Fehler beim Laden der NPCs: {}", e.getMessage()); log.warn("[WorldNpcs] Fehler beim Laden der NPCs: {}", e.getMessage());
} }
@@ -589,6 +677,102 @@ public class WorldNpcsState extends BaseAppState {
} }
} }
// ── NPC Head-Look ─────────────────────────────────────────────────────────
private void startDialogHeadLook(SpawnedNpc npc) {
AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(npc.visual());
SkinningControl sc = de.blight.game.animation.RetargetingSystem.findSkinningControl(npc.visual());
if (ac == null || sc == null) { return; }
Armature arm = sc.getArmature();
Joint head = findHeadJoint(arm);
if (head == null) {
log.debug("[WorldNpcs] Kein Head-Joint gefunden Head-Look deaktiviert.");
return;
}
ArmatureMask mask;
try {
mask = ArmatureMask.createMask(arm, head.getName());
} catch (IllegalArgumentException e) {
return;
}
HeadLookTween tween = new HeadLookTween(head, npc.visual());
BaseAction action = new BaseAction(tween);
dialogHeadTween = tween;
ac.addAction(HEAD_LAYER, action);
ac.makeLayer(HEAD_LAYER, mask);
ac.setCurrentAction(HEAD_LAYER, HEAD_LAYER);
}
private void stopDialogHeadLook() {
if (dialogHeadTween == null) { return; }
dialogHeadTween.targetPos = null;
dialogHeadTween = null;
if (dialogNpc == null) { return; }
AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(dialogNpc.visual());
if (ac != null) {
ac.removeLayer(HEAD_LAYER);
}
}
private static Joint findHeadJoint(Armature arm) {
for (String name : new String[]{"Head", "head", "HEAD",
"mixamorig:Head", "mixamorig_Head", "Bip001 Head"}) {
Joint j = arm.getJoint(name);
if (j != null) { return j; }
}
for (int i = 0; i < arm.getJointCount(); i++) {
Joint j = arm.getJoint(i);
if (j.getName().toLowerCase(java.util.Locale.ROOT).contains("head")) { return j; }
}
return null;
}
/** Procedural Tween: dreht den Head-Joint jedes Frame in Richtung Spieler. */
private static final class HeadLookTween implements Tween {
final Joint headJoint;
final Spatial npcSpatial;
volatile Vector3f targetPos;
float smoothAngle = 0f;
HeadLookTween(Joint headJoint, Spatial npcSpatial) {
this.headJoint = headJoint;
this.npcSpatial = npcSpatial;
}
@Override public double getLength() { return Double.MAX_VALUE; }
@Override
public boolean interpolate(double t) {
Vector3f target = targetPos;
if (target == null) { return true; }
// Spielerposition in NPC-Model-Lokalraum umrechnen
Vector3f local = npcSpatial.worldToLocal(target, new Vector3f());
local.y = 0f;
float goalAngle = 0f;
if (local.lengthSquared() > 0.001f) {
local.normalizeLocal();
// NPC +Z = Vorwärts; positiver Winkel = Kopf nach rechts (+X)
goalAngle = FastMath.atan2(local.x, local.z);
goalAngle = FastMath.clamp(goalAngle, -FastMath.HALF_PI * 0.7f, FastMath.HALF_PI * 0.7f);
}
smoothAngle += (goalAngle - smoothAngle) * 0.12f;
// Look-Delta in Eltern-Raum vor die Animations-Rotation setzen
Quaternion animRot = headJoint.getLocalRotation().clone();
Quaternion lookDelta = new Quaternion().fromAngleAxis(smoothAngle, Vector3f.UNIT_Y);
headJoint.setLocalRotation(lookDelta.mult(animRot));
return true;
}
}
private static float dist2d(Vector3f a, Vector3f b) { private static float dist2d(Vector3f a, Vector3f b) {
float dx = a.x - b.x; float dx = a.x - b.x;
float dz = a.z - b.z; float dz = a.z - b.z;

View File

@@ -35,6 +35,7 @@ public class WorldObjectsState extends BaseAppState {
private AssetManager assets; private AssetManager assets;
private BulletAppState bulletAppState; private BulletAppState bulletAppState;
private final List<Material> sceneLitMaterials = new ArrayList<>(); private final List<Material> sceneLitMaterials = new ArrayList<>();
private final List<Material> windMaterials = new ArrayList<>();
/** RigidBodyControl pro Interactable-ID, damit Kollision während Animationen deaktiviert werden kann. */ /** RigidBodyControl pro Interactable-ID, damit Kollision während Animationen deaktiviert werden kann. */
private final Map<String, RigidBodyControl> interactableRbcs = new HashMap<>(); private final Map<String, RigidBodyControl> interactableRbcs = new HashMap<>();
@@ -117,9 +118,9 @@ public class WorldObjectsState extends BaseAppState {
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (sceneLitMaterials.isEmpty()) return; if (!sceneLitMaterials.isEmpty()) {
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class); DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
if (dns == null || dns.getSunLight() == null) return; if (dns != null && dns.getSunLight() != null) {
Vector3f lightDir = dns.getSunDirection().negate(); Vector3f lightDir = dns.getSunDirection().negate();
ColorRGBA sc = dns.getSunLight().getColor(); ColorRGBA sc = dns.getSunLight().getColor();
ColorRGBA ac = dns.getAmbientLight().getColor(); ColorRGBA ac = dns.getAmbientLight().getColor();
@@ -131,6 +132,26 @@ public class WorldObjectsState extends BaseAppState {
mat.setVector3("AmbientColor", ambVec); mat.setVector3("AmbientColor", ambVec);
} }
} }
}
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
if (ws != null && (!sceneLitMaterials.isEmpty() || !windMaterials.isEmpty())) {
Vector3f wd3 = ws.getWindDirection();
Vector2f wDir = new Vector2f(wd3.x, wd3.z);
float speed = Math.max(0.05f, ws.getWindSpeed() * 0.04f);
float strength = com.jme3.math.FastMath.clamp(ws.getWindSpeed() * 0.009f, 0.01f, 0.45f);
for (Material mat : sceneLitMaterials) {
mat.setVector2("WindDir", wDir);
mat.setFloat("WindSpeed", speed);
mat.setFloat("WindStrength", strength);
}
for (Material mat : windMaterials) {
mat.setVector2("WindDir", wDir);
mat.setFloat("WindSpeed", speed);
mat.setFloat("WindStrength", strength);
}
}
}
private Spatial buildSpatial(PlacedModel m) { private Spatial buildSpatial(PlacedModel m) {
// Exportiertes Mesh hat Vorrang vor modelPath // Exportiertes Mesh hat Vorrang vor modelPath
@@ -244,6 +265,8 @@ public class WorldObjectsState extends BaseAppState {
String name = mat.getMaterialDef().getName(); String name = mat.getMaterialDef().getName();
if ("Tree".equals(name) || "TreeLeaf".equals(name)) { if ("Tree".equals(name) || "TreeLeaf".equals(name)) {
sceneLitMaterials.add(mat); sceneLitMaterials.add(mat);
} else if ("Fern".equals(name)) {
windMaterials.add(mat);
} }
} else if (spatial instanceof Node node) { } else if (spatial instanceof Node node) {
for (Spatial child : node.getChildren()) { for (Spatial child : node.getChildren()) {