From faba4171ec9a7df4191c78a8d2c504f07c8379b9 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Fri, 16 Oct 2026 23:23:26 +0200 Subject: [PATCH] feat: revamp 2D pipeline to use SDF's for rendering --- destrum/CMakeLists.txt | 4 +- destrum/assets_src/shaders/quad.frag | 16 - destrum/assets_src/shaders/quad.vert | 22 -- destrum/assets_src/shaders/shape2d.frag | 70 ++++ destrum/assets_src/shaders/shape2d.glsl | 32 ++ destrum/assets_src/shaders/shape2d.vert | 33 ++ .../Graphics/Managers/QuadRenderingManager.h | 45 --- .../destrum/Graphics/Managers/Renderer2D.h | 108 ++++++ .../{QuadRendererPass.h => Renderer2DPass.h} | 28 +- destrum/include/destrum/Graphics/Renderer.h | 6 +- .../Managers/QuadRenderingManager.cpp | 31 -- destrum/src/Graphics/Managers/Renderer2D.cpp | 133 +++++++ .../Graphics/Pipelines/QuadRendererPass.cpp | 338 ------------------ .../src/Graphics/Pipelines/Renderer2DPass.cpp | 217 +++++++++++ destrum/src/Graphics/Renderer.cpp | 29 +- lightkeeper/include/Lightkeeper.h | 4 +- lightkeeper/src/Lightkeeper.cpp | 2 +- lightkeeper/src/LightkeeperDebugUi.cpp | 10 +- tests/destrum_tests.cpp | 50 ++- 19 files changed, 676 insertions(+), 502 deletions(-) delete mode 100644 destrum/assets_src/shaders/quad.frag delete mode 100644 destrum/assets_src/shaders/quad.vert create mode 100644 destrum/assets_src/shaders/shape2d.frag create mode 100644 destrum/assets_src/shaders/shape2d.glsl create mode 100644 destrum/assets_src/shaders/shape2d.vert delete mode 100644 destrum/include/destrum/Graphics/Managers/QuadRenderingManager.h create mode 100644 destrum/include/destrum/Graphics/Managers/Renderer2D.h rename destrum/include/destrum/Graphics/Pipelines/{QuadRendererPass.h => Renderer2DPass.h} (60%) delete mode 100644 destrum/src/Graphics/Managers/QuadRenderingManager.cpp create mode 100644 destrum/src/Graphics/Managers/Renderer2D.cpp delete mode 100644 destrum/src/Graphics/Pipelines/QuadRendererPass.cpp create mode 100644 destrum/src/Graphics/Pipelines/Renderer2DPass.cpp diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 73fb689..4be1012 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -73,7 +73,7 @@ set(SRC_FILES "src/Graphics/Managers/FrameManager.cpp" "src/Graphics/Managers/ImageManager.cpp" "src/Graphics/Managers/LineRenderingManager.cpp" - "src/Graphics/Managers/QuadRenderingManager.cpp" + "src/Graphics/Managers/Renderer2D.cpp" "src/Graphics/Resources/GPUImage.cpp" "src/Graphics/Resources/NBuffer.cpp" @@ -82,7 +82,7 @@ set(SRC_FILES "src/Graphics/Pipelines/MeshPipeline.cpp" "src/Graphics/Pipelines/LineRenderingPass.cpp" - "src/Graphics/Pipelines/QuadRendererPass.cpp" + "src/Graphics/Pipelines/Renderer2DPass.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp" "src/Graphics/Pipelines/LightCullingPipeline.cpp" "src/Graphics/Pipelines/ShadowMapPipeline.cpp" diff --git a/destrum/assets_src/shaders/quad.frag b/destrum/assets_src/shaders/quad.frag deleted file mode 100644 index 9e0c33f..0000000 --- a/destrum/assets_src/shaders/quad.frag +++ /dev/null @@ -1,16 +0,0 @@ -#version 460 -#extension GL_GOOGLE_include_directive : require -#extension GL_EXT_nonuniform_qualifier : enable - -#include "bindless.glsl" - -layout(location = 0) in vec2 inUV; -layout(location = 1) in vec4 inColor; -layout(location = 2) flat in uint inTexId; - -layout(location = 0) out vec4 outFragColor; - -void main() { - vec4 texColor = sampleTexture2DLinear(inTexId, inUV); - outFragColor = texColor * inColor; -} \ No newline at end of file diff --git a/destrum/assets_src/shaders/quad.vert b/destrum/assets_src/shaders/quad.vert deleted file mode 100644 index c3dbb20..0000000 --- a/destrum/assets_src/shaders/quad.vert +++ /dev/null @@ -1,22 +0,0 @@ -#version 460 -#extension GL_GOOGLE_include_directive : require - -layout(location = 0) in vec2 inPosition; -layout(location = 1) in vec2 inUV; -layout(location = 2) in vec4 inColor; -layout(location = 3) in uint inTexId; - -layout(location = 0) out vec2 outUV; -layout(location = 1) out vec4 outColor; -layout(location = 2) flat out uint outTexId; - -layout(push_constant) uniform QuadPushConstants { - mat4 viewProjection; -} pcs; - -void main() { - gl_Position = pcs.viewProjection * vec4(inPosition, 0.0, 1.0); - outUV = inUV; - outColor = inColor; - outTexId = inTexId; -} \ No newline at end of file diff --git a/destrum/assets_src/shaders/shape2d.frag b/destrum/assets_src/shaders/shape2d.frag new file mode 100644 index 0000000..8a2b5e7 --- /dev/null +++ b/destrum/assets_src/shaders/shape2d.frag @@ -0,0 +1,70 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_nonuniform_qualifier : enable + +#include "bindless.glsl" +#include "shape2d.glsl" + +layout (location = 0) in vec2 inLocalPos; +layout (location = 1) in vec2 inQuadPos; +layout (location = 2) flat in uint inShapeIndex; + +layout (location = 0) out vec4 outFragColor; + +float sdRoundedRect(vec2 p, vec2 halfSize, float radius) +{ + radius = min(radius, min(halfSize.x, halfSize.y)); + vec2 q = abs(p) - halfSize + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + +// Approximate, but exact on the boundary which is what the edge and outline need +float sdEllipse(vec2 p, vec2 radii) +{ + radii = max(radii, vec2(1e-4)); + float k0 = length(p / radii); + float k1 = length(p / (radii * radii)); + return k0 < 1e-4 ? -min(radii.x, radii.y) : k0 * (k0 - 1.0) / k1; +} + +float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) +{ + vec2 e0 = p1 - p0, e1 = p2 - p1, e2 = p0 - p2; + vec2 v0 = p - p0, v1 = p - p1, v2 = p - p2; + vec2 pq0 = v0 - e0 * clamp(dot(v0, e0) / max(dot(e0, e0), 1e-8), 0.0, 1.0); + vec2 pq1 = v1 - e1 * clamp(dot(v1, e1) / max(dot(e1, e1), 1e-8), 0.0, 1.0); + vec2 pq2 = v2 - e2 * clamp(dot(v2, e2) / max(dot(e2, e2), 1e-8), 0.0, 1.0); + float winding = sign(e0.x * e2.y - e0.y * e2.x); + vec2 d = min(min(vec2(dot(pq0, pq0), winding * (v0.x * e0.y - v0.y * e0.x)), + vec2(dot(pq1, pq1), winding * (v1.x * e1.y - v1.y * e1.x))), + vec2(dot(pq2, pq2), winding * (v2.x * e2.y - v2.y * e2.x))); + return -sqrt(d.x) * sign(d.y); +} + +void main() +{ + Shape2D shape = pcs.shapeBuffer.shapes[inShapeIndex]; + + float dist; + if (shape.type == SHAPE_ELLIPSE) { + dist = sdEllipse(inLocalPos, shape.halfSize); + } else if (shape.type == SHAPE_TRIANGLE) { + dist = sdTriangle(inLocalPos, shape.p0, shape.p1, shape.p2); + } else { + dist = sdRoundedRect(inLocalPos, shape.halfSize, shape.cornerRadius); + } + + // Outlines grow inwards so the outer edge matches the filled shape + if (shape.outline > 0.0) { + dist = abs(dist + shape.outline * 0.5) - shape.outline * 0.5; + } + + float edgeWidth = max(fwidth(dist), 1e-4); + float coverage = clamp(0.5 - dist / edgeWidth, 0.0, 1.0); + + vec2 uv = mix(shape.uvRect.xy, shape.uvRect.zw, clamp(inQuadPos, 0.0, 1.0)); + vec4 texColor = sampleTexture2D(shape.textureId, shape.samplerId, uv); + + outFragColor = texColor * shape.color; + outFragColor.a *= coverage; +} diff --git a/destrum/assets_src/shaders/shape2d.glsl b/destrum/assets_src/shaders/shape2d.glsl new file mode 100644 index 0000000..0ed296c --- /dev/null +++ b/destrum/assets_src/shaders/shape2d.glsl @@ -0,0 +1,32 @@ +#extension GL_EXT_buffer_reference : require +#extension GL_EXT_scalar_block_layout : require + +#define SHAPE_RECT 0 +#define SHAPE_ELLIPSE 1 +#define SHAPE_TRIANGLE 2 + +// Mirrors Shape2D in Renderer2D.h +struct Shape2D { + vec2 center; + vec2 halfSize; + vec4 color; + vec4 uvRect; + vec2 p0; + vec2 p1; + vec2 p2; + float rotation; + float outline; + float cornerRadius; + uint type; + uint textureId; + uint samplerId; +}; + +layout (buffer_reference, scalar) readonly buffer Shape2DBuffer { + Shape2D shapes[]; +}; + +layout (push_constant, scalar) uniform Shape2DPushConstants { + mat4 projection; + Shape2DBuffer shapeBuffer; +} pcs; diff --git a/destrum/assets_src/shaders/shape2d.vert b/destrum/assets_src/shaders/shape2d.vert new file mode 100644 index 0000000..6a0ac57 --- /dev/null +++ b/destrum/assets_src/shaders/shape2d.vert @@ -0,0 +1,33 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require + +#include "shape2d.glsl" + +layout (location = 0) out vec2 outLocalPos; +layout (location = 1) out vec2 outQuadPos; +layout (location = 2) flat out uint outShapeIndex; + +// Room for the antialiased edge outside the shape's bounds +const float EDGE_MARGIN = 1.0; + +const vec2 CORNERS[6] = vec2[]( + vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(1.0, 1.0), + vec2(-1.0, -1.0), vec2(1.0, 1.0), vec2(-1.0, 1.0) +); + +void main() +{ + Shape2D shape = pcs.shapeBuffer.shapes[gl_InstanceIndex]; + + vec2 corner = CORNERS[gl_VertexIndex]; + vec2 localPos = corner * (shape.halfSize + EDGE_MARGIN); + + float c = cos(shape.rotation); + float s = sin(shape.rotation); + vec2 rotated = vec2(localPos.x * c - localPos.y * s, localPos.x * s + localPos.y * c); + + gl_Position = pcs.projection * vec4(shape.center + rotated, 0.0, 1.0); + outLocalPos = localPos; + outQuadPos = localPos / max(shape.halfSize, vec2(1e-6)) * 0.5 + 0.5; + outShapeIndex = gl_InstanceIndex; +} diff --git a/destrum/include/destrum/Graphics/Managers/QuadRenderingManager.h b/destrum/include/destrum/Graphics/Managers/QuadRenderingManager.h deleted file mode 100644 index b454383..0000000 --- a/destrum/include/destrum/Graphics/Managers/QuadRenderingManager.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef DESTRUM_QUADRENDERINGMANAGER_H -#define DESTRUM_QUADRENDERINGMANAGER_H - -#include - -#include - -#include -#include - -struct QuadSubmission { - glm::vec2 position; - glm::vec2 size; - float rotation; - glm::vec4 color; - ImageID textureId; -}; - -class QuadRenderingManager final : public Singleton { -public: - friend class Singleton; - - void SubmitQuad( - const glm::vec2& position, - const glm::vec2& size, - const glm::vec4& color = glm::vec4{1.0f}, - ImageID textureId = NULL_IMAGE_ID); - - void SubmitQuad( - const glm::vec2& position, - const glm::vec2& size, - float rotation, - const glm::vec4& color = glm::vec4{1.0f}, - ImageID textureId = NULL_IMAGE_ID); - - [[nodiscard]] const std::vector& GetQuads() const { return quads; } - void ClearQuads(); - -private: - QuadRenderingManager() = default; - - std::vector quads; -}; - -#endif // DESTRUM_QUADRENDERINGMANAGER_H \ No newline at end of file diff --git a/destrum/include/destrum/Graphics/Managers/Renderer2D.h b/destrum/include/destrum/Graphics/Managers/Renderer2D.h new file mode 100644 index 0000000..af924f3 --- /dev/null +++ b/destrum/include/destrum/Graphics/Managers/Renderer2D.h @@ -0,0 +1,108 @@ +#ifndef DESTRUM_RENDERER2D_H +#define DESTRUM_RENDERER2D_H + +#include +#include + +#include + +#include +#include + +enum class Shape2DType : std::uint32_t { + Rect = 0, + Ellipse = 1, + Triangle = 2, +}; + +enum class TextureFilter : std::uint32_t { + Nearest = 0, + Linear = 1, +}; + +enum class LineCap { + Butt, + Square, + Round, +}; + +struct Texture2D { + ImageID id{NULL_IMAGE_ID}; + glm::vec2 uvMin{0.0f, 0.0f}; + glm::vec2 uvMax{1.0f, 1.0f}; + TextureFilter filter{TextureFilter::Linear}; +}; + +struct ShapeStyle { + glm::vec4 color{1.0f}; + float outline{0.0f}; + float rotation{0.0f}; + float cornerRadius{0.0f}; + Texture2D texture{}; +}; + +// Mirrors Shape2D in shape2d.glsl (scalar layout) +struct Shape2D { + glm::vec2 center; + glm::vec2 halfSize; + glm::vec4 color; + glm::vec4 uvRect; + glm::vec2 p0; + glm::vec2 p1; + glm::vec2 p2; + float rotation; + float outline; + float cornerRadius; + Shape2DType type; + std::uint32_t textureId; + TextureFilter filter; +}; +static_assert(sizeof(Shape2D) == 96); + +// Screen space, in draw image pixels, origin top left, y down. Drawn in submission order. +class Renderer2D final : public Singleton { +public: + friend class Singleton; + + void DrawRect(const glm::vec2& topLeft, const glm::vec2& size, const glm::vec4& color); + void DrawRect(const glm::vec2& topLeft, const glm::vec2& size, const ShapeStyle& style); + void DrawRectOutline(const glm::vec2& topLeft, const glm::vec2& size, float thickness, const glm::vec4& color); + void DrawTexturedRect( + const glm::vec2& topLeft, + const glm::vec2& size, + ImageID texture, + const glm::vec4& tint = glm::vec4{1.0f}); + + void DrawEllipse(const glm::vec2& center, const glm::vec2& radii, const glm::vec4& color); + void DrawEllipse(const glm::vec2& center, const glm::vec2& radii, const ShapeStyle& style); + void DrawEllipseOutline(const glm::vec2& center, const glm::vec2& radii, float thickness, const glm::vec4& color); + void DrawCircle(const glm::vec2& center, float radius, const glm::vec4& color); + + void DrawTriangle(const glm::vec2& a, const glm::vec2& b, const glm::vec2& c, const glm::vec4& color); + void DrawTriangle(const glm::vec2& a, const glm::vec2& b, const glm::vec2& c, const ShapeStyle& style); + void DrawTriangleOutline( + const glm::vec2& a, + const glm::vec2& b, + const glm::vec2& c, + float thickness, + const glm::vec4& color); + + void DrawLine( + const glm::vec2& from, + const glm::vec2& to, + float thickness, + const glm::vec4& color, + LineCap cap = LineCap::Butt); + + [[nodiscard]] const std::vector& GetShapes() const { return shapes; } + void Clear(); + +private: + Renderer2D() = default; + + void submit(Shape2DType type, const glm::vec2& center, const glm::vec2& halfSize, const ShapeStyle& style); + + std::vector shapes; +}; + +#endif // DESTRUM_RENDERER2D_H diff --git a/destrum/include/destrum/Graphics/Pipelines/QuadRendererPass.h b/destrum/include/destrum/Graphics/Pipelines/Renderer2DPass.h similarity index 60% rename from destrum/include/destrum/Graphics/Pipelines/QuadRendererPass.h rename to destrum/include/destrum/Graphics/Pipelines/Renderer2DPass.h index a0b8992..a7bd2ef 100644 --- a/destrum/include/destrum/Graphics/Pipelines/QuadRendererPass.h +++ b/destrum/include/destrum/Graphics/Pipelines/Renderer2DPass.h @@ -1,5 +1,5 @@ -#ifndef DESTRUM_QUADRENDERERPASS_H -#define DESTRUM_QUADRENDERERPASS_H +#ifndef DESTRUM_RENDERER2DPASS_H +#define DESTRUM_RENDERER2DPASS_H #include #include @@ -8,15 +8,14 @@ #include -#include - +#include class GfxDevice; class Pipeline; struct GPUImage; class RenderResources; -class QuadRendererPass final { +class Renderer2DPass final { public: void init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat); void draw( @@ -27,21 +26,16 @@ public: const glm::mat4& projection); void cleanup(GfxDevice& gfxDevice); -private: - struct QuadVertex { - glm::vec2 position; - glm::vec2 uv; - glm::vec4 color; - std::uint32_t textureId; - }; + // Top left origin, y down, in target pixels + [[nodiscard]] static glm::mat4 screenProjection(const GPUImage& target); - void ensureCapacity(GfxDevice& gfxDevice, std::size_t requiredQuads); +private: + void ensureCapacity(GfxDevice& gfxDevice, std::size_t requiredShapes); VkPipelineLayout pipelineLayout{VK_NULL_HANDLE}; std::unique_ptr pipeline; - GPUBuffer vertexBuffer{}; - GPUBuffer indexBuffer{}; - std::size_t quadCapacity{0}; + NBuffer shapeBuffer; + std::size_t shapeCapacity{0}; }; -#endif // DESTRUM_QUADRENDERERPASS_H \ No newline at end of file +#endif // DESTRUM_RENDERER2DPASS_H diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 86cedb6..80a2055 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include #include @@ -105,7 +105,7 @@ public: return resources; } - void drawQuads( + void draw2D( VkCommandBuffer cmd, GfxDevice& gfxDevice, const glm::mat4& projection); @@ -163,7 +163,7 @@ private: std::unique_ptr meshPipeline; std::unique_ptr skyboxPipeline; std::unique_ptr lineRenderingPass; - std::unique_ptr quadRendererPass; + std::unique_ptr renderer2DPass; std::unique_ptr skinningPipeline; std::unique_ptr lightCullingPipeline; diff --git a/destrum/src/Graphics/Managers/QuadRenderingManager.cpp b/destrum/src/Graphics/Managers/QuadRenderingManager.cpp deleted file mode 100644 index 493c4d7..0000000 --- a/destrum/src/Graphics/Managers/QuadRenderingManager.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include - -void QuadRenderingManager::SubmitQuad( - const glm::vec2& position, - const glm::vec2& size, - const glm::vec4& color, - ImageID textureId) -{ - SubmitQuad(position, size, 0.0f, color, textureId); -} - -void QuadRenderingManager::SubmitQuad( - const glm::vec2& position, - const glm::vec2& size, - float rotation, - const glm::vec4& color, - ImageID textureId) -{ - quads.push_back(QuadSubmission{ - .position = position, - .size = size, - .rotation = rotation, - .color = color, - .textureId = textureId, - }); -} - -void QuadRenderingManager::ClearQuads() -{ - quads.clear(); -} \ No newline at end of file diff --git a/destrum/src/Graphics/Managers/Renderer2D.cpp b/destrum/src/Graphics/Managers/Renderer2D.cpp new file mode 100644 index 0000000..2bbfcff --- /dev/null +++ b/destrum/src/Graphics/Managers/Renderer2D.cpp @@ -0,0 +1,133 @@ +#include + +#include +#include + +void Renderer2D::DrawRect(const glm::vec2& topLeft, const glm::vec2& size, const glm::vec4& color) +{ + DrawRect(topLeft, size, ShapeStyle{.color = color}); +} + +void Renderer2D::DrawRect(const glm::vec2& topLeft, const glm::vec2& size, const ShapeStyle& style) +{ + submit(Shape2DType::Rect, topLeft + size * 0.5f, size * 0.5f, style); +} + +void Renderer2D::DrawRectOutline( + const glm::vec2& topLeft, + const glm::vec2& size, + float thickness, + const glm::vec4& color) +{ + DrawRect(topLeft, size, ShapeStyle{.color = color, .outline = thickness}); +} + +void Renderer2D::DrawTexturedRect( + const glm::vec2& topLeft, + const glm::vec2& size, + ImageID texture, + const glm::vec4& tint) +{ + DrawRect(topLeft, size, ShapeStyle{.color = tint, .texture = {.id = texture}}); +} + +void Renderer2D::DrawEllipse(const glm::vec2& center, const glm::vec2& radii, const glm::vec4& color) +{ + DrawEllipse(center, radii, ShapeStyle{.color = color}); +} + +void Renderer2D::DrawEllipse(const glm::vec2& center, const glm::vec2& radii, const ShapeStyle& style) +{ + submit(Shape2DType::Ellipse, center, radii, style); +} + +void Renderer2D::DrawEllipseOutline( + const glm::vec2& center, + const glm::vec2& radii, + float thickness, + const glm::vec4& color) +{ + DrawEllipse(center, radii, ShapeStyle{.color = color, .outline = thickness}); +} + +void Renderer2D::DrawCircle(const glm::vec2& center, float radius, const glm::vec4& color) +{ + DrawEllipse(center, glm::vec2{radius}, color); +} + +void Renderer2D::DrawTriangle(const glm::vec2& a, const glm::vec2& b, const glm::vec2& c, const glm::vec4& color) +{ + DrawTriangle(a, b, c, ShapeStyle{.color = color}); +} + +void Renderer2D::DrawTriangle(const glm::vec2& a, const glm::vec2& b, const glm::vec2& c, const ShapeStyle& style) +{ + const glm::vec2 min = glm::min(a, glm::min(b, c)); + const glm::vec2 max = glm::max(a, glm::max(b, c)); + const glm::vec2 center = (min + max) * 0.5f; + + submit(Shape2DType::Triangle, center, (max - min) * 0.5f, style); + shapes.back().p0 = a - center; + shapes.back().p1 = b - center; + shapes.back().p2 = c - center; +} + +void Renderer2D::DrawTriangleOutline( + const glm::vec2& a, + const glm::vec2& b, + const glm::vec2& c, + float thickness, + const glm::vec4& color) +{ + DrawTriangle(a, b, c, ShapeStyle{.color = color, .outline = thickness}); +} + +void Renderer2D::DrawLine( + const glm::vec2& from, + const glm::vec2& to, + float thickness, + const glm::vec4& color, + LineCap cap) +{ + const glm::vec2 delta = to - from; + const float halfThickness = thickness * 0.5f; + const float capExtension = cap == LineCap::Butt ? 0.0f : halfThickness; + + submit( + Shape2DType::Rect, + (from + to) * 0.5f, + {glm::length(delta) * 0.5f + capExtension, halfThickness}, + ShapeStyle{ + .color = color, + .rotation = std::atan2(delta.y, delta.x), + .cornerRadius = cap == LineCap::Round ? halfThickness : 0.0f, + }); +} + +void Renderer2D::Clear() +{ + shapes.clear(); +} + +void Renderer2D::submit( + Shape2DType type, + const glm::vec2& center, + const glm::vec2& halfSize, + const ShapeStyle& style) +{ + shapes.push_back(Shape2D{ + .center = center, + .halfSize = glm::abs(halfSize), + .color = style.color, + .uvRect = {style.texture.uvMin.x, style.texture.uvMin.y, style.texture.uvMax.x, style.texture.uvMax.y}, + .p0 = {}, + .p1 = {}, + .p2 = {}, + .rotation = style.rotation, + .outline = std::max(style.outline, 0.0f), + .cornerRadius = std::max(style.cornerRadius, 0.0f), + .type = type, + .textureId = style.texture.id, + .filter = style.texture.filter, + }); +} diff --git a/destrum/src/Graphics/Pipelines/QuadRendererPass.cpp b/destrum/src/Graphics/Pipelines/QuadRendererPass.cpp deleted file mode 100644 index c9c9cf6..0000000 --- a/destrum/src/Graphics/Pipelines/QuadRendererPass.cpp +++ /dev/null @@ -1,338 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "volk.h" - -namespace { - constexpr std::size_t InitialQuadCapacity = 256; - constexpr std::size_t VerticesPerQuad = 4; - constexpr std::size_t IndicesPerQuad = 6; - - glm::vec2 rotatePoint(const glm::vec2& point, float cosA, float sinA) { - return glm::vec2{ - point.x * cosA - point.y * sinA, - point.x * sinA + point.y * cosA, - }; - } -} - -void QuadRendererPass::init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat) -{ - try { - const std::size_t vertexBytes = InitialQuadCapacity * VerticesPerQuad * sizeof(QuadVertex); - vertexBuffer = gfxDevice.createBuffer( - vertexBytes, - VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, - VMA_MEMORY_USAGE_AUTO_PREFER_HOST); - vkutil::addDebugLabel(gfxDevice.getDevice(), vertexBuffer.buffer, "quad vertices"); - - const std::size_t indexBytes = InitialQuadCapacity * IndicesPerQuad * sizeof(std::uint32_t); - indexBuffer = gfxDevice.createBuffer( - indexBytes, - VK_BUFFER_USAGE_INDEX_BUFFER_BIT, - VMA_MEMORY_USAGE_AUTO_PREFER_HOST); - vkutil::addDebugLabel(gfxDevice.getDevice(), indexBuffer.buffer, "quad indices"); - - quadCapacity = InitialQuadCapacity; - - const auto vertexShader = - AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/quad.vert"); - const auto fragmentShader = - AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/quad.frag"); - - const auto pushConstantRange = VkPushConstantRange{ - .stageFlags = VK_SHADER_STAGE_VERTEX_BIT, - .offset = 0, - .size = sizeof(glm::mat4), - }; - - const auto layouts = std::array{ - resources.getBindlessDescSetLayout() - }; - - const auto pipelineLayoutInfo = VkPipelineLayoutCreateInfo{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, - .setLayoutCount = static_cast(layouts.size()), - .pSetLayouts = layouts.data(), - .pushConstantRangeCount = 1, - .pPushConstantRanges = &pushConstantRange, - }; - - VK_CHECK(vkCreatePipelineLayout( - gfxDevice.getDevice(), - &pipelineLayoutInfo, - nullptr, - &pipelineLayout)); - - PipelineConfigInfo pipelineConfig{}; - Pipeline::DefaultPipelineConfigInfo(pipelineConfig); - pipelineConfig.name = "Quad Rendering Pipeline"; - pipelineConfig.pipelineLayout = pipelineLayout; - pipelineConfig.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE; - pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE; - pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE; - pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS; - pipelineConfig.colorAttachments = {drawImageFormat}; - pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED; - - pipelineConfig.vertexBindingDescriptions = { - VkVertexInputBindingDescription{ - .binding = 0, - .stride = sizeof(QuadVertex), - .inputRate = VK_VERTEX_INPUT_RATE_VERTEX, - } - }; - pipelineConfig.vertexAttributeDescriptions = { - VkVertexInputAttributeDescription{ - .location = 0, - .binding = 0, - .format = VK_FORMAT_R32G32_SFLOAT, - .offset = offsetof(QuadVertex, position), - }, - VkVertexInputAttributeDescription{ - .location = 1, - .binding = 0, - .format = VK_FORMAT_R32G32_SFLOAT, - .offset = offsetof(QuadVertex, uv), - }, - VkVertexInputAttributeDescription{ - .location = 2, - .binding = 0, - .format = VK_FORMAT_R32G32B32A32_SFLOAT, - .offset = offsetof(QuadVertex, color), - }, - VkVertexInputAttributeDescription{ - .location = 3, - .binding = 0, - .format = VK_FORMAT_R32_UINT, - .offset = offsetof(QuadVertex, textureId), - }, - }; - - pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE; - pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - pipelineConfig.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - pipelineConfig.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; - pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - pipelineConfig.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; - - pipeline = std::make_unique( - gfxDevice, - vertexShader.string(), - fragmentShader.string(), - pipelineConfig); - } catch (...) { - cleanup(gfxDevice); - throw; - } -} - -void QuadRendererPass::draw( - VkCommandBuffer cmd, - GfxDevice& gfxDevice, - RenderResources& resources, - const GPUImage& target, - const glm::mat4& projection) -{ - auto& quadManager = QuadRenderingManager::GetInstance(); - const auto& quads = quadManager.GetQuads(); - if (!pipeline || quads.empty()) { - quadManager.ClearQuads(); - return; - } - - const std::size_t numQuads = quads.size(); - if (numQuads > std::numeric_limits::max() / VerticesPerQuad) { - throw std::overflow_error("Too many quads submitted for rendering"); - } - - const std::size_t numVertices = numQuads * VerticesPerQuad; - const std::size_t numIndices = numQuads * IndicesPerQuad; - - if (numVertices > std::numeric_limits::max()) { - throw std::overflow_error("Too many quad vertices submitted for rendering"); - } - - ensureCapacity(gfxDevice, numQuads); - - const ImageID whiteTextureId = resources.getWhiteTextureID(); - - auto* vertexMapped = reinterpret_cast(vertexBuffer.info.pMappedData); - auto* indexMapped = reinterpret_cast(indexBuffer.info.pMappedData); - - constexpr std::array localCorners = {{ - glm::vec2{-0.5f, -0.5f}, - glm::vec2{ 0.5f, -0.5f}, - glm::vec2{ 0.5f, 0.5f}, - glm::vec2{-0.5f, 0.5f}, - }}; - - constexpr std::array uvs = {{ - glm::vec2{0.0f, 1.0f}, - glm::vec2{1.0f, 1.0f}, - glm::vec2{1.0f, 0.0f}, - glm::vec2{0.0f, 0.0f}, - }}; - - auto* vertWrite = reinterpret_cast(vertexMapped); - auto* idxWrite = reinterpret_cast(indexMapped); - std::uint32_t vertexBase = 0; - - for (const auto& quad : quads) { - const float cosA = std::cos(quad.rotation); - const float sinA = std::sin(quad.rotation); - const glm::vec2 halfSize = quad.size * 0.5f; - const ImageID texId = (quad.textureId == NULL_IMAGE_ID) ? whiteTextureId : quad.textureId; - - for (std::size_t v = 0; v < VerticesPerQuad; ++v) { - glm::vec2 localPos = localCorners[v] * halfSize; - glm::vec2 rotatedPos = rotatePoint(localPos, cosA, sinA); - glm::vec2 worldPos = quad.position + rotatedPos; - - vertWrite[vertexBase + v] = QuadVertex{ - .position = worldPos, - .uv = uvs[v], - .color = quad.color, - .textureId = static_cast(texId), - }; - } - - const std::uint32_t idxOffset = vertexBase / 4 * 6; - idxWrite[idxOffset + 0] = vertexBase + 0; - idxWrite[idxOffset + 1] = vertexBase + 1; - idxWrite[idxOffset + 2] = vertexBase + 2; - idxWrite[idxOffset + 3] = vertexBase + 0; - idxWrite[idxOffset + 4] = vertexBase + 2; - idxWrite[idxOffset + 5] = vertexBase + 3; - vertexBase += VerticesPerQuad; - } - - auto& memMgr = gfxDevice.getMemoryManager(); - memMgr.flushAllocation(vertexBuffer, 0, numVertices * sizeof(QuadVertex)); - memMgr.flushAllocation(indexBuffer, 0, numIndices * sizeof(std::uint32_t)); - - auto renderInfo = vkutil::createRenderingInfo({ - .renderExtent = target.getExtent2D(), - .colorImageView = target.imageView, - }); - - vkCmdBeginRendering(cmd, &renderInfo.renderingInfo); - - pipeline->bind(cmd); - resources.bindBindlessDescSet(cmd, pipelineLayout); - - const auto viewport = VkViewport{ - .x = 0.0f, - .y = 0.0f, - .width = static_cast(target.extent.width), - .height = static_cast(target.extent.height), - .minDepth = 0.0f, - .maxDepth = 1.0f, - }; - vkCmdSetViewport(cmd, 0, 1, &viewport); - - const auto scissor = VkRect2D{ - .offset = {}, - .extent = target.getExtent2D(), - }; - vkCmdSetScissor(cmd, 0, 1, &scissor); - vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL); - - vkCmdPushConstants( - cmd, - pipelineLayout, - VK_SHADER_STAGE_VERTEX_BIT, - 0, - sizeof(glm::mat4), - &projection); - - const VkBuffer vertexBufferHandle = vertexBuffer.buffer; - const VkDeviceSize vertexBufferOffset = 0; - vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBufferHandle, &vertexBufferOffset); - - const VkBuffer indexBufferHandle = indexBuffer.buffer; - vkCmdBindIndexBuffer(cmd, indexBufferHandle, 0, VK_INDEX_TYPE_UINT32); - - vkCmdDrawIndexed(cmd, static_cast(numIndices), 1, 0, 0, 0); - - vkCmdEndRendering(cmd); - quadManager.ClearQuads(); -} - -void QuadRendererPass::cleanup(GfxDevice& gfxDevice) -{ - pipeline.reset(); - - if (pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) { - vkDestroyPipelineLayout(gfxDevice.getDevice(), pipelineLayout, nullptr); - } - pipelineLayout = VK_NULL_HANDLE; - - if (vertexBuffer.buffer != VK_NULL_HANDLE) { - gfxDevice.destroyBuffer(vertexBuffer); - } - if (indexBuffer.buffer != VK_NULL_HANDLE) { - gfxDevice.destroyBuffer(indexBuffer); - } - quadCapacity = 0; -} - -void QuadRendererPass::ensureCapacity( - GfxDevice& gfxDevice, - std::size_t requiredQuads) -{ - if (requiredQuads <= quadCapacity) { - return; - } - - std::size_t newCapacity = std::max(quadCapacity, InitialQuadCapacity); - while (newCapacity < requiredQuads) { - if (newCapacity > std::numeric_limits::max() / 2) { - newCapacity = requiredQuads; - break; - } - newCapacity *= 2; - } - - gfxDevice.waitIdle(); - - if (vertexBuffer.buffer != VK_NULL_HANDLE) { - gfxDevice.destroyBuffer(vertexBuffer); - } - vertexBuffer = gfxDevice.createBuffer( - newCapacity * VerticesPerQuad * sizeof(QuadVertex), - VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, - VMA_MEMORY_USAGE_AUTO_PREFER_HOST); - vkutil::addDebugLabel(gfxDevice.getDevice(), vertexBuffer.buffer, "quad vertices"); - - if (indexBuffer.buffer != VK_NULL_HANDLE) { - gfxDevice.destroyBuffer(indexBuffer); - } - indexBuffer = gfxDevice.createBuffer( - newCapacity * IndicesPerQuad * sizeof(std::uint32_t), - VK_BUFFER_USAGE_INDEX_BUFFER_BIT, - VMA_MEMORY_USAGE_AUTO_PREFER_HOST); - vkutil::addDebugLabel(gfxDevice.getDevice(), indexBuffer.buffer, "quad indices"); - - quadCapacity = newCapacity; -} \ No newline at end of file diff --git a/destrum/src/Graphics/Pipelines/Renderer2DPass.cpp b/destrum/src/Graphics/Pipelines/Renderer2DPass.cpp new file mode 100644 index 0000000..85081da --- /dev/null +++ b/destrum/src/Graphics/Pipelines/Renderer2DPass.cpp @@ -0,0 +1,217 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "volk.h" + +namespace { + constexpr std::size_t InitialShapeCapacity = 256; + constexpr std::uint32_t VerticesPerShape = 6; + + struct PushConstants { + glm::mat4 projection; + VkDeviceAddress shapes; + }; + + constexpr VkShaderStageFlags PushConstantStages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + constexpr VkBufferUsageFlags ShapeBufferUsage = + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; +} + +void Renderer2DPass::init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat) +{ + try { + shapeBuffer.init(gfxDevice, ShapeBufferUsage, InitialShapeCapacity * sizeof(Shape2D), "2d shapes"); + shapeCapacity = InitialShapeCapacity; + + const auto vertexShader = + AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/shape2d.vert"); + const auto fragmentShader = + AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/shape2d.frag"); + + const auto pushConstantRange = VkPushConstantRange{ + .stageFlags = PushConstantStages, + .offset = 0, + .size = sizeof(PushConstants), + }; + + const auto layouts = std::array{ + resources.getBindlessDescSetLayout() + }; + + const auto pipelineLayoutInfo = VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = static_cast(layouts.size()), + .pSetLayouts = layouts.data(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange, + }; + + VK_CHECK(vkCreatePipelineLayout( + gfxDevice.getDevice(), + &pipelineLayoutInfo, + nullptr, + &pipelineLayout)); + + PipelineConfigInfo pipelineConfig{}; + Pipeline::DefaultPipelineConfigInfo(pipelineConfig); + pipelineConfig.name = "2D Shape Pipeline"; + pipelineConfig.pipelineLayout = pipelineLayout; + pipelineConfig.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE; + pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE; + pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE; + pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS; + pipelineConfig.colorAttachments = {drawImageFormat}; + pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED; + pipelineConfig.vertexBindingDescriptions = {}; + pipelineConfig.vertexAttributeDescriptions = {}; + + pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE; + pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + pipelineConfig.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + pipelineConfig.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + pipelineConfig.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + + pipeline = std::make_unique( + gfxDevice, + vertexShader.string(), + fragmentShader.string(), + pipelineConfig); + } catch (...) { + cleanup(gfxDevice); + throw; + } +} + +void Renderer2DPass::draw( + VkCommandBuffer cmd, + GfxDevice& gfxDevice, + RenderResources& resources, + const GPUImage& target, + const glm::mat4& projection) +{ + auto& renderer2D = Renderer2D::GetInstance(); + if (!pipeline || renderer2D.GetShapes().empty()) { + renderer2D.Clear(); + return; + } + + std::vector shapes = renderer2D.GetShapes(); + renderer2D.Clear(); + + if (shapes.size() > std::numeric_limits::max()) { + throw std::overflow_error("Too many 2D shapes submitted for rendering"); + } + + const auto whiteTextureId = static_cast(resources.getWhiteTextureID()); + for (auto& shape : shapes) { + if (shape.textureId == NULL_IMAGE_ID) { + shape.textureId = whiteTextureId; + } + } + + ensureCapacity(gfxDevice, shapes.size()); + shapeBuffer.uploadNewData( + cmd, + gfxDevice.getCurrentFrameIndex(), + shapes.data(), + shapes.size() * sizeof(Shape2D)); + + auto renderInfo = vkutil::createRenderingInfo({ + .renderExtent = target.getExtent2D(), + .colorImageView = target.imageView, + }); + + vkCmdBeginRendering(cmd, &renderInfo.renderingInfo); + + pipeline->bind(cmd); + resources.bindBindlessDescSet(cmd, pipelineLayout); + + const auto viewport = VkViewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(target.extent.width), + .height = static_cast(target.extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + vkCmdSetViewport(cmd, 0, 1, &viewport); + + const auto scissor = VkRect2D{ + .offset = {}, + .extent = target.getExtent2D(), + }; + vkCmdSetScissor(cmd, 0, 1, &scissor); + vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL); + + const auto pushConstants = PushConstants{ + .projection = projection, + .shapes = shapeBuffer.getBuffer().address, + }; + vkCmdPushConstants(cmd, pipelineLayout, PushConstantStages, 0, sizeof(PushConstants), &pushConstants); + + vkCmdDraw(cmd, VerticesPerShape, static_cast(shapes.size()), 0, 0); + + vkCmdEndRendering(cmd); +} + +void Renderer2DPass::cleanup(GfxDevice& gfxDevice) +{ + pipeline.reset(); + + if (pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(gfxDevice.getDevice(), pipelineLayout, nullptr); + } + pipelineLayout = VK_NULL_HANDLE; + + shapeBuffer.cleanup(gfxDevice); + shapeCapacity = 0; +} + +glm::mat4 Renderer2DPass::screenProjection(const GPUImage& target) +{ + // Vulkan clip space has +y pointing down, so bottom = 0 puts the origin top left + return glm::ortho( + 0.0f, static_cast(target.extent.width), + 0.0f, static_cast(target.extent.height), + -1.0f, 1.0f); +} + +void Renderer2DPass::ensureCapacity(GfxDevice& gfxDevice, std::size_t requiredShapes) +{ + if (requiredShapes <= shapeCapacity) { + return; + } + + std::size_t newCapacity = std::max(shapeCapacity, InitialShapeCapacity); + while (newCapacity < requiredShapes) { + if (newCapacity > std::numeric_limits::max() / 2) { + newCapacity = requiredShapes; + break; + } + newCapacity *= 2; + } + + gfxDevice.waitIdle(); + shapeBuffer.cleanup(gfxDevice); + shapeBuffer.init(gfxDevice, ShapeBufferUsage, newCapacity * sizeof(Shape2D), "2d shapes"); + shapeCapacity = newCapacity; +} diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 8a55b1a..bf281c8 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include @@ -50,8 +50,8 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm:: lineRenderingPass = std::make_unique(); lineRenderingPass->init(gfxDevice, drawImageFormat); - quadRendererPass = std::make_unique(); - quadRendererPass->init(gfxDevice, _resources, drawImageFormat); + renderer2DPass = std::make_unique(); + renderer2DPass->init(gfxDevice, _resources, drawImageFormat); GameState::GetInstance().SetRenderer(this); initialized = true; @@ -70,7 +70,7 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice) skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); shadowLightDirection.reset(); - QuadRenderingManager::GetInstance().ClearQuads(); + Renderer2D::GetInstance().Clear(); } void GameRenderer::endDrawing() @@ -330,28 +330,25 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& } { - TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "QuadRendererPass::draw"); - vkutil::ScopedCmdLabel label(cmd, "QuadRendererPass::draw", vkutil::labelColors::ui); + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Renderer2DPass::draw"); + vkutil::ScopedCmdLabel label(cmd, "Renderer2DPass::draw", vkutil::labelColors::ui); - quadRendererPass->draw( + renderer2DPass->draw( cmd, gfxDevice, *resources, drawImage, - glm::ortho(0.0f, - static_cast(drawImage.extent.width), - static_cast(drawImage.extent.height), - 0.0f, -1.0f, 1.0f)); + Renderer2DPass::screenProjection(drawImage)); } } -void GameRenderer::drawQuads( +void GameRenderer::draw2D( VkCommandBuffer cmd, GfxDevice& gfxDevice, const glm::mat4& projection) { const auto& drawImage = resources->getImage(drawImageId); - quadRendererPass->draw(cmd, gfxDevice, *resources, drawImage, projection); + renderer2DPass->draw(cmd, gfxDevice, *resources, drawImage, projection); } void GameRenderer::cleanup(GfxDevice& gfxDevice) @@ -369,8 +366,8 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) if (lineRenderingPass) lineRenderingPass->cleanup(gfxDevice); - if (quadRendererPass) - quadRendererPass->cleanup(gfxDevice); + if (renderer2DPass) + renderer2DPass->cleanup(gfxDevice); if (lightCullingPipeline) lightCullingPipeline->cleanup(gfxDevice); @@ -399,7 +396,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) skyboxPipeline.reset(); skinningPipeline.reset(); lineRenderingPass.reset(); - quadRendererPass.reset(); + renderer2DPass.reset(); lightCullingPipeline.reset(); shadowMapPipeline.reset(); shadowLightDirection.reset(); diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index 7b77e2a..af29e91 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -49,13 +49,13 @@ private: std::unique_ptr skyboxCubemap; MainSceneObjects mainScene{}; - ImageID debugQuadTexture{NULL_IMAGE_ID}; + ImageID debug2DTexture{NULL_IMAGE_ID}; char scenePath[512]{"scenes/debug_scene.json"}; char newSceneName[128]{"EmptyScene"}; std::string sceneStatus; bool renderColliders{false}; - bool renderQuads{true}; + bool render2DShapes{true}; bool renderDebugLines{true}; bool showTestWindow{true}; bool useFreeCamera{false}; diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index cec8a23..0669f83 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -37,7 +37,7 @@ void LightKeeper::customInit() auto& scene = SceneManager::GetInstance().CreateScene("Main"); mainScene = BuildMainScene(scene, gfxDevice, resources); - debugQuadTexture = resources.loadImageFromFile( + debug2DTexture = resources.loadImageFromFile( gfxDevice, AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png")); } diff --git a/lightkeeper/src/LightkeeperDebugUi.cpp b/lightkeeper/src/LightkeeperDebugUi.cpp index c50a83d..6cb9ecc 100644 --- a/lightkeeper/src/LightkeeperDebugUi.cpp +++ b/lightkeeper/src/LightkeeperDebugUi.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -127,7 +127,7 @@ void LightKeeper::drawDebugMenuBar() if (ImGui::BeginMenu("Render")) { ImGui::MenuItem("Colliders", nullptr, &renderColliders); - ImGui::MenuItem("2D Quads", nullptr, &renderQuads); + ImGui::MenuItem("2D Shapes", nullptr, &render2DShapes); ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines); bool lightHeatmap = renderer.getRenderLightHeatmap(); @@ -246,12 +246,6 @@ void LightKeeper::submitDebugDraws() if (renderDebugLines) { LineRenderingManager::GetInstance().SubmitLine({0, 0, 0}, {0, 10, 0}, {255, 0, 0, 255}); } - - if (renderQuads && debugQuadTexture != NULL_IMAGE_ID) { - QuadRenderingManager::GetInstance().SubmitQuad( - glm::vec2{100.0f, 100.0f}, glm::vec2{200.0f, 200.0f}, 0.0f, - glm::vec4{1.0f}, debugQuadTexture); - } } // Triggers orange, then colored by body type: static blue, dynamic green, kinematic purple diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index d0b58bd..69b48d6 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -31,7 +32,7 @@ #include #include #include -#include +#include #include #include #include @@ -431,6 +432,52 @@ namespace { SceneManager::GetInstance().Destroy(); } + void testRenderer2DShapes() + { + auto& r2d = Renderer2D::GetInstance(); + r2d.Clear(); + + r2d.DrawRect({10.0f, 20.0f}, {30.0f, 40.0f}, glm::vec4{0.5f}); + const Shape2D& rect = r2d.GetShapes().back(); + check(rect.type == Shape2DType::Rect && + rect.center == glm::vec2{25.0f, 40.0f} && + rect.halfSize == glm::vec2{15.0f, 20.0f} && + rect.textureId == NULL_IMAGE_ID && + rect.outline == 0.0f, + "rects must be anchored at their top left corner and filled by default"); + + r2d.DrawEllipseOutline({50.0f, 50.0f}, {20.0f, 10.0f}, 2.0f, glm::vec4{1.0f}); + const Shape2D& ellipse = r2d.GetShapes().back(); + check(ellipse.type == Shape2DType::Ellipse && + ellipse.center == glm::vec2{50.0f} && + ellipse.halfSize == glm::vec2{20.0f, 10.0f} && + ellipse.outline == 2.0f, + "ellipses must be centered with their radii as half size"); + + r2d.DrawTriangle({0.0f, 10.0f}, {5.0f, 0.0f}, {10.0f, 10.0f}, glm::vec4{1.0f}); + const Shape2D& triangle = r2d.GetShapes().back(); + check(triangle.type == Shape2DType::Triangle && + triangle.center == glm::vec2{5.0f} && + triangle.halfSize == glm::vec2{5.0f} && + triangle.p0 == glm::vec2{-5.0f, 5.0f} && + triangle.p1 == glm::vec2{0.0f, -5.0f} && + triangle.p2 == glm::vec2{5.0f, 5.0f}, + "triangle points must be stored relative to their bounding box center"); + + r2d.DrawLine({0.0f, 0.0f}, {0.0f, 10.0f}, 4.0f, glm::vec4{1.0f}, LineCap::Round); + const Shape2D& line = r2d.GetShapes().back(); + check(line.type == Shape2DType::Rect && + line.center == glm::vec2{0.0f, 5.0f} && + close(line.halfSize.x, 7.0f) && close(line.halfSize.y, 2.0f) && + close(line.rotation, glm::half_pi()) && + close(line.cornerRadius, 2.0f), + "round capped lines must be rotated rects extended by half their thickness"); + + check(r2d.GetShapes().size() == 4, "every draw call must submit exactly one shape"); + r2d.Clear(); + check(r2d.GetShapes().empty(), "clear must drop all submitted shapes"); + } + void testCapsuleColliderDebugLines() { GameObject object{"capsule"}; @@ -1131,6 +1178,7 @@ int main() testPrimitiveBoxMesh(); testColliderCenterOffset(); testCapsuleColliderDebugLines(); + testRenderer2DShapes(); std::cout << "destrum tests passed\n"; return EXIT_SUCCESS; }