mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Implement shadow mapping pipeline for terrain and models
This commit is contained in:
parent
dede5a99d4
commit
f17b15395d
6 changed files with 306 additions and 1 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue