diff options
| author | hachem <im@hachem.wtf> | 2026-08-24 03:57:40 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-24 03:57:40 +0200 |
| commit | 59b7c407550d63e997ed1e7bed286be5c332c285 (patch) | |
| tree | 95f168df75d33326dde486b401c1abf62608ff7c /src/rendering | |
| parent | 7ae22084c74cbe0c127e0ea1c96ec22a1f47d221 (diff) | |
[feat]: work on render path exporting
Diffstat (limited to 'src/rendering')
| -rw-r--r-- | src/rendering/black_hole_renderer.cpp | 62 | ||||
| -rw-r--r-- | src/rendering/black_hole_renderer.h | 13 | ||||
| -rw-r--r-- | src/rendering/export_config.h | 28 | ||||
| -rw-r--r-- | src/rendering/render_path.cpp | 94 | ||||
| -rw-r--r-- | src/rendering/render_path.h | 5 | ||||
| -rw-r--r-- | src/rendering/rhi.h | 14 |
6 files changed, 197 insertions, 19 deletions
diff --git a/src/rendering/black_hole_renderer.cpp b/src/rendering/black_hole_renderer.cpp index 5916171..283435e 100644 --- a/src/rendering/black_hole_renderer.cpp +++ b/src/rendering/black_hole_renderer.cpp @@ -18,7 +18,7 @@ namespace Donut struct CamUBO { glm::vec3 pos; float p0; glm::vec3 right; float p1; glm::vec3 up; float p2; glm::vec3 fwd; float p3; - float tan_half_fov; float aspect; uint32_t moving; int p4; + float tan_half_fov; float aspect; uint32_t moving; int channel; int raw; }; struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; }; } @@ -61,6 +61,8 @@ namespace Donut d.topology = Topology::Triangles; d.target = { Format::RGBA8, Format::None }; // colour-only off-screen target m_geo_pipeline = device.create_pipeline(d); + d.target = { Format::RGBA32F, Format::None }; // raw-float export variant + m_geo_pipeline_hdr = device.create_pipeline(d); } { PipelineDesc d; @@ -81,11 +83,35 @@ namespace Donut const BlackHoleParams& params, const std::vector<SceneObject>& objects, RHI::Texture* cubemap) -> void { + fill_uniforms(view, params, objects, 0, false); // channel 0 = colour, display + + // Progressive resolution: small target while moving, large once settled. + RenderTarget* target = view.moving ? m_geo_lo.get() : m_geo_hi.get(); + m_last_target = target; + draw_geodesic(cmd, target, cubemap, m_geo_pipeline.get()); + } + + // Renders one channel (0 colour, 1 redshift, 2 temperature, 3 impact parameter) + // into an arbitrary RGBA8 target — used by the exporter for high-res output. + auto BlackHoleRenderer::render_export(RHI::CommandList& cmd, RHI::RenderTarget* target, + const GeodesicView& view, const BlackHoleParams& params, + const std::vector<SceneObject>& objects, + RHI::Texture* cubemap, int channel, bool raw) -> void + { + fill_uniforms(view, params, objects, channel, raw); + draw_geodesic(cmd, target, cubemap, raw ? m_geo_pipeline_hdr.get() : m_geo_pipeline.get()); + } + + auto BlackHoleRenderer::fill_uniforms(const GeodesicView& view, const BlackHoleParams& params, + const std::vector<SceneObject>& objects, int channel, bool raw) -> void + { CamUBO cam{}; cam.pos = view.position; cam.right = view.right; cam.up = view.up; cam.fwd = view.forward; cam.tan_half_fov = view.tan_half_fov; cam.aspect = view.aspect; cam.moving = view.moving ? 1u : 0u; + cam.channel = channel; + cam.raw = raw ? 1 : 0; m_cam_ubo->update(&cam, sizeof(cam)); // Same integration budget whether moving or settled (the disk vanishes at @@ -115,29 +141,27 @@ namespace Donut // Scene objects → the geodesic's Objects UBO (std140: numObjects@0, // objPosRadius[i]@16+16i, objColor[i]@272+16i). They render as spheres the // curved rays intersect, so the hole lenses them. 1 scene unit = 1 r_s. + const float k = SagA_rs / SCENE_UNITS_PER_RS; // scene units -> metres + std::vector<uint8_t> objbuf(800, 0); + int n = std::min((int)objects.size(), 16); + std::memcpy(objbuf.data(), &n, 4); + for (int i = 0; i < n; ++i) { - const float k = SagA_rs / SCENE_UNITS_PER_RS; // scene units -> metres - std::vector<uint8_t> objbuf(800, 0); - int n = std::min((int)objects.size(), 16); - std::memcpy(objbuf.data(), &n, 4); - for (int i = 0; i < n; ++i) - { - const SceneObject& o = objects[i]; - float pr[4] = { o.position.x * k, o.position.y * k, o.position.z * k, o.radius * k }; - float col[4] = { o.color.r, o.color.g, o.color.b, 1.0f }; - std::memcpy(objbuf.data() + 16 + 16 * i, pr, 16); - std::memcpy(objbuf.data() + 272 + 16 * i, col, 16); - } - m_obj_ubo->update(objbuf.data(), objbuf.size()); + const SceneObject& o = objects[i]; + float pr[4] = { o.position.x * k, o.position.y * k, o.position.z * k, o.radius * k }; + float col[4] = { o.color.r, o.color.g, o.color.b, 1.0f }; + std::memcpy(objbuf.data() + 16 + 16 * i, pr, 16); + std::memcpy(objbuf.data() + 272 + 16 * i, col, 16); } + m_obj_ubo->update(objbuf.data(), objbuf.size()); + } - // Progressive resolution: small target while moving, large once settled. - RenderTarget* target = view.moving ? m_geo_lo.get() : m_geo_hi.get(); - m_last_target = target; - + auto BlackHoleRenderer::draw_geodesic(RHI::CommandList& cmd, RHI::RenderTarget* target, + RHI::Texture* cubemap, RHI::Pipeline* pipeline) -> void + { cmd.begin_render_pass(target, glm::vec4(0, 0, 0, 1)); cmd.set_viewport(0, 0, target->width(), target->height(), false); - cmd.bind_pipeline(m_geo_pipeline.get()); + cmd.bind_pipeline(pipeline); cmd.bind_uniform(0, m_cam_ubo.get()); cmd.bind_uniform(1, m_disk_ubo.get()); cmd.bind_uniform(2, m_obj_ubo.get()); diff --git a/src/rendering/black_hole_renderer.h b/src/rendering/black_hole_renderer.h index 2ae03dd..c2cb2af 100644 --- a/src/rendering/black_hole_renderer.h +++ b/src/rendering/black_hole_renderer.h @@ -41,10 +41,23 @@ namespace Donut // swapchain render pass, before ImGui). auto blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void; + // Renders one channel (0 colour, 1 redshift g, 2 emission T, 3 impact + // parameter) into an arbitrary RGBA8 target — for high-res export. + auto render_export(RHI::CommandList& cmd, RHI::RenderTarget* target, + const GeodesicView& view, const BlackHoleParams& params, + const std::vector<SceneObject>& objects, RHI::Texture* cubemap, + int channel, bool raw) -> void; + private: + auto fill_uniforms(const GeodesicView& view, const BlackHoleParams& params, + const std::vector<SceneObject>& objects, int channel, bool raw) -> void; + auto draw_geodesic(RHI::CommandList& cmd, RHI::RenderTarget* target, + RHI::Texture* cubemap, RHI::Pipeline* pipeline) -> void; + RHI::Device* m_device = nullptr; Ref<RHI::Pipeline> m_geo_pipeline; + Ref<RHI::Pipeline> m_geo_pipeline_hdr; // RGBA32F variant for raw export Ref<RHI::RenderTarget> m_geo_lo; Ref<RHI::RenderTarget> m_geo_hi; Ref<RHI::Buffer> m_quad_vb; diff --git a/src/rendering/export_config.h b/src/rendering/export_config.h new file mode 100644 index 0000000..e490db6 --- /dev/null +++ b/src/rendering/export_config.h @@ -0,0 +1,28 @@ +#pragma once + +#include <string> + +namespace Donut +{ + // How each channel is written: + // Png = display image (tone-mapped colour / false-coloured observable). + // Pfm = raw float image (actual physical values; RGB, Portable Float Map). + // Csv = raw scalar grid (the observable's value per pixel, one row per line). + enum class ExportFormat { Png, Pfm, Csv }; + + // What the Export workspace asks the RenderPath to write to disk: which + // observable channels to render, at what resolution, in what format, and where. + // Each enabled channel becomes one file. + struct ExportConfig + { + bool color = true; // colour image (tone-mapped, or linear HDR when raw) + bool redshift = false; // redshift factor g (disk only) + bool temperature = false; // emission temperature in K (disk only) + bool impact = false; // impact parameter in r_s (whole frame) + + int width = 1920; + int height = 1080; + ExportFormat format = ExportFormat::Png; + std::string directory = "exports"; + }; +} diff --git a/src/rendering/render_path.cpp b/src/rendering/render_path.cpp index 115d182..8cb0328 100644 --- a/src/rendering/render_path.cpp +++ b/src/rendering/render_path.cpp @@ -1,10 +1,18 @@ #include "render_path.h" #include "scene/scene.h" +#include "core/log.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" #include <glm/gtc/matrix_transform.hpp> #include <algorithm> #include <cmath> +#include <filesystem> +#include <ctime> +#include <cstdio> +#include <vector> namespace Donut { @@ -90,4 +98,90 @@ namespace Donut // Backend-specific HDRI/GPU resource cleanup is the device's job (see e.g. // the OpenGL device clearing the HDRIManager texture cache on shutdown). } + + // Portable Float Map: a raw RGB float image (the actual physical values). Its + // raster is bottom-up; -1.0 scale flags little-endian. + static auto write_pfm(const std::string& path, int w, int h, const std::vector<float>& rgba) -> bool + { + std::FILE* f = std::fopen(path.c_str(), "wb"); + if (!f) return false; + std::fprintf(f, "PF\n%d %d\n-1.0\n", w, h); + std::vector<float> rgb((size_t)w * 3); + for (int y = h - 1; y >= 0; --y) + { + const float* src = &rgba[(size_t)y * w * 4]; + for (int x = 0; x < w; ++x) { rgb[x*3+0] = src[x*4+0]; rgb[x*3+1] = src[x*4+1]; rgb[x*3+2] = src[x*4+2]; } + std::fwrite(rgb.data(), sizeof(float), (size_t)w * 3, f); + } + std::fclose(f); + return true; + } + + // CSV grid of the scalar value (the R channel), one image row per text line. + static auto write_csv(const std::string& path, int w, int h, const std::vector<float>& rgba) -> bool + { + std::FILE* f = std::fopen(path.c_str(), "wb"); + if (!f) return false; + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) std::fprintf(f, x ? ",%g" : "%g", rgba[((size_t)y * w + x) * 4]); + std::fputc('\n', f); + } + std::fclose(f); + return true; + } + + auto RenderPath::export_frame(Scene& scene, const ExportConfig& cfg) -> int + { + if (!m_device) return 0; + const int w = std::max(cfg.width, 1), h = std::max(cfg.height, 1); + const bool raw = cfg.format != ExportFormat::Png; // Pfm / Csv want physical values + auto target = m_device->create_render_target( + w, h, raw ? RHI::Format::RGBA32F : RHI::Format::RGBA8, RHI::Format::None, RHI::Filter::Nearest); + + // Full-quality geodesic view from the sim camera (moving = false = settled). + GeodesicView gv; + glm::vec3 pos, fwd; + if (scene.sim_camera.get_camera_mode() == CameraMode::FPS) + { pos = scene.sim_camera.get_position(); fwd = scene.sim_camera.get_forward_direction(); } + else + { pos = scene.sim_camera.get_orbital_position(); fwd = glm::normalize(scene.sim_camera.get_orbital_target() - pos); } + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + gv.position = pos; gv.right = right; gv.up = glm::cross(right, fwd); gv.forward = fwd; + gv.tan_half_fov = (float)tan(glm::radians(scene.black_hole.fov_degrees * 0.5f)); + gv.aspect = (float)w / (float)h; + gv.moving = false; + gv.time = 0.0f; + + std::error_code ec; std::filesystem::create_directories(cfg.directory, ec); + char stamp[32]; std::time_t t = std::time(nullptr); + std::strftime(stamp, sizeof(stamp), "%Y%m%d_%H%M%S", std::localtime(&t)); + const char* ext = cfg.format == ExportFormat::Pfm ? "pfm" : cfg.format == ExportFormat::Csv ? "csv" : "png"; + + int written = 0; + std::vector<uint8_t> px8; std::vector<float> pxf; + auto do_channel = [&](bool enabled, int channel, const char* name) + { + if (!enabled) return; + m_device->run_offscreen([&](RHI::CommandList& cmd) { + m_black_hole_renderer->render_export(cmd, target.get(), gv, scene.black_hole, + scene.objects, m_cubemap.get(), channel, raw); + }); + std::string path = cfg.directory + "/donut_" + name + "_" + stamp + "." + ext; + bool ok; + if (cfg.format == ExportFormat::Png) + { m_device->read_render_target(target.get(), px8); ok = stbi_write_png(path.c_str(), w, h, 4, px8.data(), w * 4) != 0; } + else + { + m_device->read_render_target_float(target.get(), pxf); + ok = (cfg.format == ExportFormat::Pfm) ? write_pfm(path, w, h, pxf) : write_csv(path, w, h, pxf); + } + if (ok) { DONUT_INFO("Exported {}", path); ++written; } else DONUT_ERROR("Export failed: {}", path); + }; + do_channel(cfg.color, 0, "color"); + do_channel(cfg.redshift, 1, "redshift"); + do_channel(cfg.temperature, 2, "temperature"); + do_channel(cfg.impact, 3, "impact"); + return written; + } } diff --git a/src/rendering/render_path.h b/src/rendering/render_path.h index 0d97395..c029132 100644 --- a/src/rendering/render_path.h +++ b/src/rendering/render_path.h @@ -4,6 +4,7 @@ #include "view.h" #include "scene_renderer.h" #include "black_hole_renderer.h" +#include "export_config.h" #include "core/memory.h" #include <string> @@ -30,6 +31,10 @@ namespace Donut auto render(RHI::CommandList& cmd, Scene& scene, View view, int fb_width, int fb_height, bool moving, float time) -> void; + // Renders the requested export channels off-screen at cfg.width x cfg.height + // and writes one PNG per channel. Returns the number of files written. + auto export_frame(Scene& scene, const ExportConfig& cfg) -> int; + private: RHI::Device* m_device = nullptr; Scope<SceneRenderer> m_scene_renderer; diff --git a/src/rendering/rhi.h b/src/rendering/rhi.h index e4e7e62..6a2042a 100644 --- a/src/rendering/rhi.h +++ b/src/rendering/rhi.h @@ -22,6 +22,7 @@ namespace Donut::RHI Swapchain, // a pipeline's colour target = the presented image (resolved per backend) RGBA8, // 8-bit unorm colour (LDR / off-screen) RGBA16F, // half-float colour (HDR / cubemap) + RGBA32F, // full-float colour (raw observable export) D32, // 32-bit depth }; @@ -146,6 +147,19 @@ namespace Donut::RHI virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0; virtual auto end_frame() -> void = 0; + // One-shot off-screen work outside the frame loop (used by the exporter): + // records a self-contained pass into a transient command list and blocks + // until the GPU finishes, so the target can be read back immediately. + virtual auto run_offscreen(const std::function<void(CommandList&)>& record) -> void = 0; + + // Reads a render target's colour back to the CPU as tightly-packed RGBA8, + // top-left origin (out is resized to width*height*4). For export/analysis. + virtual auto read_render_target(RenderTarget* target, std::vector<uint8_t>& out) -> void = 0; + + // Same, but as full-float RGBA (out resized to width*height*4 floats). Read + // an RGBA32F target back for raw observable export. + virtual auto read_render_target_float(RenderTarget* target, std::vector<float>& out) -> void = 0; + // ImGui lives above the RHI but its platform/render backend is per-device. virtual auto init_imgui() -> void = 0; virtual auto imgui_new_frame() -> void = 0; |
