Weiter an den Dialogen gearbeitet
This commit is contained in:
BIN
assets/imported/animations/disappointed.glb
Normal file
BIN
assets/imported/animations/disappointed.glb
Normal file
Binary file not shown.
BIN
assets/imported/animations/sitting_talking.glb
Normal file
BIN
assets/imported/animations/sitting_talking.glb
Normal file
Binary file not shown.
BIN
assets/imported/animations/talking1.glb
Normal file
BIN
assets/imported/animations/talking1.glb
Normal file
Binary file not shown.
BIN
assets/imported/animations/talking2.glb
Normal file
BIN
assets/imported/animations/talking2.glb
Normal file
Binary file not shown.
BIN
assets/imported/animations/yelling.glb
Normal file
BIN
assets/imported/animations/yelling.glb
Normal file
Binary file not shown.
475
blight-assets/src/main/resources/Common/MatDefs/Water/Water.frag
Normal file
475
blight-assets/src/main/resources/Common/MatDefs/Water/Water.frag
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
27
blight-assets/src/main/resources/MatDefs/GrassSeed.j3md
Normal file
27
blight-assets/src/main/resources/MatDefs/GrassSeed.j3md
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
19
blight-assets/src/main/resources/Shaders/GrassSeed.frag
Normal file
19
blight-assets/src/main/resources/Shaders/GrassSeed.frag
Normal 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);
|
||||||
|
}
|
||||||
34
blight-assets/src/main/resources/Shaders/GrassSeed.vert
Normal file
34
blight-assets/src/main/resources/Shaders/GrassSeed.vert
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/animations/clips/talking1.j3o
Normal file
BIN
blight-assets/src/main/resources/animations/clips/talking1.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/animations/clips/talking2.j3o
Normal file
BIN
blight-assets/src/main/resources/animations/clips/talking2.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/animations/clips/yelling.j3o
Normal file
BIN
blight-assets/src/main/resources/animations/clips/yelling.j3o
Normal file
Binary file not shown.
@@ -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": {}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.5–1.0=gelb/braun)
|
* Format v2: wie v1 + float dryness pro Halm (0=grün, 0.5–1.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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ß (0–1) */
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<>();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user