feat: add forward+ rendering

This commit is contained in:
2026-10-11 00:12:23 +02:00
parent c96b2282fb
commit 4bf87bd91b
26 changed files with 1346 additions and 191 deletions
+3
View File
@@ -8,3 +8,6 @@ lightkeeper/assets_runtime
.idea/** .idea/**
build/** build/**
# Claude Code
CLAUDE.md
+2
View File
@@ -5,6 +5,7 @@ set(SRC_FILES
"src/Event.cpp" "src/Event.cpp"
"src/Components/Animator.cpp" "src/Components/Animator.cpp"
"src/Components/LightComponent.cpp"
"src/Components/MeshRendererComponent.cpp" "src/Components/MeshRendererComponent.cpp"
"src/Components/Rotator.cpp" "src/Components/Rotator.cpp"
"src/Components/Spinner.cpp" "src/Components/Spinner.cpp"
@@ -48,6 +49,7 @@ set(SRC_FILES
"src/Graphics/Pipelines/LineRenderingPass.cpp" "src/Graphics/Pipelines/LineRenderingPass.cpp"
"src/Graphics/Pipelines/QuadRendererPass.cpp" "src/Graphics/Pipelines/QuadRendererPass.cpp"
"src/Graphics/Pipelines/SkyboxPipeline.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp"
"src/Graphics/Pipelines/LightCullingPipeline.cpp"
"src/Graphics/Pipelines/SkinningPipeline.cpp" "src/Graphics/Pipelines/SkinningPipeline.cpp"
"src/Graphics/Pipelines/ImguiPass.cpp" "src/Graphics/Pipelines/ImguiPass.cpp"
@@ -0,0 +1,154 @@
#version 460
#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_buffer_reference : require
#extension GL_EXT_scalar_block_layout : require
#include "bindless.glsl"
#include "scene_data.glsl"
// Forward+ light culling.
// One workgroup per LIGHT_TILE_SIZE x LIGHT_TILE_SIZE screen tile:
// 1. find the min/max depth of the tile from the depth pre-pass
// 2. build the tile frustum in view space
// 3. test every light against it and write the visible ones to the tile's light list
layout (push_constant, scalar) uniform constants
{
SceneDataBuffer sceneData;
uvec2 screenSize;
uint depthTexID;
uint padding;
} pcs;
layout (local_size_x = LIGHT_TILE_SIZE, local_size_y = LIGHT_TILE_SIZE, local_size_z = 1) in;
#define THREADS_PER_TILE (LIGHT_TILE_SIZE * LIGHT_TILE_SIZE)
shared uint tileMinDepth;
shared uint tileMaxDepth;
shared uint tileLightCount;
// left, right, bottom, top side planes (view space, through the camera origin)
shared vec4 tilePlanes[4];
// view space distances along -Z
shared float tileNearDistance;
shared float tileFarDistance;
vec3 unprojectToView(vec2 ndc, float depth)
{
vec4 p = pcs.sceneData.invProj * vec4(ndc, depth, 1.0);
return p.xyz / p.w;
}
// Plane through the camera origin and two points, normal facing towards `inside`
vec4 makeSidePlane(vec3 a, vec3 b, vec3 inside)
{
vec3 n = normalize(cross(a, b));
if (dot(n, inside) < 0.0) {
n = -n;
}
return vec4(n, 0.0);
}
bool sphereInsideTile(vec3 center, float radius)
{
float distance = -center.z;
if (distance + radius < tileNearDistance || distance - radius > tileFarDistance) {
return false;
}
for (int i = 0; i < 4; i++) {
if (dot(tilePlanes[i].xyz, center) + tilePlanes[i].w < -radius) {
return false;
}
}
return true;
}
void main()
{
uint localIndex = gl_LocalInvocationIndex;
uvec2 tileID = gl_WorkGroupID.xy;
uint tileIndex = tileID.y * pcs.sceneData.tileCountX + tileID.x;
uint tileOffset = tileIndex * LIGHT_TILE_STRIDE;
if (localIndex == 0) {
tileMinDepth = 0xFFFFFFFFu;
tileMaxDepth = 0u;
tileLightCount = 0u;
}
memoryBarrierShared();
barrier();
// 1. min / max depth
uvec2 pixel = gl_GlobalInvocationID.xy;
if (all(lessThan(pixel, pcs.screenSize))) {
float depth = texelFetch(
sampler2D(textures[pcs.depthTexID], samplers[NEAREST_SAMPLER_ID]),
ivec2(pixel),
0).r;
// depth == 1.0 is the cleared far plane (sky), nothing there receives light.
// Depth is always positive, so its bits can be compared as uints.
if (depth < 1.0) {
uint depthBits = floatBitsToUint(depth);
atomicMin(tileMinDepth, depthBits);
atomicMax(tileMaxDepth, depthBits);
}
}
memoryBarrierShared();
barrier();
// 2. tile frustum
bool tileHasGeometry = tileMinDepth != 0xFFFFFFFFu;
if (localIndex == 0 && tileHasGeometry) {
vec2 screenSize = vec2(pcs.screenSize);
vec2 ndcMin = vec2(tileID * LIGHT_TILE_SIZE) / screenSize * 2.0 - 1.0;
vec2 ndcMax = vec2((tileID + 1) * LIGHT_TILE_SIZE) / screenSize * 2.0 - 1.0;
vec2 ndcCenter = (ndcMin + ndcMax) * 0.5;
vec3 corner00 = unprojectToView(vec2(ndcMin.x, ndcMin.y), 1.0);
vec3 corner10 = unprojectToView(vec2(ndcMax.x, ndcMin.y), 1.0);
vec3 corner11 = unprojectToView(vec2(ndcMax.x, ndcMax.y), 1.0);
vec3 corner01 = unprojectToView(vec2(ndcMin.x, ndcMax.y), 1.0);
vec3 center = unprojectToView(ndcCenter, 1.0);
tilePlanes[0] = makeSidePlane(corner01, corner00, center);
tilePlanes[1] = makeSidePlane(corner10, corner11, center);
tilePlanes[2] = makeSidePlane(corner00, corner10, center);
tilePlanes[3] = makeSidePlane(corner11, corner01, center);
tileNearDistance = -unprojectToView(ndcCenter, uintBitsToFloat(tileMinDepth)).z;
tileFarDistance = -unprojectToView(ndcCenter, uintBitsToFloat(tileMaxDepth)).z;
}
memoryBarrierShared();
barrier();
// 3. cull lights, every thread takes a slice of the light list
if (tileHasGeometry) {
for (uint i = localIndex; i < pcs.sceneData.numLights; i += THREADS_PER_TILE) {
Light light = pcs.sceneData.lights.data[i];
bool visible = light.type == TYPE_DIRECTIONAL_LIGHT;
if (!visible) {
vec3 viewPos = (pcs.sceneData.view * vec4(light.position, 1.0)).xyz;
visible = sphereInsideTile(viewPos, light.range);
}
if (visible) {
uint slot = atomicAdd(tileLightCount, 1u);
if (slot < MAX_LIGHTS_PER_TILE) {
pcs.sceneData.tileLightIndices.data[tileOffset + 1 + slot] = i;
}
}
}
}
memoryBarrierShared();
barrier();
if (localIndex == 0) {
pcs.sceneData.tileLightIndices.data[tileOffset] = min(tileLightCount, uint(MAX_LIGHTS_PER_TILE));
}
}
+96
View File
@@ -0,0 +1,96 @@
#ifndef LIGHTING_GLSL
#define LIGHTING_GLSL
#include "lights.glsl"
#define PI 3.14159265359
// GGX / Trowbridge-Reitz normal distribution
float distributionGGX(float NoH, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;
float d = NoH * NoH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
// Smith-Schlick GGX, k remapped for direct lighting
float geometrySmithGGX(float NoV, float NoL, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0;
float ggxV = NoV / (NoV * (1.0 - k) + k);
float ggxL = NoL / (NoL * (1.0 - k) + k);
return ggxV * ggxL;
}
vec3 fresnelSchlick(float VoH, vec3 f0)
{
return f0 + (1.0 - f0) * pow(1.0 - VoH, 5.0);
}
// Inverse square falloff, windowed so it reaches exactly 0 at the light range.
// The range is also the sphere the light culling uses, so no light gets cut off at a tile edge.
float calculateDistanceAttenuation(float distance, float range)
{
float window = clamp(1.0 - pow(distance / range, 4.0), 0.0, 1.0);
return (window * window) / max(distance * distance, 0.0001);
}
float calculateSpotAttenuation(Light light, vec3 l)
{
float cd = dot(normalize(light.direction), -l);
return smoothstep(light.spotOuterCos, light.spotInnerCos, cd);
}
vec3 calculateLight(Light light, vec3 fragPos, vec3 n, vec3 v,
vec3 diffuseColor, float roughness, float metallic, vec3 f0)
{
vec3 l;
float attenuation = 1.0;
if (light.type == TYPE_DIRECTIONAL_LIGHT) {
l = normalize(-light.direction);
} else {
vec3 toLight = light.position - fragPos;
float distance = length(toLight);
l = toLight / max(distance, 0.0001);
attenuation = calculateDistanceAttenuation(distance, light.range);
if (light.type == TYPE_SPOT_LIGHT) {
attenuation *= calculateSpotAttenuation(light, l);
}
}
float NoL = clamp(dot(n, l), 0.0, 1.0);
if (NoL <= 0.0 || attenuation <= 0.0) {
return vec3(0.0);
}
vec3 h = normalize(v + l);
float NoV = clamp(dot(n, v), 0.0001, 1.0);
float NoH = clamp(dot(n, h), 0.0, 1.0);
float VoH = clamp(dot(v, h), 0.0, 1.0);
float D = distributionGGX(NoH, roughness);
float G = geometrySmithGGX(NoV, NoL, roughness);
vec3 F = fresnelSchlick(VoH, f0);
vec3 specular = (D * G * F) / (4.0 * NoV * NoL + 0.0001);
vec3 kd = (vec3(1.0) - F) * (1.0 - metallic);
vec3 diffuse = kd * diffuseColor / PI;
return (diffuse + specular) * light.color * light.intensity * attenuation * NoL;
}
// blue -> green -> red, used to visualise how many lights a tile has
vec3 lightHeatmapColor(uint lightCount)
{
float t = clamp(float(lightCount) / 16.0, 0.0, 1.0);
if (t < 0.5) {
return mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), t * 2.0);
}
return mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (t - 0.5) * 2.0);
}
#endif // LIGHTING_GLSL
+41
View File
@@ -0,0 +1,41 @@
#ifndef LIGHTS_GLSL
#define LIGHTS_GLSL
#extension GL_EXT_buffer_reference : require
#define TYPE_DIRECTIONAL_LIGHT 0
#define TYPE_POINT_LIGHT 1
#define TYPE_SPOT_LIGHT 2
// Forward+ tiling, must match LightCullingPipeline::TILE_SIZE / MAX_LIGHTS_PER_TILE
#define LIGHT_TILE_SIZE 16
#define MAX_LIGHTS_PER_TILE 255
// Every tile stores its light count first, followed by MAX_LIGHTS_PER_TILE light indices
#define LIGHT_TILE_STRIDE (MAX_LIGHTS_PER_TILE + 1)
// Must match GPULight in Light.h
struct Light {
vec3 position;
float range;
vec3 color;
float intensity;
// direction the light is pointing in (directional + spot lights)
vec3 direction;
uint type;
float spotInnerCos;
float spotOuterCos;
vec2 padding;
};
layout (buffer_reference, std430) readonly buffer LightsBuffer {
Light data[];
};
layout (buffer_reference, std430) buffer TileLightIndicesBuffer {
uint data[];
};
#endif // LIGHTS_GLSL
+64 -92
View File
@@ -5,6 +5,7 @@
#include "bindless.glsl" #include "bindless.glsl"
#include "scene_data.glsl" #include "scene_data.glsl"
#include "mesh_pcs.glsl" #include "mesh_pcs.glsl"
#include "lighting.glsl"
layout (location = 0) in vec3 inPos; layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV; layout (location = 1) in vec2 inUV;
@@ -20,16 +21,13 @@ void main()
uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode; uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode;
vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor; vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
outFragColor = diffuse; vec3 baseColor = diffuse.rgb;
// if (diffuse.a < 0.1) { // if (diffuse.a < 0.1) {
// discard; // discard;
// } // }
//
// vec3 baseColor = material.baseColor.rgb * diffuse.rgb; vec3 normal = normalize(inNormal);
//
// vec3 normal = normalize(inNormal).rgb;
// if (inTangent != vec4(0.0)) { // if (inTangent != vec4(0.0)) {
// // FIXME: sometimes Blender doesn't export tangents for some objects // // FIXME: sometimes Blender doesn't export tangents for some objects
// // for some reason. When we will start computing tangents manually, // // for some reason. When we will start computing tangents manually,
@@ -39,91 +37,65 @@ void main()
// normal = inTBN * normalize(normal * 2.0 - 1.0); // normal = inTBN * normalize(normal * 2.0 - 1.0);
// normal = normalize(normal); // normal = normalize(normal);
// } // }
//
// #ifdef PBR
// float metallicF = material.metallicRoughnessEmissive.r;
// float roughnessF = material.metallicRoughnessEmissive.g;
//
// vec4 metallicRoughness = sampleTexture2DLinear(material.metallicRoughnessTex, inUV);
//
// float roughness = roughnessF * metallicRoughness.g;
// // roughness *= roughness; // from perceptual to linear
// roughness = max(roughness, 1e-2); // 0.0 roughness leads to NaNs
//
// float metallic = metallicF * metallicRoughness.b;
//
// vec3 dielectricSpecular = vec3(0.04);
// vec3 black = vec3(0.0);
// vec3 diffuseColor = mix(baseColor * (1.0 - dielectricSpecular.r), black, metallic);
// vec3 f0 = mix(dielectricSpecular, baseColor, metallic);
// #else
// vec3 diffuseColor = baseColor;
// float metallic = 0.f;
// float roughness = 1.f;
// vec3 f0 = vec3(0.0);
// #endif
//
// vec3 cameraPos = pcs.sceneData.cameraPos.xyz;
// vec3 fragPos = inPos.xyz;
// vec3 n = normal;
// vec3 v = normalize(cameraPos - fragPos);
//
// // diffuseColor = vec3(1.0);
// // baseColor = vec3(1.0);
//
// vec3 fragColor = vec3(0.0);
// for (int i = 0; i < pcs.sceneData.numLights; i++) {
// Light light = pcs.sceneData.lights.data[i];
//
// vec3 l = light.direction;
// if (light.type != TYPE_DIRECTIONAL_LIGHT) {
// l = normalize(light.position - fragPos);
// }
// float NoL = clamp(dot(n, l), 0.0, 1.0);
//
// float occlusion = 1.0;
// if (light.type == TYPE_DIRECTIONAL_LIGHT) {
// occlusion = calculateCSMOcclusion(
// fragPos, cameraPos, NoL,
// pcs.sceneData.csmShadowMapId,
// pcs.sceneData.cascadeFarPlaneZs,
// pcs.sceneData.csmLightSpaceTMs);
// } else if (light.type == TYPE_POINT_LIGHT && light.shadowMapID != 0) {
// occlusion = calculatePointShadow(
// fragPos, light.position, NoL,
// light.shadowMapID,
// pcs.sceneData.pointLightFarPlane);
// }
//
// fragColor += calculateLight(light, fragPos, n, v, l,
// diffuseColor, roughness, metallic, f0, occlusion);
// }
//
// // emissive
// float emissiveF = material.metallicRoughnessEmissive.b;
// vec3 emissiveColor = emissiveF * sampleTexture2DLinear(material.emissiveTex, inUV).rgb;
// fragColor += emissiveColor;
//
// // ambient
// fragColor += baseColor * pcs.sceneData.ambientColor * pcs.sceneData.ambientIntensity;
//
// #if 0
// // CSM DEBUG
// uint cascadeIndex = chooseCascade(inPos, cameraPos, pcs.sceneData.cascadeFarPlaneZs);
// fragColor *= debugShadowsFactor(cascadeIndex);
// #endif
//
// #if 0
// // TANGENT DEBUG
// if (inTangent == vec4(0.0)) {
// fragColor = vec3(1.0f, 0.0f, 0.0f);
// }
// #endif
//
// #if 1
// // NORMAL DEBUG
// // fragColor = normal;
// #endif
// outFragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f); float metallicF = material.metallicRoughnessEmissive.r;
float roughnessF = material.metallicRoughnessEmissive.g;
vec4 metallicRoughness = sampleTexture2DLinear(material.metallicRoughnessTex, inUV);
float roughness = roughnessF * metallicRoughness.g;
// roughness *= roughness; // from perceptual to linear
roughness = max(roughness, 1e-2); // 0.0 roughness leads to NaNs
float metallic = metallicF * metallicRoughness.b;
vec3 dielectricSpecular = vec3(0.04);
vec3 black = vec3(0.0);
vec3 diffuseColor = mix(baseColor * (1.0 - dielectricSpecular.r), black, metallic);
vec3 f0 = mix(dielectricSpecular, baseColor, metallic);
vec3 cameraPos = pcs.sceneData.cameraPos.xyz;
vec3 fragPos = inPos.xyz;
vec3 n = normal;
vec3 v = normalize(cameraPos - fragPos);
// forward+: only loop over the lights the culling pass put in this pixel's tile
uvec2 tileID = uvec2(gl_FragCoord.xy) / LIGHT_TILE_SIZE;
uint tileOffset = (tileID.y * pcs.sceneData.tileCountX + tileID.x) * LIGHT_TILE_STRIDE;
uint tileLightCount = pcs.sceneData.tileLightIndices.data[tileOffset];
vec3 fragColor = vec3(0.0);
for (uint i = 0; i < tileLightCount; i++) {
uint lightIndex = pcs.sceneData.tileLightIndices.data[tileOffset + 1 + i];
Light light = pcs.sceneData.lights.data[lightIndex];
fragColor += calculateLight(light, fragPos, n, v,
diffuseColor, roughness, metallic, f0);
}
// emissive
float emissiveF = material.metallicRoughnessEmissive.b;
vec3 emissiveColor = emissiveF * sampleTexture2DLinear(material.emissiveTex, inUV).rgb;
fragColor += emissiveColor;
// ambient
fragColor += baseColor * pcs.sceneData.ambientColor * pcs.sceneData.ambientIntensity;
if (pcs.sceneData.debugLightHeatmap != 0) {
fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5);
}
#if 0
// TANGENT DEBUG
if (inTangent == vec4(0.0)) {
fragColor = vec3(1.0f, 0.0f, 0.0f);
}
#endif
#if 0
// NORMAL DEBUG
fragColor = normal;
#endif
outFragColor = vec4(fragColor, diffuse.a);
} }
+4
View File
@@ -10,6 +10,10 @@ layout (location = 2) out vec3 outNormal;
layout (location = 3) out vec4 outTangent; layout (location = 3) out vec4 outTangent;
layout (location = 4) out mat3 outTBN; layout (location = 4) out mat3 outTBN;
// The depth pre-pass reuses this shader, positions have to match bit for bit
// so the main pass can depth test against it with LESS_OR_EQUAL
invariant gl_Position;
void main() void main()
{ {
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex]; Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
@@ -6,11 +6,14 @@
#extension GL_EXT_buffer_reference : require #extension GL_EXT_buffer_reference : require
#include "materials.glsl" #include "materials.glsl"
#include "lights.glsl"
// Must match GameRenderer::GPUSceneData
layout (buffer_reference, scalar) readonly buffer SceneDataBuffer { layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
// camera // camera
mat4 view; mat4 view;
mat4 proj; mat4 proj;
mat4 invProj;
mat4 viewProj; mat4 viewProj;
vec4 cameraPos; vec4 cameraPos;
@@ -23,6 +26,14 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
float fogDensity; float fogDensity;
MaterialsBuffer materials; MaterialsBuffer materials;
// forward+
LightsBuffer lights;
TileLightIndicesBuffer tileLightIndices;
uint numLights;
uint tileCountX;
uint debugLightHeatmap;
uint padding;
} sceneDataBuffer; } sceneDataBuffer;
#endif // SCENE_DATA_GLSL #endif // SCENE_DATA_GLSL
+2 -2
View File
@@ -10,7 +10,6 @@ layout(location = 0) out vec4 outColor;
layout(push_constant) uniform SkyboxPC { layout(push_constant) uniform SkyboxPC {
mat4 invViewProj; mat4 invViewProj;
vec4 skyboxRot[3]; vec4 skyboxRot[3];
vec3 cameraPos;
uint skyboxTextureId; uint skyboxTextureId;
} pcs; } pcs;
@@ -22,7 +21,8 @@ void main()
vec4 world = pcs.invViewProj * ndc; vec4 world = pcs.invViewProj * ndc;
vec3 worldPos = world.xyz / world.w; vec3 worldPos = world.xyz / world.w;
vec3 dir = normalize(worldPos - pcs.cameraPos); // invViewProj has no translation, so the camera sits at the origin
vec3 dir = normalize(worldPos);
dir.y *= -1.0; dir.y *= -1.0;
mat3 skyboxRot = mat3( mat3 skyboxRot = mat3(
@@ -0,0 +1,51 @@
#ifndef LIGHTCOMPONENT_H
#define LIGHTCOMPONENT_H
#include <destrum/ObjectModel/Component.h>
#include <destrum/Graphics/Light.h>
#include <glm/glm.hpp>
class LightComponent final : public Component {
public:
explicit LightComponent(GameObject& parent, LightType type = LightType::Point);
LightComponent(const LightComponent& other) = delete;
LightComponent(LightComponent&& other) noexcept = delete;
LightComponent& operator=(const LightComponent& other) = delete;
LightComponent& operator=(LightComponent&& other) noexcept = delete;
void Render(const RenderContext& ctx) override;
std::string GetTypeName() const override { return "LightComponent"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetType(LightType type) { m_Type = type; }
[[nodiscard]] LightType GetType() const { return m_Type; }
void SetColor(const glm::vec3& color) { m_Color = color; }
[[nodiscard]] const glm::vec3& GetColor() const { return m_Color; }
void SetIntensity(float intensity);
[[nodiscard]] float GetIntensity() const { return m_Intensity; }
void SetRange(float range);
[[nodiscard]] float GetRange() const { return m_Range; }
void SetConeAngles(float innerConeAngle, float outerConeAngle);
[[nodiscard]] float GetInnerConeAngle() const { return m_InnerConeAngle; }
[[nodiscard]] float GetOuterConeAngle() const { return m_OuterConeAngle; }
private:
LightType m_Type{LightType::Point};
glm::vec3 m_Color{1.0f};
float m_Intensity{1.0f};
float m_Range{10.0f};
float m_InnerConeAngle{0.35f};
float m_OuterConeAngle{0.5f};
};
#endif // LIGHTCOMPONENT_H
+47
View File
@@ -0,0 +1,47 @@
#ifndef LIGHT_H
#define LIGHT_H
#include <cstdint>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
enum class LightType : std::uint32_t {
Directional = 0,
Point = 1,
Spot = 2,
};
struct Light {
LightType type{LightType::Point};
glm::vec3 position{0.0f};
glm::vec3 direction{0.0f, -1.0f, 0.0f};
glm::vec3 color{1.0f};
float intensity{1.0f};
float range{10.0f};
float innerConeAngle{0.35f};
float outerConeAngle{0.5f};
};
struct GPULight {
glm::vec3 position;
float range;
glm::vec3 color;
float intensity;
glm::vec3 direction;
std::uint32_t type;
float spotInnerCos;
float spotOuterCos;
glm::vec2 padding;
};
static_assert(sizeof(GPULight) == 64, "GPULight must match the struct in lights.glsl");
#endif //LIGHT_H
@@ -0,0 +1,78 @@
#ifndef LIGHTCULLINGPIPELINE_H
#define LIGHTCULLINGPIPELINE_H
#include <vulkan/vulkan.h>
#include <array>
#include <memory>
#include <glm/glm.hpp>
#include <destrum/Graphics/ComputePipeline.h>
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/ids.h>
#include <destrum/Graphics/Resources/AppendableBuffer.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice;
class RenderResources;
class LightCullingPipeline final {
public:
static constexpr std::uint32_t TILE_SIZE = 16;
static constexpr std::uint32_t MAX_LIGHTS_PER_TILE = 255;
static constexpr std::uint32_t TILE_STRIDE = MAX_LIGHTS_PER_TILE + 1;
static constexpr std::size_t MAX_LIGHTS = 1024;
void init(GfxDevice& gfxDevice, RenderResources& resources, glm::ivec2 drawImageSize);
void cleanup(GfxDevice& gfxDevice);
void resize(GfxDevice& gfxDevice, glm::ivec2 drawImageSize);
void beginDrawing(std::size_t frameIndex);
void appendLight(const Light& light);
void flushCurrentFrame(VkCommandBuffer cmd, GfxDevice& gfxDevice);
void cullLights(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
ImageID depthImageId,
VkExtent2D renderExtent);
[[nodiscard]] VkDeviceAddress getLightsBufferAddress() const;
[[nodiscard]] VkDeviceAddress getTileLightIndicesBufferAddress() const { return tileLightIndicesBuffer.address; }
[[nodiscard]] std::uint32_t getNumLights() const;
[[nodiscard]] glm::uvec2 getTileCount() const { return tileCount; }
private:
void createTileBuffer(GfxDevice& gfxDevice, glm::ivec2 drawImageSize);
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<ComputePipeline> lightCullingPipeline;
struct PushConstants {
VkDeviceAddress sceneDataBuffer;
glm::uvec2 screenSize;
std::uint32_t depthTexId;
std::uint32_t padding;
};
struct PerFrameData {
AppendableBuffer<GPULight> lightsBuffer;
};
std::array<PerFrameData, FRAMES_IN_FLIGHT> framesData;
std::size_t currentFrameIndex{0};
bool warnedAboutLightLimit{false};
GPUBuffer tileLightIndicesBuffer;
glm::uvec2 tileCount{0u, 0u};
PerFrameData& getCurrentFrameData();
const PerFrameData& getCurrentFrameData() const;
};
#endif //LIGHTCULLINGPIPELINE_H
@@ -27,6 +27,15 @@ public:
VkFormat drawImageFormat, VkFormat drawImageFormat,
VkFormat depthImageFormat); VkFormat depthImageFormat);
void drawDepthPrepass(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
void draw( void draw(
VkCommandBuffer cmd, VkCommandBuffer cmd,
VkExtent2D renderExtent, VkExtent2D renderExtent,
@@ -55,9 +64,20 @@ private:
std::uint32_t padding; std::uint32_t padding;
}; };
void drawMeshes(
VkCommandBuffer cmd,
const Pipeline& pipeline,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
private: private:
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> m_pipeline; std::unique_ptr<Pipeline> m_pipeline;
std::unique_ptr<Pipeline> m_depthPrepassPipeline;
bool m_renderWireframe{false}; bool m_renderWireframe{false};
}; };
@@ -35,7 +35,6 @@ private:
struct SkyboxPushConstants { struct SkyboxPushConstants {
glm::mat4 invViewProj; glm::mat4 invViewProj;
glm::vec4 skyboxRot[3]; glm::vec4 skyboxRot[3];
glm::vec3 cameraPos;
std::uint32_t skyboxTextureId; std::uint32_t skyboxTextureId;
}; };
@@ -57,7 +57,10 @@ public:
BindlessSetManager& getBindlessSetManager(); BindlessSetManager& getBindlessSetManager();
VkDescriptorSetLayout getBindlessDescSetLayout() const; VkDescriptorSetLayout getBindlessDescSetLayout() const;
const VkDescriptorSet& getBindlessDescSet() const; const VkDescriptorSet& getBindlessDescSet() const;
void bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const; void bindBindlessDescSet(
VkCommandBuffer cmd,
VkPipelineLayout layout,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS) const;
MeshCache& meshes() { return *meshCache; } MeshCache& meshes() { return *meshCache; }
const MeshCache& meshes() const { return *meshCache; } const MeshCache& meshes() const { return *meshCache; }
+41 -12
View File
@@ -5,6 +5,8 @@
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h> #include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
#include <destrum/Graphics/Pipelines/LineRenderingPass.h> #include <destrum/Graphics/Pipelines/LineRenderingPass.h>
#include <destrum/Graphics/Pipelines/MeshPipeline.h> #include <destrum/Graphics/Pipelines/MeshPipeline.h>
#include <destrum/Graphics/Pipelines/QuadRendererPass.h> #include <destrum/Graphics/Pipelines/QuadRendererPass.h>
@@ -20,9 +22,11 @@
#include "Pipelines/SkyboxPipeline.h" #include "Pipelines/SkyboxPipeline.h"
class GameRenderer { class GameRenderer
{
public: public:
struct SceneData { struct SceneData
{
const Camera& camera; const Camera& camera;
glm::vec3 ambientColor; glm::vec3 ambientColor;
float ambientIntensity; float ambientIntensity;
@@ -40,11 +44,15 @@ public:
void cleanup(GfxDevice& gfxDevice); void cleanup(GfxDevice& gfxDevice);
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId); void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh, std::size_t jointMatricesStartIndex); void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh,
std::size_t jointMatricesStartIndex);
void submitLight(const Light& light);
const GPUImage& getDrawImage() const; const GPUImage& getDrawImage() const;
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) { void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize)
{
createDrawImage(gfxDevice, newSize, false); createDrawImage(gfxDevice, newSize, false);
lightCullingPipeline->resize(gfxDevice, newSize);
} }
Material& getMaterialMutable(MaterialID id); Material& getMaterialMutable(MaterialID id);
@@ -54,18 +62,31 @@ public:
void flushMaterialUpdates(GfxDevice& gfxDevice); void flushMaterialUpdates(GfxDevice& gfxDevice);
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const { [[nodiscard]] SkinningPipeline& getSkinningPipeline() const
{
return *skinningPipeline; return *skinningPipeline;
} }
void setRenderWireframe(bool wireframe) { void setRenderWireframe(bool wireframe)
{
meshPipeline->setRenderWireframe(wireframe); meshPipeline->setRenderWireframe(wireframe);
} }
bool getRenderWireframe() { bool getRenderWireframe()
{
return meshPipeline->getRenderWireframe(); return meshPipeline->getRenderWireframe();
} }
void setRenderLightHeatmap(bool heatmap)
{
renderLightHeatmap = heatmap;
}
bool getRenderLightHeatmap() const
{
return renderLightHeatmap;
}
RenderResources* getResources() RenderResources* getResources()
{ {
return resources; return resources;
@@ -96,33 +117,41 @@ private:
VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT};
struct GPUSceneData { struct GPUSceneData
// camera {
glm::mat4 view; glm::mat4 view;
glm::mat4 proj; glm::mat4 proj;
glm::mat4 invProj;
glm::mat4 viewProj; glm::mat4 viewProj;
glm::vec4 cameraPos; glm::vec4 cameraPos;
// ambient
glm::vec3 ambientColor; glm::vec3 ambientColor;
float ambientIntensity; float ambientIntensity;
// fog
glm::vec3 fogColor; glm::vec3 fogColor;
float fogDensity; float fogDensity;
VkDeviceAddress materialsBuffer; VkDeviceAddress materialsBuffer;
VkDeviceAddress lightsBuffer;
VkDeviceAddress tileLightIndicesBuffer;
std::uint32_t numLights;
std::uint32_t tileCountX;
std::uint32_t debugLightHeatmap;
std::uint32_t padding;
}; };
NBuffer sceneDataBuffer; NBuffer sceneDataBuffer;
std::unique_ptr<MeshPipeline> meshPipeline; std::unique_ptr<MeshPipeline> meshPipeline;
std::unique_ptr<SkyboxPipeline> skyboxPipeline; std::unique_ptr<SkyboxPipeline> skyboxPipeline;
std::unique_ptr<LineRenderingPass> lineRenderingPass; std::unique_ptr<LineRenderingPass> lineRenderingPass;
std::unique_ptr<QuadRendererPass> quadRendererPass; std::unique_ptr<QuadRendererPass> quadRendererPass;
std::unique_ptr<SkinningPipeline> skinningPipeline; std::unique_ptr<SkinningPipeline> skinningPipeline;
std::unique_ptr<LightCullingPipeline> lightCullingPipeline;
bool renderLightHeatmap{false};
bool initialized{false}; bool initialized{false};
}; };
@@ -1,9 +1,8 @@
#ifndef DESTRUM_ENGINECOMPONENTLIST_H #ifndef DESTRUM_ENGINECOMPONENTLIST_H
#define DESTRUM_ENGINECOMPONENTLIST_H #define DESTRUM_ENGINECOMPONENTLIST_H
// Keep the component manifest in one place. Consumers define X before
// expanding one of these lists to generate registration or other metadata.
#include <destrum/Components/Animator.h> #include <destrum/Components/Animator.h>
#include <destrum/Components/LightComponent.h>
#include <destrum/Components/MeshRendererComponent.h> #include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/OrbitAndSpin.h> #include <destrum/Components/OrbitAndSpin.h>
#include <destrum/Components/Rotator.h> #include <destrum/Components/Rotator.h>
@@ -15,6 +14,7 @@
#define DESTRUM_ENGINE_COMPONENTS(X) \ #define DESTRUM_ENGINE_COMPONENTS(X) \
X(MeshRendererComponent) \ X(MeshRendererComponent) \
X(LightComponent) \
X(Rotator) \ X(Rotator) \
X(Spinner) \ X(Spinner) \
X(OrbitAndSpin) \ X(OrbitAndSpin) \
@@ -22,11 +22,8 @@ public:
static bool Load(Scene& scene, const std::filesystem::path& path, static bool Load(Scene& scene, const std::filesystem::path& path,
std::function<void(float)> progressCallback = nullptr); std::function<void(float)> progressCallback = nullptr);
// Thread-safe: parse and validate a scene file on any thread.
// No engine state is touched during parsing.
static LoadResult LoadSceneFile(const std::filesystem::path& path); static LoadResult LoadSceneFile(const std::filesystem::path& path);
// Main-thread-only: construct a scene from pre-validated JSON.
static bool ConstructScene(Scene& scene, LoadResult& result, static bool ConstructScene(Scene& scene, LoadResult& result,
std::function<void(float)> progressCallback = nullptr); std::function<void(float)> progressCallback = nullptr);
+126
View File
@@ -0,0 +1,126 @@
#include <destrum/Components/LightComponent.h>
#include <destrum/Graphics/RenderContext.h>
#include <destrum/ObjectModel/Transform.h>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <array>
#include <imgui.h>
LightComponent::LightComponent(GameObject& parent, LightType type)
: Component(parent, "LightComponent")
, m_Type(type)
{
}
void LightComponent::SetIntensity(float intensity)
{
m_Intensity = std::max(intensity, 0.0f);
}
void LightComponent::SetRange(float range)
{
m_Range = std::max(range, 0.01f);
}
void LightComponent::SetConeAngles(float innerConeAngle, float outerConeAngle)
{
m_InnerConeAngle = std::clamp(innerConeAngle, 0.0f, glm::half_pi<float>());
m_OuterConeAngle = std::clamp(outerConeAngle, m_InnerConeAngle, glm::half_pi<float>());
}
void LightComponent::Render(const RenderContext& ctx)
{
auto& transform = GetTransform();
ctx.renderer.submitLight(Light{
.type = m_Type,
.position = transform.GetWorldPosition(),
.direction = transform.GetWorldRotation() * glm::vec3{0.0f, 0.0f, -1.0f},
.color = m_Color,
.intensity = m_Intensity,
.range = m_Range,
.innerConeAngle = m_InnerConeAngle,
.outerConeAngle = m_OuterConeAngle,
});
}
nlohmann::json LightComponent::Serialize() const {
return {
{"type", static_cast<std::uint32_t>(m_Type)},
{"color", {m_Color.x, m_Color.y, m_Color.z}},
{"intensity", m_Intensity},
{"range", m_Range},
{"innerConeAngle", m_InnerConeAngle},
{"outerConeAngle", m_OuterConeAngle}
};
}
void LightComponent::Deserialize(const nlohmann::json& data) {
if (data.contains("type")) {
const auto type = data.at("type").get<std::uint32_t>();
if (type <= static_cast<std::uint32_t>(LightType::Spot)) {
m_Type = static_cast<LightType>(type);
}
}
if (data.contains("color")) {
const auto& value = data.at("color");
m_Color = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
}
if (data.contains("intensity")) {
SetIntensity(data.at("intensity").get<float>());
}
if (data.contains("range")) {
SetRange(data.at("range").get<float>());
}
float innerConeAngle = m_InnerConeAngle;
float outerConeAngle = m_OuterConeAngle;
if (data.contains("innerConeAngle")) {
innerConeAngle = data.at("innerConeAngle").get<float>();
}
if (data.contains("outerConeAngle")) {
outerConeAngle = data.at("outerConeAngle").get<float>();
}
SetConeAngles(innerConeAngle, outerConeAngle);
}
void LightComponent::ImGuiInspector() {
constexpr std::array typeNames{"Directional", "Point", "Spot"};
int type = static_cast<int>(m_Type);
if (ImGui::Combo("Type", &type, typeNames.data(), static_cast<int>(typeNames.size()))) {
SetType(static_cast<LightType>(type));
}
ImGui::ColorEdit3("Color", &m_Color.x);
float intensity = m_Intensity;
if (ImGui::DragFloat("Intensity", &intensity, 0.05f, 0.0f)) {
SetIntensity(intensity);
}
if (m_Type == LightType::Directional) {
return;
}
float range = m_Range;
if (ImGui::DragFloat("Range", &range, 0.05f, 0.01f)) {
SetRange(range);
}
if (m_Type == LightType::Spot) {
float innerConeAngle = m_InnerConeAngle;
float outerConeAngle = m_OuterConeAngle;
bool changed = ImGui::SliderAngle("Inner Cone", &innerConeAngle, 0.0f, 90.0f);
changed |= ImGui::SliderAngle("Outer Cone", &outerConeAngle, 0.0f, 90.0f);
if (changed) {
SetConeAngles(innerConeAngle, outerConeAngle);
}
}
}
@@ -0,0 +1,271 @@
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
#include "destrum/FS/AssetFS.h"
#include "destrum/Graphics/GfxDevice.h"
#include "destrum/Graphics/RenderResources.h"
#include "destrum/Graphics/Util.h"
#include <array>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include "volk.h"
#include "spdlog/spdlog.h"
void LightCullingPipeline::init(GfxDevice& gfxDevice, RenderResources& resources, glm::ivec2 drawImageSize)
{
try
{
const auto& device = gfxDevice.getDevice();
const auto pushConstant = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
.offset = 0,
.size = sizeof(PushConstants),
};
const auto pushConstants = std::array{pushConstant};
const auto layouts = std::array{
resources.getBindlessDescSetLayout()
};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = static_cast<std::uint32_t>(layouts.size());
pipelineLayoutInfo.pSetLayouts = layouts.data();
pipelineLayoutInfo.pushConstantRangeCount = static_cast<std::uint32_t>(pushConstants.size());
pipelineLayoutInfo.pPushConstantRanges = pushConstants.data();
if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS)
{
throw std::runtime_error("Could not make light culling pipeline layout");
}
ComputePipelineConfigInfo pipelineConfig{};
pipelineConfig.name = "light culling compute pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
const auto lightCullingShaderPath = AssetFS::GetInstance().GetCookedPathForFile(
"engine://shaders/light_culling.comp");
lightCullingPipeline = std::make_unique<ComputePipeline>(
gfxDevice,
lightCullingShaderPath.generic_string(),
pipelineConfig
);
for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i)
{
auto& lightsBuffer = framesData[i].lightsBuffer;
lightsBuffer.capacity = MAX_LIGHTS;
lightsBuffer.buffer = gfxDevice.createBuffer(
MAX_LIGHTS * sizeof(GPULight),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vkutil::addDebugLabel(device, lightsBuffer.buffer.buffer, "lights");
}
createTileBuffer(gfxDevice, drawImageSize);
}
catch (...)
{
cleanup(gfxDevice);
throw;
}
}
void LightCullingPipeline::cleanup(GfxDevice& gfxDevice)
{
for (auto& frame : framesData)
{
gfxDevice.destroyBuffer(frame.lightsBuffer.buffer);
frame.lightsBuffer.buffer = {};
frame.lightsBuffer.size = 0;
}
gfxDevice.destroyBuffer(tileLightIndicesBuffer);
tileLightIndicesBuffer = {};
tileCount = glm::uvec2{0u, 0u};
if (m_pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE)
{
vkDestroyPipelineLayout(gfxDevice.getDevice(), m_pipelineLayout, nullptr);
}
m_pipelineLayout = VK_NULL_HANDLE;
lightCullingPipeline.reset();
}
void LightCullingPipeline::resize(GfxDevice& gfxDevice, glm::ivec2 drawImageSize)
{
gfxDevice.waitIdle();
gfxDevice.destroyBuffer(tileLightIndicesBuffer);
tileLightIndicesBuffer = {};
createTileBuffer(gfxDevice, drawImageSize);
}
void LightCullingPipeline::createTileBuffer(GfxDevice& gfxDevice, glm::ivec2 drawImageSize)
{
assert(drawImageSize.x > 0 && drawImageSize.y > 0);
tileCount = glm::uvec2{
(static_cast<std::uint32_t>(drawImageSize.x) + TILE_SIZE - 1) / TILE_SIZE,
(static_cast<std::uint32_t>(drawImageSize.y) + TILE_SIZE - 1) / TILE_SIZE,
};
const std::size_t numTiles = static_cast<std::size_t>(tileCount.x) * tileCount.y;
tileLightIndicesBuffer = gfxDevice.createBuffer(
numTiles * TILE_STRIDE * sizeof(std::uint32_t),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
vkutil::addDebugLabel(gfxDevice.getDevice(), tileLightIndicesBuffer.buffer, "tile light indices");
spdlog::debug("Forward+ light grid: {}x{} tiles", tileCount.x, tileCount.y);
}
void LightCullingPipeline::beginDrawing(std::size_t frameIndex)
{
currentFrameIndex = frameIndex;
getCurrentFrameData().lightsBuffer.clear();
}
void LightCullingPipeline::appendLight(const Light& light)
{
auto& lightsBuffer = getCurrentFrameData().lightsBuffer;
if (lightsBuffer.size >= lightsBuffer.capacity)
{
if (!warnedAboutLightLimit)
{
spdlog::warn("Light limit of {} reached, extra lights are ignored", MAX_LIGHTS);
warnedAboutLightLimit = true;
}
return;
}
const float directionLength = glm::length(light.direction);
const glm::vec3 direction = directionLength > 1e-6f
? light.direction / directionLength
: glm::vec3{0.0f, -1.0f, 0.0f};
const auto gpuLight = GPULight{
.position = light.position,
.range = light.range,
.color = light.color,
.intensity = light.intensity,
.direction = direction,
.type = static_cast<std::uint32_t>(light.type),
.spotInnerCos = std::cos(light.innerConeAngle),
.spotOuterCos = std::cos(light.outerConeAngle),
.padding = glm::vec2{0.0f},
};
lightsBuffer.append(gpuLight);
}
void LightCullingPipeline::flushCurrentFrame(VkCommandBuffer cmd, GfxDevice& gfxDevice)
{
auto& buffer = getCurrentFrameData().lightsBuffer.buffer;
gfxDevice.getMemoryManager().flushAllocation(buffer, 0, VK_WHOLE_SIZE);
vkutil::bufferHostWriteToShaderReadBarrier(
cmd,
buffer.buffer,
0,
static_cast<VkDeviceSize>(MAX_LIGHTS * sizeof(GPULight)));
}
void LightCullingPipeline::cullLights(
VkCommandBuffer cmd,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
ImageID depthImageId,
VkExtent2D renderExtent)
{
if (!lightCullingPipeline)
{
return;
}
{
// previous frame's mesh pass might still be reading the tile lists
const VkBufferMemoryBarrier2 tileBufferBarrier{
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = tileLightIndicesBuffer.buffer,
.offset = 0,
.size = VK_WHOLE_SIZE,
};
const VkDependencyInfo dependencyInfo{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &tileBufferBarrier,
};
vkCmdPipelineBarrier2(cmd, &dependencyInfo);
}
lightCullingPipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout, VK_PIPELINE_BIND_POINT_COMPUTE);
const auto cs = PushConstants{
.sceneDataBuffer = sceneDataBuffer.address,
.screenSize = glm::uvec2{renderExtent.width, renderExtent.height},
.depthTexId = static_cast<std::uint32_t>(depthImageId),
.padding = 0,
};
vkCmdPushConstants(cmd,
m_pipelineLayout,
VK_SHADER_STAGE_COMPUTE_BIT,
0,
sizeof(PushConstants),
&cs);
// one workgroup per tile
vkCmdDispatch(cmd, tileCount.x, tileCount.y, 1);
const VkBufferMemoryBarrier2 tileBufferBarrier{
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = tileLightIndicesBuffer.buffer,
.offset = 0,
.size = VK_WHOLE_SIZE,
};
const VkDependencyInfo dependencyInfo{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &tileBufferBarrier,
};
vkCmdPipelineBarrier2(cmd, &dependencyInfo);
}
VkDeviceAddress LightCullingPipeline::getLightsBufferAddress() const
{
return getCurrentFrameData().lightsBuffer.buffer.address;
}
std::uint32_t LightCullingPipeline::getNumLights() const
{
return static_cast<std::uint32_t>(getCurrentFrameData().lightsBuffer.size);
}
LightCullingPipeline::PerFrameData& LightCullingPipeline::getCurrentFrameData()
{
return framesData[currentFrameIndex % FRAMES_IN_FLIGHT];
}
const LightCullingPipeline::PerFrameData& LightCullingPipeline::getCurrentFrameData() const
{
return framesData[currentFrameIndex % FRAMES_IN_FLIGHT];
}
+100 -15
View File
@@ -23,7 +23,8 @@ void MeshPipeline::init(
VkFormat drawImageFormat, VkFormat drawImageFormat,
VkFormat depthImageFormat) VkFormat depthImageFormat)
{ {
try { try
{
const auto vertexShader = const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert"); AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert");
@@ -59,6 +60,29 @@ void MeshPipeline::init(
throw std::runtime_error("Could not create mesh pipeline layout"); throw std::runtime_error("Could not create mesh pipeline layout");
} }
{
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Mesh Depth Prepass Pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {};
pipelineConfig.colorAttachments = {};
pipelineConfig.colorBlendInfo.attachmentCount = 0;
pipelineConfig.depthAttachment = depthImageFormat;
m_depthPrepassPipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
"",
pipelineConfig
);
}
{
PipelineConfigInfo pipelineConfig{}; PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig); Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
@@ -71,18 +95,50 @@ void MeshPipeline::init(
pipelineConfig.colorAttachments = {drawImageFormat}; pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = depthImageFormat; pipelineConfig.depthAttachment = depthImageFormat;
// depth was already written by the pre-pass, only shade the visible surface
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
m_pipeline = std::make_unique<Pipeline>( m_pipeline = std::make_unique<Pipeline>(
gfxDevice, gfxDevice,
vertexShader.string(), vertexShader.string(),
fragShader.string(), fragShader.string(),
pipelineConfig pipelineConfig
); );
} catch (...) { }
}
catch (...)
{
cleanup(gfxDevice.getDevice()); cleanup(gfxDevice.getDevice());
throw; throw;
} }
} }
void MeshPipeline::drawDepthPrepass(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
{
if (!m_depthPrepassPipeline)
{
return;
}
drawMeshes(
cmd,
*m_depthPrepassPipeline,
renderExtent,
resources,
camera,
sceneDataBuffer,
drawCommands,
sortedDrawCommands);
}
void MeshPipeline::draw( void MeshPipeline::draw(
VkCommandBuffer cmd, VkCommandBuffer cmd,
VkExtent2D renderExtent, VkExtent2D renderExtent,
@@ -92,13 +148,35 @@ void MeshPipeline::draw(
const std::vector<MeshDrawCommand>& drawCommands, const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands) const std::vector<std::size_t>& sortedDrawCommands)
{ {
if (!m_pipeline) { if (!m_pipeline)
{
return; return;
} }
drawMeshes(
cmd,
*m_pipeline,
renderExtent,
resources,
camera,
sceneDataBuffer,
drawCommands,
sortedDrawCommands);
}
void MeshPipeline::drawMeshes(
VkCommandBuffer cmd,
const Pipeline& pipeline,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
{
const MeshCache& meshCache = resources.meshes(); const MeshCache& meshCache = resources.meshes();
m_pipeline->bind(cmd); pipeline.bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout); resources.bindBindlessDescSet(cmd, m_pipelineLayout);
@@ -130,8 +208,10 @@ void MeshPipeline::draw(
const auto frustum = edge::createFrustumFromCamera(camera); const auto frustum = edge::createFrustumFromCamera(camera);
const auto drawOne = [&](const MeshDrawCommand& dc) { const auto drawOne = [&](const MeshDrawCommand& dc)
if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { {
if (!edge::isInFrustum(frustum, dc.worldBoundingSphere))
{
return; return;
} }
@@ -139,7 +219,8 @@ void MeshPipeline::draw(
const auto& mesh = meshCache.getMesh(dc.meshId); const auto& mesh = meshCache.getMesh(dc.meshId);
if (dc.meshId != prevMeshId) { if (dc.meshId != prevMeshId)
{
prevMeshId = dc.meshId; prevMeshId = dc.meshId;
vkCmdBindIndexBuffer( vkCmdBindIndexBuffer(
@@ -175,26 +256,30 @@ void MeshPipeline::draw(
vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0); vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0);
}; };
if (!sortedDrawCommands.empty()) { if (!sortedDrawCommands.empty())
for (std::size_t drawIndex : sortedDrawCommands) { {
for (std::size_t drawIndex : sortedDrawCommands)
{
assert(drawIndex < drawCommands.size()); assert(drawIndex < drawCommands.size());
drawOne(drawCommands[drawIndex]); drawOne(drawCommands[drawIndex]);
} }
} else { }
for (const MeshDrawCommand& dc : drawCommands) { else
{
for (const MeshDrawCommand& dc : drawCommands)
{
drawOne(dc); drawOne(dc);
} }
} }
// Optional:
// spdlog::debug("Actual mesh draw calls: {}", actualDrawCalls);
} }
void MeshPipeline::cleanup(VkDevice device) void MeshPipeline::cleanup(VkDevice device)
{ {
m_pipeline.reset(); m_pipeline.reset();
m_depthPrepassPipeline.reset();
if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) { if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE)
{
vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr); vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr);
} }
m_pipelineLayout = VK_NULL_HANDLE; m_pipelineLayout = VK_NULL_HANDLE;
@@ -119,14 +119,16 @@ void SkyboxPipeline::draw(
const glm::mat3 r = glm::mat3(skyboxRotation); const glm::mat3 r = glm::mat3(skyboxRotation);
// rotation only, translation in the inverse makes the sky jitter far from the origin
const glm::mat4 viewRotation = glm::mat4(glm::mat3(camera.GetViewMatrix()));
const auto pcs = SkyboxPushConstants{ const auto pcs = SkyboxPushConstants{
.invViewProj = glm::inverse(camera.GetViewProjectionMatrix()), .invViewProj = glm::inverse(camera.GetProjectionMatrix() * viewRotation),
.skyboxRot = { .skyboxRot = {
glm::vec4(r[0], 0.f), glm::vec4(r[0], 0.f),
glm::vec4(r[1], 0.f), glm::vec4(r[1], 0.f),
glm::vec4(r[2], 0.f) glm::vec4(r[2], 0.f)
}, },
.cameraPos = camera.GetPosition(),
.skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId), .skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId),
}; };
+5 -2
View File
@@ -266,10 +266,13 @@ const VkDescriptorSet& RenderResources::getBindlessDescSet() const {
return imageCache->bindlessSetManager.getDescSet(); return imageCache->bindlessSetManager.getDescSet();
} }
void RenderResources::bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const { void RenderResources::bindBindlessDescSet(
VkCommandBuffer cmd,
VkPipelineLayout layout,
VkPipelineBindPoint bindPoint) const {
vkCmdBindDescriptorSets( vkCmdBindDescriptorSets(
cmd, cmd,
VK_PIPELINE_BIND_POINT_GRAPHICS, bindPoint,
layout, layout,
0, 0,
1, 1,
+94 -11
View File
@@ -21,7 +21,8 @@ GameRenderer::GameRenderer()
void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::ivec2 drawImageSize) void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::ivec2 drawImageSize)
{ {
try { try
{
resources = &_resources; resources = &_resources;
sceneDataBuffer.init( sceneDataBuffer.init(
gfxDevice, gfxDevice,
@@ -34,12 +35,15 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
meshPipeline = std::make_unique<MeshPipeline>(); meshPipeline = std::make_unique<MeshPipeline>();
meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
skyboxPipeline = std::make_unique<SkyboxPipeline>(); skyboxPipeline = std::make_unique<SkyboxPipeline>();
skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
skinningPipeline = std::make_unique<SkinningPipeline>(); skinningPipeline = std::make_unique<SkinningPipeline>();
skinningPipeline->init(gfxDevice); skinningPipeline->init(gfxDevice);
lightCullingPipeline = std::make_unique<LightCullingPipeline>();
lightCullingPipeline->init(gfxDevice, _resources, drawImageSize);
lineRenderingPass = std::make_unique<LineRenderingPass>(); lineRenderingPass = std::make_unique<LineRenderingPass>();
lineRenderingPass->init(gfxDevice, drawImageFormat); lineRenderingPass->init(gfxDevice, drawImageFormat);
@@ -48,7 +52,9 @@ skyboxPipeline = std::make_unique<SkyboxPipeline>();
GameState::GetInstance().SetRenderer(this); GameState::GetInstance().SetRenderer(this);
initialized = true; initialized = true;
} catch (...) { }
catch (...)
{
cleanup(gfxDevice); cleanup(gfxDevice);
throw; throw;
} }
@@ -59,6 +65,7 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice)
flushMaterialUpdates(gfxDevice); flushMaterialUpdates(gfxDevice);
meshDrawCommands.clear(); meshDrawCommands.clear();
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
QuadRenderingManager::GetInstance().ClearQuads(); QuadRenderingManager::GetInstance().ClearQuads();
} }
@@ -70,13 +77,16 @@ void GameRenderer::endDrawing()
std::stable_sort( std::stable_sort(
sortedMeshDrawCommands.begin(), sortedMeshDrawCommands.begin(),
sortedMeshDrawCommands.end(), sortedMeshDrawCommands.end(),
[this](std::size_t left, std::size_t right) { [this](std::size_t left, std::size_t right)
{
const auto& lhs = meshDrawCommands[left]; const auto& lhs = meshDrawCommands[left];
const auto& rhs = meshDrawCommands[right]; const auto& rhs = meshDrawCommands[right];
if (lhs.meshId != rhs.meshId) { if (lhs.meshId != rhs.meshId)
{
return lhs.meshId < rhs.meshId; return lhs.meshId < rhs.meshId;
} }
if (lhs.materialId != rhs.materialId) { if (lhs.materialId != rhs.materialId)
{
return lhs.materialId < rhs.materialId; return lhs.materialId < rhs.materialId;
} }
return (lhs.skinnedMesh != nullptr) < (rhs.skinnedMesh != nullptr); return (lhs.skinnedMesh != nullptr) < (rhs.skinnedMesh != nullptr);
@@ -106,9 +116,17 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
} }
} }
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Lights Buffer Flush");
lightCullingPipeline->flushCurrentFrame(cmd, gfxDevice);
}
// 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(),
.proj = sceneData.camera.GetProjectionMatrix(), .proj = sceneData.camera.GetProjectionMatrix(),
.invProj = glm::inverse(sceneData.camera.GetProjectionMatrix()),
.viewProj = sceneData.camera.GetViewProjectionMatrix(), .viewProj = sceneData.camera.GetViewProjectionMatrix(),
.cameraPos = glm::vec4{sceneData.camera.m_position, 1.f}, .cameraPos = glm::vec4{sceneData.camera.m_position, 1.f},
.ambientColor = {sceneData.ambientColor}, .ambientColor = {sceneData.ambientColor},
@@ -116,6 +134,12 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
.fogColor = {sceneData.fogColor}, .fogColor = {sceneData.fogColor},
.fogDensity = sceneData.fogDensity, .fogDensity = sceneData.fogDensity,
.materialsBuffer = resources->materials().getMaterialDataBufferAddress(), .materialsBuffer = resources->materials().getMaterialDataBufferAddress(),
.lightsBuffer = lightCullingPipeline->getLightsBufferAddress(),
.tileLightIndicesBuffer = lightCullingPipeline->getTileLightIndicesBufferAddress(),
.numLights = lightCullingPipeline->getNumLights(),
.tileCountX = lightCullingPipeline->getTileCount().x,
.debugLightHeatmap = renderLightHeatmap ? 1u : 0u,
.padding = 0,
}; };
{ {
@@ -165,12 +189,60 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
} }
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Depth Prepass");
auto depthPrepassInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(),
.depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f,
});
depthPrepassInfo.renderingInfo.pDepthAttachment = &depthPrepassInfo.depthAttachment;
vkCmdBeginRendering(cmd, &depthPrepassInfo.renderingInfo);
meshPipeline->drawDepthPrepass(
cmd,
drawImage.getExtent2D(),
*resources,
camera,
sceneDataBuffer.getBuffer(),
meshDrawCommands,
sortedMeshDrawCommands);
vkCmdEndRendering(cmd);
}
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Light Culling");
vkutil::transitionImage(
cmd,
depthImage.image,
depthImage.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
depthImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
lightCullingPipeline->cullLights(
cmd,
*resources,
sceneDataBuffer.getBuffer(),
depthImageId,
drawImage.getExtent2D());
vkutil::transitionImage(
cmd,
depthImage.image,
depthImage.layout,
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
}
auto renderInfo = vkutil::createRenderingInfo({ auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(), .renderExtent = drawImage.getExtent2D(),
.colorImageView = drawImage.imageView, .colorImageView = drawImage.imageView,
.colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f}, .colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f},
.depthImageView = depthImage.imageView, .depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f,
}); });
renderInfo.renderingInfo.pColorAttachments = &renderInfo.colorAttachment; renderInfo.renderingInfo.pColorAttachments = &renderInfo.colorAttachment;
renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment; renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment;
@@ -230,7 +302,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
static_cast<float>(drawImage.extent.height), static_cast<float>(drawImage.extent.height),
0.0f, -1.0f, 1.0f)); 0.0f, -1.0f, 1.0f));
} }
// vkutil::cmdEndLabel(cmd); // vkutil::cmdEndLabel(cmd);
} }
void GameRenderer::drawQuads( void GameRenderer::drawQuads(
@@ -246,11 +318,12 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
{ {
VkDevice device = gfxDevice.getDevice(); VkDevice device = gfxDevice.getDevice();
if (device != VK_NULL_HANDLE) { if (device != VK_NULL_HANDLE)
{
vkDeviceWaitIdle(device); vkDeviceWaitIdle(device);
} }
if (skinningPipeline) if (skinningPipeline)
skinningPipeline->cleanup(gfxDevice); skinningPipeline->cleanup(gfxDevice);
if (lineRenderingPass) if (lineRenderingPass)
@@ -259,6 +332,9 @@ if (skinningPipeline)
if (quadRendererPass) if (quadRendererPass)
quadRendererPass->cleanup(gfxDevice); quadRendererPass->cleanup(gfxDevice);
if (lightCullingPipeline)
lightCullingPipeline->cleanup(gfxDevice);
if (skyboxPipeline) if (skyboxPipeline)
skyboxPipeline->cleanup(device); skyboxPipeline->cleanup(device);
@@ -281,6 +357,7 @@ if (skinningPipeline)
skinningPipeline.reset(); skinningPipeline.reset();
lineRenderingPass.reset(); lineRenderingPass.reset();
quadRendererPass.reset(); quadRendererPass.reset();
lightCullingPipeline.reset();
pendingMaterialUploads.clear(); pendingMaterialUploads.clear();
meshDrawCommands.clear(); meshDrawCommands.clear();
sortedMeshDrawCommands.clear(); sortedMeshDrawCommands.clear();
@@ -323,6 +400,11 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
}); });
} }
void GameRenderer::submitLight(const Light& light)
{
lightCullingPipeline->appendLight(light);
}
const GPUImage& GameRenderer::getDrawImage() const const GPUImage& GameRenderer::getDrawImage() const
{ {
assert(resources); assert(resources);
@@ -349,7 +431,8 @@ void GameRenderer::setSkyboxTexture(ImageID skyboxImageId)
void GameRenderer::flushMaterialUpdates(GfxDevice& gfxDevice) void GameRenderer::flushMaterialUpdates(GfxDevice& gfxDevice)
{ {
if (!pendingMaterialUploads.empty()) { if (!pendingMaterialUploads.empty())
{
// Material data is currently shared by all frame contexts. Wait before // Material data is currently shared by all frame contexts. Wait before
// mutating it so an older in-flight frame cannot read the overwritten // mutating it so an older in-flight frame cannot read the overwritten
// range. This can later be replaced with per-frame material buffers. // range. This can later be replaced with per-frame material buffers.
+42
View File
@@ -6,6 +6,7 @@
#include <destrum/Assets/AssetManager.h> #include <destrum/Assets/AssetManager.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h> #include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Graphics/Managers/QuadRenderingManager.h> #include <destrum/Graphics/Managers/QuadRenderingManager.h>
#include "glm/gtc/constants.hpp"
#include "glm/gtx/transform.hpp" #include "glm/gtx/transform.hpp"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
@@ -20,6 +21,7 @@
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/ModelDoc.h" #include "destrum/Util/ModelDoc.h"
#include "destrum/Components/Animator.h" #include "destrum/Components/Animator.h"
#include "destrum/Components/LightComponent.h"
#include <components/FirstPersonController.h> #include <components/FirstPersonController.h>
#include <components/TriggerLoggerComponent.h> #include <components/TriggerLoggerComponent.h>
@@ -447,6 +449,41 @@ void LightKeeper::customInit()
triggerObj->AddComponent<TriggerLoggerComponent>(); triggerObj->AddComponent<TriggerLoggerComponent>();
triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f}); triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f});
// Forward+ test lights: a dim sun and a ring of colored point lights orbiting the origin
auto sunObj = scene.CreateGameObject("Sun");
auto* sun = sunObj->AddComponent<LightComponent>(LightType::Directional);
sun->SetColor(glm::vec3{1.0f, 0.95f, 0.85f});
sun->SetIntensity(1.0f);
sunObj->GetTransform().SetWorldRotation(glm::vec3{-50.0f, 30.0f, 0.0f});
constexpr int pointLightCount = 32;
constexpr float pointLightRingRadius = 6.0f;
for (int i = 0; i < pointLightCount; i++)
{
const float t = static_cast<float>(i) / static_cast<float>(pointLightCount);
const float angle = t * glm::two_pi<float>();
auto lightObj = scene.CreateGameObject("PointLight " + std::to_string(i));
lightObj->GetTransform().SetWorldPosition(glm::vec3{
std::cos(angle) * pointLightRingRadius,
-0.25f,
std::sin(angle) * pointLightRingRadius
});
auto* light = lightObj->AddComponent<LightComponent>(LightType::Point);
light->SetColor(glm::vec3{
0.5f + 0.5f * std::cos(glm::two_pi<float>() * t),
0.5f + 0.5f * std::cos(glm::two_pi<float>() * (t + 0.33f)),
0.5f + 0.5f * std::cos(glm::two_pi<float>() * (t + 0.67f))
});
light->SetIntensity(1.0f);
light->SetRange(3.0f);
auto* orbit = lightObj->AddComponent<Rotator>(pointLightRingRadius, 0.5f);
orbit->SetAxis(glm::vec3{0.0f, 1.0f, 0.0f});
orbit->SetPivotPosition(glm::vec3{0.0f, -0.25f, 0.0f});
}
auto playerObj = scene.CreateGameObject("Player"); auto playerObj = scene.CreateGameObject("Player");
playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f)); playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f));
auto* fps = playerObj->AddComponent<FirstPersonController>(); auto* fps = playerObj->AddComponent<FirstPersonController>();
@@ -553,6 +590,11 @@ void LightKeeper::drawDebugMenuBar()
ImGui::MenuItem("Box Colliders", nullptr, &renderBoxColliders); ImGui::MenuItem("Box Colliders", nullptr, &renderBoxColliders);
ImGui::MenuItem("2D Quads", nullptr, &renderQuads); ImGui::MenuItem("2D Quads", nullptr, &renderQuads);
ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines); ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines);
bool lightHeatmap = renderer.getRenderLightHeatmap();
if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) {
renderer.setRenderLightHeatmap(lightHeatmap);
}
ImGui::EndMenu(); ImGui::EndMenu();
} }
+36
View File
@@ -22,6 +22,7 @@
#include <destrum/Components/Physics/BoxCollider.h> #include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/Collider.h> #include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Animator.h> #include <destrum/Components/Animator.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>
#include <destrum/Graphics/Managers/LineRenderingManager.h> #include <destrum/Graphics/Managers/LineRenderingManager.h>
@@ -171,6 +172,7 @@ namespace {
const char* canonicalNames[] = { const char* canonicalNames[] = {
"MeshRendererComponent", "MeshRendererComponent",
"LightComponent",
"Rotator", "Rotator",
"Spinner", "Spinner",
"OrbitAndSpin", "OrbitAndSpin",
@@ -447,6 +449,39 @@ namespace {
"Animator scene data must not embed animation keyframes"); "Animator scene data must not embed animation keyframes");
} }
void testLightComponentRoundTrip()
{
GameObject object{"light"};
auto* light = object.AddComponent<LightComponent>(LightType::Spot);
light->SetColor(glm::vec3{1.0f, 0.5f, 0.25f});
light->SetIntensity(4.0f);
light->SetRange(7.5f);
light->SetConeAngles(0.2f, 0.4f);
const auto data = light->Serialize();
GameObject loadedObject{"loaded light"};
auto* loaded = loadedObject.AddComponent<LightComponent>();
loaded->Deserialize(data);
check(loaded->GetType() == LightType::Spot,
"LightComponent must round-trip its type");
check(loaded->GetColor() == glm::vec3{1.0f, 0.5f, 0.25f},
"LightComponent must round-trip its color");
check(loaded->GetIntensity() == 4.0f && loaded->GetRange() == 7.5f,
"LightComponent must round-trip intensity and range");
check(loaded->GetInnerConeAngle() == 0.2f && loaded->GetOuterConeAngle() == 0.4f,
"LightComponent must round-trip its cone angles");
loaded->SetConeAngles(0.5f, 0.1f);
check(loaded->GetOuterConeAngle() >= loaded->GetInnerConeAngle(),
"LightComponent outer cone must never be smaller than the inner cone");
loaded->SetRange(0.0f);
check(loaded->GetRange() > 0.0f,
"LightComponent range must stay positive");
}
void testSimpleColorMaterial() void testSimpleColorMaterial()
{ {
const auto material = Material::SimpleColor( const auto material = Material::SimpleColor(
@@ -561,6 +596,7 @@ int main()
testJoltSphereCollisions(); testJoltSphereCollisions();
testSceneJoltSphereCollisions(); testSceneJoltSphereCollisions();
testAnimatorAssetReferences(); testAnimatorAssetReferences();
testLightComponentRoundTrip();
testSimpleColorMaterial(); testSimpleColorMaterial();
testAssetReferenceCacheKeys(); testAssetReferenceCacheKeys();
testTriggerZone(); testTriggerZone();