Skip to content

Commit

Permalink
Cascaded Shadow Maps
Browse files Browse the repository at this point in the history
  • Loading branch information
fszewczyk committed Nov 10, 2024
1 parent 50357ac commit 4215c98
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 84 deletions.
67 changes: 46 additions & 21 deletions resources/shaders/fragment/uber.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,43 @@ float calculateShadowFactor(int directionalLightIndex, LodBlend lodBlend) {
}

// Define PCF kernel size and bias for shadow comparison
float bias = 0.001;
float shadowLowLod = 0.0;
float shadowHighLod = 0.0;
int pcfKernelSize =
2; // You can adjust this value for more or less smoothing
float texelSize =
1.0 / 2048.0; // Assume a shadow map resolution of 2048x2048
float bias = 0.01;
int pcfKernelSize = 1;

// Set texel sizes for low and high LODs
float texelSizeLowLod =
1.0 / 1024.0; // Low LOD texel size (assumed 1024x1024)
float texelSizeHighLod =
1.0 * texelSizeLowLod; // High LOD texel size, twice as large

// Loop over the kernel to sample neighboring depths for the low LOD
// Calculate shadow factor for low LOD
float shadowLowLod = 0.0;
for (int x = -pcfKernelSize; x <= pcfKernelSize; ++x) {
for (int y = -pcfKernelSize; y <= pcfKernelSize; ++y) {
// Sample the shadow map for the low LOD
// Sample shadow map for low LOD
float closestDepthLowLod =
texture(directionalLights[directionalLightIndex]
.shadowSampler[lodBlend.lowLod],
projCoordsLowLod.xy + vec2(x, y) * texelSize)
projCoordsLowLod.xy + vec2(x, y) * texelSizeLowLod)
.r;
closestDepthLowLod =
closestDepthLowLod * 0.5 + 0.5; // Remap to [0, 1] range
if (projCoordsLowLod.z - bias > closestDepthLowLod) {
shadowLowLod += 1.0;
}
}
}
shadowLowLod /= float((pcfKernelSize * 2 + 1) * (pcfKernelSize * 2 + 1));

// Sample the shadow map for the high LOD
// Calculate shadow factor for high LOD
float shadowHighLod = 0.0;
for (int x = -pcfKernelSize; x <= pcfKernelSize; ++x) {
for (int y = -pcfKernelSize; y <= pcfKernelSize; ++y) {
// Sample shadow map for high LOD
float closestDepthHighLod =
texture(directionalLights[directionalLightIndex]
.shadowSampler[lodBlend.highLod],
projCoordsHighLod.xy + vec2(2 * x, 2 * y) * texelSize)
projCoordsHighLod.xy + vec2(x, y) * texelSizeHighLod)
.r;
closestDepthHighLod =
closestDepthHighLod * 0.5 + 0.5; // Remap to [0, 1] range
Expand All @@ -132,38 +141,54 @@ float calculateShadowFactor(int directionalLightIndex, LodBlend lodBlend) {
}
}
}

// Normalize shadow values by the kernel size
shadowLowLod /= float((pcfKernelSize * 2 + 1) * (pcfKernelSize * 2 + 1));
shadowHighLod /= float((pcfKernelSize * 2 + 1) * (pcfKernelSize * 2 + 1));

// Blend the shadow factors between the low and high LOD based on highLodWeight
return mix(1.0 - shadowLowLod, 1.0 - shadowHighLod, lodBlend.highLodWeight);
}

LodBlend chooseLightLOD(mat4 lightSpaceMatrix[4], vec3 fragPos) {
// Transform fragment position to light space
const float blendRange = 0.2; // Range near cascade far edge to blend

for (int lod = 0; lod < 4; ++lod) {
vec4 fragPosLightSpace = lightSpaceMatrix[lod] * vec4(fragPos, 1.0);
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;

// Check if fragment is inside current LOD's view
if (projCoords.x >= 0.0 && projCoords.y >= 0.0 && projCoords.x <= 1.0 &&
projCoords.y <= 1.0 && projCoords.z < 1.0) {

// If we're at the last LOD, no blending with a higher LOD
if (lod == 3) {
return LodBlend(lod, lod, 1.0);
} else {
float distanceToEdgeX = min(projCoords.x, 1.0 - projCoords.x);
float distanceToEdgeY = min(projCoords.y, 1.0 - projCoords.y);
}

// Transform to next LOD’s light space to calculate depth blending
vec4 fragPosLightSpaceNext =
lightSpaceMatrix[lod + 1] * vec4(fragPos, 1.0);
vec3 projCoordsNext = fragPosLightSpaceNext.xyz / fragPosLightSpaceNext.w;
projCoordsNext = projCoordsNext * 0.5 + 0.5;

float distanceToEdge = min(distanceToEdgeX, distanceToEdgeY);
// Verify that fragment is also within next LOD's frustum
if (projCoordsNext.x >= 0.0 && projCoordsNext.y >= 0.0 &&
projCoordsNext.x <= 1.0 && projCoordsNext.y <= 1.0 &&
projCoordsNext.z < 1.0) {

// Compute blending weight based on fragment's depth in current LOD
float depthInCurrentLOD = projCoords.z;
float highLodWeight =
smoothstep(1.0 - blendRange, 1.0, depthInCurrentLOD);

float highLodWeight = distanceToEdge / 0.5;
return LodBlend(lod, lod + 1, highLodWeight);
}

// If fragment not inside next LOD, return current LOD without blending
return LodBlend(lod, lod, 1.0);
}
}

// Default return if fragment is outside all cascades
return LodBlend(-1, -1, 0.0);
}

Expand Down
8 changes: 3 additions & 5 deletions resources/shaders/vertex/shadowmap.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

layout(location = 0) in vec3 aPos; // Vertex position

uniform mat4 modelMatrix; // Model matrix
uniform mat4 viewMatrix; // View matrix
uniform mat4 projectionMatrix; // Projection matrix
uniform mat4 lightSpaceMatrix; // Model matrix
uniform mat4 modelMatrix; // Projection matrix

out float FragDepth; // Output the depth value to the fragment shader

void main() {
// Transform vertex position to world space, then to light space
vec4 lightSpacePosition =
projectionMatrix * viewMatrix * modelMatrix * vec4(aPos, 1.0);
vec4 lightSpacePosition = lightSpaceMatrix * modelMatrix * vec4(aPos, 1.0);

// Set the final position
gl_Position = lightSpacePosition;
Expand Down
39 changes: 30 additions & 9 deletions src/Components/CameraComponent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CameraComponent {
Orthographic
};

CameraComponent(float fov = 45.0f, float aspectRatio = 16.0f / 9.0f, float nearPlane = 0.1f, float farPlane = 10000.0f, ProjectionType projectionType = ProjectionType::Perspective)
CameraComponent(float fov = 45.0f, float aspectRatio = 16.0f / 9.0f, float nearPlane = 0.01f, float farPlane = 2000.0f, ProjectionType projectionType = ProjectionType::Perspective)
: fov(fov), aspectRatio(aspectRatio), nearPlane(nearPlane), farPlane(farPlane), projectionType(projectionType) {}

float fov;
Expand All @@ -40,14 +40,7 @@ class CameraComponent {
}

glm::mat4 getProjectionMatrix() const {
if (projectionType == ProjectionType::Perspective) {
return glm::perspective(glm::radians(fov), aspectRatio, nearPlane, farPlane);
} else {
float orthoSize = 10.0f; // Can be configurable
float halfWidth = orthoSize * aspectRatio * 0.5f;
float halfHeight = orthoSize * 0.5f;
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, nearPlane, farPlane);
}
return getProjectionMatrix(nearPlane, farPlane);
}

Ray getRayAt(const TransformComponent& transformComponent, float x, float y) const {
Expand All @@ -74,6 +67,34 @@ class CameraComponent {
return { rayOrigin, rayDirection };
}

std::vector<glm::vec3> getFrustumCornersWorldSpace(float localNearPlane, float localFarPlane, const TransformComponent& transformComponent) const {
glm::mat4 invViewProj = glm::inverse(getProjectionMatrix(localNearPlane, localFarPlane) * getViewMatrix(transformComponent));

std::vector<glm::vec3> frustumCorners;
for (int x = -1; x <= 1; x += 2) {
for (int y = -1; y <= 1; y += 2) {
for (int z = -1; z <= 1; z += 2) {
glm::vec4 corner = invViewProj * glm::vec4(x, y, z, 1.0f);
corner /= corner.w;
frustumCorners.push_back(glm::vec3(corner));
}
}
}
return frustumCorners;
}

private:
glm::mat4 getProjectionMatrix(float localNearPlane, float localFarPlane) const {
if (projectionType == ProjectionType::Perspective) {
return glm::perspective(glm::radians(fov), aspectRatio, localNearPlane, localFarPlane);
} else {
float orthoSize = 10.0f; // Can be configurable
float halfWidth = orthoSize * aspectRatio * 0.5f;
float halfHeight = orthoSize * 0.5f;
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, localNearPlane, localFarPlane);
}
}

};

} // namespace shkyera
47 changes: 26 additions & 21 deletions src/Components/DirectionalLightComponent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <Components/BaseComponent.hpp>
#include <Components/TransformComponent.hpp>
#include <Components/CameraComponent.hpp>

namespace shkyera {

Expand All @@ -15,33 +16,37 @@ class DirectionalLightComponent : public BaseComponent<DirectionalLightComponent
float intensity = 0.5;
glm::vec3 color = {1.0f, 1.0f, 1.0f};

glm::mat4 getViewMatrix(const TransformComponent& lightTransformComponent, const TransformComponent& cameraTransformComponent) const {
glm::vec3 orientation = lightTransformComponent.getOrientation();

glm::vec3 front;
front.x = std::cos(orientation.y) * std::cos(orientation.x);
front.y = std::sin(orientation.x);
front.z = std::sin(orientation.y) * std::cos(orientation.x);
front = glm::normalize(front);
inline static uint8_t LevelsOfDetail = 4;
inline static std::vector<float> CascadePlanes = {0.01, 2.0, 5.0, 10.0, 100.0};

glm::mat4 getLightSpaceMatrix(
const TransformComponent& lightTransformComponent,
const TransformComponent& cameraTransformComponent,
const CameraComponent& cameraComponent,
uint8_t levelOfDetail) const
{
auto frustumCorners = cameraComponent.getFrustumCornersWorldSpace(CascadePlanes[levelOfDetail], CascadePlanes[levelOfDetail + 1], cameraTransformComponent);

glm::vec3 position = cameraTransformComponent.getPosition() - 100.0f * front;
glm::vec3 front = getDirection(lightTransformComponent);
glm::vec3 center = glm::vec3(0.0f);

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 right = glm::normalize(glm::cross(front, up));
up = -glm::normalize(glm::cross(right, front));
for (const auto& corner : frustumCorners)
center += corner;
center /= frustumCorners.size();

return glm::lookAt(position, position + front, up);
}
glm::mat4 lightView = glm::lookAt(center + front * 50.0f, center, glm::vec3(0.0f, 1.0f, 0.0f));

glm::mat4 getProjectionMatrix(uint8_t levelOfDetail) const {
constexpr float NearPlane = 0.1f;
constexpr float FarPlane = 1000.0f;
constexpr float DefaultHalfSize = 10.0f;
constexpr float levelOfDetailExpansion = 2.0f;
glm::vec3 min = glm::vec3(std::numeric_limits<float>::max());
glm::vec3 max = glm::vec3(std::numeric_limits<float>::lowest());

float halfSize = powf(levelOfDetailExpansion, levelOfDetail) * DefaultHalfSize;
for (const auto& corner : frustumCorners) {
glm::vec3 cornerInLightSpace = glm::vec3(lightView * glm::vec4(corner, 1.0f));
min = glm::min(min, cornerInLightSpace);
max = glm::max(max, cornerInLightSpace);
}

return glm::ortho(-halfSize, halfSize, -halfSize, halfSize, NearPlane, FarPlane);
glm::mat4 lightProjection = glm::ortho(min.x, max.x, min.y, max.y, 0.1f, 100.0f);
return lightProjection * lightView;
}

static glm::vec3 getDirection(const TransformComponent& lightTransformComponent)
Expand Down
10 changes: 3 additions & 7 deletions src/Rendering/FrameBuffers/DepthFrameBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ void DepthFrameBuffer::setSize(uint32_t width, uint32_t height) {
_width = width;
_height = height;

_textureDepthBuffer.setData(GL_R16F, _width, _height, GL_RED, GL_FLOAT);
_textureDepthBuffer.setData(GL_R32F, _width, _height, GL_RED, GL_FLOAT);

glBindRenderbuffer(GL_RENDERBUFFER, _rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, _width, _height);
Expand All @@ -94,13 +94,9 @@ void DepthFrameBuffer::setupFramebuffer() {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, _rbo);

glBindTexture(GL_TEXTURE_2D, _textureDepthBuffer.getID());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

GLfloat borderColor[] = { 0.0f, 0.0f, 0.0f, 0.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

// Verify the framebuffer completeness
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cout << "ERROR::FRAMEBUFFER:: Depth Frame Buffer is not complete!" << std::endl;
}
Expand Down
22 changes: 12 additions & 10 deletions src/Systems/RenderingSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <Components/WireframeComponent.hpp>
#include <Components/CameraComponent.hpp>
#include <Components/SkyboxComponent.hpp>
#include <Components/NameComponent.hpp>
#include <Components/AmbientLightComponent.hpp>
#include <Components/PointLightComponent.hpp>
#include <Components/DirectionalLightComponent.hpp>
Expand Down Expand Up @@ -155,14 +156,15 @@ void RenderingSystem::renderModels()
glEnable(GL_DEPTH_TEST);

const auto& cameraTransform = _registry->getComponent<TransformComponent>(_registry->getCamera());
const auto& cameraComponent = _registry->getComponent<CameraComponent>(_registry->getCamera());

// ********* Rendering the shadow maps *********
std::unordered_set<Entity> directionalLightEntities;
for(const auto& [entity, directionalLightComponent] : _registry->getComponentSet<DirectionalLightComponent>()) {
directionalLightEntities.insert(entity);
if(_directionalLightToShadowMaps.find(entity) == _directionalLightToShadowMaps.end())
{
_directionalLightToShadowMaps.emplace(entity, std::array<DepthFrameBuffer, DirectionalLightLOD>{});
_directionalLightToShadowMaps.emplace(entity, std::vector<DepthFrameBuffer>(DirectionalLightComponent::LevelsOfDetail));
}
}

Expand All @@ -183,20 +185,19 @@ void RenderingSystem::renderModels()
for(auto& [lightEntity, lodBuffers] : _directionalLightToShadowMaps)
{
uint8_t levelOfDetail = 0;
lodBuffers.resize(DirectionalLightComponent::LevelsOfDetail);
for(auto& buffer : lodBuffers)
{
buffer.bind();
buffer.setSize(2048, 2048);
buffer.setSize(1600, 1600);
buffer.clear();
_shadowMapShaderProgram.use();

const auto& directionalLightComponent = _registry->getComponent<DirectionalLightComponent>(lightEntity);
const auto& lightTransform = _registry->getComponent<TransformComponent>(lightEntity);
const glm::mat4& viewMatrix = directionalLightComponent.getViewMatrix(lightTransform, cameraTransform);
const glm::mat4& projectionMatrix = directionalLightComponent.getProjectionMatrix(levelOfDetail++);
const glm::mat4& lightSpaceMatrix = directionalLightComponent.getLightSpaceMatrix(lightTransform, cameraTransform, cameraComponent, levelOfDetail);

_shadowMapShaderProgram.setUniform("viewMatrix", viewMatrix);
_shadowMapShaderProgram.setUniform("projectionMatrix", projectionMatrix);
_shadowMapShaderProgram.setUniform("lightSpaceMatrix", lightSpaceMatrix);
for (const auto& [modelEntity, modelComponent] : _registry->getComponentSet<ModelComponent>()) {
const auto& transformComponent = _registry->getComponent<TransformComponent>(modelEntity);
_shadowMapShaderProgram.setUniform("modelMatrix", transformComponent.getTransformMatrix());
Expand All @@ -206,6 +207,7 @@ void RenderingSystem::renderModels()

_shadowMapShaderProgram.stopUsing();
buffer.unbind();
levelOfDetail++;
}
}

Expand Down Expand Up @@ -240,23 +242,23 @@ void RenderingSystem::renderModels()
int directionalLightIndex = 0;
int textureIndex = 0;
for (const auto& [entity, directionalLightComponent] : _registry->getComponentSet<DirectionalLightComponent>()) {
const auto& transformComponent = _registry->getComponent<TransformComponent>(entity);
const auto& orientation = transformComponent.getOrientation();
const auto& lightTransform = _registry->getComponent<TransformComponent>(entity);
const auto& orientation = lightTransform.getOrientation();

const auto& depthFrameBuffers = _directionalLightToShadowMaps.at(entity);

for(size_t levelOfDetail = 0; levelOfDetail < depthFrameBuffers.size(); levelOfDetail++)
{
depthFrameBuffers[levelOfDetail].getTexture().activate(GL_TEXTURE0 + textureIndex);
const glm::mat4 lightSpaceMatrix = directionalLightComponent.getProjectionMatrix(levelOfDetail) * directionalLightComponent.getViewMatrix(transformComponent, cameraTransform);
const glm::mat4 lightSpaceMatrix = directionalLightComponent.getLightSpaceMatrix(lightTransform, cameraTransform, cameraComponent, levelOfDetail);

_modelShaderProgram.setUniform("directionalLights[" + std::to_string(directionalLightIndex) + "].shadowSampler[" + std::to_string(levelOfDetail) + "]", textureIndex);
_modelShaderProgram.setUniform("directionalLights[" + std::to_string(directionalLightIndex) + "].lightSpaceMatrix[" + std::to_string(levelOfDetail) + "]", lightSpaceMatrix);
++textureIndex;
}


glm::vec3 lightDirection = DirectionalLightComponent::getDirection(transformComponent);
glm::vec3 lightDirection = DirectionalLightComponent::getDirection(lightTransform);
_modelShaderProgram.setUniform("directionalLights[" + std::to_string(directionalLightIndex) + "].direction", lightDirection);
_modelShaderProgram.setUniform("directionalLights[" + std::to_string(directionalLightIndex) + "].color", directionalLightComponent.intensity * directionalLightComponent.color);
++directionalLightIndex;
Expand Down
4 changes: 2 additions & 2 deletions src/Systems/RenderingSystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <Common/Types.hpp>
#include <ECS/Registry.hpp>
#include <Components/DirectionalLightComponent.hpp>
#include <Rendering/ShaderProgram.hpp>
#include <Rendering/FrameBuffers/SceneFrameBuffer.hpp>
#include <Rendering/FrameBuffers/DepthFrameBuffer.hpp>
Expand Down Expand Up @@ -47,8 +48,7 @@ class RenderingSystem {
ShaderProgram _skyboxShaderProgram;

// Light rendering
constexpr static size_t DirectionalLightLOD = 4;
std::unordered_map<Entity, std::array<DepthFrameBuffer, DirectionalLightLOD>> _directionalLightToShadowMaps;
std::unordered_map<Entity, std::vector<DepthFrameBuffer>> _directionalLightToShadowMaps;
ShaderProgram _shadowMapShaderProgram;
};

Expand Down
Loading

0 comments on commit 4215c98

Please sign in to comment.