Added shadow maps

This commit is contained in:
Vladislav Khorev 2026-04-15 19:54:21 +03:00
parent a47306533e
commit d8795ec076
15 changed files with 880 additions and 6 deletions

View File

@ -48,6 +48,8 @@ add_executable(space-game001
# ../src/planet/StoneObject.h # ../src/planet/StoneObject.h
../src/render/FrameBuffer.cpp ../src/render/FrameBuffer.cpp
../src/render/FrameBuffer.h ../src/render/FrameBuffer.h
../src/render/ShadowMap.cpp
../src/render/ShadowMap.h
../src/UiManager.cpp ../src/UiManager.cpp
../src/UiManager.h ../src/UiManager.h
../src/Projectile.h ../src/Projectile.h
@ -109,6 +111,7 @@ target_compile_definitions(space-game001 PRIVATE
WIN32_LEAN_AND_MEAN WIN32_LEAN_AND_MEAN
PNG_ENABLED PNG_ENABLED
SDL_MAIN_HANDLED SDL_MAIN_HANDLED
# DEBUG_LIGHT
# NETWORK # NETWORK
# SIMPLIFIED # SIMPLIFIED
) )

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
attribute vec3 vPosition;
attribute vec2 vTexCoord;
uniform mat4 ProjectionModelViewMatrix;
void main()
{
gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0);
}

View File

@ -0,0 +1,5 @@
void main()
{
// Depth is written automatically by the GPU.
// Nothing to do here.
}

View File

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

View File

@ -0,0 +1,6 @@
precision mediump float;
void main()
{
// Depth is written automatically by the GPU.
}

View File

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

View File

@ -249,7 +249,11 @@ void Character::draw(Renderer& renderer) {
} }
void Character::prepareGpuSkinningVBOs(AnimationData& anim) { void Character::prepareGpuSkinningVBOs(AnimationData& anim) {
if (anim.gpuSkinningPrepared) return; if (anim.gpuSkinningPrepared)
{
return;
}
anim.model.PrepareGpuSkinningData(); anim.model.PrepareGpuSkinningData();
@ -284,13 +288,18 @@ void Character::prepareGpuSkinningVBOs(AnimationData& anim) {
void Character::drawGpuSkinning(Renderer& renderer) { void Character::drawGpuSkinning(Renderer& renderer) {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState();
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end() || !texture) return; if (it == animations.end() || !texture)
{
return;
}
auto& anim = it->second; auto& anim = it->second;
prepareGpuSkinningVBOs(anim); prepareGpuSkinningVBOs(anim);
if (anim.skinningMatrices.empty()) return; if (anim.skinningMatrices.empty())
{
return;
}
static const std::string skinningShaderName = "skinning"; static const std::string skinningShaderName = "skinning";
static const std::string boneMatricesUniform = "uBoneMatrices[0]"; static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@ -354,4 +363,217 @@ void Character::drawGpuSkinning(Renderer& renderer) {
renderer.shaderManager.PopShader(); 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<int>(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<GLsizei>(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<float>(Environment::width) / static_cast<float>(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<float>(Environment::width) / static_cast<float>(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<int>(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<GLsizei>(anim.bindPoseMutable.data.PositionData.size()));
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
} // namespace ZL } // namespace ZL

View File

@ -34,6 +34,8 @@ public:
void setTarget(const Eigen::Vector3f& target, void setTarget(const Eigen::Vector3f& target,
std::function<void()> onArrived = nullptr); std::function<void()> onArrived = nullptr);
void draw(Renderer& renderer); 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 // Public: read by Game for camera tracking and ray-cast origin
Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f); Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f);
@ -97,6 +99,12 @@ private:
void prepareGpuSkinningVBOs(AnimationData& anim); void prepareGpuSkinningVBOs(AnimationData& anim);
// GPU skinning: draw using shader-based skinning // GPU skinning: draw using shader-based skinning
void drawGpuSkinning(Renderer& renderer); 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 } // namespace ZL

View File

@ -250,7 +250,25 @@ namespace ZL
npcs.push_back(std::move(npc02)); npcs.push_back(std::move(npc02));
std::cout << "Load resurces step 12" << std::endl; 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<ShadowMap>(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; loadingCompleted = true;
dialogueSystem.init(renderer, CONST_ZIP_FILE); 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<float>(Environment::width) / static_cast<float>(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() { void Game::drawScene() {
glViewport(0, 0, Environment::width, Environment::height); glViewport(0, 0, Environment::width, Environment::height);
if (!loadingCompleted) { if (!loadingCompleted) {
@ -462,7 +635,12 @@ namespace ZL
} }
else else
{ {
drawGame(); if (shadowMap) {
drawShadowDepthPass();
drawGameWithShadows();
} else {
drawGame();
}
drawUI(); drawUI();
} }
CheckGlError(); CheckGlError();

View File

@ -22,6 +22,7 @@
#include "ScriptEngine.h" #include "ScriptEngine.h"
#include <unordered_map> #include <unordered_map>
#include "dialogue/DialogueSystem.h" #include "dialogue/DialogueSystem.h"
#include "render/ShadowMap.h"
#include <unordered_set> #include <unordered_set>
namespace ZL { namespace ZL {
@ -98,6 +99,8 @@ namespace ZL {
void drawUI(); void drawUI();
void drawGame(); void drawGame();
void drawLoading(); void drawLoading();
void drawShadowDepthPass();
void drawGameWithShadows();
void handleDown(int64_t fingerId, int mx, int my); void handleDown(int64_t fingerId, int mx, int my);
void handleUp(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); void handleMotion(int64_t fingerId, int mx, int my);
@ -121,6 +124,9 @@ namespace ZL {
//MenuManager menuManager; //MenuManager menuManager;
Dialogue::DialogueSystem dialogueSystem; Dialogue::DialogueSystem dialogueSystem;
//ScriptEngine scriptEngine; //ScriptEngine scriptEngine;
std::unique_ptr<ShadowMap> shadowMap;
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
}; };

124
src/render/ShadowMap.cpp Normal file
View File

@ -0,0 +1,124 @@
#include "ShadowMap.h"
#include "Environment.h"
#include <iostream>
#include <cmath>
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

44
src/render/ShadowMap.h Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include "render/OpenGlExtensions.h"
#include <Eigen/Dense>
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