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
+12 -1
View File
@@ -7,6 +7,10 @@
#define TYPE_POINT_LIGHT 1 #define TYPE_POINT_LIGHT 1
#define TYPE_SPOT_LIGHT 2 #define TYPE_SPOT_LIGHT 2
#define NO_SHADOW 0xFFFFFFFFu
// Must match ShadowMapPipeline::MAX_SPOT_SHADOWS
#define MAX_SPOT_SHADOWS 16
// Forward+ tiling, must match LightCullingPipeline::TILE_SIZE / MAX_LIGHTS_PER_TILE // Forward+ tiling, must match LightCullingPipeline::TILE_SIZE / MAX_LIGHTS_PER_TILE
#define LIGHT_TILE_SIZE 16 #define LIGHT_TILE_SIZE 16
#define MAX_LIGHTS_PER_TILE 255 #define MAX_LIGHTS_PER_TILE 255
@@ -27,10 +31,17 @@ struct Light {
float spotInnerCos; float spotInnerCos;
float spotOuterCos; float spotOuterCos;
uint castShadows; // NO_SHADOW, or the light's slot in the shadow data of its type
uint shadowIndex;
float padding; float padding;
}; };
// Must match GPUSpotShadow in Light.h
struct SpotShadow {
mat4 lightViewProj;
float normalOffsetPerMeter;
};
layout (buffer_reference, std430) readonly buffer LightsBuffer { layout (buffer_reference, std430) readonly buffer LightsBuffer {
Light data[]; Light data[];
}; };
+28 -8
View File
@@ -34,6 +34,32 @@ vec3 calculateNormal(MaterialData material, uint samplerID)
return normalize(mat3(t, b, n) * tangentNormal); return normalize(mat3(t, b, n) * tangentNormal);
} }
float calculateShadow(Light light, vec3 fragPos, vec3 geometryNormal)
{
if (light.shadowIndex == NO_SHADOW) {
return 1.0;
}
if (light.type == TYPE_DIRECTIONAL_LIGHT) {
return calculateDirectionalShadow(
fragPos, geometryNormal,
pcs.sceneData.lightViewProj,
pcs.sceneData.shadowMapId,
pcs.sceneData.shadowNormalOffset);
}
if (light.type == TYPE_SPOT_LIGHT) {
return calculateSpotShadow(
fragPos, geometryNormal, light.position,
pcs.sceneData.spotShadows[light.shadowIndex],
light.shadowIndex,
pcs.sceneData.spotShadowAtlasId,
pcs.sceneData.spotShadowTilesPerRow);
}
return 1.0;
}
void main() void main()
{ {
MaterialData material = pcs.sceneData.materials.data[pcs.materialID]; MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
@@ -73,19 +99,13 @@ void main()
uint tileOffset = (tileID.y * pcs.sceneData.tileCountX + tileID.x) * LIGHT_TILE_STRIDE; uint tileOffset = (tileID.y * pcs.sceneData.tileCountX + tileID.x) * LIGHT_TILE_STRIDE;
uint tileLightCount = pcs.sceneData.tileLightIndices.data[tileOffset]; uint tileLightCount = pcs.sceneData.tileLightIndices.data[tileOffset];
vec3 geometryNormal = normalize(inNormal);
vec3 fragColor = vec3(0.0); vec3 fragColor = vec3(0.0);
for (uint i = 0; i < tileLightCount; i++) { for (uint i = 0; i < tileLightCount; i++) {
uint lightIndex = pcs.sceneData.tileLightIndices.data[tileOffset + 1 + i]; uint lightIndex = pcs.sceneData.tileLightIndices.data[tileOffset + 1 + i];
Light light = pcs.sceneData.lights.data[lightIndex]; Light light = pcs.sceneData.lights.data[lightIndex];
float shadow = 1.0; float shadow = calculateShadow(light, fragPos, geometryNormal);
if (light.castShadows != 0) {
shadow = calculateDirectionalShadow(
fragPos, normalize(inNormal),
pcs.sceneData.lightViewProj,
pcs.sceneData.shadowMapId,
pcs.sceneData.shadowNormalOffset);
}
fragColor += calculateLight(light, fragPos, n, v, fragColor += calculateLight(light, fragPos, n, v,
diffuseColor, roughness, metallic, f0, shadow); diffuseColor, roughness, metallic, f0, shadow);
@@ -39,6 +39,11 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
float shadowNormalOffset; float shadowNormalOffset;
uint padding; uint padding;
mat4 lightViewProj; mat4 lightViewProj;
// spot shadows
uint spotShadowAtlasId;
uint spotShadowTilesPerRow;
SpotShadow spotShadows[MAX_SPOT_SHADOWS];
} sceneDataBuffer; } sceneDataBuffer;
#endif // SCENE_DATA_GLSL #endif // SCENE_DATA_GLSL
+10 -2
View File
@@ -1,12 +1,20 @@
#version 460 #version 460
#extension GL_GOOGLE_include_directive : require #extension GL_GOOGLE_include_directive : require
#extension GL_EXT_buffer_reference : require
#extension GL_EXT_scalar_block_layout : require
#include "mesh_pcs.glsl" #include "vertex.glsl"
layout (push_constant, scalar) uniform constants
{
mat4 modelToLightClip;
VertexBuffer vertexBuffer;
} pcs;
void main() void main()
{ {
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex]; Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
gl_Position = pcs.sceneData.lightViewProj * pcs.transform * vec4(v.position, 1.0f); gl_Position = pcs.modelToLightClip * vec4(v.position, 1.0f);
} }
+49 -13
View File
@@ -1,7 +1,30 @@
#ifndef SHADOWS_GLSL #ifndef SHADOWS_GLSL
#define SHADOWS_GLSL #define SHADOWS_GLSL
// expects bindless.glsl to be included before this file (it has no include guard) // expects bindless.glsl and lights.glsl to be included before this file
// Returns how much of a 3x3 area around uv is lit, every tap stays inside [minUV, maxUV]
float sampleShadowPCF(uint shadowMapID, vec2 uv, float depth, vec2 minUV, vec2 maxUV)
{
vec2 texelSize = 1.0 / vec2(textureSize(sampler2DShadow(textures[shadowMapID], samplers[SHADOW_SAMPLER_ID]), 0));
float shadowed = 0.0;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec2 sampleUV = clamp(uv + vec2(x, y) * texelSize, minUV, maxUV);
shadowed += sampleTexture2DShadow(shadowMapID, vec3(sampleUV, depth));
}
}
// the shadow sampler compares with GREATER_OR_EQUAL (it was set up for reversed Z),
// with regular depth that returns 1 when the fragment is behind the occluder
return 1.0 - shadowed / 9.0;
}
bool isOutsideShadowMap(vec2 uv, float depth)
{
return any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))) || depth > 1.0;
}
// Returns 1.0 when fully lit, 0.0 when fully in shadow // Returns 1.0 when fully lit, 0.0 when fully in shadow
float calculateDirectionalShadow(vec3 fragPos, vec3 n, mat4 lightViewProj, uint shadowMapID, float normalOffset) float calculateDirectionalShadow(vec3 fragPos, vec3 n, mat4 lightViewProj, uint shadowMapID, float normalOffset)
@@ -11,24 +34,37 @@ float calculateDirectionalShadow(vec3 fragPos, vec3 n, mat4 lightViewProj, uint
vec3 lightNDC = lightClip.xyz / lightClip.w; vec3 lightNDC = lightClip.xyz / lightClip.w;
vec2 uv = lightNDC.xy * 0.5 + 0.5; vec2 uv = lightNDC.xy * 0.5 + 0.5;
// outside the shadow map counts as lit if (isOutsideShadowMap(uv, lightNDC.z)) {
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))) || lightNDC.z > 1.0) {
return 1.0; return 1.0;
} }
vec2 texelSize = 1.0 / vec2(textureSize(sampler2DShadow(textures[shadowMapID], samplers[SHADOW_SAMPLER_ID]), 0)); return sampleShadowPCF(shadowMapID, uv, lightNDC.z, vec2(0.0), vec2(1.0));
}
// 3x3 PCF, every tap is already bilinearly filtered by the compare sampler float calculateSpotShadow(vec3 fragPos, vec3 n, vec3 lightPos, SpotShadow spotShadow,
float shadowed = 0.0; uint shadowIndex, uint atlasID, uint tilesPerRow)
for (int x = -1; x <= 1; x++) { {
for (int y = -1; y <= 1; y++) { float normalOffset = distance(fragPos, lightPos) * spotShadow.normalOffsetPerMeter;
shadowed += sampleTexture2DShadow(shadowMapID, vec3(uv + vec2(x, y) * texelSize, lightNDC.z)); vec4 lightClip = spotShadow.lightViewProj * vec4(fragPos + n * normalOffset, 1.0);
} if (lightClip.w <= 0.0) {
return 1.0;
} }
// the shadow sampler compares with GREATER_OR_EQUAL (it was set up for reversed Z), vec3 lightNDC = lightClip.xyz / lightClip.w;
// with regular depth that returns 1 when the fragment is behind the occluder vec2 tileUV = lightNDC.xy * 0.5 + 0.5;
return 1.0 - shadowed / 9.0; if (isOutsideShadowMap(tileUV, lightNDC.z)) {
return 1.0;
}
vec2 tile = vec2(shadowIndex % tilesPerRow, shadowIndex / tilesPerRow);
float tileScale = 1.0 / float(tilesPerRow);
vec2 atlasTexelSize = 1.0 / vec2(textureSize(sampler2DShadow(textures[atlasID], samplers[SHADOW_SAMPLER_ID]), 0));
vec2 atlasUV = (tile + tileUV) * tileScale;
vec2 tileMinUV = tile * tileScale + atlasTexelSize;
vec2 tileMaxUV = (tile + 1.0) * tileScale - atlasTexelSize;
return sampleShadowPCF(atlasID, atlasUV, lightNDC.z, tileMinUV, tileMaxUV);
} }
#endif // SHADOWS_GLSL #endif // SHADOWS_GLSL
@@ -1,6 +1,8 @@
#ifndef FRUSTUM_H #ifndef FRUSTUM_H
#define FRUSTUM_H #define FRUSTUM_H
#include <array>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "Camera.h" #include "Camera.h"
@@ -71,7 +73,12 @@ namespace edge
std::array<glm::vec3, 8> calculateFrustumCornersWorldSpace(const Camera& camera); std::array<glm::vec3, 8> calculateFrustumCornersWorldSpace(const Camera& camera);
Frustum createFrustumFromCamera(const Camera& camera); Frustum createFrustumFromCamera(const Camera& camera);
Frustum createFrustumFromViewProj(const glm::mat4& viewProj);
std::array<glm::vec3, 8> calculateFrustumCorners(const glm::mat4& viewProj);
bool isInFrustum(const Frustum& frustum, const Sphere& s); bool isInFrustum(const Frustum& frustum, const Sphere& s);
// Conservative: frustums that only come close can still count as overlapping
bool frustumsOverlap(const Frustum& frustum, const glm::mat4& otherViewProj);
bool isInFrustum(const Frustum& frustum, const AABB& aabb); bool isInFrustum(const Frustum& frustum, const AABB& aabb);
Sphere calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton); Sphere calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton);
} }
+10 -2
View File
@@ -3,9 +3,12 @@
#include <cstdint> #include <cstdint>
#include <glm/mat4x4.hpp>
#include <glm/vec2.hpp> #include <glm/vec2.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
inline constexpr std::uint32_t NO_SHADOW = 0xFFFFFFFF;
enum class LightType : std::uint32_t { enum class LightType : std::uint32_t {
Directional = 0, Directional = 0,
Point = 1, Point = 1,
@@ -26,7 +29,7 @@ struct Light {
float innerConeAngle{0.35f}; float innerConeAngle{0.35f};
float outerConeAngle{0.5f}; float outerConeAngle{0.5f};
// directional lights only, the first one submitted each frame gets the shadow map // directional and spot lights, point lights don't have shadows yet
bool castShadows{false}; bool castShadows{false};
}; };
@@ -42,10 +45,15 @@ struct GPULight {
float spotInnerCos; float spotInnerCos;
float spotOuterCos; float spotOuterCos;
std::uint32_t castShadows; std::uint32_t shadowIndex;
float padding; float padding;
}; };
struct GPUSpotShadow {
glm::mat4 lightViewProj;
float normalOffsetPerMeter;
};
static_assert(sizeof(GPULight) == 64, "GPULight must match the struct in lights.glsl"); static_assert(sizeof(GPULight) == 64, "GPULight must match the struct in lights.glsl");
#endif //LIGHT_H #endif //LIGHT_H
@@ -30,7 +30,7 @@ public:
void resize(GfxDevice& gfxDevice, glm::ivec2 drawImageSize); void resize(GfxDevice& gfxDevice, glm::ivec2 drawImageSize);
void beginDrawing(std::size_t frameIndex); void beginDrawing(std::size_t frameIndex);
void appendLight(const Light& light); void appendLight(const Light& light, std::uint32_t shadowIndex);
void flushCurrentFrame(VkCommandBuffer cmd, GfxDevice& gfxDevice); void flushCurrentFrame(VkCommandBuffer cmd, GfxDevice& gfxDevice);
@@ -8,33 +8,46 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/Pipeline.h> #include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/ids.h> #include <destrum/Graphics/ids.h>
#include <destrum/Graphics/MeshDrawCommand.h> #include <destrum/Graphics/MeshDrawCommand.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice; class GfxDevice;
struct GPUImage;
class RenderResources; class RenderResources;
// Renders the depth of the scene as seen from a directional light, centered around the camera // Renders scene depth from the lights: one map for the directional light, one atlas tile per spot light
class ShadowMapPipeline final class ShadowMapPipeline final
{ {
public: public:
static constexpr std::uint32_t SHADOW_MAP_SIZE = 2048; static constexpr std::uint32_t SHADOW_MAP_SIZE = 2048;
static constexpr std::uint32_t SPOT_SHADOW_TILE_SIZE = 1024;
static constexpr std::uint32_t SPOT_SHADOW_TILES_PER_ROW = 4;
static constexpr std::uint32_t MAX_SPOT_SHADOWS = SPOT_SHADOW_TILES_PER_ROW * SPOT_SHADOW_TILES_PER_ROW;
void init(GfxDevice& gfxDevice, RenderResources& resources); void init(GfxDevice& gfxDevice, RenderResources& resources);
void cleanup(VkDevice device); void cleanup(VkDevice device);
void draw( void drawDirectionalShadow(
VkCommandBuffer cmd, VkCommandBuffer cmd,
const RenderResources& resources, const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands, const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands); const std::vector<std::size_t>& sortedDrawCommands,
const glm::mat4& lightViewProj);
[[nodiscard]] glm::mat4 calculateLightViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const; void drawSpotShadows(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const std::vector<glm::mat4>& lightViewProjs);
[[nodiscard]] glm::mat4 calculateDirectionalViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const;
[[nodiscard]] static GPUSpotShadow calculateSpotShadow(const Light& spotLight);
[[nodiscard]] ImageID getShadowMapId() const { return shadowMapId; } [[nodiscard]] ImageID getShadowMapId() const { return shadowMapId; }
[[nodiscard]] ImageID getSpotShadowAtlasId() const { return spotShadowAtlasId; }
[[nodiscard]] float getNormalOffset() const; [[nodiscard]] float getNormalOffset() const;
// half the width of the area around the camera that receives shadows // half the width of the area around the camera that receives shadows
@@ -44,18 +57,26 @@ public:
private: private:
struct PushConstants struct PushConstants
{ {
glm::mat4 transform; glm::mat4 modelToLightClip;
VkDeviceAddress sceneDataBuffer;
VkDeviceAddress vertexBuffer; VkDeviceAddress vertexBuffer;
std::uint32_t materialId;
std::uint32_t padding;
}; };
void beginDepthRendering(VkCommandBuffer cmd, const RenderResources& resources, const GPUImage& depthImage);
void endDepthRendering(VkCommandBuffer cmd, const GPUImage& depthImage);
void setRenderRegion(VkCommandBuffer cmd, const VkRect2D& region);
void drawShadowCasters(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const glm::mat4& lightViewProj);
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> m_pipeline; std::unique_ptr<Pipeline> m_pipeline;
VkFormat shadowMapFormat{VK_FORMAT_D32_SFLOAT}; VkFormat shadowMapFormat{VK_FORMAT_D32_SFLOAT};
ImageID shadowMapId{NULL_IMAGE_ID}; ImageID shadowMapId{NULL_IMAGE_ID};
ImageID spotShadowAtlasId{NULL_IMAGE_ID};
float shadowDistance{25.0f}; float shadowDistance{25.0f};
// how far in front of / behind the camera (along the light) casters are still captured // how far in front of / behind the camera (along the light) casters are still captured
@@ -17,8 +17,10 @@
#include <destrum/Graphics/RenderResources.h> #include <destrum/Graphics/RenderResources.h>
#include <array>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <vector>
#include <destrum/Graphics/Pipelines/SkinningPipeline.h> #include <destrum/Graphics/Pipelines/SkinningPipeline.h>
@@ -112,6 +114,8 @@ public:
private: private:
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
void uploadLights(const Camera& camera);
[[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum);
RenderResources* resources = nullptr; RenderResources* resources = nullptr;
@@ -156,6 +160,10 @@ private:
float shadowNormalOffset; float shadowNormalOffset;
std::uint32_t padding; std::uint32_t padding;
glm::mat4 lightViewProj; glm::mat4 lightViewProj;
std::uint32_t spotShadowAtlasId;
std::uint32_t spotShadowTilesPerRow;
std::array<GPUSpotShadow, ShadowMapPipeline::MAX_SPOT_SHADOWS> spotShadows;
}; };
NBuffer sceneDataBuffer; NBuffer sceneDataBuffer;
@@ -170,6 +178,8 @@ private:
std::unique_ptr<ShadowMapPipeline> shadowMapPipeline; std::unique_ptr<ShadowMapPipeline> shadowMapPipeline;
std::optional<glm::vec3> shadowLightDirection; std::optional<glm::vec3> shadowLightDirection;
std::vector<Light> submittedLights;
std::vector<GPUSpotShadow> spotShadows;
bool renderLightHeatmap{false}; bool renderLightHeatmap{false};
bool initialized{false}; bool initialized{false};
@@ -118,6 +118,10 @@ void LightComponent::ImGuiInspector() {
return; return;
} }
if (m_Type == LightType::Spot) {
ImGui::Checkbox("Cast Shadows", &m_CastShadows);
}
float range = m_Range; float range = m_Range;
if (ImGui::DragFloat("Range", &range, 0.05f, 0.01f)) { if (ImGui::DragFloat("Range", &range, 0.05f, 0.01f)) {
SetRange(range); SetRange(range);
+68
View File
@@ -55,6 +55,74 @@ Frustum edge::createFrustumFromCamera(const Camera& camera) {
return frustum; 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) bool isOnOrForwardPlane(const Frustum::Plane& plane, const Sphere& sphere)
{ {
return plane.getSignedDistanceToPlane(sphere.center) > -sphere.radius; return plane.getSignedDistanceToPlane(sphere.center) > -sphere.radius;
@@ -131,7 +131,7 @@ void LightCullingPipeline::beginDrawing(std::size_t frameIndex)
getCurrentFrameData().lightsBuffer.clear(); getCurrentFrameData().lightsBuffer.clear();
} }
void LightCullingPipeline::appendLight(const Light& light) void LightCullingPipeline::appendLight(const Light& light, std::uint32_t shadowIndex)
{ {
auto& lightsBuffer = getCurrentFrameData().lightsBuffer; auto& lightsBuffer = getCurrentFrameData().lightsBuffer;
if (lightsBuffer.size >= lightsBuffer.capacity) if (lightsBuffer.size >= lightsBuffer.capacity)
@@ -158,7 +158,7 @@ void LightCullingPipeline::appendLight(const Light& light)
.type = static_cast<std::uint32_t>(light.type), .type = static_cast<std::uint32_t>(light.type),
.spotInnerCos = std::cos(light.innerConeAngle), .spotInnerCos = std::cos(light.innerConeAngle),
.spotOuterCos = std::cos(light.outerConeAngle), .spotOuterCos = std::cos(light.outerConeAngle),
.castShadows = light.castShadows ? 1u : 0u, .shadowIndex = shadowIndex,
.padding = 0.0f, .padding = 0.0f,
}; };
lightsBuffer.append(gpuLight); lightsBuffer.append(gpuLight);
@@ -1,5 +1,6 @@
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h> #include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
#include <algorithm>
#include <array> #include <array>
#include <cassert> #include <cassert>
#include <cmath> #include <cmath>
@@ -11,11 +12,43 @@
#include <destrum/Graphics/GfxDevice.h> #include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/RenderResources.h> #include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Caches/MeshCache.h> #include <destrum/Graphics/Caches/MeshCache.h>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Util.h> #include <destrum/Graphics/Util.h>
#include "volk.h" #include "volk.h"
#include "spdlog/spdlog.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) void ShadowMapPipeline::init(GfxDevice& gfxDevice, RenderResources& resources)
{ {
try try
@@ -77,14 +110,32 @@ void ShadowMapPipeline::init(GfxDevice& gfxDevice, RenderResources& resources)
pipelineConfig pipelineConfig
); );
const auto createInfo = vkutil::CreateImageInfo{ const auto depthUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
.format = shadowMapFormat,
.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.extent = VkExtent3D{SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 1},
};
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); 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 (...) catch (...)
{ {
@@ -104,14 +155,13 @@ void ShadowMapPipeline::cleanup(VkDevice device)
m_pipelineLayout = VK_NULL_HANDLE; m_pipelineLayout = VK_NULL_HANDLE;
shadowMapId = NULL_IMAGE_ID; 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 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, upVectorFor(direction));
const glm::mat4 lightView = glm::lookAt(glm::vec3{0.0f}, direction, up);
const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE); const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE);
glm::vec3 lightSpaceCenter = glm::vec3(lightView * glm::vec4(center, 1.0f)); 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; 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 float ShadowMapPipeline::getNormalOffset() const
{ {
const float texelSize = (2.0f * shadowDistance) / static_cast<float>(SHADOW_MAP_SIZE); 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, VkCommandBuffer cmd,
const RenderResources& resources, const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands, 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) if (!m_pipeline || shadowMapId == NULL_IMAGE_ID)
{ {
@@ -149,16 +217,55 @@ void ShadowMapPipeline::draw(
const auto& shadowMap = resources.getImage(shadowMapId); 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( vkutil::transitionImage(
cmd, cmd,
shadowMap.image, depthImage.image,
shadowMap.layout, depthImage.layout,
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); 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({ auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = shadowMap.getExtent2D(), .renderExtent = depthImage.getExtent2D(),
.depthImageView = shadowMap.imageView, .depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f, .depthImageClearValue = 1.f,
}); });
renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment; renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment;
@@ -167,31 +274,49 @@ void ShadowMapPipeline::draw(
m_pipeline->bind(cmd); m_pipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout); 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{ const auto viewport = VkViewport{
.x = 0.f, .x = static_cast<float>(region.offset.x),
.y = 0.f, .y = static_cast<float>(region.offset.y),
.width = static_cast<float>(SHADOW_MAP_SIZE), .width = static_cast<float>(region.extent.width),
.height = static_cast<float>(SHADOW_MAP_SIZE), .height = static_cast<float>(region.extent.height),
.minDepth = 0.f, .minDepth = 0.f,
.maxDepth = 1.f, .maxDepth = 1.f,
}; };
vkCmdSetViewport(cmd, 0, 1, &viewport); vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdSetScissor(cmd, 0, 1, &region);
}
const auto scissor = VkRect2D{ void ShadowMapPipeline::drawShadowCasters(
.offset = {}, VkCommandBuffer cmd,
.extent = shadowMap.getExtent2D(), const RenderResources& resources,
}; const std::vector<MeshDrawCommand>& drawCommands,
vkCmdSetScissor(cmd, 0, 1, &scissor); const std::vector<std::size_t>& sortedDrawCommands,
const glm::mat4& lightViewProj)
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL); {
const MeshCache& meshCache = resources.meshes(); 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; return;
} }
@@ -208,21 +333,18 @@ void ShadowMapPipeline::draw(
dc.materialId, dc.materialId,
dc.skinnedMesh != nullptr ? ", skinned" : ""); 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); vkCmdBindIndexBuffer(cmd, mesh.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
} }
const auto pushConstants = PushConstants{ const auto pushConstants = PushConstants{
.transform = dc.transformMatrix, .modelToLightClip = lightViewProj * dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address,
.vertexBuffer = .vertexBuffer =
dc.skinnedMesh != nullptr dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address ? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address, : mesh.vertexBuffer.address,
.materialId = dc.materialId,
.padding = 0,
}; };
vkCmdPushConstants( vkCmdPushConstants(
@@ -243,23 +365,14 @@ void ShadowMapPipeline::draw(
for (std::size_t drawIndex : sortedDrawCommands) for (std::size_t drawIndex : sortedDrawCommands)
{ {
assert(drawIndex < drawCommands.size()); assert(drawIndex < drawCommands.size());
drawOne(drawCommands[drawIndex]); drawCaster(drawCommands[drawIndex]);
} }
} }
else else
{ {
for (const MeshDrawCommand& dc : drawCommands) 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()); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
shadowLightDirection.reset(); shadowLightDirection.reset();
submittedLights.clear();
spotShadows.clear();
Renderer2D::GetInstance().Clear(); Renderer2D::GetInstance().Clear();
} }
@@ -126,15 +128,26 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Lights Buffer Flush"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Lights Buffer Flush");
vkutil::ScopedCmdLabel label(cmd, "Lights Buffer Flush", vkutil::labelColors::transfer); vkutil::ScopedCmdLabel label(cmd, "Lights Buffer Flush", vkutil::labelColors::transfer);
uploadLights(sceneData.camera);
lightCullingPipeline->flushCurrentFrame(cmd, gfxDevice); lightCullingPipeline->flushCurrentFrame(cmd, gfxDevice);
} }
const glm::mat4 lightViewProj = shadowLightDirection const glm::mat4 lightViewProj = shadowLightDirection
? shadowMapPipeline->calculateLightViewProj( ? shadowMapPipeline->calculateDirectionalViewProj(
*shadowLightDirection, *shadowLightDirection,
sceneData.camera.GetPosition()) sceneData.camera.GetPosition())
: glm::mat4{1.0f}; : 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 // TODO: there has to be a better way to do this
const auto gpuSceneData = GPUSceneData{ const auto gpuSceneData = GPUSceneData{
.view = sceneData.camera.GetViewMatrix(), .view = sceneData.camera.GetViewMatrix(),
@@ -156,6 +169,9 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
.shadowNormalOffset = shadowMapPipeline->getNormalOffset(), .shadowNormalOffset = shadowMapPipeline->getNormalOffset(),
.padding = 0, .padding = 0,
.lightViewProj = lightViewProj, .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"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Shadow Map");
vkutil::ScopedCmdLabel label(cmd, "Shadow Map", vkutil::labelColors::shadow); vkutil::ScopedCmdLabel label(cmd, "Shadow Map", vkutil::labelColors::shadow);
shadowMapPipeline->draw( shadowMapPipeline->drawDirectionalShadow(
cmd, cmd,
*resources, *resources,
sceneDataBuffer.getBuffer(),
meshDrawCommands, 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(); lightCullingPipeline.reset();
shadowMapPipeline.reset(); shadowMapPipeline.reset();
shadowLightDirection.reset(); shadowLightDirection.reset();
submittedLights.clear();
spotShadows.clear();
pendingMaterialUploads.clear(); pendingMaterialUploads.clear();
meshDrawCommands.clear(); meshDrawCommands.clear();
sortedMeshDrawCommands.clear(); sortedMeshDrawCommands.clear();
@@ -444,18 +475,46 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
void GameRenderer::submitLight(const Light& light) 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 void GameRenderer::uploadLights(const Camera& camera)
submittedLight.castShadows = {
light.castShadows && light.type == LightType::Directional && !shadowLightDirection; 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 const GPUImage& GameRenderer::getDrawImage() const
-1
View File
@@ -32,7 +32,6 @@ public:
private: private:
void setupSkybox(); void setupSkybox();
void drawCrosshair(); void drawCrosshair();
void drawViewModeHint();
void updateViewMode(); void updateViewMode();
[[nodiscard]] CameraComponent* getSceneCamera() const; [[nodiscard]] CameraComponent* getSceneCamera() const;
[[nodiscard]] Camera& getViewCamera(); [[nodiscard]] Camera& getViewCamera();
-20
View File
@@ -103,25 +103,6 @@ void LightKeeper::updateViewMode()
} }
} }
void LightKeeper::drawViewModeHint()
{
if (isPlayerViewActive() || mainScene.playerController == nullptr) {
return;
}
constexpr std::string_view hint = "Free camera RMB look WASD / Q E move F5 play";
constexpr float hintSize = 18.0f;
const VkExtent3D extent = renderer.getDrawImage().extent;
const glm::vec2 hintExtent = hudFont.MeasureText(hint, hintSize);
Renderer2D::GetInstance().DrawText(
hudFont,
hint,
{16.0f, static_cast<float>(extent.height) - hintExtent.y - 12.0f},
hintSize,
TextStyle{.color = {1.0f, 1.0f, 1.0f, 0.85f}, .outline = 2.0f, .outlineColor = {0.0f, 0.0f, 0.0f, 0.6f}});
}
void LightKeeper::drawCrosshair() void LightKeeper::drawCrosshair()
{ {
if (!isPlayerViewActive()) { if (!isPlayerViewActive()) {
@@ -185,7 +166,6 @@ void LightKeeper::customDraw()
renderer.beginDrawing(gfxDevice); renderer.beginDrawing(gfxDevice);
submitDebugDraws(); submitDebugDraws();
drawCrosshair(); drawCrosshair();
drawViewModeHint();
if (isPlayerViewActive()) if (isPlayerViewActive())
{ {
+18
View File
@@ -60,6 +60,23 @@ namespace {
sun->SetCastShadows(true); sun->SetCastShadows(true);
} }
void CreateHelmetSpotlight(Scene& scene)
{
const glm::vec3 position{2.0f, 3.5f, -2.5f};
const glm::vec3 target{2.0f, GroundHeight, 0.0f};
GameObject* spotObj = scene.CreateGameObject("HelmetSpotlight");
spotObj->GetTransform().SetWorldPosition(position);
spotObj->GetTransform().SetWorldRotation(glm::quatLookAt(glm::normalize(target - position), glm::vec3{0.0f, 1.0f, 0.0f}));
auto* spot = spotObj->AddComponent<LightComponent>(LightType::Spot);
spot->SetColor(glm::vec3{1.0f, 0.85f, 0.7f});
spot->SetIntensity(25.0f);
spot->SetRange(12.0f);
spot->SetConeAngles(glm::radians(20.0f), glm::radians(30.0f));
spot->SetCastShadows(true);
}
void CreateGround(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources) void CreateGround(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{ {
const auto planeModel = AssetManager::GetInstance().LoadModel("game://plane.glb", StaticModelOptions()); const auto planeModel = AssetManager::GetInstance().LoadModel("game://plane.glb", StaticModelOptions());
@@ -443,6 +460,7 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour
CreateGround(scene, gfxDevice, resources); CreateGround(scene, gfxDevice, resources);
CreateParkourCourse(scene, gfxDevice, resources); CreateParkourCourse(scene, gfxDevice, resources);
CreateFlightHelmet(scene, gfxDevice, resources); CreateFlightHelmet(scene, gfxDevice, resources);
CreateHelmetSpotlight(scene);
CreateTree(scene, gfxDevice, resources); CreateTree(scene, gfxDevice, resources);
// CreateCapybara(scene, gfxDevice, resources); // CreateCapybara(scene, gfxDevice, resources);
CreateTriggerZone(scene); CreateTriggerZone(scene);
+30
View File
@@ -35,6 +35,8 @@
#include <destrum/Graphics/Resources/PrimitiveMeshes.h> #include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/Graphics/Managers/Renderer2D.h> #include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/Resources/Font.h> #include <destrum/Graphics/Resources/Font.h>
#include <glm/gtc/matrix_transform.hpp>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Components/LightComponent.h> #include <destrum/Components/LightComponent.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Graphics/Material.h> #include <destrum/Graphics/Material.h>
@@ -511,6 +513,33 @@ namespace {
SceneManager::GetInstance().Destroy(); SceneManager::GetInstance().Destroy();
} }
void testFrustumFromViewProj()
{
const auto lookingAlongPositiveZ = [](const glm::vec3& position, float range) {
const glm::mat4 view = glm::lookAt(position, position + glm::vec3{0.0f, 0.0f, 1.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
return glm::perspectiveRH_ZO(glm::radians(60.0f), 1.0f, 0.1f, range) * view;
};
const glm::mat4 viewProj = lookingAlongPositiveZ(glm::vec3{0.0f}, 20.0f);
const Frustum frustum = edge::createFrustumFromViewProj(viewProj);
check(edge::isInFrustum(frustum, Sphere{{0.0f, 0.0f, 10.0f}, 0.5f}), "a sphere in front must be inside");
check(!edge::isInFrustum(frustum, Sphere{{0.0f, 0.0f, -5.0f}, 0.5f}), "a sphere behind must be outside");
check(!edge::isInFrustum(frustum, Sphere{{0.0f, 0.0f, 30.0f}, 0.5f}), "a sphere past the far plane must be outside");
check(!edge::isInFrustum(frustum, Sphere{{15.0f, 0.0f, 10.0f}, 0.5f}), "a sphere to the side must be outside");
const auto corners = edge::calculateFrustumCorners(viewProj);
const bool cornersOnNearAndFar = std::ranges::all_of(corners, [](const glm::vec3& corner) {
return std::abs(corner.z - 0.1f) < 0.001f || std::abs(corner.z - 20.0f) < 0.01f;
});
check(cornersOnNearAndFar, "frustum corners must lie on the near and far planes");
check(edge::frustumsOverlap(frustum, lookingAlongPositiveZ(glm::vec3{0.0f, 0.0f, 5.0f}, 10.0f)),
"a frustum in front of the camera must overlap");
check(!edge::frustumsOverlap(frustum, lookingAlongPositiveZ(glm::vec3{0.0f, 0.0f, -30.0f}, 10.0f)),
"a frustum entirely behind the camera must not overlap");
}
void testRenderer2DShapes() void testRenderer2DShapes()
{ {
auto& r2d = Renderer2D::GetInstance(); auto& r2d = Renderer2D::GetInstance();
@@ -1305,6 +1334,7 @@ int main()
testPrimitiveBoxMesh(); testPrimitiveBoxMesh();
testColliderCenterOffset(); testColliderCenterOffset();
testCapsuleColliderDebugLines(); testCapsuleColliderDebugLines();
testFrustumFromViewProj();
testRenderer2DShapes(); testRenderer2DShapes();
testFontAtlasAndLayout(); testFontAtlasAndLayout();
std::cout << "destrum tests passed\n"; std::cout << "destrum tests passed\n";