From 4bf87bd91b3d47415715a73c5201b87e44a73d03 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Sun, 11 Oct 2026 00:12:23 +0200 Subject: [PATCH] feat: add forward+ rendering --- .gitignore | 3 + destrum/CMakeLists.txt | 2 + destrum/assets_src/shaders/light_culling.comp | 154 ++++++++++ destrum/assets_src/shaders/lighting.glsl | 96 +++++++ destrum/assets_src/shaders/lights.glsl | 41 +++ destrum/assets_src/shaders/mesh.frag | 156 +++++----- destrum/assets_src/shaders/mesh.vert | 4 + destrum/assets_src/shaders/scene_data.glsl | 11 + destrum/assets_src/shaders/skybox.frag | 4 +- .../destrum/Components/LightComponent.h | 51 ++++ destrum/include/destrum/Graphics/Light.h | 47 +++ .../Graphics/Pipelines/LightCullingPipeline.h | 78 +++++ .../destrum/Graphics/Pipelines/MeshPipeline.h | 20 ++ .../Graphics/Pipelines/SkyboxPipeline.h | 1 - .../destrum/Graphics/RenderResources.h | 5 +- destrum/include/destrum/Graphics/Renderer.h | 53 +++- .../Serialization/EngineComponentList.h | 4 +- .../destrum/Serialization/SceneSerializer.h | 3 - destrum/src/Components/LightComponent.cpp | 126 ++++++++ .../Pipelines/LightCullingPipeline.cpp | 271 ++++++++++++++++++ .../src/Graphics/Pipelines/MeshPipeline.cpp | 205 +++++++++---- .../src/Graphics/Pipelines/SkyboxPipeline.cpp | 6 +- destrum/src/Graphics/RenderResources.cpp | 7 +- destrum/src/Graphics/Renderer.cpp | 111 ++++++- lightkeeper/src/Lightkeeper.cpp | 42 +++ tests/destrum_tests.cpp | 36 +++ 26 files changed, 1346 insertions(+), 191 deletions(-) create mode 100644 destrum/assets_src/shaders/light_culling.comp create mode 100644 destrum/assets_src/shaders/lighting.glsl create mode 100644 destrum/assets_src/shaders/lights.glsl create mode 100644 destrum/include/destrum/Components/LightComponent.h create mode 100644 destrum/include/destrum/Graphics/Light.h create mode 100644 destrum/include/destrum/Graphics/Pipelines/LightCullingPipeline.h create mode 100644 destrum/src/Components/LightComponent.cpp create mode 100644 destrum/src/Graphics/Pipelines/LightCullingPipeline.cpp diff --git a/.gitignore b/.gitignore index cd2a7cc..5caffa1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ lightkeeper/assets_runtime .idea/** build/** + +# Claude Code +CLAUDE.md diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 5a1e96d..2e1d146 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -5,6 +5,7 @@ set(SRC_FILES "src/Event.cpp" "src/Components/Animator.cpp" + "src/Components/LightComponent.cpp" "src/Components/MeshRendererComponent.cpp" "src/Components/Rotator.cpp" "src/Components/Spinner.cpp" @@ -48,6 +49,7 @@ set(SRC_FILES "src/Graphics/Pipelines/LineRenderingPass.cpp" "src/Graphics/Pipelines/QuadRendererPass.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp" + "src/Graphics/Pipelines/LightCullingPipeline.cpp" "src/Graphics/Pipelines/SkinningPipeline.cpp" "src/Graphics/Pipelines/ImguiPass.cpp" diff --git a/destrum/assets_src/shaders/light_culling.comp b/destrum/assets_src/shaders/light_culling.comp new file mode 100644 index 0000000..aa47d18 --- /dev/null +++ b/destrum/assets_src/shaders/light_culling.comp @@ -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)); + } +} diff --git a/destrum/assets_src/shaders/lighting.glsl b/destrum/assets_src/shaders/lighting.glsl new file mode 100644 index 0000000..0e5a322 --- /dev/null +++ b/destrum/assets_src/shaders/lighting.glsl @@ -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 diff --git a/destrum/assets_src/shaders/lights.glsl b/destrum/assets_src/shaders/lights.glsl new file mode 100644 index 0000000..bc26b82 --- /dev/null +++ b/destrum/assets_src/shaders/lights.glsl @@ -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 diff --git a/destrum/assets_src/shaders/mesh.frag b/destrum/assets_src/shaders/mesh.frag index 62b5290..397be3c 100644 --- a/destrum/assets_src/shaders/mesh.frag +++ b/destrum/assets_src/shaders/mesh.frag @@ -5,6 +5,7 @@ #include "bindless.glsl" #include "scene_data.glsl" #include "mesh_pcs.glsl" +#include "lighting.glsl" layout (location = 0) in vec3 inPos; layout (location = 1) in vec2 inUV; @@ -20,16 +21,13 @@ void main() uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode; vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor; - outFragColor = diffuse; - + vec3 baseColor = diffuse.rgb; // if (diffuse.a < 0.1) { // discard; // } -// -// vec3 baseColor = material.baseColor.rgb * diffuse.rgb; -// -// vec3 normal = normalize(inNormal).rgb; + + vec3 normal = normalize(inNormal); // if (inTangent != vec4(0.0)) { // // FIXME: sometimes Blender doesn't export tangents for some objects // // 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 = 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); } diff --git a/destrum/assets_src/shaders/mesh.vert b/destrum/assets_src/shaders/mesh.vert index 88754f3..42d9f80 100644 --- a/destrum/assets_src/shaders/mesh.vert +++ b/destrum/assets_src/shaders/mesh.vert @@ -10,6 +10,10 @@ layout (location = 2) out vec3 outNormal; layout (location = 3) out vec4 outTangent; 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() { Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex]; diff --git a/destrum/assets_src/shaders/scene_data.glsl b/destrum/assets_src/shaders/scene_data.glsl index c04d0f5..251241e 100644 --- a/destrum/assets_src/shaders/scene_data.glsl +++ b/destrum/assets_src/shaders/scene_data.glsl @@ -6,11 +6,14 @@ #extension GL_EXT_buffer_reference : require #include "materials.glsl" +#include "lights.glsl" +// Must match GameRenderer::GPUSceneData layout (buffer_reference, scalar) readonly buffer SceneDataBuffer { // camera mat4 view; mat4 proj; + mat4 invProj; mat4 viewProj; vec4 cameraPos; @@ -23,6 +26,14 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer { float fogDensity; MaterialsBuffer materials; + + // forward+ + LightsBuffer lights; + TileLightIndicesBuffer tileLightIndices; + uint numLights; + uint tileCountX; + uint debugLightHeatmap; + uint padding; } sceneDataBuffer; #endif // SCENE_DATA_GLSL diff --git a/destrum/assets_src/shaders/skybox.frag b/destrum/assets_src/shaders/skybox.frag index e769719..9032536 100644 --- a/destrum/assets_src/shaders/skybox.frag +++ b/destrum/assets_src/shaders/skybox.frag @@ -10,7 +10,6 @@ layout(location = 0) out vec4 outColor; layout(push_constant) uniform SkyboxPC { mat4 invViewProj; vec4 skyboxRot[3]; - vec3 cameraPos; uint skyboxTextureId; } pcs; @@ -22,7 +21,8 @@ void main() vec4 world = pcs.invViewProj * ndc; 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; mat3 skyboxRot = mat3( diff --git a/destrum/include/destrum/Components/LightComponent.h b/destrum/include/destrum/Components/LightComponent.h new file mode 100644 index 0000000..f051bdf --- /dev/null +++ b/destrum/include/destrum/Components/LightComponent.h @@ -0,0 +1,51 @@ +#ifndef LIGHTCOMPONENT_H +#define LIGHTCOMPONENT_H + +#include +#include + +#include + +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 diff --git a/destrum/include/destrum/Graphics/Light.h b/destrum/include/destrum/Graphics/Light.h new file mode 100644 index 0000000..67d485e --- /dev/null +++ b/destrum/include/destrum/Graphics/Light.h @@ -0,0 +1,47 @@ +#ifndef LIGHT_H +#define LIGHT_H + +#include + +#include +#include + +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 diff --git a/destrum/include/destrum/Graphics/Pipelines/LightCullingPipeline.h b/destrum/include/destrum/Graphics/Pipelines/LightCullingPipeline.h new file mode 100644 index 0000000..0898daf --- /dev/null +++ b/destrum/include/destrum/Graphics/Pipelines/LightCullingPipeline.h @@ -0,0 +1,78 @@ +#ifndef LIGHTCULLINGPIPELINE_H +#define LIGHTCULLINGPIPELINE_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +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 lightCullingPipeline; + + struct PushConstants { + VkDeviceAddress sceneDataBuffer; + glm::uvec2 screenSize; + std::uint32_t depthTexId; + std::uint32_t padding; + }; + + struct PerFrameData { + AppendableBuffer lightsBuffer; + }; + + std::array 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 diff --git a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h index e2ce042..7feec65 100644 --- a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h @@ -27,6 +27,15 @@ public: VkFormat drawImageFormat, VkFormat depthImageFormat); + void drawDepthPrepass( + VkCommandBuffer cmd, + VkExtent2D renderExtent, + const RenderResources& resources, + const Camera& camera, + const GPUBuffer& sceneDataBuffer, + const std::vector& drawCommands, + const std::vector& sortedDrawCommands); + void draw( VkCommandBuffer cmd, VkExtent2D renderExtent, @@ -55,9 +64,20 @@ private: 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& drawCommands, + const std::vector& sortedDrawCommands); + private: VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; std::unique_ptr m_pipeline; + std::unique_ptr m_depthPrepassPipeline; bool m_renderWireframe{false}; }; diff --git a/destrum/include/destrum/Graphics/Pipelines/SkyboxPipeline.h b/destrum/include/destrum/Graphics/Pipelines/SkyboxPipeline.h index f627cfc..5db2935 100644 --- a/destrum/include/destrum/Graphics/Pipelines/SkyboxPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/SkyboxPipeline.h @@ -35,7 +35,6 @@ private: struct SkyboxPushConstants { glm::mat4 invViewProj; glm::vec4 skyboxRot[3]; - glm::vec3 cameraPos; std::uint32_t skyboxTextureId; }; diff --git a/destrum/include/destrum/Graphics/RenderResources.h b/destrum/include/destrum/Graphics/RenderResources.h index dccdca4..7638681 100644 --- a/destrum/include/destrum/Graphics/RenderResources.h +++ b/destrum/include/destrum/Graphics/RenderResources.h @@ -57,7 +57,10 @@ public: BindlessSetManager& getBindlessSetManager(); VkDescriptorSetLayout getBindlessDescSetLayout() 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; } const MeshCache& meshes() const { return *meshCache; } diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index db8f448..1bab387 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -20,9 +22,11 @@ #include "Pipelines/SkyboxPipeline.h" -class GameRenderer { +class GameRenderer +{ public: - struct SceneData { + struct SceneData + { const Camera& camera; glm::vec3 ambientColor; float ambientIntensity; @@ -40,11 +44,15 @@ public: void cleanup(GfxDevice& gfxDevice); 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; - void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) { + void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) + { createDrawImage(gfxDevice, newSize, false); + lightCullingPipeline->resize(gfxDevice, newSize); } Material& getMaterialMutable(MaterialID id); @@ -54,18 +62,31 @@ public: void flushMaterialUpdates(GfxDevice& gfxDevice); - [[nodiscard]] SkinningPipeline& getSkinningPipeline() const { + [[nodiscard]] SkinningPipeline& getSkinningPipeline() const + { return *skinningPipeline; } - void setRenderWireframe(bool wireframe) { + void setRenderWireframe(bool wireframe) + { meshPipeline->setRenderWireframe(wireframe); } - bool getRenderWireframe() { + bool getRenderWireframe() + { return meshPipeline->getRenderWireframe(); } + void setRenderLightHeatmap(bool heatmap) + { + renderLightHeatmap = heatmap; + } + + bool getRenderLightHeatmap() const + { + return renderLightHeatmap; + } + RenderResources* getResources() { return resources; @@ -96,33 +117,41 @@ private: VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; - struct GPUSceneData { - // camera + struct GPUSceneData + { glm::mat4 view; glm::mat4 proj; + glm::mat4 invProj; glm::mat4 viewProj; glm::vec4 cameraPos; - // ambient glm::vec3 ambientColor; float ambientIntensity; - // fog glm::vec3 fogColor; float fogDensity; VkDeviceAddress materialsBuffer; + + VkDeviceAddress lightsBuffer; + VkDeviceAddress tileLightIndicesBuffer; + std::uint32_t numLights; + std::uint32_t tileCountX; + std::uint32_t debugLightHeatmap; + std::uint32_t padding; }; NBuffer sceneDataBuffer; - std::unique_ptr meshPipeline; std::unique_ptr skyboxPipeline; std::unique_ptr lineRenderingPass; std::unique_ptr quadRendererPass; std::unique_ptr skinningPipeline; + std::unique_ptr lightCullingPipeline; + + bool renderLightHeatmap{false}; bool initialized{false}; }; diff --git a/destrum/include/destrum/Serialization/EngineComponentList.h b/destrum/include/destrum/Serialization/EngineComponentList.h index 3f02a02..6c70648 100644 --- a/destrum/include/destrum/Serialization/EngineComponentList.h +++ b/destrum/include/destrum/Serialization/EngineComponentList.h @@ -1,9 +1,8 @@ #ifndef 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 +#include #include #include #include @@ -15,6 +14,7 @@ #define DESTRUM_ENGINE_COMPONENTS(X) \ X(MeshRendererComponent) \ + X(LightComponent) \ X(Rotator) \ X(Spinner) \ X(OrbitAndSpin) \ diff --git a/destrum/include/destrum/Serialization/SceneSerializer.h b/destrum/include/destrum/Serialization/SceneSerializer.h index 51f2e8a..dd583c3 100644 --- a/destrum/include/destrum/Serialization/SceneSerializer.h +++ b/destrum/include/destrum/Serialization/SceneSerializer.h @@ -22,11 +22,8 @@ public: static bool Load(Scene& scene, const std::filesystem::path& path, std::function 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); - // Main-thread-only: construct a scene from pre-validated JSON. static bool ConstructScene(Scene& scene, LoadResult& result, std::function progressCallback = nullptr); diff --git a/destrum/src/Components/LightComponent.cpp b/destrum/src/Components/LightComponent.cpp new file mode 100644 index 0000000..b28052a --- /dev/null +++ b/destrum/src/Components/LightComponent.cpp @@ -0,0 +1,126 @@ +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include + +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()); + m_OuterConeAngle = std::clamp(outerConeAngle, m_InnerConeAngle, glm::half_pi()); +} + +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(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(); + if (type <= static_cast(LightType::Spot)) { + m_Type = static_cast(type); + } + } + if (data.contains("color")) { + const auto& value = data.at("color"); + m_Color = {value.at(0).get(), value.at(1).get(), value.at(2).get()}; + } + if (data.contains("intensity")) { + SetIntensity(data.at("intensity").get()); + } + if (data.contains("range")) { + SetRange(data.at("range").get()); + } + + float innerConeAngle = m_InnerConeAngle; + float outerConeAngle = m_OuterConeAngle; + if (data.contains("innerConeAngle")) { + innerConeAngle = data.at("innerConeAngle").get(); + } + if (data.contains("outerConeAngle")) { + outerConeAngle = data.at("outerConeAngle").get(); + } + SetConeAngles(innerConeAngle, outerConeAngle); +} + +void LightComponent::ImGuiInspector() { + constexpr std::array typeNames{"Directional", "Point", "Spot"}; + + int type = static_cast(m_Type); + if (ImGui::Combo("Type", &type, typeNames.data(), static_cast(typeNames.size()))) { + SetType(static_cast(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); + } + } +} diff --git a/destrum/src/Graphics/Pipelines/LightCullingPipeline.cpp b/destrum/src/Graphics/Pipelines/LightCullingPipeline.cpp new file mode 100644 index 0000000..6a9b3fc --- /dev/null +++ b/destrum/src/Graphics/Pipelines/LightCullingPipeline.cpp @@ -0,0 +1,271 @@ +#include + +#include "destrum/FS/AssetFS.h" +#include "destrum/Graphics/GfxDevice.h" +#include "destrum/Graphics/RenderResources.h" +#include "destrum/Graphics/Util.h" + +#include +#include +#include +#include +#include + +#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(layouts.size()); + pipelineLayoutInfo.pSetLayouts = layouts.data(); + pipelineLayoutInfo.pushConstantRangeCount = static_cast(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( + 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(drawImageSize.x) + TILE_SIZE - 1) / TILE_SIZE, + (static_cast(drawImageSize.y) + TILE_SIZE - 1) / TILE_SIZE, + }; + + const std::size_t numTiles = static_cast(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(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(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(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(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]; +} diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index aff552b..c9043fa 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -23,66 +23,122 @@ void MeshPipeline::init( VkFormat drawImageFormat, VkFormat depthImageFormat) { - try { - const auto vertexShader = - AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert"); + try + { + const auto vertexShader = + AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert"); - const auto fragShader = - AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag"); + const auto fragShader = + AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag"); - const auto bufferRange = VkPushConstantRange{ - .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, - .offset = 0, - .size = sizeof(PushConstants), - }; + const auto bufferRange = VkPushConstantRange{ + .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; - const auto pushConstantRanges = std::array{bufferRange}; + const auto pushConstantRanges = std::array{bufferRange}; - const auto layouts = std::array{ - resources.getBindlessDescSetLayout() - }; + const auto layouts = std::array{ + resources.getBindlessDescSetLayout() + }; - VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; - pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - pipelineLayoutInfo.setLayoutCount = static_cast(layouts.size()); - pipelineLayoutInfo.pSetLayouts = layouts.data(); - pipelineLayoutInfo.pushConstantRangeCount = - static_cast(pushConstantRanges.size()); - pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutInfo.setLayoutCount = static_cast(layouts.size()); + pipelineLayoutInfo.pSetLayouts = layouts.data(); + pipelineLayoutInfo.pushConstantRangeCount = + static_cast(pushConstantRanges.size()); + pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); - if (vkCreatePipelineLayout( + if (vkCreatePipelineLayout( gfxDevice.getDevice(), &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) - { - 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( + gfxDevice, + vertexShader.string(), + "", + pipelineConfig + ); + } + + { + PipelineConfigInfo pipelineConfig{}; + Pipeline::DefaultPipelineConfigInfo(pipelineConfig); + + pipelineConfig.name = "Mesh Pipeline"; + pipelineConfig.pipelineLayout = m_pipelineLayout; + + pipelineConfig.vertexAttributeDescriptions = {}; + pipelineConfig.vertexBindingDescriptions = {}; + + pipelineConfig.colorAttachments = {drawImageFormat}; + 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( + gfxDevice, + vertexShader.string(), + fragShader.string(), + pipelineConfig + ); + } } - - PipelineConfigInfo pipelineConfig{}; - Pipeline::DefaultPipelineConfigInfo(pipelineConfig); - - pipelineConfig.name = "Mesh Pipeline"; - pipelineConfig.pipelineLayout = m_pipelineLayout; - - pipelineConfig.vertexAttributeDescriptions = {}; - pipelineConfig.vertexBindingDescriptions = {}; - - pipelineConfig.colorAttachments = {drawImageFormat}; - pipelineConfig.depthAttachment = depthImageFormat; - - m_pipeline = std::make_unique( - gfxDevice, - vertexShader.string(), - fragShader.string(), - pipelineConfig - ); - } catch (...) { + catch (...) + { cleanup(gfxDevice.getDevice()); throw; } } +void MeshPipeline::drawDepthPrepass( + VkCommandBuffer cmd, + VkExtent2D renderExtent, + const RenderResources& resources, + const Camera& camera, + const GPUBuffer& sceneDataBuffer, + const std::vector& drawCommands, + const std::vector& sortedDrawCommands) +{ + if (!m_depthPrepassPipeline) + { + return; + } + + drawMeshes( + cmd, + *m_depthPrepassPipeline, + renderExtent, + resources, + camera, + sceneDataBuffer, + drawCommands, + sortedDrawCommands); +} + void MeshPipeline::draw( VkCommandBuffer cmd, VkExtent2D renderExtent, @@ -92,13 +148,35 @@ void MeshPipeline::draw( const std::vector& drawCommands, const std::vector& sortedDrawCommands) { - if (!m_pipeline) { + if (!m_pipeline) + { 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& drawCommands, + const std::vector& sortedDrawCommands) +{ const MeshCache& meshCache = resources.meshes(); - m_pipeline->bind(cmd); + pipeline.bind(cmd); resources.bindBindlessDescSet(cmd, m_pipelineLayout); @@ -130,8 +208,10 @@ void MeshPipeline::draw( const auto frustum = edge::createFrustumFromCamera(camera); - const auto drawOne = [&](const MeshDrawCommand& dc) { - if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { + const auto drawOne = [&](const MeshDrawCommand& dc) + { + if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) + { return; } @@ -139,7 +219,8 @@ void MeshPipeline::draw( const auto& mesh = meshCache.getMesh(dc.meshId); - if (dc.meshId != prevMeshId) { + if (dc.meshId != prevMeshId) + { prevMeshId = dc.meshId; vkCmdBindIndexBuffer( @@ -156,9 +237,9 @@ void MeshPipeline::draw( .transform = dc.transformMatrix, .sceneDataBuffer = sceneDataBuffer.address, .vertexBuffer = - dc.skinnedMesh != nullptr - ? dc.skinnedMesh->skinnedVertexBuffer.address - : mesh.vertexBuffer.address, + dc.skinnedMesh != nullptr + ? dc.skinnedMesh->skinnedVertexBuffer.address + : mesh.vertexBuffer.address, .materialId = dc.materialId, .padding = 0, }; @@ -175,26 +256,30 @@ void MeshPipeline::draw( vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0); }; - if (!sortedDrawCommands.empty()) { - for (std::size_t drawIndex : sortedDrawCommands) { + if (!sortedDrawCommands.empty()) + { + for (std::size_t drawIndex : sortedDrawCommands) + { assert(drawIndex < drawCommands.size()); drawOne(drawCommands[drawIndex]); } - } else { - for (const MeshDrawCommand& dc : drawCommands) { + } + else + { + for (const MeshDrawCommand& dc : drawCommands) + { drawOne(dc); } } - - // Optional: - // spdlog::debug("Actual mesh draw calls: {}", actualDrawCalls); } void MeshPipeline::cleanup(VkDevice device) { 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); } m_pipelineLayout = VK_NULL_HANDLE; diff --git a/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp b/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp index 7b53623..b46db4e 100644 --- a/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp @@ -119,14 +119,16 @@ void SkyboxPipeline::draw( 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{ - .invViewProj = glm::inverse(camera.GetViewProjectionMatrix()), + .invViewProj = glm::inverse(camera.GetProjectionMatrix() * viewRotation), .skyboxRot = { glm::vec4(r[0], 0.f), glm::vec4(r[1], 0.f), glm::vec4(r[2], 0.f) }, - .cameraPos = camera.GetPosition(), .skyboxTextureId = static_cast(skyboxTextureId), }; diff --git a/destrum/src/Graphics/RenderResources.cpp b/destrum/src/Graphics/RenderResources.cpp index 5add01c..d5106b0 100644 --- a/destrum/src/Graphics/RenderResources.cpp +++ b/destrum/src/Graphics/RenderResources.cpp @@ -266,10 +266,13 @@ const VkDescriptorSet& RenderResources::getBindlessDescSet() const { return imageCache->bindlessSetManager.getDescSet(); } -void RenderResources::bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const { +void RenderResources::bindBindlessDescSet( + VkCommandBuffer cmd, + VkPipelineLayout layout, + VkPipelineBindPoint bindPoint) const { vkCmdBindDescriptorSets( cmd, - VK_PIPELINE_BIND_POINT_GRAPHICS, + bindPoint, layout, 0, 1, diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index af8640e..899245b 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -21,7 +21,8 @@ GameRenderer::GameRenderer() void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::ivec2 drawImageSize) { - try { + try + { resources = &_resources; sceneDataBuffer.init( gfxDevice, @@ -34,12 +35,15 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm:: meshPipeline = std::make_unique(); meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); -skyboxPipeline = std::make_unique(); + skyboxPipeline = std::make_unique(); skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); skinningPipeline = std::make_unique(); skinningPipeline->init(gfxDevice); + lightCullingPipeline = std::make_unique(); + lightCullingPipeline->init(gfxDevice, _resources, drawImageSize); + lineRenderingPass = std::make_unique(); lineRenderingPass->init(gfxDevice, drawImageFormat); @@ -48,7 +52,9 @@ skyboxPipeline = std::make_unique(); GameState::GetInstance().SetRenderer(this); initialized = true; - } catch (...) { + } + catch (...) + { cleanup(gfxDevice); throw; } @@ -59,6 +65,7 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice) flushMaterialUpdates(gfxDevice); meshDrawCommands.clear(); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); + lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); QuadRenderingManager::GetInstance().ClearQuads(); } @@ -70,13 +77,16 @@ void GameRenderer::endDrawing() std::stable_sort( sortedMeshDrawCommands.begin(), 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& rhs = meshDrawCommands[right]; - if (lhs.meshId != rhs.meshId) { + if (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.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{ .view = sceneData.camera.GetViewMatrix(), .proj = sceneData.camera.GetProjectionMatrix(), + .invProj = glm::inverse(sceneData.camera.GetProjectionMatrix()), .viewProj = sceneData.camera.GetViewProjectionMatrix(), .cameraPos = glm::vec4{sceneData.camera.m_position, 1.f}, .ambientColor = {sceneData.ambientColor}, @@ -116,6 +134,12 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& .fogColor = {sceneData.fogColor}, .fogDensity = sceneData.fogDensity, .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); } + { + 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({ .renderExtent = drawImage.getExtent2D(), .colorImageView = drawImage.imageView, .colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f}, .depthImageView = depthImage.imageView, - .depthImageClearValue = 1.f, }); renderInfo.renderingInfo.pColorAttachments = &renderInfo.colorAttachment; renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment; @@ -226,11 +298,11 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& *resources, drawImage, glm::ortho(0.0f, - static_cast(drawImage.extent.width), - static_cast(drawImage.extent.height), - 0.0f, -1.0f, 1.0f)); + static_cast(drawImage.extent.width), + static_cast(drawImage.extent.height), + 0.0f, -1.0f, 1.0f)); } -// vkutil::cmdEndLabel(cmd); + // vkutil::cmdEndLabel(cmd); } void GameRenderer::drawQuads( @@ -246,11 +318,12 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) { VkDevice device = gfxDevice.getDevice(); - if (device != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) + { vkDeviceWaitIdle(device); } -if (skinningPipeline) + if (skinningPipeline) skinningPipeline->cleanup(gfxDevice); if (lineRenderingPass) @@ -259,6 +332,9 @@ if (skinningPipeline) if (quadRendererPass) quadRendererPass->cleanup(gfxDevice); + if (lightCullingPipeline) + lightCullingPipeline->cleanup(gfxDevice); + if (skyboxPipeline) skyboxPipeline->cleanup(device); @@ -281,6 +357,7 @@ if (skinningPipeline) skinningPipeline.reset(); lineRenderingPass.reset(); quadRendererPass.reset(); + lightCullingPipeline.reset(); pendingMaterialUploads.clear(); meshDrawCommands.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 { assert(resources); @@ -349,7 +431,8 @@ void GameRenderer::setSkyboxTexture(ImageID skyboxImageId) void GameRenderer::flushMaterialUpdates(GfxDevice& gfxDevice) { - if (!pendingMaterialUploads.empty()) { + if (!pendingMaterialUploads.empty()) + { // Material data is currently shared by all frame contexts. Wait before // mutating it so an older in-flight frame cannot read the overwritten // range. This can later be replaced with per-frame material buffers. diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 575b526..2fef89b 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "glm/gtc/constants.hpp" #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" #include @@ -20,6 +21,7 @@ #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/ModelDoc.h" #include "destrum/Components/Animator.h" +#include "destrum/Components/LightComponent.h" #include #include @@ -447,6 +449,41 @@ void LightKeeper::customInit() triggerObj->AddComponent(); 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(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(i) / static_cast(pointLightCount); + const float angle = t * glm::two_pi(); + + 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(LightType::Point); + light->SetColor(glm::vec3{ + 0.5f + 0.5f * std::cos(glm::two_pi() * t), + 0.5f + 0.5f * std::cos(glm::two_pi() * (t + 0.33f)), + 0.5f + 0.5f * std::cos(glm::two_pi() * (t + 0.67f)) + }); + light->SetIntensity(1.0f); + light->SetRange(3.0f); + + auto* orbit = lightObj->AddComponent(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"); playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f)); auto* fps = playerObj->AddComponent(); @@ -553,6 +590,11 @@ void LightKeeper::drawDebugMenuBar() ImGui::MenuItem("Box Colliders", nullptr, &renderBoxColliders); ImGui::MenuItem("2D Quads", nullptr, &renderQuads); ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines); + + bool lightHeatmap = renderer.getRenderLightHeatmap(); + if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) { + renderer.setRenderLightHeatmap(lightHeatmap); + } ImGui::EndMenu(); } diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index f1bf47b..6aac3bc 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -171,6 +172,7 @@ namespace { const char* canonicalNames[] = { "MeshRendererComponent", + "LightComponent", "Rotator", "Spinner", "OrbitAndSpin", @@ -447,6 +449,39 @@ namespace { "Animator scene data must not embed animation keyframes"); } + void testLightComponentRoundTrip() + { + GameObject object{"light"}; + auto* light = object.AddComponent(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(); + 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() { const auto material = Material::SimpleColor( @@ -561,6 +596,7 @@ int main() testJoltSphereCollisions(); testSceneJoltSphereCollisions(); testAnimatorAssetReferences(); + testLightComponentRoundTrip(); testSimpleColorMaterial(); testAssetReferenceCacheKeys(); testTriggerZone();