From ab4cb878eaa765177e58d91098214ae5ec5aadb2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 4 Feb 2026 16:30:24 -0800 Subject: [PATCH] Improve shadow stability and reduce foliage pop-in --- assets/shaders/terrain.frag | 5 ++++- include/rendering/m2_renderer.hpp | 1 + src/rendering/character_renderer.cpp | 3 +++ src/rendering/m2_renderer.cpp | 26 ++++++++++++++++++-------- src/rendering/renderer.cpp | 26 +++++++++++++++++++------- src/rendering/wmo_renderer.cpp | 3 +++ 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/assets/shaders/terrain.frag b/assets/shaders/terrain.frag index 4ed9870e..ad15021a 100644 --- a/assets/shaders/terrain.frag +++ b/assets/shaders/terrain.frag @@ -47,6 +47,8 @@ float calcShadow() { vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; if (proj.z > 1.0) return 1.0; + float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); + float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); vec3 norm = normalize(Normal); vec3 lightDir = normalize(-uLightDir); float bias = max(0.005 * (1.0 - dot(norm, lightDir)), 0.001); @@ -57,7 +59,8 @@ float calcShadow() { shadow += texture(uShadowMap, vec3(proj.xy + vec2(x, y) * texelSize, proj.z - bias)); } } - return shadow / 9.0; + shadow /= 9.0; + return mix(1.0, shadow, coverageFade); } void main() { diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index d484e93c..d93b2624 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -60,6 +60,7 @@ struct M2ModelGPU { std::vector globalSequenceDurations; // Loop durations for global sequence tracks bool hasAnimation = false; // True if any bone has keyframes bool isSmoke = false; // True for smoke models (UV scroll animation) + bool disableAnimation = false; // Keep foliage/tree doodads visually stable std::vector idleVariationIndices; // Sequence indices for idle variations (animId 0) bool isValid() const { return vao != 0 && indexCount > 0; } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 9b220517..aef01e26 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -124,6 +124,8 @@ bool CharacterRenderer::initialize() { vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) { + float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); + float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001); shadow = 0.0; vec2 texelSize = vec2(1.0 / 2048.0); @@ -133,6 +135,7 @@ bool CharacterRenderer::initialize() { } } shadow /= 9.0; + shadow = mix(1.0, shadow, coverageFade); } } shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 11ea18e7..d5ec8f44 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -310,6 +310,8 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) { + float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); + float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001); shadow = 0.0; vec2 texelSize = vec2(1.0 / 2048.0); @@ -319,6 +321,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { } } shadow /= 9.0; + shadow = mix(1.0, shadow, coverageFade); } } shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); @@ -497,6 +500,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { tightMin = glm::min(tightMin, v.position); tightMax = glm::max(tightMax, v.position); } + bool foliageOrTreeLike = false; { std::string lowerName = model.name; std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), @@ -545,6 +549,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("lily") != std::string::npos) || (lowerName.find("weed") != std::string::npos); bool treeLike = (lowerName.find("tree") != std::string::npos); + foliageOrTreeLike = (foliageName || treeLike); bool hardTreePart = (lowerName.find("trunk") != std::string::npos) || (lowerName.find("stump") != std::string::npos) || @@ -609,6 +614,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { break; } } + gpuModel.disableAnimation = foliageOrTreeLike; // Flag smoke models for UV scroll animation (particle emitters not implemented) { @@ -773,7 +779,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, // Initialize animation: play first sequence (usually Stand/Idle) const auto& mdl = models[modelId]; - if (mdl.hasAnimation && !mdl.sequences.empty()) { + if (mdl.hasAnimation && !mdl.disableAnimation && !mdl.sequences.empty()) { instance.currentSequenceIndex = 0; instance.idleSequenceIndex = 0; instance.animDuration = static_cast(mdl.sequences[0].duration); @@ -827,7 +833,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax); // Initialize animation const auto& mdl2 = models[modelId]; - if (mdl2.hasAnimation && !mdl2.sequences.empty()) { + if (mdl2.hasAnimation && !mdl2.disableAnimation && !mdl2.sequences.empty()) { instance.currentSequenceIndex = 0; instance.idleSequenceIndex = 0; instance.animDuration = static_cast(mdl2.sequences[0].duration); @@ -1040,7 +1046,7 @@ void M2Renderer::update(float deltaTime) { if (it == models.end()) continue; const M2ModelGPU& model = it->second; - if (!model.hasAnimation) { + if (!model.hasAnimation || model.disableAnimation) { instance.animTime += dtMs; continue; } @@ -1139,8 +1145,8 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; - // Adaptive render distance: shorter in dense areas (cities), longer in open terrain - const float maxRenderDistance = (instances.size() > 600) ? 180.0f : 2000.0f; + // Adaptive render distance: keep longer tree/foliage visibility to reduce pop-in. + const float maxRenderDistance = (instances.size() > 600) ? 320.0f : 2800.0f; const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const float fadeStartFraction = 0.75f; const glm::vec3 camPos = camera.getPosition(); @@ -1161,10 +1167,14 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: float worldRadius = model.boundRadius * instance.scale; // Cull small objects (radius < 20) at distance, keep larger objects visible longer float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, worldRadius / 12.0f); + if (model.disableAnimation) { + // Trees/foliage keep a larger horizon before culling. + effectiveMaxDistSq *= 1.8f; + } if (worldRadius < 0.8f) { - effectiveMaxDistSq = std::min(effectiveMaxDistSq, 65.0f * 65.0f); - } else if (worldRadius < 1.5f) { effectiveMaxDistSq = std::min(effectiveMaxDistSq, 95.0f * 95.0f); + } else if (worldRadius < 1.5f) { + effectiveMaxDistSq = std::min(effectiveMaxDistSq, 140.0f * 140.0f); } if (distSq > effectiveMaxDistSq) { continue; @@ -1189,7 +1199,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: shader->setUniform("uFadeAlpha", fadeAlpha); // Upload bone matrices if model has skeletal animation - bool useBones = model.hasAnimation && !instance.boneMatrices.empty(); + bool useBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty(); shader->setUniform("uUseBones", useBones); if (useBones) { int numBones = std::min(static_cast(instance.boneMatrices.size()), 128); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 1afcb233..38991dd7 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1607,6 +1607,11 @@ uint32_t Renderer::compileShadowShader() { } glm::mat4 Renderer::computeLightSpaceMatrix() { + constexpr float kShadowHalfExtent = 180.0f; + constexpr float kShadowLightDistance = 280.0f; + constexpr float kShadowNearPlane = 1.0f; + constexpr float kShadowFarPlane = 600.0f; + // Sun direction matching WMO light dir glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); @@ -1630,7 +1635,7 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { glm::vec3 center = shadowCenter; // Texel snapping: round center to shadow texel boundaries to prevent shimmer - float halfExtent = 120.0f; + float halfExtent = kShadowHalfExtent; float texelWorld = (2.0f * halfExtent) / static_cast(SHADOW_MAP_SIZE); // Build light view to get stable axes @@ -1639,7 +1644,7 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { if (std::abs(glm::dot(sunDir, up)) > 0.99f) { up = glm::vec3(0.0f, 1.0f, 0.0f); } - glm::mat4 lightView = glm::lookAt(center - sunDir * 200.0f, center, up); + glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up); // Snap center in light space to texel grid glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f); @@ -1650,13 +1655,19 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { shadowCenter = center; // Rebuild with snapped center - lightView = glm::lookAt(center - sunDir * 200.0f, center, up); - glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, 1.0f, 400.0f); + lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up); + glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, + kShadowNearPlane, kShadowFarPlane); return lightProj * lightView; } void Renderer::renderShadowPass() { + constexpr float kShadowHalfExtent = 180.0f; + constexpr float kShadowLightDistance = 280.0f; + constexpr float kShadowNearPlane = 1.0f; + constexpr float kShadowFarPlane = 600.0f; + // Compute light space matrix lightSpaceMatrix = computeLightSpaceMatrix(); @@ -1698,11 +1709,12 @@ void Renderer::renderShadowPass() { // For simplicity, compute the split: glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); glm::vec3 center = shadowCenterInitialized ? shadowCenter : characterPosition; - float halfExtent = 120.0f; + float halfExtent = kShadowHalfExtent; glm::vec3 up(0.0f, 0.0f, 1.0f); if (std::abs(glm::dot(sunDir, up)) > 0.99f) up = glm::vec3(0.0f, 1.0f, 0.0f); - glm::mat4 lightView = glm::lookAt(center - sunDir * 200.0f, center, up); - glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, 1.0f, 400.0f); + glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up); + glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, + kShadowNearPlane, kShadowFarPlane); // WMO renderShadow needs a Shader reference — but it only uses setUniform("uModel", ...) // We'll create a thin wrapper. Actually, WMO's renderShadow takes a Shader& and calls diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 6fbf5364..68dc7fb1 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -127,6 +127,8 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) { vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) { + float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); + float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001); shadow = 0.0; vec2 texelSize = vec2(1.0 / 2048.0); @@ -136,6 +138,7 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) { } } shadow /= 9.0; + shadow = mix(1.0, shadow, coverageFade); } } shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));