feat: add spot light shadows with a tilled buffer
This commit is contained in:
@@ -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[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 shadowed = 0.0;
|
|
||||||
for (int x = -1; x <= 1; x++) {
|
|
||||||
for (int y = -1; y <= 1; y++) {
|
|
||||||
shadowed += sampleTexture2DShadow(shadowMapID, vec3(uv + vec2(x, y) * texelSize, lightNDC.z));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the shadow sampler compares with GREATER_OR_EQUAL (it was set up for reversed Z),
|
float calculateSpotShadow(vec3 fragPos, vec3 n, vec3 lightPos, SpotShadow spotShadow,
|
||||||
// with regular depth that returns 1 when the fragment is behind the occluder
|
uint shadowIndex, uint atlasID, uint tilesPerRow)
|
||||||
return 1.0 - shadowed / 9.0;
|
{
|
||||||
|
float normalOffset = distance(fragPos, lightPos) * spotShadow.normalOffsetPerMeter;
|
||||||
|
vec4 lightClip = spotShadow.lightViewProj * vec4(fragPos + n * normalOffset, 1.0);
|
||||||
|
if (lightClip.w <= 0.0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 lightNDC = lightClip.xyz / lightClip.w;
|
||||||
|
vec2 tileUV = lightNDC.xy * 0.5 + 0.5;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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, ®ion);
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
MeshID prevMeshId = NULL_MESH_ID;
|
|
||||||
|
|
||||||
const auto drawOne = [&](const MeshDrawCommand& dc)
|
|
||||||
{
|
{
|
||||||
if (!dc.castShadow)
|
const MeshCache& meshCache = resources.meshes();
|
||||||
|
const Frustum lightFrustum = edge::createFrustumFromViewProj(lightViewProj);
|
||||||
|
MeshID previousMeshId = NULL_MESH_ID;
|
||||||
|
|
||||||
|
const auto drawCaster = [&](const MeshDrawCommand& dc)
|
||||||
|
{
|
||||||
|
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,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
|
|
||||||
submittedLight.castShadows =
|
|
||||||
light.castShadows && light.type == LightType::Directional && !shadowLightDirection;
|
|
||||||
|
|
||||||
if (submittedLight.castShadows)
|
|
||||||
{
|
|
||||||
shadowLightDirection = light.direction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lightCullingPipeline->appendLight(submittedLight);
|
void GameRenderer::uploadLights(const Camera& camera)
|
||||||
|
{
|
||||||
|
const Frustum cameraFrustum = edge::createFrustumFromCamera(camera);
|
||||||
|
|
||||||
|
for (const Light& light : submittedLights)
|
||||||
|
{
|
||||||
|
lightCullingPipeline->appendLight(light, assignShadowIndex(light, cameraFrustum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t GameRenderer::assignShadowIndex(const Light& light, const Frustum& cameraFrustum)
|
||||||
|
{
|
||||||
|
if (!light.castShadows)
|
||||||
|
{
|
||||||
|
return NO_SHADOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user