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
|
||||
Float WindStrength : 0.15
|
||||
Float WindSpeed : 0.6
|
||||
Vector2 WindDir : 0.0 1.0
|
||||
Texture2D DiffuseMap
|
||||
Boolean HasDiffuseMap : false
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ MaterialDef Tree {
|
||||
Color Diffuse (Color) : 0.42 0.26 0.10 1.0
|
||||
Float WindStrength : 0.15
|
||||
Float WindSpeed : 0.5
|
||||
Vector2 WindDir : 0.0 1.0
|
||||
Texture2D BarkMap
|
||||
Boolean HasBarkMap : false
|
||||
Vector3 LightDir
|
||||
|
||||
@@ -4,6 +4,7 @@ MaterialDef TreeLeaf {
|
||||
Color Diffuse (Color) : 0.18 0.60 0.10 1.0
|
||||
Float WindStrength : 0.30
|
||||
Float WindSpeed : 0.7
|
||||
Vector2 WindDir : 0.0 1.0
|
||||
Texture2D LeafMap
|
||||
Boolean HasLeafMap : false
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ uniform mat4 g_WorldMatrix;
|
||||
uniform float g_Time;
|
||||
uniform float m_WindStrength;
|
||||
uniform float m_WindSpeed;
|
||||
uniform vec2 m_WindDir;
|
||||
|
||||
in vec3 inPosition;
|
||||
in vec3 inNormal;
|
||||
@@ -21,10 +22,21 @@ void main() {
|
||||
float windW = inColor.r;
|
||||
float t = g_Time * m_WindSpeed;
|
||||
vec4 wp = g_WorldMatrix * vec4(inPosition, 1.0);
|
||||
float phase = wp.x * 0.08 + wp.z * 0.06;
|
||||
float swayX = sin(t + phase) * windW * m_WindStrength;
|
||||
float swayZ = cos(t*0.73 + phase) * windW * m_WindStrength * 0.55;
|
||||
vec3 anim = inPosition + vec3(swayX, 0.0, swayZ);
|
||||
vec2 worldXZ = wp.xz;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
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_WindStrength;
|
||||
uniform vec2 m_WindDir; // normierter XZ-Windvektor
|
||||
|
||||
in vec3 inPosition;
|
||||
in vec3 inNormal;
|
||||
@@ -18,22 +19,28 @@ void main() {
|
||||
float wf = inTexCoord.x;
|
||||
|
||||
if (wf > 0.001) {
|
||||
// Weltposition als Phasenbasis → jeder Halm schwingt anders
|
||||
vec2 worldXZ = (g_WorldMatrix * pos).xz;
|
||||
float t = g_Time * m_WindSpeed;
|
||||
|
||||
float sway = sin(t * 2.1 + worldXZ.x * 0.08 + worldXZ.y * 0.06) * 0.6
|
||||
+ sin(t * 1.4 - worldXZ.x * 0.05 + worldXZ.y * 0.09) * 0.4;
|
||||
// Windrichtung (Fallback: Süd)
|
||||
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
|
||||
float bend = sway * m_WindStrength * wf * wf;
|
||||
pos.x += bend;
|
||||
pos.z += bend * 0.3;
|
||||
pos.x += windN.x * bend;
|
||||
pos.z += windN.y * bend;
|
||||
}
|
||||
|
||||
varColor = inColor;
|
||||
// Normal in Weltkoordinaten (WorldMatrix ist für Gras typischerweise Identität)
|
||||
varNormal = normalize(mat3(g_WorldMatrix) * inNormal);
|
||||
|
||||
gl_Position = g_WorldViewProjectionMatrix * pos;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#import "Common/ShaderLib/GLSLCompat.glsllib"
|
||||
|
||||
|
||||
uniform mat4 g_WorldViewProjectionMatrix;
|
||||
uniform mat4 g_WorldMatrix;
|
||||
uniform float g_Time;
|
||||
uniform float m_WindStrength;
|
||||
uniform float m_WindSpeed;
|
||||
uniform vec2 m_WindDir;
|
||||
|
||||
in vec3 inPosition;
|
||||
in vec3 inNormal;
|
||||
@@ -16,18 +16,27 @@ out vec2 texCoord;
|
||||
out vec3 worldNormal;
|
||||
|
||||
void main() {
|
||||
float windW = inColor.r;
|
||||
float t = g_Time * m_WindSpeed;
|
||||
float windW = inColor.r;
|
||||
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);
|
||||
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;
|
||||
vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
|
||||
vec2 worldXZ = worldPos.xz;
|
||||
|
||||
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);
|
||||
texCoord = inTexCoord;
|
||||
worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz);
|
||||
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 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_bench",
|
||||
"tpose",
|
||||
"walking"
|
||||
"walking",
|
||||
"sitting_talking"
|
||||
],
|
||||
"actionMap": {
|
||||
"DEFAULT": "tpose",
|
||||
@@ -31,7 +32,22 @@
|
||||
},
|
||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||
"animOffsets": {
|
||||
"sitting": {"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}
|
||||
"sitting": {
|
||||
"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;
|
||||
|
||||
/** 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 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 {
|
||||
|
||||
private static final int MAGIC = 0x47565458; // "GVTX"
|
||||
private static final int VERSION = 2;
|
||||
private static final int VERSION = 3;
|
||||
|
||||
private GrassVertexIO() {}
|
||||
|
||||
@@ -37,6 +38,7 @@ public final class GrassVertexIO {
|
||||
out.writeFloat(b.z());
|
||||
out.writeFloat(b.height());
|
||||
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 h = in.readFloat();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ public class DialogOption {
|
||||
private TextReference textNpc;
|
||||
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> 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 {
|
||||
|
||||
/** Standard-Tagesdauer in Echtzeit-Sekunden (5 Minuten). */
|
||||
public static final float DEFAULT_DAY_DURATION = 300f;
|
||||
/** Standard-Tagesdauer in Echtzeit-Sekunden (15 Minuten). */
|
||||
public static final float DEFAULT_DAY_DURATION = 900f;
|
||||
|
||||
private float timeOfDay;
|
||||
private float timeScale;
|
||||
|
||||
@@ -11,12 +11,14 @@ import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Ray;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassVertexBlade;
|
||||
import de.blight.common.GrassVertexIO;
|
||||
@@ -26,7 +28,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
@@ -47,7 +51,7 @@ public class GrassVertexState extends BaseAppState {
|
||||
// ── Geometrie ─────────────────────────────────────────────────────────────
|
||||
static final int BLADES_PER_TUFT = 3; // Halme pro Büschel
|
||||
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 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_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 ───────────────────────────────────────────────────────────────
|
||||
private final SharedInput input;
|
||||
private AssetManager assetManager;
|
||||
@@ -70,6 +83,8 @@ public class GrassVertexState extends BaseAppState {
|
||||
private TerrainQuad terrain;
|
||||
private Node grassNode;
|
||||
private Material material;
|
||||
private Material[] seedMaterials = new Material[0];
|
||||
private Material seedStalkMaterial;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
|
||||
@@ -98,6 +113,8 @@ public class GrassVertexState extends BaseAppState {
|
||||
grassNode = new Node("grassVertexNode");
|
||||
((SimpleApplication) app).getRootNode().attachChild(grassNode);
|
||||
material = buildMaterial();
|
||||
seedMaterials = loadSeedMaterials();
|
||||
seedStalkMaterial = buildSeedStalkMaterial();
|
||||
|
||||
try {
|
||||
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
|
||||
protected void cleanup(Application app) {
|
||||
((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 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++) {
|
||||
List<GrassVertexBlade> list = chunkBlades[ci];
|
||||
boolean changed = false;
|
||||
@@ -215,7 +266,7 @@ public class GrassVertexState extends BaseAppState {
|
||||
// Halm ist deutlich kleiner als die aktuelle Pinselposition erlaubt → ersetzen
|
||||
if (b.height() < idealH * 0.88f) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -236,7 +287,11 @@ public class GrassVertexState extends BaseAppState {
|
||||
h = Math.max(0.05f, h);
|
||||
float bladeDryness = rng.nextFloat() < witherPct
|
||||
? 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);
|
||||
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;
|
||||
float newY = terrain.getHeight(new Vector2f(b.x(), b.z()));
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -333,10 +388,144 @@ public class GrassVertexState extends BaseAppState {
|
||||
|
||||
Node node = new Node("gvc_" + ci);
|
||||
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;
|
||||
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) ──
|
||||
|
||||
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);
|
||||
/** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */
|
||||
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)"; }
|
||||
|
||||
@@ -17,5 +19,5 @@ public class GrassVertexTool extends EditorTool {
|
||||
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
|
||||
|
||||
@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 Label derivedLabelKey;
|
||||
private Label derivedHeroKey;
|
||||
private Label derivedNpcKey;
|
||||
private ListView<String> heroStepsView;
|
||||
private ListView<String> npcStepsView;
|
||||
private CheckBox rootCheck;
|
||||
private Spinner<Integer> chapterSpinner;
|
||||
private ComboBox<String> statusCombo;
|
||||
@@ -70,6 +70,7 @@ public class DialogEditorView extends BorderPane {
|
||||
private ListView<Trigger> triggersView;
|
||||
private ListView<String> nextOptionsView;
|
||||
private ListView<String> disablesOptionsView;
|
||||
private ComboBox<String> npcAnimCombo;
|
||||
|
||||
// ── Graph-mode ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -252,20 +253,16 @@ public class DialogEditorView extends BorderPane {
|
||||
idField.setPromptText("z. B. begruessung");
|
||||
|
||||
derivedLabelKey = derivedKeyLabel("—");
|
||||
derivedHeroKey = derivedKeyLabel("—");
|
||||
derivedNpcKey = derivedKeyLabel("—");
|
||||
|
||||
idField.textProperty().addListener((obs, oldVal, newVal) -> {
|
||||
String id = newVal.trim();
|
||||
if (id.isBlank()) {
|
||||
derivedLabelKey.setText("—");
|
||||
derivedHeroKey.setText("—");
|
||||
derivedNpcKey.setText("—");
|
||||
refreshStepKeys("");
|
||||
} else {
|
||||
String base = "dialog." + currentNpcId + "." + id;
|
||||
derivedLabelKey.setText(base + ".description");
|
||||
derivedHeroKey.setText(base + ".textmainchar");
|
||||
derivedNpcKey.setText(base + ".textnpc");
|
||||
refreshStepKeys(base);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -310,12 +307,31 @@ public class DialogEditorView extends BorderPane {
|
||||
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(
|
||||
sectionTitle("Texte"),
|
||||
row("Text Held:", derivedHeroKey),
|
||||
row("Text NPC:", derivedNpcKey),
|
||||
row("Audio Held:", placeholder("(wird später implementiert)")),
|
||||
row("Audio NPC:", placeholder("(wird später implementiert)")),
|
||||
sectionTitle("Held-Steps:"),
|
||||
heroStepsView,
|
||||
new HBox(4, addHeroBtn, delHeroBtn),
|
||||
sectionTitle("NPC-Steps:"),
|
||||
npcStepsView,
|
||||
new HBox(4, addNpcBtn, delNpcBtn),
|
||||
row("NPC-Animation:", npcAnimCombo),
|
||||
new Separator()
|
||||
);
|
||||
|
||||
@@ -469,7 +485,26 @@ public class DialogEditorView extends BorderPane {
|
||||
questCompleteField.setText(opt.getRequiresQuestComplete() != null
|
||||
? 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) {
|
||||
reqItemIdField.setText(safe(opt.getRequiredItem().getItem().getItemId()));
|
||||
@@ -498,6 +533,9 @@ public class DialogEditorView extends BorderPane {
|
||||
.map(Quest::getQuestId)
|
||||
.forEach(abortsQuestsView.getItems()::add);
|
||||
|
||||
String anim = opt.getNpcAnimation();
|
||||
npcAnimCombo.setValue(anim != null && !anim.isBlank() ? anim : "— (keine)");
|
||||
|
||||
enablesTradeCheck.setSelected(opt.isEnablesTrade());
|
||||
|
||||
triggersView.getItems().clear();
|
||||
@@ -543,8 +581,24 @@ public class DialogEditorView extends BorderPane {
|
||||
if (!currentId.isBlank()) {
|
||||
String base = "dialog." + currentNpcId + "." + currentId;
|
||||
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()) {
|
||||
@@ -578,6 +632,9 @@ public class DialogEditorView extends BorderPane {
|
||||
}
|
||||
opt.setAbortsQuests(aborts);
|
||||
|
||||
String animVal = npcAnimCombo.getValue();
|
||||
opt.setNpcAnimation(animVal == null || animVal.startsWith("—") ? null : animVal);
|
||||
|
||||
opt.setEnablesTrade(enablesTradeCheck.isSelected());
|
||||
opt.setOnChosenTriggers(new ArrayList<>(triggersView.getItems()));
|
||||
|
||||
@@ -594,8 +651,8 @@ public class DialogEditorView extends BorderPane {
|
||||
private void clearDetailForm() {
|
||||
if (idField != null) idField.clear();
|
||||
if (derivedLabelKey != null) derivedLabelKey.setText("—");
|
||||
if (derivedHeroKey != null) derivedHeroKey.setText("—");
|
||||
if (derivedNpcKey != null) derivedNpcKey.setText("—");
|
||||
if (heroStepsView != null) heroStepsView.getItems().clear();
|
||||
if (npcStepsView != null) npcStepsView.getItems().clear();
|
||||
if (rootCheck != null) rootCheck.setSelected(false);
|
||||
if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(0);
|
||||
if (statusCombo != null) statusCombo.setValue("— (keine)");
|
||||
@@ -608,6 +665,7 @@ public class DialogEditorView extends BorderPane {
|
||||
if (recvQuestField != null) recvQuestField.clear();
|
||||
if (fulfillsQuestField != null) fulfillsQuestField.clear();
|
||||
if (abortsQuestsView != null) abortsQuestsView.getItems().clear();
|
||||
if (npcAnimCombo != null) npcAnimCombo.setValue("— (keine)");
|
||||
if (enablesTradeCheck != null) enablesTradeCheck.setSelected(false);
|
||||
if (triggersView != null) triggersView.getItems().clear();
|
||||
if (nextOptionsView != null) nextOptionsView.getItems().clear();
|
||||
@@ -649,6 +707,10 @@ public class DialogEditorView extends BorderPane {
|
||||
opt.setLabel(new TextReference(base + ".description"));
|
||||
opt.setTextHero(new TextReference(base + ".textmainchar"));
|
||||
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);
|
||||
if (asRoot) {
|
||||
rootIds.add(id);
|
||||
@@ -1145,4 +1207,56 @@ public class DialogEditorView extends BorderPane {
|
||||
});
|
||||
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-plugins:${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 'org.slf4j:slf4j-api: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) {}
|
||||
settings.setResolution(gs.width, gs.height);
|
||||
settings.setFullscreen(gs.fullscreen);
|
||||
settings.setBitsPerPixel(32);
|
||||
settings.setVSync(gs.vsync);
|
||||
settings.setSamples(gs.samples);
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ public enum AnimationAction {
|
||||
SIT_DOWN_FLOOR,
|
||||
SITTING_FLOOR,
|
||||
GET_UP_FLOOR,
|
||||
REVIVE;
|
||||
REVIVE,
|
||||
TALK1,
|
||||
TALK2,
|
||||
DISAPPOINT,
|
||||
SECRET,
|
||||
TALK_SITTING;
|
||||
|
||||
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
|
||||
public String displayName() {
|
||||
@@ -50,6 +55,11 @@ public enum AnimationAction {
|
||||
case SITTING_FLOOR -> TextRegistry.resolve(null, key, "Sitzen (Boden)");
|
||||
case GET_UP_FLOOR -> TextRegistry.resolve(null, key, "Aufstehen (Boden)");
|
||||
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();
|
||||
s.setResolution(live.width, live.height);
|
||||
s.setFullscreen(live.fullscreen);
|
||||
s.setBitsPerPixel(32);
|
||||
s.setVSync(live.vsync);
|
||||
s.setSamples(live.samples);
|
||||
app.setSettings(s);
|
||||
|
||||
@@ -75,7 +75,7 @@ public class ThirdPersonCamera {
|
||||
}
|
||||
|
||||
public void update(float tpf) {
|
||||
if (target == null) return;
|
||||
if (target == null || paused) return;
|
||||
|
||||
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.filters.FogFilter;
|
||||
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.GrassVertexRenderState;
|
||||
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.audio.FootstepSystem footstepSystem;
|
||||
private de.blight.game.state.DrownState drownState;
|
||||
private WaterInteractionState waterInteractionState;
|
||||
|
||||
public WorldScene(KeyBindings keyBindings) {
|
||||
this.keyBindings = keyBindings;
|
||||
@@ -221,6 +224,8 @@ public class WorldScene extends BaseAppState {
|
||||
com.jme3.asset.plugins.FileLocator.class);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
app.getCamera().setFrustumFar(4000f);
|
||||
|
||||
BlightGame.status("Baue Beleuchtung und Himmel...");
|
||||
buildLighting();
|
||||
|
||||
@@ -256,6 +261,9 @@ public class WorldScene extends BaseAppState {
|
||||
// Physik-Aktivierung wird in update() verzögert bis BulletAppState und
|
||||
// Terrain-Physics für den Spawn-Bereich bereit sind (verhindert Fall-durch-Terrain).
|
||||
physicsCharPending = true;
|
||||
if (waterInteractionState != null) {
|
||||
waterInteractionState.setPlayerControl(physicsChar, 0f);
|
||||
}
|
||||
|
||||
playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings);
|
||||
playerInput.setPhysicsCharacter(physicsChar);
|
||||
@@ -620,7 +628,7 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
// Globales Wasser bei Y=0 (bedeckt die gesamte Karte unterhalb der Wasserlinie)
|
||||
try {
|
||||
WaterFilter waterFilter = new WaterFilter(rootNode, sunDir);
|
||||
DynamicWaterFilter waterFilter = new DynamicWaterFilter(rootNode, sunDir);
|
||||
waterFilter.setWaterHeight(0f);
|
||||
waterFilter.setWaterColor(new ColorRGBA(0.05f, 0.25f, 0.55f, 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);
|
||||
fpp.addFilter(waterFilter);
|
||||
|
||||
waterInteractionState = new WaterInteractionState(waterFilter);
|
||||
app.getStateManager().attach(waterInteractionState);
|
||||
|
||||
WeatherState weather = new WeatherState();
|
||||
weather.setWaterFilter(waterFilter);
|
||||
|
||||
@@ -640,6 +651,7 @@ public class WorldScene extends BaseAppState {
|
||||
fpp.addFilter(fogFilter);
|
||||
|
||||
weather.setFogFilter(fogFilter);
|
||||
weather.setSkyControl(dayNight.getSkyControl());
|
||||
app.getStateManager().attach(weather);
|
||||
} catch (Exception e) {
|
||||
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.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.shadow.DirectionalLightShadowFilter;
|
||||
import com.jme3.shadow.EdgeFilteringMode;
|
||||
import de.blight.common.time.DayTime;
|
||||
import de.blight.common.time.TimeListener;
|
||||
import jme3utilities.sky.CloudLayer;
|
||||
import jme3utilities.sky.SkyControl;
|
||||
import jme3utilities.sky.StarsOption;
|
||||
import org.slf4j.Logger;
|
||||
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).
|
||||
*/
|
||||
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 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 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 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -47,8 +45,13 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
private DirectionalLight sun;
|
||||
private AmbientLight ambient;
|
||||
private DirectionalLightShadowFilter shadowFilter;
|
||||
private Spatial sky;
|
||||
private Geometry sunSphere;
|
||||
private Node skyNode;
|
||||
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 ────────────────────────────────────────────────────
|
||||
|
||||
@@ -56,16 +59,11 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
private float caveFactor = 0f;
|
||||
private float targetCaveFactor = 0f;
|
||||
private float caveCheckTimer = 0f;
|
||||
/** Sonnenlicht-Basisfarbe (Tag/Nacht ohne Höhlen-Faktor). */
|
||||
private ColorRGBA sunBaseColor = new ColorRGBA(1, 1, 1, 1);
|
||||
/** Schatten-Intensität ohne Höhlen-Faktor. */
|
||||
private float shadowBaseIntensity = 0f;
|
||||
|
||||
/** Raycast-Abstand nach oben (m) – trifft Decken bis zu dieser Höhe. */
|
||||
private static final float CAVE_RAY_HEIGHT = 12f;
|
||||
/** Zeitabstand zwischen Raycasts (Sek.). */
|
||||
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;
|
||||
|
||||
// ── Konstruktoren ─────────────────────────────────────────────────────────
|
||||
@@ -86,8 +84,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
}
|
||||
|
||||
public DayTime getDayTime() { return dayTime; }
|
||||
|
||||
public void setPaused(boolean paused) { dayTime.setPaused(paused); }
|
||||
public void setPaused(boolean p) { dayTime.setPaused(p); }
|
||||
|
||||
/** Aktuelle Sonnenrichtung (normalisiert), oder (0,-1,0) falls noch nicht initialisiert. */
|
||||
public Vector3f getSunDirection() {
|
||||
@@ -96,6 +93,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
|
||||
public DirectionalLight getSunLight() { return sun; }
|
||||
public AmbientLight getAmbientLight() { return ambient; }
|
||||
public SkyControl getSkyControl() { return skyControl; }
|
||||
|
||||
/**
|
||||
* Ü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.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();
|
||||
rootNode.addLight(sun);
|
||||
|
||||
// Ambient
|
||||
ambient = new AmbientLight();
|
||||
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);
|
||||
|
||||
// Schatten (nur im Game) – als Filter, damit WorldScene ihn in den FPP einhängen kann
|
||||
if (withShadows) {
|
||||
try {
|
||||
shadowFilter = new DirectionalLightShadowFilter(app.getAssetManager(), 4096, 4);
|
||||
@@ -156,12 +127,33 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
shadowFilter.setShadowZExtend(40f);
|
||||
shadowFilter.setShadowZFadeLength(8f);
|
||||
shadowFilter.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
|
||||
// Nicht direkt zum Viewport hinzufügen – WorldScene hängt ihn in den FPP ein
|
||||
} catch (Exception 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);
|
||||
onTimeChanged(dayTime.getTimeOfDay());
|
||||
}
|
||||
@@ -174,9 +166,15 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
dayTime.removeListener(this);
|
||||
rootNode.removeLight(sun);
|
||||
rootNode.removeLight(ambient);
|
||||
shadowFilter = null; // FPP-Cleanup liegt bei WorldScene
|
||||
if (sky != null && sky.getParent() != null) rootNode.detachChild(sky);
|
||||
if (sunSphere != null && sunSphere.getParent() != null) rootNode.detachChild(sunSphere);
|
||||
shadowFilter = null;
|
||||
if (skyControl != null) {
|
||||
skyNode.removeControl(SkyControl.class);
|
||||
skyControl = null;
|
||||
}
|
||||
if (skyNode != null && skyNode.getParent() != null) {
|
||||
rootNode.detachChild(skyNode);
|
||||
}
|
||||
skyNode = null;
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@@ -184,31 +182,19 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
sunDirTimer += tpf;
|
||||
dayTime.update(tpf);
|
||||
|
||||
// Sky/Sonnen-Sphere nach rootNode.detachAllChildren() wiederherstellen
|
||||
if (sky != null && sky.getParent() == null)
|
||||
rootNode.attachChild(sky);
|
||||
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)));
|
||||
// skyNode nach rootNode.detachAllChildren() wiederherstellen
|
||||
if (skyNode != null && skyNode.getParent() == null) {
|
||||
rootNode.attachChild(skyNode);
|
||||
}
|
||||
|
||||
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. */
|
||||
private void updateCaveLighting(float tpf, Vector3f camPos) {
|
||||
// Raycast nur alle CAVE_CHECK_INTERVAL Sekunden
|
||||
caveCheckTimer += tpf;
|
||||
if (caveCheckTimer >= CAVE_CHECK_INTERVAL) {
|
||||
caveCheckTimer = 0f;
|
||||
@@ -220,7 +206,6 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
}
|
||||
}
|
||||
|
||||
// Glatte Überblendung des Höhlen-Faktors
|
||||
float prev = caveFactor;
|
||||
if (caveFactor < targetCaveFactor) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Licht nur aktualisieren wenn sich der Faktor geändert hat
|
||||
if (caveFactor != prev) {
|
||||
float scale = 1f - caveFactor;
|
||||
sun.setColor(sunBaseColor.mult(scale));
|
||||
if (shadowFilter != null)
|
||||
if (shadowFilter != null) {
|
||||
shadowFilter.setShadowIntensity(shadowBaseIntensity * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zeit-Callback ─────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void onTimeChanged(float t) {
|
||||
float elev = sunElevation(t);
|
||||
float elevC = FastMath.clamp(elev, 0f, 1f);
|
||||
if (skyControl == null) return;
|
||||
|
||||
// ── Sonnenrichtung ──────────────────────────────────────────────────
|
||||
float angle = t * FastMath.TWO_PI;
|
||||
// Sonne bewegt sich von Ost (X+) über Süden nach West (X-)
|
||||
Vector3f sunPos = new Vector3f(
|
||||
FastMath.sin(angle) * 0.85f,
|
||||
elev,
|
||||
-0.15f
|
||||
);
|
||||
sun.setDirection(sunPos.negate().normalizeLocal());
|
||||
// Sonne positionieren und Richtung holen
|
||||
skyControl.getSunAndStars().setHour(t * 24f);
|
||||
Vector3f toSun = skyControl.getSunAndStars().sunDirection(null);
|
||||
float elevation = toSun.y; // +1 = Zenit, 0 = Horizont, -1 = Nadir
|
||||
float elevC = FastMath.clamp(elevation, 0f, 1f);
|
||||
|
||||
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) ──────────────
|
||||
float dawnFactor = 1f - FastMath.clamp(elev * 5f, 0f, 1f);
|
||||
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) — jedes Frame ──
|
||||
float dawnFactor = 1f - FastMath.clamp(elevation * 5f, 0f, 1f);
|
||||
ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor);
|
||||
sunBaseColor = sunColor.mult(elevC * 0.68f);
|
||||
sun.setColor(sunBaseColor.mult(1f - caveFactor));
|
||||
|
||||
// ── Sonnen-Sphere ausblenden wenn unter Horizont ────────────────────
|
||||
if (sunSphere != null) {
|
||||
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: Nacht → Tag — jedes Frame ─────────────────────────────
|
||||
float ambFactor = FastMath.clamp((elevation + 0.2f) / 0.6f, 0f, 1f);
|
||||
ambient.setColor(AMB_NIGHT.clone().interpolateLocal(AMB_DAY, ambFactor));
|
||||
|
||||
// ── Schatten ────────────────────────────────────────────────────────
|
||||
shadowBaseIntensity = FastMath.clamp(elev * 0.8f, 0f, 0.5f);
|
||||
if (shadowFilter != null)
|
||||
shadowBaseIntensity = FastMath.clamp(elevation * 0.8f, 0f, 0.5f);
|
||||
|
||||
// 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));
|
||||
|
||||
// ── 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.input.KeyInput;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.RawInputListener;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
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.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
@@ -20,12 +27,14 @@ import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import de.blight.common.model.DialogOption;
|
||||
import de.blight.common.model.DialogStep;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.NPC;
|
||||
import de.blight.common.model.TextReference;
|
||||
import de.blight.common.model.trigger.ChangeRoutineTrigger;
|
||||
import de.blight.common.model.trigger.NpcStatusTrigger;
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
import de.blight.game.audio.DialogTtsService;
|
||||
import de.blight.game.config.MenuCanvas;
|
||||
import de.blight.game.config.NinePatch;
|
||||
import de.blight.lang.TextResolver;
|
||||
@@ -57,12 +66,12 @@ public class DialogHudState extends BaseAppState {
|
||||
|
||||
// ── Layout-Konstanten (virtuelle Koordinaten 1376 × 768) ─────────────────
|
||||
|
||||
private static final float PNL_X = 63f;
|
||||
private static final float PNL_Y = 20f;
|
||||
private static final float PNL_W = 1250f;
|
||||
private static final float PNL_H = 255f;
|
||||
private static final float PNL_X = 0f;
|
||||
private static final float PNL_Y = 0f;
|
||||
private static final float PNL_W = MenuCanvas.REF_W; // volle Breite
|
||||
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_TEXT = 16f;
|
||||
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 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_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_TEXT = ColorRGBA.White;
|
||||
@@ -87,7 +97,6 @@ public class DialogHudState extends BaseAppState {
|
||||
private static final String ACT_DOWN = "_DlgDown";
|
||||
private static final String ACT_CONFIRM = "_DlgConfirm";
|
||||
private static final String ACT_SKIP = "_DlgSkip";
|
||||
private static final String ACT_CLICK = "_DlgClick";
|
||||
|
||||
// ── Zustände ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -113,9 +122,35 @@ public class DialogHudState extends BaseAppState {
|
||||
/** Verfügbare Optionen (inkl. Exit). */
|
||||
private List<DisplayOption> displayOptions = new ArrayList<>();
|
||||
private int selectedOpt = 0;
|
||||
/** Erster sichtbarer Options-Index bei mehr Optionen als Slots. */
|
||||
private int scrollOffset = 0;
|
||||
|
||||
/** Panel-Bounds für Maus-Hit-Tests. */
|
||||
private float[][] optBounds = new float[MAX_OPTIONS][4]; // x,y,w,h
|
||||
/** Sekunden bis zur automatischen Weiterleitung nach der letzten Textseite. */
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -129,6 +164,12 @@ public class DialogHudState extends BaseAppState {
|
||||
private BitmapText[] lineTexts = new BitmapText[MAX_TEXT_LINES];
|
||||
private BitmapText hintText;
|
||||
private BitmapText[] optTexts = new BitmapText[MAX_OPTIONS];
|
||||
private BitmapText scrollUpText;
|
||||
private BitmapText scrollDownText;
|
||||
|
||||
// ── TTS ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private DialogTtsService tts;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -137,17 +178,79 @@ public class DialogHudState extends BaseAppState {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.guiNode = this.app.getGuiNode();
|
||||
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
|
||||
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
|
||||
protected void onDisable() {
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -166,31 +269,27 @@ public class DialogHudState extends BaseAppState {
|
||||
this.onOptionsShown = onOptionsShown;
|
||||
this.optionsShownFired = false;
|
||||
|
||||
buildPanel();
|
||||
setHudsVisible(false);
|
||||
canvasNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
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);
|
||||
|
||||
if (greetText != null && !greetText.isBlank()) {
|
||||
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, () -> {
|
||||
if (available.isEmpty()) {
|
||||
log.info("[DialogHud] NPC '{}' hat keine Optionen nach Begrüßung.", npc.getCharacterId());
|
||||
closeDialog();
|
||||
if (!available.isEmpty()) {
|
||||
// Optionen sofort anzeigen – keine Begrüßungsverzögerung
|
||||
showOptions(available);
|
||||
} else {
|
||||
showOptions(available);
|
||||
}
|
||||
});
|
||||
} else if (!available.isEmpty()) {
|
||||
showOptions(available);
|
||||
// Kein Optionen: Begrüßungstext zeigen und dann schließen
|
||||
TextReference greeting = npc.getDefaultMessage();
|
||||
String greetText = greeting != null ? TextResolver.get().resolve(greeting) : null;
|
||||
if (greetText != null && !greetText.isBlank()) {
|
||||
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, this::closeDialog);
|
||||
} else {
|
||||
log.info("[DialogHud] NPC '{}' hat keine Optionen und keine Begrüßung – Dialog übersprungen.", npc.getCharacterId());
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return phase != Phase.HIDDEN;
|
||||
@@ -200,12 +299,27 @@ public class DialogHudState extends BaseAppState {
|
||||
|
||||
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) {
|
||||
this.phase = textPhase;
|
||||
this.afterText = after;
|
||||
this.textPages = paginate(wrapText(rawText, MAX_CHARS_LINE), MAX_TEXT_LINES);
|
||||
this.pageIdx = 0;
|
||||
renderCurrentTextPage(speaker);
|
||||
if (tts != null) tts.speak(rawText);
|
||||
}
|
||||
|
||||
private void renderCurrentTextPage(String speaker) {
|
||||
@@ -219,9 +333,13 @@ public class DialogHudState extends BaseAppState {
|
||||
}
|
||||
|
||||
boolean more = (pageIdx + 1) < textPages.size();
|
||||
hintText.setText(more
|
||||
? t("dialog.hint.advance")
|
||||
: t("dialog.hint.continue"));
|
||||
if (more) {
|
||||
autoAdvanceTimer = -1f;
|
||||
hintText.setText(t("dialog.hint.advance"));
|
||||
} else {
|
||||
autoAdvanceTimer = AUTO_ADVANCE_DELAY;
|
||||
hintText.setText("");
|
||||
}
|
||||
hintText.setCullHint(Spatial.CullHint.Inherit);
|
||||
|
||||
for (BitmapText o : optTexts) o.setCullHint(Spatial.CullHint.Always);
|
||||
@@ -233,6 +351,7 @@ public class DialogHudState extends BaseAppState {
|
||||
phase = Phase.OPTIONS;
|
||||
displayOptions.clear();
|
||||
selectedOpt = 0;
|
||||
scrollOffset = 0;
|
||||
|
||||
if (!optionsShownFired && onOptionsShown != null) {
|
||||
optionsShownFired = true;
|
||||
@@ -240,11 +359,27 @@ public class DialogHudState extends BaseAppState {
|
||||
}
|
||||
|
||||
for (DialogOption opt : options) {
|
||||
String label = opt.getLabel() != null
|
||||
? TextResolver.get().resolveId(opt.getLabel().id()) : "";
|
||||
// Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten)
|
||||
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()) {
|
||||
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));
|
||||
}
|
||||
@@ -260,40 +395,51 @@ public class DialogHudState extends BaseAppState {
|
||||
|
||||
private void renderOptions() {
|
||||
float baseY = PNL_Y + PNL_H - 80f;
|
||||
int total = displayOptions.size();
|
||||
|
||||
for (int i = 0; i < MAX_OPTIONS; i++) {
|
||||
if (i < displayOptions.size()) {
|
||||
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);
|
||||
boolean canScrollUp = scrollOffset > 0;
|
||||
boolean canScrollDown = (scrollOffset + MAX_OPTIONS) < total;
|
||||
|
||||
float ty = baseY - i * LINE_H_OPT;
|
||||
optTexts[i].setLocalTranslation(PNL_X + MARGIN_X, ty, 2f);
|
||||
scrollUpText .setCullHint(canScrollUp ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
scrollDownText.setCullHint(canScrollDown ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
|
||||
optBounds[i][0] = PNL_X + MARGIN_X;
|
||||
optBounds[i][1] = ty - FONT_OPT;
|
||||
optBounds[i][2] = PNL_W - MARGIN_X * 2;
|
||||
optBounds[i][3] = FONT_OPT + 4;
|
||||
for (int slot = 0; slot < MAX_OPTIONS; slot++) {
|
||||
int optIdx = scrollOffset + slot;
|
||||
if (optIdx < total) {
|
||||
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 {
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void onUp() {
|
||||
if (phase != Phase.OPTIONS) return;
|
||||
selectedOpt = Math.max(0, selectedOpt - 1);
|
||||
ensureVisible();
|
||||
renderOptions();
|
||||
}
|
||||
|
||||
private void onDown() {
|
||||
if (phase != Phase.OPTIONS) return;
|
||||
selectedOpt = Math.min(displayOptions.size() - 1, selectedOpt + 1);
|
||||
ensureVisible();
|
||||
renderOptions();
|
||||
}
|
||||
|
||||
@@ -307,42 +453,71 @@ public class DialogHudState extends BaseAppState {
|
||||
|
||||
private void onSkip() {
|
||||
if (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO) {
|
||||
// Alle verbleibenden Seiten überspringen
|
||||
autoAdvanceTimer = -1f;
|
||||
if (tts != null) tts.stop();
|
||||
pageIdx = textPages.size();
|
||||
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) {
|
||||
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(
|
||||
app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||
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 vx = (cursor.x - ox2) / scale;
|
||||
float vy = (cursor.y - oy2) / scale;
|
||||
|
||||
for (int i = 0; i < displayOptions.size() && i < MAX_OPTIONS; i++) {
|
||||
float bx = optBounds[i][0], by = optBounds[i][1];
|
||||
float bw = optBounds[i][2], bh = optBounds[i][3];
|
||||
if (vx >= bx && vx <= bx + bw && vy >= by && vy <= by + bh) {
|
||||
selectedOpt = i;
|
||||
log.debug("[DialogHud] Klick vy={} (Panel {}-{})", vy, PNL_Y, PNL_Y + PNL_H);
|
||||
|
||||
if (vy < PNL_Y || vy > PNL_Y + PNL_H) return;
|
||||
|
||||
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();
|
||||
confirmOption(i);
|
||||
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() {
|
||||
autoAdvanceTimer = -1f;
|
||||
if (pageIdx + 1 < textPages.size()) {
|
||||
pageIdx++;
|
||||
renderCurrentTextPage(nameText.getText());
|
||||
@@ -365,30 +540,46 @@ public class DialogHudState extends BaseAppState {
|
||||
|
||||
DialogOption opt = selected.option();
|
||||
|
||||
// Held-Text
|
||||
String heroText = opt.getTextHero() != null
|
||||
? TextResolver.get().resolve(opt.getTextHero()) : null;
|
||||
// NPC-Text
|
||||
String npcText = opt.getTextNpc() != null
|
||||
? TextResolver.get().resolve(opt.getTextNpc()) : null;
|
||||
// Steps mit Fallback auf Legacy-Felder
|
||||
List<DialogStep> heroSteps = opt.getHeroSteps();
|
||||
List<DialogStep> npcSteps = opt.getNpcSteps();
|
||||
if (heroSteps == null || heroSteps.isEmpty()) {
|
||||
if (opt.getTextHero() != 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.)
|
||||
applyOption(opt);
|
||||
|
||||
List<DialogOption> nextOpts = resolveOptions(currentNpc, mainChar);
|
||||
|
||||
if (heroText != null && !heroText.isBlank()) {
|
||||
showText(Phase.TEXT_HERO, t("dialog.speaker.player"), heroText, () -> {
|
||||
if (npcText != null && !npcText.isBlank()) {
|
||||
showText(Phase.TEXT_NPC, resolveNpcName(currentNpc), npcText, () -> {
|
||||
if (!fHeroSteps.isEmpty()) {
|
||||
playSteps(Phase.TEXT_HERO, t("dialog.speaker.player"), fHeroSteps, 0, () -> {
|
||||
if (!fNpcSteps.isEmpty()) {
|
||||
triggerNpcDialogAnim(fNpcAnim);
|
||||
playSteps(Phase.TEXT_NPC, resolveNpcName(currentNpc), fNpcSteps, 0, () -> {
|
||||
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||
});
|
||||
} else {
|
||||
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||
}
|
||||
});
|
||||
} else if (npcText != null && !npcText.isBlank()) {
|
||||
showText(Phase.TEXT_NPC, resolveNpcName(currentNpc), npcText, () -> {
|
||||
} else if (!fNpcSteps.isEmpty()) {
|
||||
triggerNpcDialogAnim(fNpcAnim);
|
||||
playSteps(Phase.TEXT_NPC, resolveNpcName(currentNpc), fNpcSteps, 0, () -> {
|
||||
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||
});
|
||||
} 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 ────────────────────────────────────────────────────
|
||||
|
||||
private List<DialogOption> resolveOptions(NPC npc, MainCharacter mc) {
|
||||
@@ -454,7 +655,8 @@ public class DialogHudState extends BaseAppState {
|
||||
// ── UI-Aufbau / -Abbau ────────────────────────────────────────────────────
|
||||
|
||||
private void buildPanel() {
|
||||
canvasNode = MenuCanvas.createFixedCanvas(app.getCamera());
|
||||
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||
canvasNode.setCullHint(Spatial.CullHint.Always);
|
||||
guiNode.attachChild(canvasNode);
|
||||
|
||||
panel = new Node("dialog-panel");
|
||||
@@ -494,16 +696,29 @@ public class DialogHudState extends BaseAppState {
|
||||
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);
|
||||
}
|
||||
|
||||
private void closePanel() {
|
||||
phase = Phase.HIDDEN;
|
||||
autoAdvanceTimer = -1f;
|
||||
if (tts != null) tts.stop();
|
||||
setHudsVisible(true);
|
||||
unregisterInput();
|
||||
if (canvasNode != null) {
|
||||
guiNode.detachChild(canvasNode);
|
||||
canvasNode = null;
|
||||
panel = null;
|
||||
canvasNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
textPages.clear();
|
||||
displayOptions.clear();
|
||||
@@ -522,17 +737,19 @@ public class DialogHudState extends BaseAppState {
|
||||
im.addMapping(ACT_CONFIRM, new KeyTrigger(KeyInput.KEY_RETURN),
|
||||
new KeyTrigger(KeyInput.KEY_NUMPADENTER));
|
||||
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, ACT_CLICK);
|
||||
im.addListener(inputListener, ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP);
|
||||
im.addRawInputListener(rawMouseListener);
|
||||
im.setCursorVisible(true);
|
||||
}
|
||||
|
||||
private void unregisterInput() {
|
||||
var im = app.getInputManager();
|
||||
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) {}
|
||||
}
|
||||
pendingClick = false;
|
||||
im.setCursorVisible(false);
|
||||
}
|
||||
|
||||
@@ -543,7 +760,6 @@ public class DialogHudState extends BaseAppState {
|
||||
case ACT_DOWN -> onDown();
|
||||
case ACT_CONFIRM -> onConfirm();
|
||||
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 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. */
|
||||
private static List<String> wrapText(String text, int maxChars) {
|
||||
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.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassVertexBlade;
|
||||
import de.blight.common.GrassVertexIO;
|
||||
@@ -21,7 +25,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 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) ─────────────────────────────
|
||||
private static final int BLADES_PER_TUFT = 3;
|
||||
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 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);
|
||||
@@ -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_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 ───────────────────────────────────────────────────────────────
|
||||
private final TerrainChunkState terrainChunkState;
|
||||
private AssetManager assetManager;
|
||||
private Node grassNode;
|
||||
private Material material;
|
||||
private Material[] seedMaterials = new Material[0];
|
||||
private Material seedStalkMaterial;
|
||||
private int nextChunk = 0;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -88,6 +102,40 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
}
|
||||
|
||||
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
|
||||
@@ -110,9 +158,39 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
if (material != null && material.getMaterialDef().getMaterialParam("SunColor") != null) {
|
||||
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
|
||||
if (dns != null && dns.getSunLight() != null) {
|
||||
material.setVector3("SunDir", dns.getSunDirection().negate());
|
||||
com.jme3.math.ColorRGBA sc = dns.getSunLight().getColor();
|
||||
Vector3f sunDir = dns.getSunDirection().negate();
|
||||
ColorRGBA sc = dns.getSunLight().getColor();
|
||||
material.setVector3("SunDir", sunDir);
|
||||
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.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;
|
||||
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 ───────────────────────────────
|
||||
|
||||
@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;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.post.filters.FogFilter;
|
||||
import com.jme3.water.WaterFilter;
|
||||
import jme3utilities.sky.SkyControl;
|
||||
import org.slf4j.Logger;
|
||||
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[] 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[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f };
|
||||
|
||||
private static final ColorRGBA[] FOG_COLOR = {
|
||||
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.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 = {
|
||||
new ColorRGBA(0.05f, 0.25f, 0.55f, 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 float changeTimer = 120f;
|
||||
|
||||
// Aktuell interpolierte Werte
|
||||
private float fogDensity = 0f;
|
||||
private float fogDistance = 600f;
|
||||
private float windSpeed = 4f;
|
||||
@@ -67,10 +62,10 @@ public class WeatherState extends BaseAppState {
|
||||
private float waveScale = 0.008f;
|
||||
private float waterTrans = 0.15f;
|
||||
private float foamIntensity = 0f;
|
||||
private float cloudOpacity = 0f;
|
||||
private float windAngle = 0f;
|
||||
private float windAngleTgt = 0.4f;
|
||||
private final ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f);
|
||||
private final ColorRGBA 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 deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f);
|
||||
|
||||
@@ -78,12 +73,11 @@ public class WeatherState extends BaseAppState {
|
||||
|
||||
private FogFilter fogFilter;
|
||||
private WaterFilter waterFilter;
|
||||
private CloudsNode cloudsNode;
|
||||
private Application app;
|
||||
private SkyControl skyControl;
|
||||
|
||||
public void setFogFilter(FogFilter f) { this.fogFilter = 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 float getWindSpeed() { return windSpeed; }
|
||||
@@ -105,7 +99,7 @@ public class WeatherState extends BaseAppState {
|
||||
|
||||
// ── 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 onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
@@ -128,10 +122,10 @@ public class WeatherState extends BaseAppState {
|
||||
waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f);
|
||||
waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f);
|
||||
foamIntensity = approach(foamIntensity, FOAM_INTENSITY[i], tpf * 0.020f);
|
||||
cloudOpacity = approach(cloudOpacity, CLOUD_OPACITY[i], tpf * 0.025f);
|
||||
windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f);
|
||||
|
||||
fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f);
|
||||
cloudColor.interpolateLocal(CLOUD_COLOR[i], tpf * 0.025f);
|
||||
waterColor.interpolateLocal(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.setDeepWaterColor(deepWaterColor.clone());
|
||||
waterFilter.setWindDirection(
|
||||
new com.jme3.math.Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle)));
|
||||
new Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle)));
|
||||
}
|
||||
|
||||
if (cloudsNode != null) {
|
||||
cloudsNode.setCloudColor(cloudColor.clone());
|
||||
Vector3f camPos = ((SimpleApplication) app).getCamera().getLocation();
|
||||
cloudsNode.update(tpf, getWindDirection(), windSpeed * 0.5f, camPos);
|
||||
if (skyControl != null) {
|
||||
skyControl.getCloudLayer(0).setOpacity(cloudOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,16 @@ import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.*;
|
||||
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.PlacedModelIO;
|
||||
import de.blight.common.SaveGameIO;
|
||||
import de.blight.common.model.*;
|
||||
import de.blight.common.model.trigger.ChangeRoutineTrigger;
|
||||
import de.blight.common.model.trigger.NpcStatusTrigger;
|
||||
@@ -106,8 +114,17 @@ public class WorldNpcsState extends BaseAppState {
|
||||
|
||||
private int hoveredIdx = -1;
|
||||
private boolean dialogActive = false;
|
||||
private SpawnedNpc dialogNpc = null;
|
||||
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 ───────────────────────────────────────────────
|
||||
|
||||
private static final float ROT_DURATION = 0.5f;
|
||||
@@ -214,6 +231,9 @@ public class WorldNpcsState extends BaseAppState {
|
||||
}
|
||||
updateNpcRotation(tpf);
|
||||
updateNpcRevert(tpf);
|
||||
if (dialogActive && dialogHeadTween != null) {
|
||||
dialogHeadTween.targetPos = physicsChar.getPhysicsLocation();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spawn-Logik ───────────────────────────────────────────────────────────
|
||||
@@ -260,6 +280,13 @@ public class WorldNpcsState extends BaseAppState {
|
||||
spawned.add(s);
|
||||
playNpcAction(s, activityActionFor(npc, currentHour));
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
SpawnedNpc entry = spawned.get(idx);
|
||||
dialogNpc = entry;
|
||||
if (entry.currentAction == AnimationAction.SITTING) {
|
||||
startDialogHeadLook(entry);
|
||||
}
|
||||
DialogHudState dialog = getApplication().getStateManager().getState(DialogHudState.class);
|
||||
if (dialog == null) return;
|
||||
|
||||
@@ -370,12 +415,11 @@ public class WorldNpcsState extends BaseAppState {
|
||||
Quaternion facingRot = computeFacingQuat(npcPos, playerPos);
|
||||
beginSmoothRotation(entry, origRot, facingRot);
|
||||
|
||||
// Kamera wird erst gesetzt wenn Optionen erscheinen
|
||||
boolean[] optionsWereShown = {false};
|
||||
Runnable onOptions = () -> {
|
||||
optionsWereShown[0] = true;
|
||||
// Dialog-Kamera sofort aktivieren (nicht erst bei Optionen)
|
||||
enterDialogCamera(npcPos, playerPos);
|
||||
};
|
||||
|
||||
boolean[] optionsWereShown = {false};
|
||||
Runnable onOptions = () -> { optionsWereShown[0] = true; };
|
||||
|
||||
dialog.startDialog(entry.npc(), mainCharacter, onOptions, () -> {
|
||||
exitDialogCamera();
|
||||
@@ -402,6 +446,12 @@ public class WorldNpcsState extends BaseAppState {
|
||||
}
|
||||
|
||||
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;
|
||||
revertFrom = from.clone();
|
||||
revertTo = to.clone();
|
||||
@@ -429,49 +479,80 @@ public class WorldNpcsState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) {
|
||||
// Mittelpunkt auf Hüfthöhe
|
||||
Vector3f mid = npcPos.add(playerPos).multLocal(0.5f);
|
||||
mid.y += 0.9f;
|
||||
// NPC-Spatial auf Bodenniveau; playerPos = Physik-Kapsel-Mitte → auf NPC-Boden projizieren
|
||||
float groundY = npcPos.y;
|
||||
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)
|
||||
Vector3f axis = npcPos.subtract(playerPos).normalizeLocal();
|
||||
Vector3f sideA = axis.cross(Vector3f.UNIT_Y).normalizeLocal();
|
||||
Vector3f sideB = sideA.negate();
|
||||
// Lookat: Brust-/Schulterhöhe zwischen beiden Figuren
|
||||
Vector3f lookAt = npcFeet.add(playerFeet).multLocal(0.5f);
|
||||
lookAt.y += 1.2f;
|
||||
|
||||
float camSide = 3.5f;
|
||||
float camUp = 1.2f;
|
||||
// Senkrechte Seite zur NPC↔Spieler-Achse
|
||||
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);
|
||||
Vector3f posB = mid.add(sideB.mult(camSide)).add(0, camUp, 0);
|
||||
// FOV 45° → tan(22.5°) ≈ 0.41 → für charDist/2 sichtbar: dist = (charDist/2)/0.41 × Puffer
|
||||
// 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);
|
||||
posB = avoidClipping(mid, posB);
|
||||
// Seite wählen: erhöhter Horizontalstrahl (2 m über lookAt), um Terrain-Clipping zu vermeiden.
|
||||
// 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 = posA.distance(mid) >= posB.distance(mid) ? posA : posB;
|
||||
Vector3f camPos = lookAt.add(chosen.mult(dist)).add(0f, camUp, 0f);
|
||||
|
||||
if (thirdPersonCam != null) {
|
||||
thirdPersonCam.setPaused(true);
|
||||
}
|
||||
if (thirdPersonCam != null) { thirdPersonCam.setPaused(true); }
|
||||
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() {
|
||||
if (thirdPersonCam != null) {
|
||||
thirdPersonCam.setPaused(false);
|
||||
stopDialogHeadLook();
|
||||
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) {
|
||||
Vector3f dir = to.subtract(from);
|
||||
float maxDist = dir.length();
|
||||
dir.normalizeLocal();
|
||||
CollisionResults results = new CollisionResults();
|
||||
rootNode.collideWith(new Ray(from, dir), results);
|
||||
if (results.size() == 0) return to;
|
||||
if (results.size() == 0) { return to; }
|
||||
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);
|
||||
return from.add(dir.mult(safeDist));
|
||||
}
|
||||
@@ -516,17 +597,24 @@ public class WorldNpcsState extends BaseAppState {
|
||||
private void loadAllNpcs() {
|
||||
allNpcs.clear();
|
||||
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 {
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (gc instanceof NPC npc) {
|
||||
java.util.List<String> savedState = saveState != null
|
||||
java.util.List<String> savedState = useSaved
|
||||
? saveState.getDialogState(npc.getCharacterId()) : null;
|
||||
npc.initRuntimeTree(savedState);
|
||||
allNpcs.add(npc);
|
||||
}
|
||||
}
|
||||
log.info("[WorldNpcs] {} NPCs geladen.", allNpcs.size());
|
||||
log.info("[WorldNpcs] {} NPCs geladen (Dialogzustand: {}).",
|
||||
allNpcs.size(), useSaved ? "gespeichert" : "neu");
|
||||
} catch (Exception e) {
|
||||
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) {
|
||||
float dx = a.x - b.x;
|
||||
float dz = a.z - b.z;
|
||||
|
||||
@@ -35,6 +35,7 @@ public class WorldObjectsState extends BaseAppState {
|
||||
private AssetManager assets;
|
||||
private BulletAppState bulletAppState;
|
||||
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. */
|
||||
private final Map<String, RigidBodyControl> interactableRbcs = new HashMap<>();
|
||||
@@ -117,9 +118,9 @@ public class WorldObjectsState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (sceneLitMaterials.isEmpty()) return;
|
||||
if (!sceneLitMaterials.isEmpty()) {
|
||||
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();
|
||||
ColorRGBA sc = dns.getSunLight().getColor();
|
||||
ColorRGBA ac = dns.getAmbientLight().getColor();
|
||||
@@ -131,6 +132,26 @@ public class WorldObjectsState extends BaseAppState {
|
||||
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) {
|
||||
// Exportiertes Mesh hat Vorrang vor modelPath
|
||||
@@ -244,6 +265,8 @@ public class WorldObjectsState extends BaseAppState {
|
||||
String name = mat.getMaterialDef().getName();
|
||||
if ("Tree".equals(name) || "TreeLeaf".equals(name)) {
|
||||
sceneLitMaterials.add(mat);
|
||||
} else if ("Fern".equals(name)) {
|
||||
windMaterials.add(mat);
|
||||
}
|
||||
} else if (spatial instanceof Node node) {
|
||||
for (Spatial child : node.getChildren()) {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user