feat: add spot light shadows with a tilled buffer

This commit is contained in:
2026-10-18 12:56:52 +02:00
parent 4bd99b25d1
commit 65a7310b56
19 changed files with 519 additions and 122 deletions
@@ -118,6 +118,10 @@ void LightComponent::ImGuiInspector() {
return;
}
if (m_Type == LightType::Spot) {
ImGui::Checkbox("Cast Shadows", &m_CastShadows);
}
float range = m_Range;
if (ImGui::DragFloat("Range", &range, 0.05f, 0.01f)) {
SetRange(range);
+68
View File
@@ -55,6 +55,74 @@ Frustum edge::createFrustumFromCamera(const Camera& camera) {
return frustum;
}
namespace {
glm::vec4 matrixRow(const glm::mat4& matrix, int row)
{
return {matrix[0][row], matrix[1][row], matrix[2][row], matrix[3][row]};
}
Frustum::Plane planeFromCoefficients(const glm::vec4& coefficients)
{
const glm::vec3 normal{coefficients};
const float length = glm::length(normal);
Frustum::Plane plane;
plane.normal = normal / length;
plane.distance = -coefficients.w / length;
return plane;
}
}
Frustum edge::createFrustumFromViewProj(const glm::mat4& viewProj)
{
const glm::vec4 rowX = matrixRow(viewProj, 0);
const glm::vec4 rowY = matrixRow(viewProj, 1);
const glm::vec4 rowZ = matrixRow(viewProj, 2);
const glm::vec4 rowW = matrixRow(viewProj, 3);
Frustum frustum;
frustum.leftFace = planeFromCoefficients(rowW + rowX);
frustum.rightFace = planeFromCoefficients(rowW - rowX);
frustum.bottomFace = planeFromCoefficients(rowW + rowY);
frustum.topFace = planeFromCoefficients(rowW - rowY);
frustum.nearFace = planeFromCoefficients(rowZ);
frustum.farFace = planeFromCoefficients(rowW - rowZ);
return frustum;
}
std::array<glm::vec3, 8> edge::calculateFrustumCorners(const glm::mat4& viewProj)
{
const glm::mat4 clipToWorld = glm::inverse(viewProj);
std::array<glm::vec3, 8> corners{};
std::size_t cornerIndex = 0;
for (const float x : {-1.0f, 1.0f}) {
for (const float y : {-1.0f, 1.0f}) {
for (const float z : {0.0f, 1.0f}) {
const glm::vec4 corner = clipToWorld * glm::vec4{x, y, z, 1.0f};
corners[cornerIndex++] = glm::vec3{corner} / corner.w;
}
}
}
return corners;
}
bool edge::frustumsOverlap(const Frustum& frustum, const glm::mat4& otherViewProj)
{
const auto otherCorners = calculateFrustumCorners(otherViewProj);
for (int planeIndex = 0; planeIndex < 6; ++planeIndex) {
const Frustum::Plane& plane = frustum.getPlane(planeIndex);
const bool allCornersOutside = std::ranges::all_of(otherCorners, [&plane](const glm::vec3& corner) {
return plane.getSignedDistanceToPlane(corner) < 0.0f;
});
if (allCornersOutside) {
return false;
}
}
return true;
}
bool isOnOrForwardPlane(const Frustum::Plane& plane, const Sphere& sphere)
{
return plane.getSignedDistanceToPlane(sphere.center) > -sphere.radius;
@@ -131,7 +131,7 @@ void LightCullingPipeline::beginDrawing(std::size_t frameIndex)
getCurrentFrameData().lightsBuffer.clear();
}
void LightCullingPipeline::appendLight(const Light& light)
void LightCullingPipeline::appendLight(const Light& light, std::uint32_t shadowIndex)
{
auto& lightsBuffer = getCurrentFrameData().lightsBuffer;
if (lightsBuffer.size >= lightsBuffer.capacity)
@@ -158,7 +158,7 @@ void LightCullingPipeline::appendLight(const Light& light)
.type = static_cast<std::uint32_t>(light.type),
.spotInnerCos = std::cos(light.innerConeAngle),
.spotOuterCos = std::cos(light.outerConeAngle),
.castShadows = light.castShadows ? 1u : 0u,
.shadowIndex = shadowIndex,
.padding = 0.0f,
};
lightsBuffer.append(gpuLight);
@@ -1,5 +1,6 @@
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
@@ -11,11 +12,43 @@
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Caches/MeshCache.h>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Util.h>
#include "volk.h"
#include "spdlog/spdlog.h"
namespace
{
constexpr float MAX_SPOT_SHADOW_FIELD_OF_VIEW = glm::radians(170.0f);
constexpr float SPOT_SHADOW_NEAR_PLANE = 0.05f;
constexpr float NORMAL_OFFSET_IN_TEXELS = 1.5f;
glm::vec3 upVectorFor(const glm::vec3& direction)
{
return std::abs(direction.y) > 0.99f ? glm::vec3{0.0f, 0.0f, 1.0f} : glm::vec3{0.0f, 1.0f, 0.0f};
}
float spotShadowFieldOfView(const Light& spotLight)
{
return std::min(spotLight.outerConeAngle * 2.0f, MAX_SPOT_SHADOW_FIELD_OF_VIEW);
}
VkRect2D spotShadowTileRegion(std::uint32_t shadowIndex)
{
const std::uint32_t column = shadowIndex % ShadowMapPipeline::SPOT_SHADOW_TILES_PER_ROW;
const std::uint32_t row = shadowIndex / ShadowMapPipeline::SPOT_SHADOW_TILES_PER_ROW;
return VkRect2D{
.offset = {
static_cast<std::int32_t>(column * ShadowMapPipeline::SPOT_SHADOW_TILE_SIZE),
static_cast<std::int32_t>(row * ShadowMapPipeline::SPOT_SHADOW_TILE_SIZE),
},
.extent = {ShadowMapPipeline::SPOT_SHADOW_TILE_SIZE, ShadowMapPipeline::SPOT_SHADOW_TILE_SIZE},
};
}
}
void ShadowMapPipeline::init(GfxDevice& gfxDevice, RenderResources& resources)
{
try
@@ -77,14 +110,32 @@ void ShadowMapPipeline::init(GfxDevice& gfxDevice, RenderResources& resources)
pipelineConfig
);
const auto createInfo = vkutil::CreateImageInfo{
.format = shadowMapFormat,
.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.extent = VkExtent3D{SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 1},
};
const auto depthUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
shadowMapId = resources.createImage(gfxDevice, createInfo, "shadow map", nullptr, shadowMapId);
shadowMapId = resources.createImage(
gfxDevice,
{
.format = shadowMapFormat,
.usage = depthUsage,
.extent = VkExtent3D{SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 1},
},
"shadow map",
nullptr,
shadowMapId);
spdlog::info("Created {0}x{0} shadow map with id {1}", SHADOW_MAP_SIZE, shadowMapId);
constexpr std::uint32_t spotShadowAtlasSize = SPOT_SHADOW_TILE_SIZE * SPOT_SHADOW_TILES_PER_ROW;
spotShadowAtlasId = resources.createImage(
gfxDevice,
{
.format = shadowMapFormat,
.usage = depthUsage,
.extent = VkExtent3D{spotShadowAtlasSize, spotShadowAtlasSize, 1},
},
"spot shadow atlas",
nullptr,
spotShadowAtlasId);
spdlog::info("Created {0}x{0} spot shadow atlas with id {1}", spotShadowAtlasSize, spotShadowAtlasId);
}
catch (...)
{
@@ -104,14 +155,13 @@ void ShadowMapPipeline::cleanup(VkDevice device)
m_pipelineLayout = VK_NULL_HANDLE;
shadowMapId = NULL_IMAGE_ID;
spotShadowAtlasId = NULL_IMAGE_ID;
}
glm::mat4 ShadowMapPipeline::calculateLightViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const
glm::mat4 ShadowMapPipeline::calculateDirectionalViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const
{
const glm::vec3 direction = glm::normalize(lightDirection);
const glm::vec3 up = std::abs(direction.y) > 0.99f ? glm::vec3{0.0f, 0.0f, 1.0f} : glm::vec3{0.0f, 1.0f, 0.0f};
const glm::mat4 lightView = glm::lookAt(glm::vec3{0.0f}, direction, up);
const glm::mat4 lightView = glm::lookAt(glm::vec3{0.0f}, direction, upVectorFor(direction));
const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE);
glm::vec3 lightSpaceCenter = glm::vec3(lightView * glm::vec4(center, 1.0f));
@@ -129,18 +179,36 @@ glm::mat4 ShadowMapPipeline::calculateLightViewProj(const glm::vec3& lightDirect
return lightProj * lightView;
}
GPUSpotShadow ShadowMapPipeline::calculateSpotShadow(const Light& spotLight)
{
const glm::vec3 direction = glm::normalize(spotLight.direction);
const float fieldOfView = spotShadowFieldOfView(spotLight);
const float farPlane = std::max(spotLight.range, SPOT_SHADOW_NEAR_PLANE * 2.0f);
const glm::mat4 lightView = glm::lookAt(spotLight.position, spotLight.position + direction, upVectorFor(direction));
const glm::mat4 lightProj = glm::perspectiveRH_ZO(fieldOfView, 1.0f, SPOT_SHADOW_NEAR_PLANE, farPlane);
const float texelSizeAtOneMeter =
2.0f * std::tan(fieldOfView * 0.5f) / static_cast<float>(SPOT_SHADOW_TILE_SIZE);
return GPUSpotShadow{
.lightViewProj = lightProj * lightView,
.normalOffsetPerMeter = texelSizeAtOneMeter * NORMAL_OFFSET_IN_TEXELS,
};
}
float ShadowMapPipeline::getNormalOffset() const
{
const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE);
return texelSize * 1.5f;
return texelSize * NORMAL_OFFSET_IN_TEXELS;
}
void ShadowMapPipeline::draw(
void ShadowMapPipeline::drawDirectionalShadow(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
const std::vector<std::size_t>& sortedDrawCommands,
const glm::mat4& lightViewProj)
{
if (!m_pipeline || shadowMapId == NULL_IMAGE_ID)
{
@@ -149,16 +217,55 @@ void ShadowMapPipeline::draw(
const auto& shadowMap = resources.getImage(shadowMapId);
beginDepthRendering(cmd, resources, shadowMap);
setRenderRegion(cmd, VkRect2D{.offset = {}, .extent = shadowMap.getExtent2D()});
drawShadowCasters(cmd, resources, drawCommands, sortedDrawCommands, lightViewProj);
endDepthRendering(cmd, shadowMap);
}
void ShadowMapPipeline::drawSpotShadows(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const std::vector<glm::mat4>& lightViewProjs)
{
if (!m_pipeline || spotShadowAtlasId == NULL_IMAGE_ID || lightViewProjs.empty())
{
return;
}
assert(lightViewProjs.size() <= MAX_SPOT_SHADOWS);
const auto& spotShadowAtlas = resources.getImage(spotShadowAtlasId);
beginDepthRendering(cmd, resources, spotShadowAtlas);
for (std::uint32_t shadowIndex = 0; shadowIndex < lightViewProjs.size(); ++shadowIndex)
{
vkutil::cmdBeginLabelFormatted(cmd, vkutil::labelColors::shadow, "Spot shadow %u", shadowIndex);
setRenderRegion(cmd, spotShadowTileRegion(shadowIndex));
drawShadowCasters(cmd, resources, drawCommands, sortedDrawCommands, lightViewProjs[shadowIndex]);
vkutil::cmdEndLabel(cmd);
}
endDepthRendering(cmd, spotShadowAtlas);
}
void ShadowMapPipeline::beginDepthRendering(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUImage& depthImage)
{
vkutil::transitionImage(
cmd,
shadowMap.image,
shadowMap.layout,
depthImage.image,
depthImage.layout,
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
shadowMap.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = shadowMap.getExtent2D(),
.depthImageView = shadowMap.imageView,
.renderExtent = depthImage.getExtent2D(),
.depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f,
});
renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment;
@@ -167,31 +274,49 @@ void ShadowMapPipeline::draw(
m_pipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
}
void ShadowMapPipeline::endDepthRendering(VkCommandBuffer cmd, const GPUImage& depthImage)
{
vkCmdEndRendering(cmd);
vkutil::transitionImage(
cmd,
depthImage.image,
depthImage.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
depthImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
void ShadowMapPipeline::setRenderRegion(VkCommandBuffer cmd, const VkRect2D& region)
{
const auto viewport = VkViewport{
.x = 0.f,
.y = 0.f,
.width = static_cast<float>(SHADOW_MAP_SIZE),
.height = static_cast<float>(SHADOW_MAP_SIZE),
.x = static_cast<float>(region.offset.x),
.y = static_cast<float>(region.offset.y),
.width = static_cast<float>(region.extent.width),
.height = static_cast<float>(region.extent.height),
.minDepth = 0.f,
.maxDepth = 1.f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdSetScissor(cmd, 0, 1, &region);
}
const auto scissor = VkRect2D{
.offset = {},
.extent = shadowMap.getExtent2D(),
};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
void ShadowMapPipeline::drawShadowCasters(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const glm::mat4& lightViewProj)
{
const MeshCache& meshCache = resources.meshes();
MeshID prevMeshId = NULL_MESH_ID;
const Frustum lightFrustum = edge::createFrustumFromViewProj(lightViewProj);
MeshID previousMeshId = NULL_MESH_ID;
const auto drawOne = [&](const MeshDrawCommand& dc)
const auto drawCaster = [&](const MeshDrawCommand& dc)
{
if (!dc.castShadow)
if (!dc.castShadow || !edge::isInFrustum(lightFrustum, dc.worldBoundingSphere))
{
return;
}
@@ -208,21 +333,18 @@ void ShadowMapPipeline::draw(
dc.materialId,
dc.skinnedMesh != nullptr ? ", skinned" : "");
if (dc.meshId != prevMeshId)
if (dc.meshId != previousMeshId)
{
prevMeshId = dc.meshId;
previousMeshId = dc.meshId;
vkCmdBindIndexBuffer(cmd, mesh.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
}
const auto pushConstants = PushConstants{
.transform = dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address,
.modelToLightClip = lightViewProj * dc.transformMatrix,
.vertexBuffer =
dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address,
.materialId = dc.materialId,
.padding = 0,
};
vkCmdPushConstants(
@@ -243,23 +365,14 @@ void ShadowMapPipeline::draw(
for (std::size_t drawIndex : sortedDrawCommands)
{
assert(drawIndex < drawCommands.size());
drawOne(drawCommands[drawIndex]);
drawCaster(drawCommands[drawIndex]);
}
}
else
{
for (const MeshDrawCommand& dc : drawCommands)
{
drawOne(dc);
drawCaster(dc);
}
}
vkCmdEndRendering(cmd);
vkutil::transitionImage(
cmd,
shadowMap.image,
shadowMap.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
shadowMap.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
+70 -11
View File
@@ -70,6 +70,8 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice)
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
shadowLightDirection.reset();
submittedLights.clear();
spotShadows.clear();
Renderer2D::GetInstance().Clear();
}
@@ -126,15 +128,26 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Lights Buffer Flush");
vkutil::ScopedCmdLabel label(cmd, "Lights Buffer Flush", vkutil::labelColors::transfer);
uploadLights(sceneData.camera);
lightCullingPipeline->flushCurrentFrame(cmd, gfxDevice);
}
const glm::mat4 lightViewProj = shadowLightDirection
? shadowMapPipeline->calculateLightViewProj(
? shadowMapPipeline->calculateDirectionalViewProj(
*shadowLightDirection,
sceneData.camera.GetPosition())
: glm::mat4{1.0f};
std::array<GPUSpotShadow, ShadowMapPipeline::MAX_SPOT_SHADOWS> gpuSpotShadows{};
std::ranges::copy(spotShadows, gpuSpotShadows.begin());
std::vector<glm::mat4> spotShadowViewProjs;
spotShadowViewProjs.reserve(spotShadows.size());
for (const GPUSpotShadow& spotShadow : spotShadows)
{
spotShadowViewProjs.push_back(spotShadow.lightViewProj);
}
// TODO: there has to be a better way to do this
const auto gpuSceneData = GPUSceneData{
.view = sceneData.camera.GetViewMatrix(),
@@ -156,6 +169,9 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
.shadowNormalOffset = shadowMapPipeline->getNormalOffset(),
.padding = 0,
.lightViewProj = lightViewProj,
.spotShadowAtlasId = static_cast<std::uint32_t>(shadowMapPipeline->getSpotShadowAtlasId()),
.spotShadowTilesPerRow = ShadowMapPipeline::SPOT_SHADOW_TILES_PER_ROW,
.spotShadows = gpuSpotShadows,
};
{
@@ -212,12 +228,25 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Shadow Map");
vkutil::ScopedCmdLabel label(cmd, "Shadow Map", vkutil::labelColors::shadow);
shadowMapPipeline->draw(
shadowMapPipeline->drawDirectionalShadow(
cmd,
*resources,
sceneDataBuffer.getBuffer(),
meshDrawCommands,
sortedMeshDrawCommands);
sortedMeshDrawCommands,
lightViewProj);
}
if (!spotShadowViewProjs.empty())
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Spot Shadows");
vkutil::ScopedCmdLabel label(cmd, "Spot Shadows", vkutil::labelColors::shadow);
shadowMapPipeline->drawSpotShadows(
cmd,
*resources,
meshDrawCommands,
sortedMeshDrawCommands,
spotShadowViewProjs);
}
{
@@ -400,6 +429,8 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
lightCullingPipeline.reset();
shadowMapPipeline.reset();
shadowLightDirection.reset();
submittedLights.clear();
spotShadows.clear();
pendingMaterialUploads.clear();
meshDrawCommands.clear();
sortedMeshDrawCommands.clear();
@@ -444,18 +475,46 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
void GameRenderer::submitLight(const Light& light)
{
Light submittedLight = light;
submittedLights.push_back(light);
}
// there's a single shadow map, the first shadow casting directional light gets it
submittedLight.castShadows =
light.castShadows && light.type == LightType::Directional && !shadowLightDirection;
void GameRenderer::uploadLights(const Camera& camera)
{
const Frustum cameraFrustum = edge::createFrustumFromCamera(camera);
if (submittedLight.castShadows)
for (const Light& light : submittedLights)
{
shadowLightDirection = light.direction;
lightCullingPipeline->appendLight(light, assignShadowIndex(light, cameraFrustum));
}
}
std::uint32_t GameRenderer::assignShadowIndex(const Light& light, const Frustum& cameraFrustum)
{
if (!light.castShadows)
{
return NO_SHADOW;
}
lightCullingPipeline->appendLight(submittedLight);
// there's a single directional shadow map, the first shadow casting directional light gets it
if (light.type == LightType::Directional && !shadowLightDirection)
{
shadowLightDirection = light.direction;
return 0;
}
if (light.type == LightType::Spot && spotShadows.size() < ShadowMapPipeline::MAX_SPOT_SHADOWS)
{
const GPUSpotShadow spotShadow = ShadowMapPipeline::calculateSpotShadow(light);
if (!edge::frustumsOverlap(cameraFrustum, spotShadow.lightViewProj))
{
return NO_SHADOW;
}
spotShadows.push_back(spotShadow);
return static_cast<std::uint32_t>(spotShadows.size() - 1);
}
return NO_SHADOW;
}
const GPUImage& GameRenderer::getDrawImage() const