Improve shadow stability and reduce foliage pop-in

This commit is contained in:
Kelsi 2026-02-04 16:30:24 -08:00
parent 979c0b5592
commit ab4cb878ea
6 changed files with 48 additions and 16 deletions

View file

@ -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() {

View file

@ -60,6 +60,7 @@ struct M2ModelGPU {
std::vector<uint32_t> 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<int> idleVariationIndices; // Sequence indices for idle variations (animId 0)
bool isValid() const { return vao != 0 && indexCount > 0; }

View file

@ -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));

View file

@ -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<float>(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<float>(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<int>(instance.boneMatrices.size()), 128);

View file

@ -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<float>(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

View file

@ -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));