Implement shadow mapping pipeline for terrain and models

This commit is contained in:
Kelsi 2026-02-04 16:08:35 -08:00
parent dede5a99d4
commit f17b15395d
6 changed files with 306 additions and 1 deletions

View file

@ -37,6 +37,28 @@ uniform vec3 uFogColor;
uniform float uFogStart;
uniform float uFogEnd;
// Shadow mapping
uniform sampler2DShadow uShadowMap;
uniform mat4 uLightSpaceMatrix;
uniform bool uShadowEnabled;
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;
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(-uLightDir);
float bias = max(0.005 * (1.0 - dot(norm, lightDir)), 0.001);
float shadow = 0.0;
vec2 texelSize = vec2(1.0 / 2048.0);
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
shadow += texture(uShadowMap, vec3(proj.xy + vec2(x, y) * texelSize, proj.z - bias));
}
}
return shadow / 9.0;
}
void main() {
// Sample base texture
vec4 baseColor = texture(uBaseTexture, TexCoord);
@ -75,8 +97,11 @@ void main() {
diff = max(diff, 0.2); // Minimum light to prevent completely dark faces
vec3 diffuse = diff * uLightColor * finalColor.rgb;
// Shadow
float shadow = uShadowEnabled ? calcShadow() : 1.0;
// Combine lighting (terrain is purely diffuse — no specular on ground)
vec3 result = ambient + diffuse;
vec3 result = ambient + shadow * diffuse;
// Apply fog
float distance = length(uViewPos - FragPos);

View file

@ -174,6 +174,18 @@ private:
void resizePostProcess(int w, int h);
void shutdownPostProcess();
// Shadow mapping
static constexpr int SHADOW_MAP_SIZE = 2048;
uint32_t shadowFBO = 0;
uint32_t shadowDepthTex = 0;
uint32_t shadowShaderProgram = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
void initShadowMap();
void renderShadowPass();
uint32_t compileShadowShader();
glm::mat4 computeLightSpaceMatrix();
pipeline::AssetManager* cachedAssetManager = nullptr;
uint32_t currentZoneId = 0;
std::string currentZoneName;

View file

@ -30,6 +30,11 @@ public:
GLuint getProgram() const { return program; }
// Adopt an externally-created program (no ownership of individual shaders)
void setProgram(GLuint prog) { program = prog; }
// Release ownership without deleting (caller retains the GL program)
void releaseProgram() { program = 0; vertexShader = 0; fragmentShader = 0; }
private:
bool compile(const std::string& vertexSource, const std::string& fragmentSource);
GLint getUniformLocation(const std::string& name) const;

View file

@ -125,6 +125,19 @@ public:
void setFogEnabled(bool enabled) { fogEnabled = enabled; }
bool isFogEnabled() const { return fogEnabled; }
/**
* Render terrain geometry into shadow depth map
*/
void renderShadow(GLuint shaderProgram);
/**
* Set shadow map for receiving shadows
*/
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpaceMat) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpaceMat; shadowEnabled = true;
}
void clearShadowMap() { shadowEnabled = false; }
/**
* Get statistics
*/
@ -187,6 +200,11 @@ private:
// Default white texture (fallback)
GLuint whiteTexture = 0;
// Shadow mapping (receiving)
GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
bool shadowEnabled = false;
};
} // namespace rendering

View file

@ -228,6 +228,9 @@ bool Renderer::initialize(core::Window* win) {
// Initialize post-process FBO pipeline
initPostProcess(window->getWidth(), window->getHeight());
// Initialize shadow map
initShadowMap();
LOG_INFO("Renderer initialized");
return true;
}
@ -317,6 +320,11 @@ void Renderer::shutdown() {
}
underwaterOverlayShader.reset();
// Cleanup shadow map resources
if (shadowFBO) { glDeleteFramebuffers(1, &shadowFBO); shadowFBO = 0; }
if (shadowDepthTex) { glDeleteTextures(1, &shadowDepthTex); shadowDepthTex = 0; }
if (shadowShaderProgram) { glDeleteProgram(shadowShaderProgram); shadowShaderProgram = 0; }
shutdownPostProcess();
zoneManager.reset();
@ -901,6 +909,11 @@ void Renderer::renderWorld(game::World* world) {
lastWMORenderMs = 0.0;
lastM2RenderMs = 0.0;
// Shadow pass (before main scene)
if (shadowFBO && shadowShaderProgram && terrainLoaded) {
renderShadowPass();
}
// Bind HDR scene framebuffer for world rendering
glBindFramebuffer(GL_FRAMEBUFFER, sceneFBO);
glViewport(0, 0, fbWidth, fbHeight);
@ -1462,5 +1475,213 @@ void Renderer::renderHUD() {
}
}
// ──────────────────────────────────────────────────────
// Shadow mapping helpers
// ──────────────────────────────────────────────────────
void Renderer::initShadowMap() {
// Compile shadow shader
shadowShaderProgram = compileShadowShader();
if (!shadowShaderProgram) {
LOG_ERROR("Failed to compile shadow shader");
return;
}
// Create depth texture
glGenTextures(1, &shadowDepthTex);
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24,
SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0,
GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glBindTexture(GL_TEXTURE_2D, 0);
// Create depth-only FBO
glGenFramebuffers(1, &shadowFBO);
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTex, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("Shadow FBO incomplete!");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
LOG_INFO("Shadow map initialized (", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
}
uint32_t Renderer::compileShadowShader() {
const char* vertSrc = R"(
#version 330 core
uniform mat4 uLightSpaceMatrix;
uniform mat4 uModel;
layout(location = 0) in vec3 aPos;
void main() {
gl_Position = uLightSpaceMatrix * uModel * vec4(aPos, 1.0);
}
)";
const char* fragSrc = R"(
#version 330 core
void main() { }
)";
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &vertSrc, nullptr);
glCompileShader(vs);
GLint success;
glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
if (!success) {
char log[512];
glGetShaderInfoLog(vs, 512, nullptr, log);
LOG_ERROR("Shadow vertex shader error: ", log);
glDeleteShader(vs);
return 0;
}
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &fragSrc, nullptr);
glCompileShader(fs);
glGetShaderiv(fs, GL_COMPILE_STATUS, &success);
if (!success) {
char log[512];
glGetShaderInfoLog(fs, 512, nullptr, log);
LOG_ERROR("Shadow fragment shader error: ", log);
glDeleteShader(vs);
glDeleteShader(fs);
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
char log[512];
glGetProgramInfoLog(program, 512, nullptr, log);
LOG_ERROR("Shadow shader link error: ", log);
glDeleteProgram(program);
program = 0;
}
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
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;
// Texel snapping: round center to shadow texel boundaries to prevent shimmer
float halfExtent = 120.0f;
float texelWorld = (2.0f * halfExtent) / static_cast<float>(SHADOW_MAP_SIZE);
// Build light view to get stable axes
glm::vec3 up(0.0f, 0.0f, 1.0f);
// If sunDir is nearly parallel to up, pick a different up vector
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);
// 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;
glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS;
center = glm::vec3(snappedCenter);
// 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);
return lightProj * lightView;
}
void Renderer::renderShadowPass() {
// Compute light space matrix
lightSpaceMatrix = computeLightSpaceMatrix();
// Bind shadow FBO
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glViewport(0, 0, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);
glClear(GL_DEPTH_BUFFER_BIT);
// Caster-side bias: front-face culling + polygon offset
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(2.0f, 4.0f);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
// Use shadow shader
glUseProgram(shadowShaderProgram);
GLint lsmLoc = glGetUniformLocation(shadowShaderProgram, "uLightSpaceMatrix");
glUniformMatrix4fv(lsmLoc, 1, GL_FALSE, &lightSpaceMatrix[0][0]);
// Render terrain into shadow map
if (terrainRenderer) {
terrainRenderer->renderShadow(shadowShaderProgram);
}
// Render WMO into shadow map
if (wmoRenderer) {
// WMO renderShadow takes separate view/proj matrices and a Shader ref.
// We need to decompose our lightSpaceMatrix or use the raw shader program.
// Since WMO::renderShadow sets uModel per instance, we use the shadow shader
// 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;
float halfExtent = 120.0f;
float texelWorld = (2.0f * halfExtent) / static_cast<float>(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", ...)
// We'll create a thin wrapper. Actually, WMO's renderShadow takes a Shader& and calls
// shadowShader.setUniform("uModel", ...). We need a Shader object wrapping our program.
// Instead, let's use the lower-level approach: WMO renderShadow uses the shader passed in.
// We need to temporarily wrap our GL program in a Shader object.
Shader shadowShaderWrapper;
shadowShaderWrapper.setProgram(shadowShaderProgram);
wmoRenderer->renderShadow(lightView, lightProj, shadowShaderWrapper);
shadowShaderWrapper.releaseProgram(); // Don't let wrapper delete our program
}
// Restore state
glDisable(GL_POLYGON_OFFSET_FILL);
glCullFace(GL_BACK);
// Restore main viewport
glViewport(0, 0, fbWidth, fbHeight);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Distribute shadow map to all receivers
if (terrainRenderer) terrainRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
if (wmoRenderer) wmoRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
if (m2Renderer) m2Renderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
if (characterRenderer) characterRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
}
} // namespace rendering
} // namespace wowee

View file

@ -279,6 +279,21 @@ GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData
return textureID;
}
void TerrainRenderer::renderShadow(GLuint shaderProgram) {
if (chunks.empty()) return;
GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel");
glm::mat4 identity(1.0f);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, &identity[0][0]);
for (const auto& chunk : chunks) {
if (!chunk.isValid()) continue;
glBindVertexArray(chunk.vao);
glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
}
void TerrainRenderer::render(const Camera& camera) {
if (chunks.empty() || !shader) {
return;
@ -340,6 +355,15 @@ void TerrainRenderer::render(const Camera& camera) {
shader->setUniform("uFogEnd", 100001.0f); // Effectively disabled
}
// Shadow map
shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0);
if (shadowEnabled) {
shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix);
glActiveTexture(GL_TEXTURE7);
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
shader->setUniform("uShadowMap", 7);
}
// Extract frustum for culling
Frustum frustum;
if (frustumCullingEnabled) {