#include "render_path.h" #include "scene/scene.h" #include "core/log.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" #include #include #include #include #include #include #include namespace Donut { auto RenderPath::init(RHI::Device& device, const std::string& hdri_path) -> bool { m_device = &device; m_hdri_path = hdri_path; m_cubemap = device.create_cubemap_from_hdri(hdri_path); m_scene_renderer = create_scope(); m_scene_renderer->init(device); m_black_hole_renderer = create_scope(); m_black_hole_renderer->init(device); return true; } auto RenderPath::sync_hdri(const std::string& hdri_path) -> void { if (hdri_path == m_hdri_path || !m_device) return; m_device->wait_idle(); m_cubemap = m_device->create_cubemap_from_hdri(hdri_path); // old cube freed after idle m_hdri_path = hdri_path; } auto RenderPath::render(RHI::CommandList& cmd, Scene& scene, View view, int fb_width, int fb_height, bool moving, float time) -> void { const glm::vec4 clear(0.05f, 0.06f, 0.10f, 1.0f); // The black hole is the only view with an off-screen pass; it renders (and // runs the expensive geodesic) ONLY on the Simulation tab. Scene and None // are a single swapchain pass — None draws nothing (empty viewport). if (view != View::BlackHole) { cmd.begin_render_pass(nullptr, clear); if (view == View::Scene) { // Projection was set by the Application this frame (shared with the gizmo). CameraView cv; cv.view = scene.scene_camera.get_view_matrix(); cv.projection = scene.scene_camera.get_projection_matrix(); cv.position = scene.scene_camera.get_orbital_position(); cv.fb_width = fb_width; cv.fb_height = fb_height; m_scene_renderer->render(cmd, cv, scene.objects, scene.selected_object, m_cubemap.get()); } m_device->imgui_render(cmd); cmd.end_render_pass(); } else { 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)BlackHoleRenderer::GEO_HI_W / (float)BlackHoleRenderer::GEO_HI_H; gv.moving = moving; gv.time = time; m_black_hole_renderer->render_geodesic(cmd, gv, scene.black_hole, scene.objects, m_cubemap.get()); cmd.begin_render_pass(nullptr, clear); m_black_hole_renderer->blit(cmd, fb_width, fb_height); m_device->imgui_render(cmd); cmd.end_render_pass(); } } auto RenderPath::shutdown() -> void { m_scene_renderer.reset(); m_black_hole_renderer.reset(); m_cubemap.reset(); // 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& 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 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& 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 px8; std::vector 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; } }