From e3aa33fc9a99d8b817bda42a567a4c347e18ab32 Mon Sep 17 00:00:00 2001 From: hachem Date: Sun, 23 Aug 2026 05:53:33 +0200 Subject: [add]: unified interface --- src/platform/opengl/opengl_renderer.cpp | 377 ++++++++++++++++++++++++++++++++ src/platform/opengl/opengl_renderer.h | 72 ++++++ 2 files changed, 449 insertions(+) create mode 100644 src/platform/opengl/opengl_renderer.cpp create mode 100644 src/platform/opengl/opengl_renderer.h (limited to 'src/platform/opengl') diff --git a/src/platform/opengl/opengl_renderer.cpp b/src/platform/opengl/opengl_renderer.cpp new file mode 100644 index 0000000..ae12f10 --- /dev/null +++ b/src/platform/opengl/opengl_renderer.cpp @@ -0,0 +1,377 @@ +#include "opengl_renderer.h" + +#include "engine/engine.h" +#include "core/log.h" +#include "core/camera.h" +#include "core/hdri_manager.h" +#include "rendering/renderer.h" // Renderer::init / RenderCommand + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/hdri_manager.h" +#include "rendering/texture.h" // CubemapTexture::bind + +namespace Donut +{ + // Scroll is accumulated through a chained GLFW callback (keeps ImGui's scroll + // handling intact), consumed once per frame for orbital zoom. + static double g_scroll_accum = 0.0; + static GLFWscrollfun g_prev_scroll = nullptr; + static void gl_scroll_cb(GLFWwindow* w, double x, double y) + { + if (g_prev_scroll) g_prev_scroll(w, x, y); + g_scroll_accum += y; + } + + OpenGLRenderer::OpenGLRenderer() = default; + OpenGLRenderer::~OpenGLRenderer() = default; + + auto OpenGLRenderer::init(void* glfwWindow, int width, int height) -> bool + { + m_window = glfwWindow; + auto* win = static_cast(glfwWindow); + glfwMakeContextCurrent(win); + + Renderer::init(); // GLAD load + default GL state + RenderCommand::set_face_culling(false); + + const GLubyte* name = glGetString(GL_RENDERER); + m_device_name = name ? reinterpret_cast(name) : "OpenGL"; + + m_engine = create_scope(); + m_engine->set_window_dimensions(width, height); +#ifdef __APPLE__ + m_engine->set_compute_height(256); // capped for the macOS watchdog (tiled fragment pass) +#else + m_engine->set_compute_height(540); +#endif + m_engine->update_compute_dimensions(); + + // Match the Vulkan framing exactly (camera sits outside the disk). + Camera& cam = m_engine->get_camera(); + cam.set_camera_mode(CameraMode::Orbital); + cam.set_orbital_target(glm::vec3(0.0f)); + cam.set_orbital_radius(4e11); + cam.set_orbital_limits(2.2e11, 1.5e12); + cam.set_orbital_speed(0.01f); + cam.set_zoom_speed(3e10); + cam.set_azimuth(0.0f); + cam.set_elevation(1.25f); + + m_hdri_path = "assets/hdri/HDR_blue_nebulae-1.hdr"; + HDRIManager::get().set_current_hdri(m_hdri_path); + + create_scene_resources(); + + DONUT_INFO("OpenGL renderer ready: {} ({}x{})", m_device_name, width, height); + return true; + } + + auto OpenGLRenderer::init_ui() -> bool + { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui::StyleColorsDark(); + ImGui_ImplGlfw_InitForOpenGL(static_cast(m_window), true); + ImGui_ImplOpenGL3_Init("#version 410"); + g_prev_scroll = glfwSetScrollCallback(static_cast(m_window), gl_scroll_cb); + m_ui_ready = true; + DONUT_INFO("OpenGL: ImGui backend initialized"); + return true; + } + + auto OpenGLRenderer::shutdown() -> void + { + if (m_ui_ready) + { + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + m_ui_ready = false; + } + m_engine.reset(); + } + + auto OpenGLRenderer::resize(int width, int height) -> void + { + if (m_engine) m_engine->set_window_dimensions(width, height); + } + + auto OpenGLRenderer::process_input() -> void + { + auto* win = static_cast(m_window); + Camera& cam = m_scene_mode ? m_scene_camera : m_engine->get_camera(); + bool over_ui = m_ui_ready && ImGui::GetIO().WantCaptureMouse; + + double now = glfwGetTime(); + float dt = m_last_frame_time > 0.0 ? static_cast(now - m_last_frame_time) : 0.0f; + m_last_frame_time = now; + + double mx = 0, my = 0; glfwGetCursorPos(win, &mx, &my); + bool left_down = glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + + if (!m_scene_mode && cam.get_camera_mode() == CameraMode::FPS) + { + if (left_down && !m_left_was_down) { m_last_x = mx; m_last_y = my; } + if (left_down && !over_ui) + cam.on_mouse_move(static_cast(mx - m_last_x), static_cast(m_last_y - my)); + m_last_x = mx; m_last_y = my; m_left_was_down = left_down; + + bool over_kb = m_ui_ready && ImGui::GetIO().WantCaptureKeyboard; + if (!over_kb && dt > 0.0f) + { + float mdt = dt * (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f); + if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) cam.move_forward(mdt); + if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) cam.move_backward(mdt); + if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) cam.move_left(mdt); + if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) cam.move_right(mdt); + if (glfwGetKey(win, GLFW_KEY_E) == GLFW_PRESS) cam.move_up(mdt); + if (glfwGetKey(win, GLFW_KEY_Q) == GLFW_PRESS) cam.move_down(mdt); + } + g_scroll_accum = 0.0; // scroll unused in free-fly + } + else + { + if (left_down && !m_left_was_down && !over_ui) + cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); + else if (!left_down && m_left_was_down) + cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); + m_left_was_down = left_down; + + cam.process_orbital_mouse_move(mx, my); + double scroll = g_scroll_accum; g_scroll_accum = 0.0; + if (scroll != 0.0 && !over_ui) cam.process_orbital_scroll(0.0, scroll); + } + } + + // 36-vertex unit cube for the skybox (positions only; the shader forces depth=1). + static const float SKYBOX_CUBE[] = { + -1, 1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1, 1,-1, -1, 1,-1, + -1,-1, 1, -1,-1,-1, -1, 1,-1, -1, 1,-1, -1, 1, 1, -1,-1, 1, + 1,-1,-1, 1,-1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1, 1,-1,-1, + -1,-1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1, 1, -1,-1, 1, + -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1, + -1,-1,-1, -1,-1, 1, 1,-1,-1, 1,-1,-1, -1,-1, 1, 1,-1, 1, + }; + + auto OpenGLRenderer::create_scene_resources() -> void + { + const float PI = 3.14159265358979f; + + m_grid_shader = Ref(Shader::create("assets/shaders/Grid.glsl")); + m_sphere_shader = Ref(Shader::create("assets/shaders/Sphere.glsl")); + m_skybox_shader = Ref(Shader::create("assets/shaders/Skybox.glsl")); + + m_scene_camera.set_camera_mode(CameraMode::Orbital); + m_scene_camera.set_orbital_target(glm::vec3(0.0f)); + m_scene_camera.set_orbital_radius(15.0); + m_scene_camera.set_orbital_limits(2.0, 200.0); + m_scene_camera.set_orbital_speed(0.01f); + m_scene_camera.set_zoom_speed(2.0); + m_scene_camera.set_azimuth(0.0f); + m_scene_camera.set_elevation(PI / 3.0f); + + // Grid: +/-50 lines on the XZ plane (Grid shader scales by u_GridSize/50). + std::vector lines; + for (int i = -50; i <= 50; ++i) + { + lines.push_back({ (float)i, 0.0f, -50.0f }); lines.push_back({ (float)i, 0.0f, 50.0f }); + lines.push_back({ -50.0f, 0.0f, (float)i }); lines.push_back({ 50.0f, 0.0f, (float)i }); + } + m_grid_vertex_count = (int)lines.size(); + glGenVertexArrays(1, &m_grid_vao); glGenBuffers(1, &m_grid_vbo); + glBindVertexArray(m_grid_vao); glBindBuffer(GL_ARRAY_BUFFER, m_grid_vbo); + glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(glm::vec3), lines.data(), GL_STATIC_DRAW); + glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); + + // Sphere: indexed UV sphere (position + normal), unit radius. + const int RINGS = 24, SECT = 48; + std::vector sv; std::vector si; + for (int r = 0; r <= RINGS; ++r) + { + float th = PI * r / RINGS; + for (int s = 0; s <= SECT; ++s) + { + float ph = 2.0f * PI * s / SECT; + glm::vec3 p(sinf(th) * cosf(ph), cosf(th), sinf(th) * sinf(ph)); + sv.insert(sv.end(), { p.x, p.y, p.z, p.x, p.y, p.z }); // pos, normal (unit sphere) + } + } + for (int r = 0; r < RINGS; ++r) + for (int s = 0; s < SECT; ++s) + { + int a = r * (SECT + 1) + s, b = a + SECT + 1; + si.insert(si.end(), { (unsigned)a, (unsigned)b, (unsigned)(a + 1), + (unsigned)(a + 1), (unsigned)b, (unsigned)(b + 1) }); + } + m_sphere_index_count = (int)si.size(); + glGenVertexArrays(1, &m_sphere_vao); glGenBuffers(1, &m_sphere_vbo); glGenBuffers(1, &m_sphere_ebo); + glBindVertexArray(m_sphere_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_sphere_vbo); + glBufferData(GL_ARRAY_BUFFER, sv.size() * sizeof(float), sv.data(), GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_sphere_ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, si.size() * sizeof(unsigned), si.data(), GL_STATIC_DRAW); + glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); + glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); + + // Skybox cube. + glGenVertexArrays(1, &m_skybox_vao); glGenBuffers(1, &m_skybox_vbo); + glBindVertexArray(m_skybox_vao); glBindBuffer(GL_ARRAY_BUFFER, m_skybox_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(SKYBOX_CUBE), SKYBOX_CUBE, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + + glBindVertexArray(0); + m_scene_ready = true; + } + + auto OpenGLRenderer::render_scene(int w, int h) -> void + { + if (!m_grid_shader || !m_sphere_shader || !m_skybox_shader) return; + glViewport(0, 0, w, h); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + float aspect = h > 0 ? (float)w / (float)h : 1.0f; + m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); + glm::mat4 proj = m_scene_camera.get_projection_matrix(); + glm::mat4 view = m_scene_camera.get_view_matrix(); + glm::mat4 vp = proj * view; + glm::vec3 cam = m_scene_camera.get_orbital_position(); + + // Skybox behind everything (depth test on, write off; shader forces depth=1). + glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(GL_FALSE); + m_skybox_shader->bind(); + m_skybox_shader->set_mat4("u_Projection", proj); + m_skybox_shader->set_mat4("u_View", glm::mat4(glm::mat3(view))); // strip translation + auto hdri = HDRIManager::get().get_current_hdri(); + if (hdri) { hdri->bind(0); m_skybox_shader->set_int("u_Skybox", 0); } + glBindVertexArray(m_skybox_vao); glDrawArrays(GL_TRIANGLES, 0, 36); + + // Lit spheres. + glDepthFunc(GL_LESS); glDepthMask(GL_TRUE); + m_sphere_shader->bind(); glBindVertexArray(m_sphere_vao); + for (int i = 0; i < (int)m_scene_objects.size(); ++i) + { + const SceneObject& o = m_scene_objects[i]; + m_sphere_shader->set_mat4("u_ViewProjection", vp); + m_sphere_shader->set_mat4("u_Transform", + glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius))); + m_sphere_shader->set_float3("u_Color", o.color); + m_sphere_shader->set_float("u_Specular", 0.6f); + m_sphere_shader->set_float("u_Emission", 0.0f); + m_sphere_shader->set_float3("u_LightPos", glm::vec3(10.0f, 20.0f, 10.0f)); + m_sphere_shader->set_float3("u_CameraPos", cam); + m_sphere_shader->set_int("u_IsSelected", i == m_selected_object ? 1 : 0); + m_sphere_shader->set_float3("u_OutlineColor", glm::vec3(1.0f, 1.0f, 0.0f)); + m_sphere_shader->set_float("u_OutlineWidth", 0.15f); + glDrawElements(GL_TRIANGLES, m_sphere_index_count, GL_UNSIGNED_INT, 0); + } + + // Transparent grid on top (test, no depth write). + glDepthMask(GL_FALSE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + m_grid_shader->bind(); + m_grid_shader->set_mat4("u_ViewProjection", vp); + m_grid_shader->set_mat4("u_Transform", glm::mat4(1.0f)); + m_grid_shader->set_float("u_GridSize", 50.0f); + m_grid_shader->set_float3("u_GridColor", glm::vec3(0.55f, 0.55f, 0.6f)); + m_grid_shader->set_float("u_GridAlpha", 0.75f); + m_grid_shader->set_float3("u_CameraPos", cam); + glBindVertexArray(m_grid_vao); glDrawArrays(GL_LINES, 0, m_grid_vertex_count); + glDisable(GL_BLEND); glDepthMask(GL_TRUE); glBindVertexArray(0); + } + + auto OpenGLRenderer::draw_frame(const glm::vec4& clear_color, + const std::function& build_ui) -> void + { + auto* win = static_cast(m_window); + process_input(); + + int w = 0, h = 0; glfwGetFramebufferSize(win, &w, &h); + + if (m_scene_mode) + { + render_scene(w, h); + } + else + { + m_engine->set_window_dimensions(w, h); + // Geodesic pass -> low-res texture (watchdog-tiled on macOS), then + // upscale to the window. Disk/camera come from the shared BlackHoleParams. + m_engine->dispatch_compute(m_engine->get_camera()); + RenderCommand::set_viewport(0, 0, w, h); + RenderCommand::set_clear_color(clear_color); + RenderCommand::clear(); + m_engine->draw_full_screen_quad(); + } + + if (m_ui_ready) + { + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + if (build_ui) build_ui(); + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + glfwSwapBuffers(win); + } + + auto OpenGLRenderer::set_scene_mode(bool enabled) -> void { m_scene_mode = enabled; } + auto OpenGLRenderer::is_scene_mode() const -> bool { return m_scene_mode; } + + auto OpenGLRenderer::set_free_fly(bool enabled) -> void + { + Camera& cam = m_engine->get_camera(); + if (enabled && cam.get_camera_mode() != CameraMode::FPS) + { + glm::vec3 pos = cam.get_orbital_position(); + glm::vec3 fwd = glm::normalize(cam.get_orbital_target() - pos); + float pitch = glm::degrees(asinf(glm::clamp(fwd.y, -1.0f, 1.0f))); + float yaw = glm::degrees(atan2f(fwd.z, fwd.x)); + cam.set_position(pos); + cam.set_rotation(glm::vec3(pitch, yaw, 0.0f)); + cam.set_movement_speed(2.0e10f); + cam.set_mouse_sensitivity(0.15f); + cam.set_camera_mode(CameraMode::FPS); + } + else if (!enabled) + { + cam.set_camera_mode(CameraMode::Orbital); + } + } + + auto OpenGLRenderer::is_free_fly() const -> bool + { + return m_engine->get_camera().get_camera_mode() == CameraMode::FPS; + } + + auto OpenGLRenderer::reset_camera() -> void + { + Camera& cam = m_engine->get_camera(); + cam.set_camera_mode(CameraMode::Orbital); + cam.set_orbital_radius(4e11); + cam.set_azimuth(0.0f); + cam.set_elevation(1.25f); + } + + auto OpenGLRenderer::set_hdri(const std::string& path) -> void + { + HDRIManager::get().set_current_hdri(path); // the Engine re-reads it each frame + m_hdri_path = path; + } + + auto OpenGLRenderer::current_hdri() const -> const std::string& { return m_hdri_path; } + auto OpenGLRenderer::scene_objects() -> std::vector& { return m_scene_objects; } + auto OpenGLRenderer::set_selected_object(int index) -> void { m_selected_object = index; } + auto OpenGLRenderer::black_hole_params() -> BlackHoleParams& { return m_engine->black_hole_params(); } + auto OpenGLRenderer::device_name() const -> const std::string& { return m_device_name; } +} diff --git a/src/platform/opengl/opengl_renderer.h b/src/platform/opengl/opengl_renderer.h new file mode 100644 index 0000000..4258f40 --- /dev/null +++ b/src/platform/opengl/opengl_renderer.h @@ -0,0 +1,72 @@ +#pragma once + +#include "rendering/renderer_backend.h" +#include "core/memory.h" +#include "core/camera.h" +#include "rendering/shader.h" + +// OpenGL implementation of the shared RendererBackend interface. It renders the +// SAME cross-compiled Geodesic shader the Vulkan backend does (driven by the +// same BlackHoleParams + camera), so the application behaves identically on +// either backend. Wraps the Engine, which owns the GL geodesic/present passes +// and the macOS watchdog-safe tiled rendering. +namespace Donut +{ + class Engine; + + class OpenGLRenderer : public RendererBackend + { + public: + OpenGLRenderer(); + ~OpenGLRenderer() override; + + auto init(void* glfwWindow, int width, int height) -> bool override; + auto init_ui() -> bool override; + auto shutdown() -> void override; + auto resize(int width, int height) -> void override; + auto draw_frame(const glm::vec4& clear_color, + const std::function& build_ui) -> void override; + + auto set_scene_mode(bool enabled) -> void override; + auto is_scene_mode() const -> bool override; + auto set_free_fly(bool enabled) -> void override; + auto is_free_fly() const -> bool override; + auto reset_camera() -> void override; + + auto set_hdri(const std::string& path) -> void override; + auto current_hdri() const -> const std::string& override; + auto scene_objects() -> std::vector& override; + auto set_selected_object(int index) -> void override; + auto black_hole_params() -> BlackHoleParams& override; + + auto device_name() const -> const std::string& override; + + private: + auto process_input() -> void; + auto create_scene_resources() -> void; // grid + sphere + skybox meshes/shaders + auto render_scene(int width, int height) -> void; + + void* m_window = nullptr; + Scope m_engine; + + // World-builder scene view (normal-scale): mirrors the Vulkan scene view + // with the same shared Grid/Sphere/Skybox shaders. + Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; + Ref m_grid_shader, m_sphere_shader, m_skybox_shader; + unsigned int m_grid_vao = 0, m_grid_vbo = 0; + unsigned int m_sphere_vao = 0, m_sphere_vbo = 0, m_sphere_ebo = 0; + unsigned int m_skybox_vao = 0, m_skybox_vbo = 0; + int m_grid_vertex_count = 0, m_sphere_index_count = 0; + bool m_scene_ready = false; + std::vector m_scene_objects{ SceneObject{} }; + int m_selected_object = 0; + std::string m_hdri_path; + std::string m_device_name; + bool m_scene_mode = false; + bool m_ui_ready = false; + + double m_last_x = 0.0, m_last_y = 0.0; + bool m_left_was_down = false; + double m_last_frame_time = 0.0; + }; +} -- cgit v1.3