From 01fadbecbed89afffb18b2cf5cf3972845a7a169 Mon Sep 17 00:00:00 2001 From: hachem Date: Fri, 22 Aug 2025 00:22:12 +0200 Subject: [add]: frame exporting --- .gitignore | 1 + .gitmodules | 3 + Vendor/stb | 1 + premake5.lua | 6 + src/Engine/Engine.cpp | 154 ++++++++++++++++++- src/Engine/Engine.h | 1 + src/Platform/OpenGL/OpenGLFramebuffer.cpp | 238 ++++++++++++++++++++++++++++++ src/Platform/OpenGL/OpenGLFramebuffer.h | 35 +++++ src/Platform/OpenGL/OpenGLRendererAPI.cpp | 6 + src/Platform/OpenGL/OpenGLRendererAPI.h | 4 + src/Platform/Vulkan/VulkanRendererAPI.cpp | 12 ++ src/Platform/Vulkan/VulkanRendererAPI.h | 2 + src/Rendering/Framebuffer.cpp | 17 +++ src/Rendering/Framebuffer.h | 65 ++++++++ src/Rendering/Renderer.h | 9 ++ src/States/SimulationState.cpp | 48 ++++++ 16 files changed, 599 insertions(+), 3 deletions(-) create mode 160000 Vendor/stb create mode 100644 src/Platform/OpenGL/OpenGLFramebuffer.cpp create mode 100644 src/Platform/OpenGL/OpenGLFramebuffer.h create mode 100644 src/Rendering/Framebuffer.cpp create mode 100644 src/Rendering/Framebuffer.h diff --git a/.gitignore b/.gitignore index dace220..9392e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ config/ *.log imgui.ini Scene.json +frame_*.png bin/ bin-int/ diff --git a/.gitmodules b/.gitmodules index 5aa96f8..a19e60c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,6 @@ [submodule "Vendor/json"] path = Vendor/json url = https://github.com/nlohmann/json.git +[submodule "Vendor/stb"] + path = Vendor/stb + url = https://github.com/nothings/stb.git diff --git a/Vendor/stb b/Vendor/stb new file mode 160000 index 0000000..f58f558 --- /dev/null +++ b/Vendor/stb @@ -0,0 +1 @@ +Subproject commit f58f558c120e9b32c217290b80bad1a0729fbb2c diff --git a/premake5.lua b/premake5.lua index bd10630..6225227 100644 --- a/premake5.lua +++ b/premake5.lua @@ -32,6 +32,7 @@ IncludeDir["imgui_backends"] = "Vendor/imgui/backends" IncludeDir["imguizmo"] = "Vendor/ImGuizmo" IncludeDir["toml11"] = "Vendor/toml11/include" IncludeDir["nlohmann"] = "Vendor/json/single_include" +IncludeDir["stb"] = "Vendor/stb" group "Dependencies" @@ -312,6 +313,10 @@ project "Donut" "src/**.h", "src/**.cpp", + "src/Rendering/Framebuffer.h", + "src/Rendering/Framebuffer.cpp", + "src/Platform/OpenGL/OpenGLFramebuffer.h", + "src/Platform/OpenGL/OpenGLFramebuffer.cpp", "Vendor/imgui/backends/imgui_impl_glfw.cpp", "Vendor/imgui/backends/imgui_impl_opengl3.cpp", @@ -333,6 +338,7 @@ project "Donut" "%{IncludeDir.imguizmo}", "%{IncludeDir.toml11}", "%{IncludeDir.nlohmann}", + "%{IncludeDir.stb}", } links diff --git a/src/Engine/Engine.cpp b/src/Engine/Engine.cpp index fa010a7..245f6fd 100644 --- a/src/Engine/Engine.cpp +++ b/src/Engine/Engine.cpp @@ -3,6 +3,10 @@ #include #include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" #include "Engine.h" #include "Core/Log.h" @@ -326,7 +330,7 @@ namespace Donut for (size_t i = 0; i < m_Objects.size(); ++i) { const auto& obj = m_Objects[i]; - DONUT_INFO("Object {}: Pos=({:.2e}, {:.2e}, {:.2e}), Radius={:.2e}, Mass={:.2e}, Color=({:.2f}, {:.2f}, {:.2f})", + DONUT_INFO("Object {}: Pos=({}, {}, {}), Radius={}, Mass={}, Color=({}, {}, {})", i, obj.m_PosRadius.x, obj.m_PosRadius.y, obj.m_PosRadius.z, obj.m_PosRadius.w, @@ -334,12 +338,156 @@ namespace Donut obj.m_Color.x, obj.m_Color.y, obj.m_Color.z ); } - DONUT_INFO("Camera position: ({:.2e}, {:.2e}, {:.2e})", + DONUT_INFO("Camera position: ({}, {}, {})", m_Camera.GetOrbitalPosition().x, m_Camera.GetOrbitalPosition().y, m_Camera.GetOrbitalPosition().z ); - DONUT_INFO("Camera radius: {:.2e}", m_Camera.GetOrbitalRadius()); + DONUT_INFO("Camera radius: {}", m_Camera.GetOrbitalRadius()); DONUT_INFO("========================"); } + + void Engine::ExportHighResFrame(const std::string& filename, int width, int height) + { + DONUT_INFO("Exporting high-resolution frame: {}x{} to {}", width, height, filename); + + if (width <= 0 || height <= 0) + { + DONUT_ERROR("Invalid dimensions for export: {}x{}", width, height); + return; + } + + if (filename.empty()) + { + DONUT_ERROR("Invalid filename for export"); + return; + } + + int originalWidth = m_Width; + int originalHeight = m_Height; + int originalComputeHeight = m_ComputeHeight; + + m_Width = width; + m_Height = height; + m_ComputeHeight = height; + + int computeHeight = height; + int computeWidth = (width * computeHeight) / height; + + if (computeWidth <= 0 || computeHeight <= 0) + { + DONUT_ERROR("Invalid compute dimensions: {}x{}", computeWidth, computeHeight); + return; + } + + FramebufferSpecification fbSpec; + fbSpec.Width = width; + fbSpec.Height = height; + fbSpec.Attachments = { FramebufferTextureFormat::RGBA8 }; + + auto highResFramebuffer = Framebuffer::Create(fbSpec); + if (!highResFramebuffer) + { + DONUT_ERROR("Failed to create high-resolution framebuffer"); + return; + } + + highResFramebuffer->Bind(); + + RenderCommand::SetViewport(0, 0, width, height); + RenderCommand::Clear(); + + auto highResTexture = Texture2D::Create(computeWidth, computeHeight); + if (!highResTexture) + { + DONUT_ERROR("Failed to create high-resolution texture"); + return; + } + + highResTexture->SetData(nullptr, computeWidth * computeHeight * 4); + m_ComputeProgram->Bind(); + + struct UBOData + { + glm::vec3 pos; float _pad0; + glm::vec3 right; float _pad1; + glm::vec3 up; float _pad2; + glm::vec3 forward; float _pad3; + float tanHalfFov; + float aspect; + bool moving; + int _pad4; + } data; + + glm::vec3 fwd = glm::normalize(m_Camera.GetOrbitalTarget() - m_Camera.GetOrbitalPosition()); + glm::vec3 up = glm::vec3(0, 1, 0); + glm::vec3 right = glm::normalize(glm::cross(fwd, up)); + up = glm::cross(right, fwd); + + data.pos = m_Camera.GetOrbitalPosition(); + data.right = right; + data.up = up; + data.forward = fwd; + data.tanHalfFov = static_cast(tan(glm::radians(60.0f * 0.5f))); + data.aspect = static_cast(computeWidth) / static_cast(computeHeight); + data.moving = m_Camera.IsDragging() || m_Camera.IsPanning(); + + m_CameraUBO->SetData(&data, sizeof(UBOData)); + m_CameraUBO->Bind(1); + + UploadDiskUBO(); + UploadObjectsUBO(m_Objects); + UploadSimulationUBO(); + highResTexture->BindAsImage(0, false); + + uint32_t groupsX = static_cast(std::ceil(computeWidth / 16.0f)); + uint32_t groupsY = static_cast(std::ceil(computeHeight / 16.0f)); + m_ComputeProgram->Dispatch(groupsX, groupsY, 1); + m_ComputeProgram->MemoryBarrier(IMAGE_ACCESS_BARRIER_BIT); + + m_ShaderProgram->Bind(); + m_QuadVAO->Bind(); + + highResTexture->Bind(0); + m_ShaderProgram->SetInt("u_ScreenTexture", 0); + + RenderCommand::DisableDepthTest(); + RenderCommand::DrawArrays(6); + RenderCommand::EnableDepthTest(); + + std::vector pixels(width * height * 4); + DONUT_INFO("Reading {} pixels from framebuffer...", width * height); + RenderCommand::ReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + + DONUT_INFO("Flipping image vertically..."); + std::vector flippedPixels(width * height * 4); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + int srcIndex = (y * width + x) * 4; + int dstIndex = ((height - 1 - y) * width + x) * 4; + flippedPixels[dstIndex + 0] = pixels[srcIndex + 0]; // R + flippedPixels[dstIndex + 1] = pixels[srcIndex + 1]; // G + flippedPixels[dstIndex + 2] = pixels[srcIndex + 2]; // B + flippedPixels[dstIndex + 3] = pixels[srcIndex + 3]; // A + } + } + + DONUT_INFO("Saving PNG file: {}...", filename); + int result = stbi_write_png(filename.c_str(), width, height, 4, flippedPixels.data(), width * 4); + + if (result) + DONUT_INFO("Successfully exported high-resolution frame to: {}", filename); + else + DONUT_ERROR("Failed to export high-resolution frame to: {}", filename); + + highResFramebuffer->Unbind(); + + m_Width = originalWidth; + m_Height = originalHeight; + m_ComputeHeight = originalComputeHeight; + + RenderCommand::SetViewport(0, 0, originalWidth, originalHeight); + } } diff --git a/src/Engine/Engine.h b/src/Engine/Engine.h index f3724ff..328435a 100644 --- a/src/Engine/Engine.h +++ b/src/Engine/Engine.h @@ -101,6 +101,7 @@ namespace Donut void SetEarlyExitDistance(float distance) { m_EarlyExitDistance = distance; } void LoadObjectsFromScene(const std::vector& objects); + void ExportHighResFrame(const std::string& filename, int width = 4096, int height = 3072); void PrintObjectInfo() const; private: Ref CreateComputeProgram(const char* path); diff --git a/src/Platform/OpenGL/OpenGLFramebuffer.cpp b/src/Platform/OpenGL/OpenGLFramebuffer.cpp new file mode 100644 index 0000000..523d748 --- /dev/null +++ b/src/Platform/OpenGL/OpenGLFramebuffer.cpp @@ -0,0 +1,238 @@ +#include "OpenGLFramebuffer.h" +#include "Core/Log.h" + +#include + +namespace Donut +{ + namespace Utils + { + static GLenum TextureTarget(bool multisampled) + { + return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; + } + + static void BindTexture(bool multisampled, uint32_t id) + { + glBindTexture(TextureTarget(multisampled), id); + } + + static void CreateTextures(bool multisampled, uint32_t* outID, uint32_t count) + { + glCreateTextures(TextureTarget(multisampled), count, outID); + } + + static void AttachColorTexture(uint32_t id, int samples, GLenum internalFormat, GLenum format, uint32_t width, uint32_t height, int index) + { + bool multisampled = samples > 1; + if (multisampled) + { + glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internalFormat, width, height, GL_FALSE); + } + else + { + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, nullptr); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0); + } + + static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachmentType, uint32_t width, uint32_t height) + { + bool multisampled = samples > 1; + if (multisampled) + { + glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE); + } + else + { + glTexStorage2D(GL_TEXTURE_2D, 1, format, width, height); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentType, TextureTarget(multisampled), id, 0); + } + + static bool IsDepthFormat(FramebufferTextureFormat format) + { + switch (format) + { + case FramebufferTextureFormat::DEPTH24STENCIL8: return true; + } + return false; + } + + static GLenum DonutFBTextureFormatToGL(FramebufferTextureFormat format) + { + switch (format) + { + case FramebufferTextureFormat::RGBA8: return GL_RGBA8; + case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER; + } + + return 0; + } + } + + OpenGLFramebuffer::OpenGLFramebuffer(const FramebufferSpecification& spec) + : m_Specification(spec) + { + for (auto spec : m_Specification.Attachments.Attachments) + { + if (!Utils::IsDepthFormat(spec.TextureFormat)) + m_ColorAttachmentSpecifications.emplace_back(spec); + else + m_DepthAttachmentSpecification = spec; + } + + Invalidate(); + } + + OpenGLFramebuffer::~OpenGLFramebuffer() + { + glDeleteFramebuffers(1, &m_RendererID); + glDeleteTextures(static_cast(m_ColorAttachments.size()), m_ColorAttachments.data()); + glDeleteTextures(1, &m_DepthAttachment); + } + + void OpenGLFramebuffer::Invalidate() + { + if (m_RendererID) + { + glDeleteFramebuffers(1, &m_RendererID); + glDeleteTextures(static_cast(m_ColorAttachments.size()), m_ColorAttachments.data()); + glDeleteTextures(1, &m_DepthAttachment); + + m_ColorAttachments.clear(); + m_DepthAttachment = 0; + } + + glCreateFramebuffers(1, &m_RendererID); + glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID); + + bool multisample = m_Specification.Samples > 1; + + // Attachments + if (m_ColorAttachmentSpecifications.size()) + { + m_ColorAttachments.resize(m_ColorAttachmentSpecifications.size()); + Utils::CreateTextures(multisample, m_ColorAttachments.data(), static_cast(m_ColorAttachments.size())); + + for (size_t i = 0; i < m_ColorAttachments.size(); i++) + { + Utils::BindTexture(multisample, m_ColorAttachments[i]); + switch (m_ColorAttachmentSpecifications[i].TextureFormat) + { + case FramebufferTextureFormat::RGBA8: + Utils::AttachColorTexture(m_ColorAttachments[i], m_Specification.Samples, GL_RGBA8, GL_RGBA, m_Specification.Width, m_Specification.Height, static_cast(i)); + break; + case FramebufferTextureFormat::RED_INTEGER: + Utils::AttachColorTexture(m_ColorAttachments[i], m_Specification.Samples, GL_R32I, GL_RED_INTEGER, m_Specification.Width, m_Specification.Height, static_cast(i)); + break; + } + } + } + + if (m_DepthAttachmentSpecification.TextureFormat != FramebufferTextureFormat::None) + { + Utils::CreateTextures(multisample, &m_DepthAttachment, 1); + Utils::BindTexture(multisample, m_DepthAttachment); + switch (m_DepthAttachmentSpecification.TextureFormat) + { + case FramebufferTextureFormat::DEPTH24STENCIL8: + Utils::AttachDepthTexture(m_DepthAttachment, m_Specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_Specification.Width, m_Specification.Height); + break; + } + } + + if (m_ColorAttachments.size() > 1) + { + GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; + glDrawBuffers(static_cast(m_ColorAttachments.size()), buffers); + } + else if (m_ColorAttachments.empty()) + glDrawBuffer(GL_NONE); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + switch (status) + { + case GL_FRAMEBUFFER_UNDEFINED: + DONUT_ERROR("Framebuffer is undefined"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + DONUT_ERROR("Framebuffer has incomplete attachment"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + DONUT_ERROR("Framebuffer is missing attachment"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: + DONUT_ERROR("Framebuffer has incomplete draw buffer"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: + DONUT_ERROR("Framebuffer has incomplete read buffer"); + break; + case GL_FRAMEBUFFER_UNSUPPORTED: + DONUT_ERROR("Framebuffer format is unsupported"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: + DONUT_ERROR("Framebuffer has incomplete multisample"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: + DONUT_ERROR("Framebuffer has incomplete layer targets"); + break; + default: + DONUT_ERROR("Framebuffer is incomplete (unknown error: {})", status); + break; + } + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + void OpenGLFramebuffer::Bind() + { + glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID); + glViewport(0, 0, m_Specification.Width, m_Specification.Height); + } + + void OpenGLFramebuffer::Unbind() + { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + void OpenGLFramebuffer::Resize(uint32_t width, uint32_t height) + { + m_Specification.Width = width; + m_Specification.Height = height; + + Invalidate(); + } + + int OpenGLFramebuffer::ReadPixel(uint32_t attachmentIndex, int x, int y) + { + glReadBuffer(GL_COLOR_ATTACHMENT0 + attachmentIndex); + int pixelData; + glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixelData); + return pixelData; + } + + void OpenGLFramebuffer::ClearAttachment(uint32_t attachmentIndex, int value) + { + auto& spec = m_ColorAttachmentSpecifications[attachmentIndex]; + glClearTexImage(m_ColorAttachments[attachmentIndex], 0, + Utils::DonutFBTextureFormatToGL(spec.TextureFormat), GL_INT, &value); + } +}; diff --git a/src/Platform/OpenGL/OpenGLFramebuffer.h b/src/Platform/OpenGL/OpenGLFramebuffer.h new file mode 100644 index 0000000..8cd376c --- /dev/null +++ b/src/Platform/OpenGL/OpenGLFramebuffer.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Rendering/Framebuffer.h" + +namespace Donut +{ + class OpenGLFramebuffer : public Framebuffer + { + public: + OpenGLFramebuffer(const FramebufferSpecification& spec); + virtual ~OpenGLFramebuffer(); + + void Invalidate(); + + virtual void Bind() override; + virtual void Unbind() override; + + virtual void Resize(uint32_t width, uint32_t height) override; + virtual int ReadPixel(uint32_t attachmentIndex, int x, int y) override; + + virtual void ClearAttachment(uint32_t attachmentIndex, int value) override; + virtual uint32_t GetColorAttachmentRendererID(uint32_t index = 0) const override { return m_ColorAttachments[index]; } + + virtual const FramebufferSpecification& GetSpecification() const override { return m_Specification; } + private: + uint32_t m_RendererID = 0; + FramebufferSpecification m_Specification; + + std::vector m_ColorAttachmentSpecifications; + FramebufferTextureSpecification m_DepthAttachmentSpecification = FramebufferTextureFormat::None; + + std::vector m_ColorAttachments; + uint32_t m_DepthAttachment = 0; + }; +}; diff --git a/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/src/Platform/OpenGL/OpenGLRendererAPI.cpp index cd54f82..230b19b 100644 --- a/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ b/src/Platform/OpenGL/OpenGLRendererAPI.cpp @@ -106,4 +106,10 @@ namespace Donut glBindImageTexture(slot, textureID, 0, GL_FALSE, 0, readOnly ? GL_READ_ONLY : GL_WRITE_ONLY, GL_RGBA8); } + + void OpenGLRendererAPI::ReadPixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) + { + glReadPixels(x, y, width, height, format, type, pixels); + } }; diff --git a/src/Platform/OpenGL/OpenGLRendererAPI.h b/src/Platform/OpenGL/OpenGLRendererAPI.h index 0576db8..5a68622 100644 --- a/src/Platform/OpenGL/OpenGLRendererAPI.h +++ b/src/Platform/OpenGL/OpenGLRendererAPI.h @@ -36,5 +36,9 @@ namespace Donut virtual void BindImageTexture(uint32_t textureID, uint32_t slot = 0, bool readOnly = false) override; + virtual void ReadPixels(uint32_t x, uint32_t y, + uint32_t width, uint32_t height, + uint32_t format, uint32_t type, + void* pixels) override; }; }; diff --git a/src/Platform/Vulkan/VulkanRendererAPI.cpp b/src/Platform/Vulkan/VulkanRendererAPI.cpp index 0dc9f0e..79f7633 100644 --- a/src/Platform/Vulkan/VulkanRendererAPI.cpp +++ b/src/Platform/Vulkan/VulkanRendererAPI.cpp @@ -71,4 +71,16 @@ namespace Donut { // TODO(Hachem): Implement Vulkan image texture binding } + + void VulkanRendererAPI::ReadPixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) + { + // TODO(Hachem): Implement Vulkan pixel reading + // For now, this is a placeholder implementation + // In a real Vulkan implementation, this would involve: + // 1. Creating a staging buffer + // 2. Copying the framebuffer to the staging buffer + // 3. Mapping the staging buffer and copying to the pixels array + // 4. Unmapping and destroying the staging buffer + } }; diff --git a/src/Platform/Vulkan/VulkanRendererAPI.h b/src/Platform/Vulkan/VulkanRendererAPI.h index dba7fe6..63a623f 100644 --- a/src/Platform/Vulkan/VulkanRendererAPI.h +++ b/src/Platform/Vulkan/VulkanRendererAPI.h @@ -32,5 +32,7 @@ namespace Donut virtual void BindImageTexture(uint32_t textureID, uint32_t slot = 0, bool readOnly = false) override; + virtual void ReadPixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) override; }; }; diff --git a/src/Rendering/Framebuffer.cpp b/src/Rendering/Framebuffer.cpp new file mode 100644 index 0000000..1e95c6d --- /dev/null +++ b/src/Rendering/Framebuffer.cpp @@ -0,0 +1,17 @@ +#include "Framebuffer.h" +#include "Renderer.h" + +#include "Platform/OpenGL/OpenGLFramebuffer.h" + +namespace Donut +{ + Ref Framebuffer::Create(const FramebufferSpecification& spec) + { + switch (Renderer::GetAPI()) + { + case RendererAPI::API::OpenGL: return CreateRef(spec); + } + + return nullptr; + } +}; diff --git a/src/Rendering/Framebuffer.h b/src/Rendering/Framebuffer.h new file mode 100644 index 0000000..e4c1481 --- /dev/null +++ b/src/Rendering/Framebuffer.h @@ -0,0 +1,65 @@ +#pragma once + +#include "Core/Memory.h" +#include "Texture.h" + +namespace Donut +{ + enum class FramebufferTextureFormat + { + None = 0, + + RGBA8, + RED_INTEGER, + + DEPTH24STENCIL8, + + Depth = DEPTH24STENCIL8 + }; + + struct FramebufferTextureSpecification + { + FramebufferTextureSpecification() = default; + FramebufferTextureSpecification(FramebufferTextureFormat format) + : TextureFormat(format) {} + + FramebufferTextureFormat TextureFormat = FramebufferTextureFormat::None; + }; + + struct FramebufferAttachmentSpecification + { + FramebufferAttachmentSpecification() = default; + FramebufferAttachmentSpecification(std::initializer_list attachments) + : Attachments(attachments) {} + + std::vector Attachments; + }; + + struct FramebufferSpecification + { + uint32_t Width = 0, Height = 0; + FramebufferAttachmentSpecification Attachments; + uint32_t Samples = 1; + + bool SwapChainTarget = false; + }; + + class Framebuffer + { + public: + virtual ~Framebuffer() = default; + + virtual void Bind() = 0; + virtual void Unbind() = 0; + + virtual void Resize(uint32_t width, uint32_t height) = 0; + virtual int ReadPixel(uint32_t attachmentIndex, int x, int y) = 0; + + virtual void ClearAttachment(uint32_t attachmentIndex, int value) = 0; + virtual uint32_t GetColorAttachmentRendererID(uint32_t index = 0) const = 0; + + virtual const FramebufferSpecification& GetSpecification() const = 0; + + static Ref Create(const FramebufferSpecification& spec); + }; +}; diff --git a/src/Rendering/Renderer.h b/src/Rendering/Renderer.h index d6c4154..6e87411 100644 --- a/src/Rendering/Renderer.h +++ b/src/Rendering/Renderer.h @@ -3,6 +3,7 @@ #include "Core/Memory.h" #include "VertexArray.h" #include "Shader.h" +#include "Framebuffer.h" #include @@ -41,6 +42,8 @@ namespace Donut virtual void BindImageTexture(uint32_t textureID, uint32_t slot = 0, bool readOnly = false) = 0; + virtual void ReadPixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) = 0; inline static API GetAPI() { return s_API; } inline static void SetAPI(API api) { s_API = api; } @@ -122,6 +125,12 @@ namespace Donut s_RendererAPI->BindImageTexture(textureID, slot, readOnly); } + inline static void ReadPixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) + { + s_RendererAPI->ReadPixels(x, y, width, height, format, type, pixels); + } + private: static Scope s_RendererAPI; }; diff --git a/src/States/SimulationState.cpp b/src/States/SimulationState.cpp index fa3c4f5..d961f9e 100644 --- a/src/States/SimulationState.cpp +++ b/src/States/SimulationState.cpp @@ -7,6 +7,9 @@ #include #include +#include +#include +#include namespace Donut { @@ -230,6 +233,51 @@ namespace Donut ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Export"); + ImGui::Separator(); + + if (ImGui::Button("Export Frame (1080p)", ImVec2(-1, 30))) + { + auto& engine = Application::Get().GetEngine(); + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << "frame_1080p_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; + engine.ExportHighResFrame(ss.str(), 1920, 1080); + } + + if (ImGui::Button("Export High-Res Frame (4K)", ImVec2(-1, 30))) + { + auto& engine = Application::Get().GetEngine(); + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << "high_res_frame_4k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; + engine.ExportHighResFrame(ss.str(), 4096, 3072); + } + + if (ImGui::Button("Export High-Res Frame (8K)", ImVec2(-1, 30))) + { + auto& engine = Application::Get().GetEngine(); + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << "high_res_frame_8k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; + engine.ExportHighResFrame(ss.str(), 8192, 6144); + } + + if (ImGui::Button("Export Ultra High-Res Frame (16K)", ImVec2(-1, 30))) + { + auto& engine = Application::Get().GetEngine(); + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << "high_res_frame_16k_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".png"; + engine.ExportHighResFrame(ss.str(), 16384, 12288); + } + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Controls"); ImGui::Separator(); -- cgit v1.3