diff --git a/assets/shaders/terrain.frag b/assets/shaders/terrain.frag index 5723c471..4ed9870e 100644 --- a/assets/shaders/terrain.frag +++ b/assets/shaders/terrain.frag @@ -41,6 +41,7 @@ uniform float uFogEnd; uniform sampler2DShadow uShadowMap; uniform mat4 uLightSpaceMatrix; uniform bool uShadowEnabled; +uniform float uShadowStrength; float calcShadow() { vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); @@ -99,6 +100,7 @@ void main() { // Shadow float shadow = uShadowEnabled ? calcShadow() : 1.0; + shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); // Combine lighting (terrain is purely diffuse — no specular on ground) vec3 result = ambient + shadow * diffuse; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 62db8321..d484e93c 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -158,6 +158,11 @@ public: */ void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection); + /** + * Render depth-only pass for shadow casting + */ + void renderShadow(GLuint shadowShaderProgram); + /** * Render smoke particles (call after render()) */ diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 22d6efda..3691bbf2 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -180,6 +180,8 @@ private: 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; void initShadowMap(); void renderShadowPass(); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index ff010f41..9b220517 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -101,6 +101,7 @@ bool CharacterRenderer::initialize() { uniform sampler2DShadow uShadowMap; uniform mat4 uLightSpaceMatrix; uniform int uShadowEnabled; + uniform float uShadowStrength; out vec4 FragColor; @@ -134,6 +135,7 @@ bool CharacterRenderer::initialize() { shadow /= 9.0; } } + shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); // Ambient vec3 ambient = vec3(0.3); @@ -1029,6 +1031,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons // Shadows characterShader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); + characterShader->setUniform("uShadowStrength", 0.65f); if (shadowEnabled) { characterShader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index d03d3cc4..11ea18e7 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -269,6 +269,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { uniform sampler2DShadow uShadowMap; uniform mat4 uLightSpaceMatrix; uniform bool uShadowEnabled; + uniform float uShadowStrength; out vec4 FragColor; @@ -320,6 +321,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { shadow /= 9.0; } } + shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); vec3 ambient = uAmbientColor * texColor.rgb; vec3 diffuse = diff * texColor.rgb; @@ -1127,6 +1129,7 @@ void M2Renderer::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); if (shadowEnabled) { shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7); @@ -1231,6 +1234,58 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: glEnable(GL_CULL_FACE); } +void M2Renderer::renderShadow(GLuint shadowShaderProgram) { + if (instances.empty() || shadowShaderProgram == 0) { + return; + } + + GLint modelLoc = glGetUniformLocation(shadowShaderProgram, "uModel"); + GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture"); + GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture"); + GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest"); + GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity"); + if (modelLoc < 0) { + return; + } + + if (useTexLoc >= 0) glUniform1i(useTexLoc, 0); + if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); + if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f); + if (texLoc >= 0) glUniform1i(texLoc, 0); + glActiveTexture(GL_TEXTURE0); + + for (const auto& instance : instances) { + auto it = models.find(instance.modelId); + if (it == models.end()) continue; + + const M2ModelGPU& model = it->second; + if (!model.isValid() || model.isSmoke) continue; + + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, &instance.modelMatrix[0][0]); + glBindVertexArray(model.vao); + + for (const auto& batch : model.batches) { + 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; + + if (useTexLoc >= 0) glUniform1i(useTexLoc, useTexture ? 1 : 0); + if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, alphaCutout ? 1 : 0); + if (opacityLoc >= 0) glUniform1f(opacityLoc, shadowOpacity); + if (useTexture) { + glBindTexture(GL_TEXTURE_2D, batch.texture); + } + glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT, + (void*)(batch.indexStart * sizeof(uint16_t))); + } + } + + glBindVertexArray(0); +} + void M2Renderer::renderSmokeParticles(const Camera& /*camera*/, const glm::mat4& view, const glm::mat4& projection) { if (smokeParticles.empty() || !smokeShader || smokeVAO == 0) return; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 083faa85..1afcb233 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1524,13 +1524,42 @@ uint32_t Renderer::compileShadowShader() { uniform mat4 uLightSpaceMatrix; uniform mat4 uModel; layout(location = 0) in vec3 aPos; + layout(location = 4) in vec2 aTexCoord; + out vec2 vTexCoord; void main() { + vTexCoord = aTexCoord; gl_Position = uLightSpaceMatrix * uModel * vec4(aPos, 1.0); } )"; const char* fragSrc = R"( #version 330 core - void main() { } + in vec2 vTexCoord; + 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); + } + + 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; + } + + // 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; + } + } )"; GLuint vs = glCreateShader(GL_VERTEX_SHADER); @@ -1581,8 +1610,24 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { // Sun direction matching WMO light dir glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); - // Center on character position - glm::vec3 center = characterPosition; + // Keep a stable shadow focus center and only recentre occasionally. + 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; + } + } + glm::vec3 center = shadowCenter; // Texel snapping: round center to shadow texel boundaries to prevent shimmer float halfExtent = 120.0f; @@ -1598,10 +1643,11 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { // Snap center in light space to texel grid glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f); - centerLS.x = std::floor(centerLS.x / texelWorld) * texelWorld; - centerLS.y = std::floor(centerLS.y / texelWorld) * texelWorld; + 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 * 200.0f, center, up); @@ -1629,6 +1675,14 @@ void Renderer::renderShadowPass() { glUseProgram(shadowShaderProgram); GLint lsmLoc = glGetUniformLocation(shadowShaderProgram, "uLightSpaceMatrix"); glUniformMatrix4fv(lsmLoc, 1, GL_FALSE, &lightSpaceMatrix[0][0]); + GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture"); + GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture"); + GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest"); + GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity"); + if (useTexLoc >= 0) glUniform1i(useTexLoc, 0); + if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); + if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f); + if (texLoc >= 0) glUniform1i(texLoc, 0); // Render terrain into shadow map if (terrainRenderer) { @@ -1643,18 +1697,11 @@ void Renderer::renderShadowPass() { // directly by calling renderShadow with the light view/proj split. // For simplicity, compute the split: glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); - glm::vec3 center = characterPosition; + glm::vec3 center = shadowCenterInitialized ? shadowCenter : characterPosition; float halfExtent = 120.0f; - float texelWorld = (2.0f * halfExtent) / static_cast(SHADOW_MAP_SIZE); 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::vec4 centerLS = lightView * glm::vec4(center, 1.0f); - centerLS.x = std::floor(centerLS.x / texelWorld) * texelWorld; - centerLS.y = std::floor(centerLS.y / texelWorld) * texelWorld; - glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS; - center = glm::vec3(snappedCenter); - lightView = glm::lookAt(center - sunDir * 200.0f, center, up); glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, 1.0f, 400.0f); // WMO renderShadow needs a Shader reference — but it only uses setUniform("uModel", ...) @@ -1668,6 +1715,11 @@ void Renderer::renderShadowPass() { shadowShaderWrapper.releaseProgram(); // Don't let wrapper delete our program } + // Render M2 doodads into shadow map + if (m2Renderer) { + m2Renderer->renderShadow(shadowShaderProgram); + } + // Restore state glDisable(GL_POLYGON_OFFSET_FILL); glCullFace(GL_BACK); diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index b4399be5..b5b62e7b 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -357,6 +357,7 @@ void TerrainRenderer::render(const Camera& camera) { // Shadow map shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); + shader->setUniform("uShadowStrength", 0.65f); if (shadowEnabled) { shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index e09daac4..6fbf5364 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -89,6 +89,7 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) { uniform sampler2DShadow uShadowMap; uniform mat4 uLightSpaceMatrix; uniform bool uShadowEnabled; + uniform float uShadowStrength; out vec4 FragColor; @@ -137,6 +138,7 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) { shadow /= 9.0; } } + shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); // Combine lighting with texture vec3 result = (ambient + (diffuse + specular) * shadow) * texColor.rgb; @@ -499,6 +501,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); if (shadowEnabled) { shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE7);