From 7717ab8d6b7497846d767a9740485159a245e5fb Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 21 Feb 2026 02:23:08 -0800 Subject: [PATCH] Stabilize foliage shadows and smooth motion transitions - keep shadow projection center fixed while moving to remove per-frame projection churn flicker - replace delayed post-move catch-up with immediate stop transition and idle smoothing - rework foliage shadow caster motion to use blended phase-shifted UV samples for continuous position transitions - reduce high-frequency foliage threshold popping by removing threshold warping path - sharpen terrain receive filtering with tuned 5-tap PCF weights/offset for more detailed shadows - raise shadow map resolution to 1536 and keep light-space texel snapping for stable sampling - set shadows enabled by default and lower global shadow strength from 0.65 to 0.62 - keep foliage animation speed consistent between moving and idle at 80% --- assets/shaders/terrain.frag | 12 ++- include/rendering/m2_renderer.hpp | 1 + include/rendering/renderer.hpp | 6 +- src/rendering/character_renderer.cpp | 2 +- src/rendering/m2_renderer.cpp | 13 ++- src/rendering/renderer.cpp | 127 +++++++++++++++++++-------- src/rendering/terrain_renderer.cpp | 2 +- src/rendering/wmo_renderer.cpp | 2 +- 8 files changed, 111 insertions(+), 54 deletions(-) diff --git a/assets/shaders/terrain.frag b/assets/shaders/terrain.frag index 194e23cf..662f137f 100644 --- a/assets/shaders/terrain.frag +++ b/assets/shaders/terrain.frag @@ -52,8 +52,16 @@ float calcShadow() { vec3 norm = normalize(Normal); vec3 lightDir = normalize(-uLightDir); float bias = max(0.005 * (1.0 - dot(norm, lightDir)), 0.001); - // Single hardware PCF tap — GL_LINEAR + compare mode gives 2×2 bilinear PCF for free - float shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + // 5-tap PCF tuned for slightly sharper detail while keeping stability. + vec2 texel = vec2(1.0 / 1536.0); + float ref = proj.z - bias; + vec2 off = texel * 0.7; + float shadow = 0.0; + shadow += texture(uShadowMap, vec3(proj.xy, ref)) * 0.55; + shadow += texture(uShadowMap, vec3(proj.xy + vec2(off.x, 0.0), ref)) * 0.1125; + shadow += texture(uShadowMap, vec3(proj.xy - vec2(off.x, 0.0), ref)) * 0.1125; + shadow += texture(uShadowMap, vec3(proj.xy + vec2(0.0, off.y), ref)) * 0.1125; + shadow += texture(uShadowMap, vec3(proj.xy - vec2(0.0, off.y), ref)) * 0.1125; return mix(1.0, shadow, coverageFade); } diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index c0b59a60..ea7b4853 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -101,6 +101,7 @@ struct M2ModelGPU { bool isSmoke = false; // True for smoke models (UV scroll animation) bool isSpellEffect = false; // True for spell effect models (skip particle dampeners) bool disableAnimation = false; // Keep foliage/tree doodads visually stable + bool shadowWindFoliage = false; // Apply wind sway in shadow pass for foliage/tree cards bool hasTextureAnimation = false; // True if any batch has UV animation // Particle emitter data (kept from M2Model) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index dc14c4fd..16775f9e 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -235,15 +235,15 @@ private: void shutdownPostProcess(); // Shadow mapping - static constexpr int SHADOW_MAP_SIZE = 1024; + static constexpr int SHADOW_MAP_SIZE = 1536; uint32_t shadowFBO = 0; uint32_t shadowDepthTex = 0; uint32_t shadowShaderProgram = 0; glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); glm::vec3 shadowCenter = glm::vec3(0.0f); bool shadowCenterInitialized = false; - bool shadowsEnabled = false; - int shadowFrameCounter_ = 0; // throttle: only re-render depth map every 2 frames + bool shadowsEnabled = true; + int shadowPostMoveFrames_ = 0; // transition marker for movement->idle shadow recenter public: void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 8032832a..fd3dc686 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1415,7 +1415,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons // Shadows characterShader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); - characterShader->setUniform("uShadowStrength", 0.65f); + characterShader->setUniform("uShadowStrength", 0.62f); characterShader->setUniform("uTexture0", 0); characterShader->setUniform("uAlphaTest", 0); characterShader->setUniform("uColorKeyBlack", 0); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 8885ad81..f60a2fd1 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1050,6 +1050,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } gpuModel.disableAnimation = foliageOrTreeLike || chestName; + gpuModel.shadowWindFoliage = foliageOrTreeLike; gpuModel.isGroundDetail = groundDetailModel; if (groundDetailModel) { // Ground clutter (grass/pebbles/detail cards) should never block camera/movement. @@ -1871,7 +1872,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: shader->setUniform("uFogEnd", fogEnd); bool useShadows = shadowEnabled; shader->setUniform("uShadowEnabled", useShadows ? 1 : 0); - shader->setUniform("uShadowStrength", 0.65f); + shader->setUniform("uShadowStrength", 0.62f); if (useShadows) { shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7); @@ -2357,14 +2358,14 @@ void M2Renderer::renderShadow(GLuint shadowShaderProgram, const glm::vec3& shado GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture"); GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture"); GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest"); - GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity"); + GLint foliageSwayLoc = glGetUniformLocation(shadowShaderProgram, "uFoliageSway"); if (modelLoc < 0) { return; } if (useTexLoc >= 0) glUniform1i(useTexLoc, 0); if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); - if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f); + if (foliageSwayLoc >= 0) glUniform1i(foliageSwayLoc, 0); if (texLoc >= 0) glUniform1i(texLoc, 0); glActiveTexture(GL_TEXTURE0); @@ -2388,13 +2389,11 @@ void M2Renderer::renderShadow(GLuint shadowShaderProgram, const glm::vec3& shado if (batch.indexCount == 0) continue; bool useTexture = (batch.texture != 0); bool alphaCutout = batch.hasAlpha; - - // Foliage/leaf cutout batches cast softer shadows than opaque trunk geometry. - float shadowOpacity = alphaCutout ? 0.55f : 1.0f; + bool foliageSway = model.shadowWindFoliage && alphaCutout; if (useTexLoc >= 0) glUniform1i(useTexLoc, useTexture ? 1 : 0); if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, alphaCutout ? 1 : 0); - if (opacityLoc >= 0) glUniform1f(opacityLoc, shadowOpacity); + if (foliageSwayLoc >= 0) glUniform1i(foliageSwayLoc, foliageSway ? 1 : 0); if (useTexture) { glBindTexture(GL_TEXTURE_2D, batch.texture); } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 4c0b9317..946487c9 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2661,11 +2661,9 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { lastWMORenderMs = 0.0; lastM2RenderMs = 0.0; - // Shadow pass (before main scene) — throttled to every 2 frames (depth buffer persists) + // Shadow pass (before main scene) — update every frame to avoid temporal popping. if (shadowsEnabled && shadowFBO && shadowShaderProgram && terrainLoaded) { - if (shadowFrameCounter_++ % 2 == 0) { - renderShadowPass(); - } + renderShadowPass(); } else { // Clear shadow maps when disabled if (terrainRenderer) terrainRenderer->clearShadowMap(); @@ -3495,6 +3493,7 @@ uint32_t Renderer::compileShadowShader() { uniform bool uUseBones; uniform mat4 uBones[200]; out vec2 vTexCoord; + out vec3 vWorldPos; void main() { vec3 pos = aPos; if (uUseBones) { @@ -3506,36 +3505,46 @@ uint32_t Renderer::compileShadowShader() { pos = vec3(boneTransform * vec4(aPos, 1.0)); } vTexCoord = aTexCoord; - gl_Position = uLightSpaceMatrix * uModel * vec4(pos, 1.0); + vec4 worldPos = uModel * vec4(pos, 1.0); + vWorldPos = worldPos.xyz; + gl_Position = uLightSpaceMatrix * worldPos; } )"; const char* fragSrc = R"( #version 330 core in vec2 vTexCoord; + in vec3 vWorldPos; uniform bool uUseTexture; uniform sampler2D uTexture; uniform bool uAlphaTest; - uniform float uShadowOpacity; - - float hash12(vec2 p) { - vec3 p3 = fract(vec3(p.xyx) * 0.1031); - p3 += dot(p3, p3.yzx + 33.33); - return fract((p3.x + p3.y) * p3.z); - } + uniform bool uFoliageSway; + uniform float uWindTime; + uniform float uFoliageMotionDamp; void main() { - float opacity = clamp(uShadowOpacity, 0.0, 1.0); if (uUseTexture) { - vec4 tex = texture(uTexture, vTexCoord); - if (uAlphaTest && tex.a < 0.5) discard; - opacity *= tex.a; - } + vec2 uv = vTexCoord; + vec2 uv2 = vTexCoord; + if (uFoliageSway && uAlphaTest) { + // Slow, coherent wind-driven sway for foliage shadow cutouts. + float gust = sin(uWindTime * 0.32 + vWorldPos.x * 0.05 + vWorldPos.y * 0.04); + float flutter = sin(uWindTime * 0.55 + vWorldPos.y * 0.09 + vWorldPos.z * 0.18); + float damp = clamp(uFoliageMotionDamp, 0.2, 1.0); + uv += vec2(gust * 0.0040 * damp, flutter * 0.0022 * damp); - // Stochastic alpha for soft/translucent shadow casters (foliage). - // Use UV-space hash so pattern stays stable with camera movement. - if (opacity < 0.999) { - float d = hash12(floor(vTexCoord * 4096.0)); - if (d > opacity) discard; + // Second, phase-shifted sample gives smooth position-to-position + // transitions (less on/off popping during motion). + float gust2 = sin(uWindTime * 0.32 + 1.57 + vWorldPos.x * 0.05 + vWorldPos.y * 0.04); + float flutter2 = sin(uWindTime * 0.55 + 2.17 + vWorldPos.y * 0.09 + vWorldPos.z * 0.18); + uv2 += vec2(gust2 * 0.0040 * damp, flutter2 * 0.0022 * damp); + } + // Force base mip for alpha-cutout casters to avoid temporal + // shadow holes from mip-level transitions on thin foliage cards. + vec4 tex = textureLod(uTexture, uv, 0.0); + vec4 tex2 = textureLod(uTexture, uv2, 0.0); + float alphaCut = 0.5; + float alphaVal = (tex.a + tex2.a) * 0.5; + if (uAlphaTest && alphaVal < alphaCut) discard; } } )"; @@ -3590,29 +3599,52 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { constexpr float kShadowNearPlane = 1.0f; constexpr float kShadowFarPlane = 600.0f; - // Sun direction matching WMO light dir + // Fixed sun direction matching current world lighting setup. glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); - // Keep a stable shadow focus center and only recentre occasionally. + // Keep a stable shadow focus center and move it smoothly toward the player + // to avoid visible shadow "state jumps" during movement. glm::vec3 desiredCenter = characterPosition; if (!shadowCenterInitialized) { shadowCenter = desiredCenter; shadowCenterInitialized = true; } else { - constexpr float recenterThreshold = 30.0f; // world units - if (std::abs(desiredCenter.x - shadowCenter.x) > recenterThreshold || - std::abs(desiredCenter.y - shadowCenter.y) > recenterThreshold) { - shadowCenter.x = desiredCenter.x; - shadowCenter.y = desiredCenter.y; - } - // Avoid vertical jitter from tiny terrain/camera height changes. - if (std::abs(desiredCenter.z - shadowCenter.z) > 4.0f) { - shadowCenter.z = desiredCenter.z; + const bool movingNow = cameraController && cameraController->isMoving(); + if (movingNow) { + // Hold projection center fixed while moving to eliminate + // frame-to-frame surface flicker from projection churn. + shadowPostMoveFrames_ = 1; // transition marker: was moving last frame + } else { + if (shadowPostMoveFrames_ == 1) { + // First frame after movement: snap once so there's no delayed catch-up. + shadowCenter = desiredCenter; + } else { + // Normal idle smoothing. + constexpr float kCenterLerp = 0.12f; + constexpr float kMaxHorizontalStep = 1.5f; + constexpr float kMaxVerticalStep = 0.6f; + + glm::vec2 deltaXY(desiredCenter.x - shadowCenter.x, desiredCenter.y - shadowCenter.y); + float distXY = glm::length(deltaXY); + if (distXY > 0.001f) { + float step = std::min(distXY * kCenterLerp, kMaxHorizontalStep); + glm::vec2 move = (deltaXY / distXY) * step; + shadowCenter.x += move.x; + shadowCenter.y += move.y; + } + + float deltaZ = desiredCenter.z - shadowCenter.z; + if (std::abs(deltaZ) > 0.001f) { + float stepZ = std::clamp(deltaZ * kCenterLerp, -kMaxVerticalStep, kMaxVerticalStep); + shadowCenter.z += stepZ; + } + } + shadowPostMoveFrames_ = 0; } } glm::vec3 center = shadowCenter; - // Texel snapping: round center to shadow texel boundaries to prevent shimmer + // Snap to shadow texel grid to keep projection stable while moving. float halfExtent = kShadowHalfExtent; float texelWorld = (2.0f * halfExtent) / static_cast(SHADOW_MAP_SIZE); @@ -3624,16 +3656,15 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { } glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up); - // Snap center in light space to texel grid + // Stable texel snapping in light space removes movement shimmer. glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f); centerLS.x = std::round(centerLS.x / texelWorld) * texelWorld; centerLS.y = std::round(centerLS.y / texelWorld) * texelWorld; glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS; center = glm::vec3(snappedCenter); shadowCenter = center; - - // Rebuild with snapped center lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up); + glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, kShadowNearPlane, kShadowFarPlane); @@ -3667,13 +3698,31 @@ void Renderer::renderShadowPass() { GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture"); GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture"); GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest"); - GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity"); GLint useBonesLoc = glGetUniformLocation(shadowShaderProgram, "uUseBones"); + GLint foliageSwayLoc = glGetUniformLocation(shadowShaderProgram, "uFoliageSway"); + GLint windTimeLoc = glGetUniformLocation(shadowShaderProgram, "uWindTime"); + GLint foliageDampLoc = glGetUniformLocation(shadowShaderProgram, "uFoliageMotionDamp"); if (useTexLoc >= 0) glUniform1i(useTexLoc, 0); if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); - if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f); if (useBonesLoc >= 0) glUniform1i(useBonesLoc, 0); if (texLoc >= 0) glUniform1i(texLoc, 0); + if (foliageSwayLoc >= 0) glUniform1i(foliageSwayLoc, 0); + if (foliageDampLoc >= 0) glUniform1f(foliageDampLoc, 1.0f); + if (windTimeLoc >= 0) { + const auto now = std::chrono::steady_clock::now(); + static auto prev = now; + static float windPhaseSec = 0.0f; + float dt = std::chrono::duration(now - prev).count(); + prev = now; + dt = std::clamp(dt, 0.0f, 0.1f); + // Match moving and idle foliage evolution speed at 80% of original. + float phaseRate = 0.8f; + windPhaseSec += dt * phaseRate; + glUniform1f(windTimeLoc, windPhaseSec); + if (foliageDampLoc >= 0) { + glUniform1f(foliageDampLoc, 1.0f); + } + } // Render terrain into shadow map (only chunks within shadow frustum) if (terrainRenderer) { diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index a15f2c01..42ef0969 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -457,7 +457,7 @@ void TerrainRenderer::render(const Camera& camera) { // Shadow map shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); - shader->setUniform("uShadowStrength", 0.65f); + shader->setUniform("uShadowStrength", 0.62f); if (shadowEnabled) { shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index bd872ef7..ae1daee8 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1038,7 +1038,7 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: shader->setUniform("uFogStart", fogStart); shader->setUniform("uFogEnd", fogEnd); shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); - shader->setUniform("uShadowStrength", 0.65f); + shader->setUniform("uShadowStrength", 0.62f); if (shadowEnabled) { shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7);