aboutsummaryrefslogtreecommitdiff
path: root/src/rendering
diff options
context:
space:
mode:
Diffstat (limited to 'src/rendering')
-rw-r--r--src/rendering/black_hole_renderer.cpp62
-rw-r--r--src/rendering/black_hole_renderer.h13
-rw-r--r--src/rendering/export_config.h28
-rw-r--r--src/rendering/render_path.cpp94
-rw-r--r--src/rendering/render_path.h5
-rw-r--r--src/rendering/rhi.h14
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;