From d8795ec076888b941dee9a49a31547cfb74aeda5 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Wed, 15 Apr 2026 19:54:21 +0300 Subject: [PATCH] Added shadow maps --- proj-windows/CMakeLists.txt | 3 + resources/shaders/default_shadow.vertex | 19 ++ .../shaders/default_shadow_desktop.fragment | 70 ++++++ resources/shaders/default_shadow_web.fragment | 68 ++++++ resources/shaders/shadow_depth.vertex | 9 + .../shaders/shadow_depth_desktop.fragment | 5 + .../shaders/shadow_depth_skinning.vertex | 47 ++++ resources/shaders/shadow_depth_web.fragment | 6 + resources/shaders/skinning_shadow.vertex | 65 +++++ src/Character.cpp | 230 +++++++++++++++++- src/Character.h | 8 + src/Game.cpp | 182 +++++++++++++- src/Game.h | 6 + src/render/ShadowMap.cpp | 124 ++++++++++ src/render/ShadowMap.h | 44 ++++ 15 files changed, 880 insertions(+), 6 deletions(-) create mode 100644 resources/shaders/default_shadow.vertex create mode 100644 resources/shaders/default_shadow_desktop.fragment create mode 100644 resources/shaders/default_shadow_web.fragment create mode 100644 resources/shaders/shadow_depth.vertex create mode 100644 resources/shaders/shadow_depth_desktop.fragment create mode 100644 resources/shaders/shadow_depth_skinning.vertex create mode 100644 resources/shaders/shadow_depth_web.fragment create mode 100644 resources/shaders/skinning_shadow.vertex create mode 100644 src/render/ShadowMap.cpp create mode 100644 src/render/ShadowMap.h diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index b89dca9..7fdac84 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -48,6 +48,8 @@ add_executable(space-game001 # ../src/planet/StoneObject.h ../src/render/FrameBuffer.cpp ../src/render/FrameBuffer.h + ../src/render/ShadowMap.cpp + ../src/render/ShadowMap.h ../src/UiManager.cpp ../src/UiManager.h ../src/Projectile.h @@ -109,6 +111,7 @@ target_compile_definitions(space-game001 PRIVATE WIN32_LEAN_AND_MEAN PNG_ENABLED SDL_MAIN_HANDLED +# DEBUG_LIGHT # NETWORK # SIMPLIFIED ) diff --git a/resources/shaders/default_shadow.vertex b/resources/shaders/default_shadow.vertex new file mode 100644 index 0000000..7f3b9c5 --- /dev/null +++ b/resources/shaders/default_shadow.vertex @@ -0,0 +1,19 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec3 vNormal; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 ModelViewMatrix; +uniform mat4 uLightFromCamera; + +void main() +{ + gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0); + texCoord = vTexCoord; + fragPosLightSpace = uLightFromCamera * ModelViewMatrix * vec4(vPosition, 1.0); + fragNormal = mat3(ModelViewMatrix) * vNormal; +} diff --git a/resources/shaders/default_shadow_desktop.fragment b/resources/shaders/default_shadow_desktop.fragment new file mode 100644 index 0000000..55589a0 --- /dev/null +++ b/resources/shaders/default_shadow_desktop.fragment @@ -0,0 +1,70 @@ +uniform sampler2D Texture; +uniform sampler2D uShadowMap; +uniform vec3 uLightDir; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; + +float computeShadow(vec4 lightSpacePos, vec3 normal) +{ + vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.x < 0.0 || projCoords.x > 1.0 || + projCoords.y < 0.0 || projCoords.y > 1.0 || + projCoords.z > 1.0) + { + return 0.0; + } + + float currentDepth = projCoords.z; + + // Slope-dependent bias: large for grazing angles, tiny for surfaces facing the light + float bias = 0.0004; + if (dot(normal, normal) > 0.001) + { + float cosTheta = dot(normalize(normal), -normalize(uLightDir)); + bias = max(0.002 * (1.0 - cosTheta), 0.0002); + } + + // 3x3 PCF (percentage-closer filtering) for softer shadows + float shadow = 0.0; + float texelSize = 1.0 / 2048.0; + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float pcfDepth = texture2D(uShadowMap, projCoords.xy + vec2(float(x), float(y)) * texelSize).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; + + return shadow; +} + +void main() +{ + vec4 color = texture2D(Texture, texCoord); + + float ambient = 0.4; + float diffuseStrength = 0.6; + + // Compute diffuse term; if normals are missing (zero-length) treat as fully lit + float diffuse = 1.0; + vec3 n = fragNormal; + if (dot(n, n) > 0.001) + { + n = normalize(n); + diffuse = max(dot(n, -normalize(uLightDir)), 0.0); + } + + float shadow = computeShadow(fragPosLightSpace, fragNormal); + + // Shadow dims only the diffuse contribution; ambient is always present + float lighting = ambient + diffuseStrength * diffuse * (1.0 - shadow); + color.rgb *= lighting; + + gl_FragColor = color; +} diff --git a/resources/shaders/default_shadow_web.fragment b/resources/shaders/default_shadow_web.fragment new file mode 100644 index 0000000..52a8b75 --- /dev/null +++ b/resources/shaders/default_shadow_web.fragment @@ -0,0 +1,68 @@ +precision mediump float; + +uniform sampler2D Texture; +uniform sampler2D uShadowMap; +uniform vec3 uLightDir; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; + +float computeShadow(vec4 lightSpacePos, vec3 normal) +{ + vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.x < 0.0 || projCoords.x > 1.0 || + projCoords.y < 0.0 || projCoords.y > 1.0 || + projCoords.z > 1.0) + { + return 0.0; + } + + float currentDepth = projCoords.z; + + float bias = 0.0004; + if (dot(normal, normal) > 0.001) + { + float cosTheta = dot(normalize(normal), -normalize(uLightDir)); + bias = max(0.002 * (1.0 - cosTheta), 0.0002); + } + + float shadow = 0.0; + float texelSize = 1.0 / 2048.0; + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float pcfDepth = texture2D(uShadowMap, projCoords.xy + vec2(float(x), float(y)) * texelSize).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; + + return shadow; +} + +void main() +{ + vec4 color = texture2D(Texture, texCoord); + + float ambient = 0.4; + float diffuseStrength = 0.6; + + float diffuse = 1.0; + vec3 n = fragNormal; + if (dot(n, n) > 0.001) + { + n = normalize(n); + diffuse = max(dot(n, -normalize(uLightDir)), 0.0); + } + + float shadow = computeShadow(fragPosLightSpace, fragNormal); + + float lighting = ambient + diffuseStrength * diffuse * (1.0 - shadow); + color.rgb *= lighting; + + gl_FragColor = color; +} diff --git a/resources/shaders/shadow_depth.vertex b/resources/shaders/shadow_depth.vertex new file mode 100644 index 0000000..52a6b24 --- /dev/null +++ b/resources/shaders/shadow_depth.vertex @@ -0,0 +1,9 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; + +uniform mat4 ProjectionModelViewMatrix; + +void main() +{ + gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0); +} diff --git a/resources/shaders/shadow_depth_desktop.fragment b/resources/shaders/shadow_depth_desktop.fragment new file mode 100644 index 0000000..4d01401 --- /dev/null +++ b/resources/shaders/shadow_depth_desktop.fragment @@ -0,0 +1,5 @@ +void main() +{ + // Depth is written automatically by the GPU. + // Nothing to do here. +} diff --git a/resources/shaders/shadow_depth_skinning.vertex b/resources/shaders/shadow_depth_skinning.vertex new file mode 100644 index 0000000..7c11b90 --- /dev/null +++ b/resources/shaders/shadow_depth_skinning.vertex @@ -0,0 +1,47 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec4 aBoneIndices0; +attribute vec2 aBoneIndices1; +attribute vec4 aBoneWeights0; +attribute vec2 aBoneWeights1; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 uBoneMatrices[64]; + +void main() +{ + vec4 skinnedPos = vec4(0.0, 0.0, 0.0, 0.0); + vec4 originalPos = vec4(vPosition, 1.0); + float totalWeight = 0.0; + + if (aBoneWeights0.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.x)] * originalPos * aBoneWeights0.x; + totalWeight += aBoneWeights0.x; + } + if (aBoneWeights0.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.y)] * originalPos * aBoneWeights0.y; + totalWeight += aBoneWeights0.y; + } + if (aBoneWeights0.z > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.z)] * originalPos * aBoneWeights0.z; + totalWeight += aBoneWeights0.z; + } + if (aBoneWeights0.w > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.w)] * originalPos * aBoneWeights0.w; + totalWeight += aBoneWeights0.w; + } + if (aBoneWeights1.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.x)] * originalPos * aBoneWeights1.x; + totalWeight += aBoneWeights1.x; + } + if (aBoneWeights1.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.y)] * originalPos * aBoneWeights1.y; + totalWeight += aBoneWeights1.y; + } + + if (totalWeight < 0.001) { + skinnedPos = originalPos; + } + + gl_Position = ProjectionModelViewMatrix * skinnedPos; +} diff --git a/resources/shaders/shadow_depth_web.fragment b/resources/shaders/shadow_depth_web.fragment new file mode 100644 index 0000000..e2137ee --- /dev/null +++ b/resources/shaders/shadow_depth_web.fragment @@ -0,0 +1,6 @@ +precision mediump float; + +void main() +{ + // Depth is written automatically by the GPU. +} diff --git a/resources/shaders/skinning_shadow.vertex b/resources/shaders/skinning_shadow.vertex new file mode 100644 index 0000000..4aa14cb --- /dev/null +++ b/resources/shaders/skinning_shadow.vertex @@ -0,0 +1,65 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec3 vNormal; +attribute vec4 aBoneIndices0; +attribute vec2 aBoneIndices1; +attribute vec4 aBoneWeights0; +attribute vec2 aBoneWeights1; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 ModelViewMatrix; +uniform mat4 uLightFromCamera; +uniform mat4 uBoneMatrices[64]; + +void main() +{ + vec4 skinnedPos = vec4(0.0, 0.0, 0.0, 0.0); + vec3 skinnedNormal = vec3(0.0, 0.0, 0.0); + vec4 originalPos = vec4(vPosition, 1.0); + float totalWeight = 0.0; + + if (aBoneWeights0.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.x)] * originalPos * aBoneWeights0.x; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.x)]) * vNormal * aBoneWeights0.x; + totalWeight += aBoneWeights0.x; + } + if (aBoneWeights0.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.y)] * originalPos * aBoneWeights0.y; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.y)]) * vNormal * aBoneWeights0.y; + totalWeight += aBoneWeights0.y; + } + if (aBoneWeights0.z > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.z)] * originalPos * aBoneWeights0.z; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.z)]) * vNormal * aBoneWeights0.z; + totalWeight += aBoneWeights0.z; + } + if (aBoneWeights0.w > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.w)] * originalPos * aBoneWeights0.w; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.w)]) * vNormal * aBoneWeights0.w; + totalWeight += aBoneWeights0.w; + } + if (aBoneWeights1.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.x)] * originalPos * aBoneWeights1.x; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices1.x)]) * vNormal * aBoneWeights1.x; + totalWeight += aBoneWeights1.x; + } + if (aBoneWeights1.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.y)] * originalPos * aBoneWeights1.y; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices1.y)]) * vNormal * aBoneWeights1.y; + totalWeight += aBoneWeights1.y; + } + + if (totalWeight < 0.001) { + skinnedPos = originalPos; + skinnedNormal = vNormal; + } + + gl_Position = ProjectionModelViewMatrix * skinnedPos; + texCoord = vTexCoord; + fragPosLightSpace = uLightFromCamera * ModelViewMatrix * skinnedPos; + fragNormal = mat3(ModelViewMatrix) * skinnedNormal; +} diff --git a/src/Character.cpp b/src/Character.cpp index a910cef..7e19693 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -249,7 +249,11 @@ void Character::draw(Renderer& renderer) { } void Character::prepareGpuSkinningVBOs(AnimationData& anim) { - if (anim.gpuSkinningPrepared) return; + if (anim.gpuSkinningPrepared) + { + + return; + } anim.model.PrepareGpuSkinningData(); @@ -284,13 +288,18 @@ void Character::prepareGpuSkinningVBOs(AnimationData& anim) { void Character::drawGpuSkinning(Renderer& renderer) { AnimationState drawState = resolveActiveState(); auto it = animations.find(drawState); - if (it == animations.end() || !texture) return; + if (it == animations.end() || !texture) + { + return; + } auto& anim = it->second; prepareGpuSkinningVBOs(anim); - if (anim.skinningMatrices.empty()) return; - + if (anim.skinningMatrices.empty()) + { + return; + } static const std::string skinningShaderName = "skinning"; static const std::string boneMatricesUniform = "uBoneMatrices[0]"; @@ -354,4 +363,217 @@ void Character::drawGpuSkinning(Renderer& renderer) { renderer.shaderManager.PopShader(); } +// ==================== Shadow depth pass ==================== + +void Character::drawShadowDepth(Renderer& renderer) { + if (useGpuSkinning) { + drawShadowDepthGpuSkinning(renderer); + } else { + drawShadowDepthCpu(renderer); + } +} + +void Character::drawShadowDepthCpu(Renderer& renderer) { + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end()) return; + + // The caller has already pushed the shadow_depth shader and the + // light's projection + view onto the renderer stacks. + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + auto& anim = it->second; + anim.modelMutable.AssignFrom(anim.model.mesh); + anim.modelMutable.RefreshVBO(); + renderer.DrawVertexRenderStruct(anim.modelMutable); + + renderer.PopMatrix(); +} + +void Character::drawShadowDepthGpuSkinning(Renderer& renderer) { + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end()) return; + + auto& anim = it->second; + prepareGpuSkinningVBOs(anim); + if (anim.skinningMatrices.empty()) return; + + static const std::string shadowSkinningShader = "shadow_depth_skinning"; + static const std::string boneMatricesUniform = "uBoneMatrices[0]"; + + // Switch to the skinning depth shader (caller has the static depth shader active). + renderer.shaderManager.PushShader(shadowSkinningShader); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, + static_cast(anim.skinningMatrices.size()), false, + anim.skinningMatrices[0].data()); + +#ifndef EMSCRIPTEN +#ifndef __ANDROID__ + if (anim.bindPoseMutable.vao) { + glBindVertexArray(anim.bindPoseMutable.vao->getBuffer()); + renderer.shaderManager.EnableVertexAttribArrays(); + } +#endif +#endif + + glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.positionVBO->getBuffer()); + renderer.VertexAttribPointer3fv("vPosition", 0, NULL); + + if (anim.bindPoseMutable.texCoordVBO) { + glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.texCoordVBO->getBuffer()); + renderer.VertexAttribPointer2fv("vTexCoord", 0, NULL); + } + + glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices0VBO->getBuffer()); + renderer.VertexAttribPointer4fv("aBoneIndices0", 0, NULL); + glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices1VBO->getBuffer()); + renderer.VertexAttribPointer2fv("aBoneIndices1", 0, NULL); + glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights0VBO->getBuffer()); + renderer.VertexAttribPointer4fv("aBoneWeights0", 0, NULL); + glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights1VBO->getBuffer()); + renderer.VertexAttribPointer2fv("aBoneWeights1", 0, NULL); + + glDrawArrays(GL_TRIANGLES, 0, static_cast(anim.bindPoseMutable.data.PositionData.size())); + + renderer.PopMatrix(); + renderer.shaderManager.PopShader(); +} + +// ==================== Main pass with shadows ==================== + +void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { + if (useGpuSkinning) { + drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera); + } else { + drawCpuWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera); + } +} + +void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end() || !texture) return; + + static const std::string shadowShader = "default_shadow"; + + renderer.shaderManager.PushShader(shadowShader); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1i("uShadowMap", 1); + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + renderer.RenderUniform3fv("uLightDir", lightDirCamera.data()); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shadowMapTex); + glActiveTexture(GL_TEXTURE0); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + auto& anim = it->second; + anim.modelMutable.AssignFrom(anim.model.mesh); + anim.modelMutable.RefreshVBO(); + glBindTexture(GL_TEXTURE_2D, texture->getTexID()); + renderer.DrawVertexRenderStruct(anim.modelMutable); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + +void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end() || !texture) return; + + auto& anim = it->second; + prepareGpuSkinningVBOs(anim); + if (anim.skinningMatrices.empty()) return; + + static const std::string skinningShadowShader = "skinning_shadow"; + static const std::string boneMatricesUniform = "uBoneMatrices[0]"; + + renderer.shaderManager.PushShader(skinningShadowShader); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1i("uShadowMap", 1); + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + renderer.RenderUniform3fv("uLightDir", lightDirCamera.data()); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shadowMapTex); + glActiveTexture(GL_TEXTURE0); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, + static_cast(anim.skinningMatrices.size()), false, + anim.skinningMatrices[0].data()); + + glBindTexture(GL_TEXTURE_2D, texture->getTexID()); + +#ifndef EMSCRIPTEN +#ifndef __ANDROID__ + if (anim.bindPoseMutable.vao) { + glBindVertexArray(anim.bindPoseMutable.vao->getBuffer()); + renderer.shaderManager.EnableVertexAttribArrays(); + } +#endif +#endif + + glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.positionVBO->getBuffer()); + renderer.VertexAttribPointer3fv("vPosition", 0, NULL); + + if (anim.bindPoseMutable.texCoordVBO) { + glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.texCoordVBO->getBuffer()); + renderer.VertexAttribPointer2fv("vTexCoord", 0, NULL); + } + + // Bind normals for diffuse lighting (skinning_shadow shader skins them) + if (anim.bindPoseMutable.normalVBO) { + glBindBuffer(GL_ARRAY_BUFFER, anim.bindPoseMutable.normalVBO->getBuffer()); + renderer.VertexAttribPointer3fv("vNormal", 0, NULL); + } + + glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices0VBO->getBuffer()); + renderer.VertexAttribPointer4fv("aBoneIndices0", 0, NULL); + glBindBuffer(GL_ARRAY_BUFFER, anim.boneIndices1VBO->getBuffer()); + renderer.VertexAttribPointer2fv("aBoneIndices1", 0, NULL); + glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights0VBO->getBuffer()); + renderer.VertexAttribPointer4fv("aBoneWeights0", 0, NULL); + glBindBuffer(GL_ARRAY_BUFFER, anim.boneWeights1VBO->getBuffer()); + renderer.VertexAttribPointer2fv("aBoneWeights1", 0, NULL); + + glDrawArrays(GL_TRIANGLES, 0, static_cast(anim.bindPoseMutable.data.PositionData.size())); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + } // namespace ZL diff --git a/src/Character.h b/src/Character.h index 6c800e2..416c1be 100644 --- a/src/Character.h +++ b/src/Character.h @@ -34,6 +34,8 @@ public: void setTarget(const Eigen::Vector3f& target, std::function onArrived = nullptr); void draw(Renderer& renderer); + void drawShadowDepth(Renderer& renderer); + void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); // Public: read by Game for camera tracking and ray-cast origin Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f); @@ -97,6 +99,12 @@ private: void prepareGpuSkinningVBOs(AnimationData& anim); // GPU skinning: draw using shader-based skinning void drawGpuSkinning(Renderer& renderer); + // Shadow: draw into depth map (no texture, no projection push) + void drawShadowDepthGpuSkinning(Renderer& renderer); + void drawShadowDepthCpu(Renderer& renderer); + // Shadow: draw with shadow-aware shaders + void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); + void drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); }; } // namespace ZL diff --git a/src/Game.cpp b/src/Game.cpp index d29e4af..e2e602d 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -250,7 +250,25 @@ namespace ZL npcs.push_back(std::move(npc02)); std::cout << "Load resurces step 12" << std::endl; - + + // Shadow mapping shaders +#ifdef EMSCRIPTEN + renderer.shaderManager.AddShaderFromFiles("shadow_depth", "resources/shaders/shadow_depth.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("shadow_depth_skinning", "resources/shaders/shadow_depth_skinning.vertex", "resources/shaders/shadow_depth_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("default_shadow", "resources/shaders/default_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_web.fragment", CONST_ZIP_FILE); +#else + renderer.shaderManager.AddShaderFromFiles("shadow_depth", "resources/shaders/shadow_depth.vertex", "resources/shaders/shadow_depth_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("shadow_depth_skinning", "resources/shaders/shadow_depth_skinning.vertex", "resources/shaders/shadow_depth_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("default_shadow", "resources/shaders/default_shadow.vertex", "resources/shaders/default_shadow_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_desktop.fragment", CONST_ZIP_FILE); +#endif + + // Create shadow map (2048x2048, ortho size 40, near 0.1, far 100) + shadowMap = std::make_unique(2048, 40.0f, 0.1f, 100.0f); + shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f)); + std::cout << "Shadow map initialized" << std::endl; + loadingCompleted = true; dialogueSystem.init(renderer, CONST_ZIP_FILE); @@ -455,6 +473,161 @@ namespace ZL } + void Game::drawShadowDepthPass() + { + if (!shadowMap) return; + + const Eigen::Vector3f& sceneCenter = player ? player->position : Eigen::Vector3f::Zero(); + shadowMap->updateLightSpaceMatrix(sceneCenter); + + shadowMap->bind(); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + // Use front-face culling during depth pass to reduce shadow acne on lit faces + //glCullFace(GL_FRONT); + glEnable(GL_CULL_FACE); + + renderer.shaderManager.PushShader("shadow_depth"); + + // Set up light's orthographic projection + const Eigen::Matrix4f& lightProj = shadowMap->getLightProjectionMatrix(); + const Eigen::Matrix4f& lightView = shadowMap->getLightViewMatrix(); + + // Push the light's projection matrix via the 6-param ortho overload won't + // match our pre-computed matrix. Instead, use the raw stack approach: + // push a dummy projection then overwrite via PushSpecialMatrix-style. + // Simpler: push ortho then push the light view as modelview. + renderer.PushProjectionMatrix( + -40.0f, 40.0f, + -40.0f, 40.0f, + 0.1f, 100.0f); + + const Eigen::Vector3f& lightDir = shadowMap->getLightDirection(); + Eigen::Vector3f lightPos = sceneCenter - lightDir * 50.0f; + Eigen::Vector3f up(0.0f, 1.0f, 0.0f); + if (std::abs(lightDir.dot(up)) > 0.99f) { + up = Eigen::Vector3f(0.0f, 0.0f, 1.0f); + } + + // Build the light view matrix and push it + renderer.PushSpecialMatrix(lightView); + + // Draw static geometry + renderer.DrawVertexRenderStruct(roomMesh); + + for (auto& [name, gameObj] : gameObjects) { + renderer.DrawVertexRenderStruct(gameObj.mesh); + } + + for (auto& intObj : interactiveObjects) { + if (intObj.isActive && intObj.texture) { + renderer.PushMatrix(); + renderer.TranslateMatrix(intObj.position); + renderer.DrawVertexRenderStruct(intObj.mesh); + renderer.PopMatrix(); + } + } + + // Draw characters (they handle their own skinning shader switch internally) + if (player) player->drawShadowDepth(renderer); + for (auto& npc : npcs) npc->drawShadowDepth(renderer); + + renderer.PopMatrix(); // light view + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + //glCullFace(GL_BACK); + glDisable(GL_CULL_FACE); + + shadowMap->unbind(); + } + + void Game::drawGameWithShadows() + { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + +#ifdef DEBUG_LIGHT + // Debug mode: render from the light's point of view using the plain + // textured shader so we can see what the shadow map "sees". + renderer.shaderManager.PushShader(defaultShaderName); + renderer.RenderUniform1i(textureUniformName, 0); + + renderer.PushProjectionMatrix( + -40.0f, 40.0f, + -40.0f, 40.0f, + 0.1f, 1000.0f); + + renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix()); + +#else + static const std::string shadowShaderName = "default_shadow"; + + renderer.shaderManager.PushShader(shadowShaderName); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1i("uShadowMap", 1); + + // Bind shadow map texture to unit 1 + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture()); + glActiveTexture(GL_TEXTURE0); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + renderer.PushMatrix(); + + renderer.LoadIdentity(); + renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); + + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); + const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero(); + renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); + + // Capture the camera view matrix and compute uLightFromCamera + cameraViewMatrix = renderer.GetCurrentModelViewMatrix(); + Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse(); + Eigen::Matrix4f lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse; + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + + // Light direction in camera space for diffuse lighting + Eigen::Vector3f lightDirCamera = cameraViewMatrix.block<3,3>(0,0) * shadowMap->getLightDirection(); + renderer.RenderUniform3fv("uLightDir", lightDirCamera.data()); +#endif + + glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID()); + renderer.DrawVertexRenderStruct(roomMesh); + + for (auto& [name, gameObj] : gameObjects) { + glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID()); + renderer.DrawVertexRenderStruct(gameObj.mesh); + } + + for (auto& intObj : interactiveObjects) { + if (intObj.isActive) { + intObj.draw(renderer); + } + } + +#ifdef DEBUG_LIGHT + // In debug-light mode characters use the plain shaders (draw normally + // but from the light's viewpoint — projection/view already on stack). + if (player) player->draw(renderer); + for (auto& npc : npcs) npc->draw(renderer); +#else + // Characters use their own shadow-aware shaders + if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera); + for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera); +#endif + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + } + void Game::drawScene() { glViewport(0, 0, Environment::width, Environment::height); if (!loadingCompleted) { @@ -462,7 +635,12 @@ namespace ZL } else { - drawGame(); + if (shadowMap) { + drawShadowDepthPass(); + drawGameWithShadows(); + } else { + drawGame(); + } drawUI(); } CheckGlError(); diff --git a/src/Game.h b/src/Game.h index 81535ed..45f4753 100644 --- a/src/Game.h +++ b/src/Game.h @@ -22,6 +22,7 @@ #include "ScriptEngine.h" #include #include "dialogue/DialogueSystem.h" +#include "render/ShadowMap.h" #include namespace ZL { @@ -98,6 +99,8 @@ namespace ZL { void drawUI(); void drawGame(); void drawLoading(); + void drawShadowDepthPass(); + void drawGameWithShadows(); void handleDown(int64_t fingerId, int mx, int my); void handleUp(int64_t fingerId, int mx, int my); void handleMotion(int64_t fingerId, int mx, int my); @@ -121,6 +124,9 @@ namespace ZL { //MenuManager menuManager; Dialogue::DialogueSystem dialogueSystem; //ScriptEngine scriptEngine; + + std::unique_ptr shadowMap; + Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity(); }; diff --git a/src/render/ShadowMap.cpp b/src/render/ShadowMap.cpp new file mode 100644 index 0000000..f689ed5 --- /dev/null +++ b/src/render/ShadowMap.cpp @@ -0,0 +1,124 @@ +#include "ShadowMap.h" +#include "Environment.h" +#include +#include + +namespace ZL { + + // Build a look-at view matrix (column-major, same convention as the engine). + static Eigen::Matrix4f lookAt(const Eigen::Vector3f& eye, + const Eigen::Vector3f& center, + const Eigen::Vector3f& up) { + Eigen::Vector3f f = (center - eye).normalized(); + Eigen::Vector3f s = f.cross(up).normalized(); + Eigen::Vector3f u = s.cross(f); + + Eigen::Matrix4f m = Eigen::Matrix4f::Identity(); + m(0, 0) = s.x(); m(0, 1) = s.y(); m(0, 2) = s.z(); + m(1, 0) = u.x(); m(1, 1) = u.y(); m(1, 2) = u.z(); + m(2, 0) = -f.x(); m(2, 1) = -f.y(); m(2, 2) = -f.z(); + + m(0, 3) = -s.dot(eye); + m(1, 3) = -u.dot(eye); + m(2, 3) = f.dot(eye); + return m; + } + + // Build an orthographic projection matrix that matches the engine's + // MakeOrthoMatrix(xmin, xmax, ymin, ymax, zNear, zFar) exactly. + static Eigen::Matrix4f ortho(float left, float right, + float bottom, float top, + float zNear, float zFar) { + float width = right - left; + float height = top - bottom; + float depthRange = zFar - zNear; + + Eigen::Matrix4f m = Eigen::Matrix4f::Zero(); + m(0, 0) = 2.0f / width; + m(1, 1) = 2.0f / height; + m(2, 2) = -1.0f / depthRange; + m(0, 3) = -(right + left) / width; + m(1, 3) = -(top + bottom) / height; + m(2, 3) = zNear / depthRange; + m(3, 3) = 1.0f; + return m; + } + + ShadowMap::ShadowMap(int res, float orthoSz, float zNear, float zFar) + : resolution(res) + , orthoSize(orthoSz) + , nearPlane(zNear) + , farPlane(zFar) + , lightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f).normalized()) + , lightViewMatrix(Eigen::Matrix4f::Identity()) + , lightProjectionMatrix(Eigen::Matrix4f::Identity()) + , lightSpaceMatrix(Eigen::Matrix4f::Identity()) + { + glGenFramebuffers(1, &fbo); + + glGenTextures(1, &depthTexture); + glBindTexture(GL_TEXTURE_2D, depthTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, + resolution, resolution, 0, + GL_DEPTH_COMPONENT, GL_FLOAT, NULL); + 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_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, depthTexture, 0); + + // No color buffer for this FBO — depth only. + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + std::cerr << "Error: Shadow map framebuffer is not complete!" << std::endl; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + ShadowMap::~ShadowMap() { + if (fbo) glDeleteFramebuffers(1, &fbo); + if (depthTexture) glDeleteTextures(1, &depthTexture); + } + + void ShadowMap::setLightDirection(const Eigen::Vector3f& dir) { + lightDirection = dir.normalized(); + } + + void ShadowMap::updateLightSpaceMatrix(const Eigen::Vector3f& sceneCenter) { + // Place the light "camera" looking at the scene center from the light direction. + Eigen::Vector3f lightPos = sceneCenter - lightDirection * (farPlane * 0.5f); + + // Choose an up vector that isn't parallel to the light direction. + Eigen::Vector3f up(0.0f, 1.0f, 0.0f); + if (std::abs(lightDirection.dot(up)) > 0.99f) { + up = Eigen::Vector3f(0.0f, 0.0f, 1.0f); + } + + lightViewMatrix = lookAt(lightPos, sceneCenter, up); + + lightProjectionMatrix = ortho( + -orthoSize, orthoSize, + -orthoSize, orthoSize, + nearPlane, farPlane); + + lightSpaceMatrix = lightProjectionMatrix * lightViewMatrix; + } + + void ShadowMap::bind() { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glViewport(0, 0, resolution, resolution); + glClear(GL_DEPTH_BUFFER_BIT); + } + + void ShadowMap::unbind() { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, Environment::width, Environment::height); + } + +} // namespace ZL diff --git a/src/render/ShadowMap.h b/src/render/ShadowMap.h new file mode 100644 index 0000000..11b6d67 --- /dev/null +++ b/src/render/ShadowMap.h @@ -0,0 +1,44 @@ +#pragma once +#include "render/OpenGlExtensions.h" +#include + +namespace ZL { + + class ShadowMap { + private: + GLuint fbo = 0; + GLuint depthTexture = 0; + int resolution; + + Eigen::Vector3f lightDirection; + Eigen::Matrix4f lightViewMatrix; + Eigen::Matrix4f lightProjectionMatrix; + Eigen::Matrix4f lightSpaceMatrix; + + float orthoSize; + float nearPlane; + float farPlane; + + public: + ShadowMap(int resolution = 2048, float orthoSize = 40.0f, + float zNear = 0.1f, float zFar = 100.0f); + ~ShadowMap(); + + ShadowMap(const ShadowMap&) = delete; + ShadowMap& operator=(const ShadowMap&) = delete; + + void setLightDirection(const Eigen::Vector3f& dir); + void updateLightSpaceMatrix(const Eigen::Vector3f& sceneCenter); + + void bind(); + void unbind(); + + GLuint getDepthTexture() const { return depthTexture; } + int getResolution() const { return resolution; } + const Eigen::Matrix4f& getLightSpaceMatrix() const { return lightSpaceMatrix; } + const Eigen::Matrix4f& getLightViewMatrix() const { return lightViewMatrix; } + const Eigen::Matrix4f& getLightProjectionMatrix() const { return lightProjectionMatrix; } + const Eigen::Vector3f& getLightDirection() const { return lightDirection; } + }; + +} // namespace ZL