aboutsummaryrefslogtreecommitdiff
path: root/src/platform/opengl
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform/opengl')
-rw-r--r--src/platform/opengl/opengl_renderer.cpp377
-rw-r--r--src/platform/opengl/opengl_renderer.h72
2 files changed, 449 insertions, 0 deletions
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 <glad/glad.h>
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+#include <imgui_impl_glfw.h>
+#include <imgui_impl_opengl3.h>
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <cmath>
+#include <vector>
+
+#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*>(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<const char*>(name) : "OpenGL";
+
+ m_engine = create_scope<Engine>();
+ 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<GLFWwindow*>(m_window), true);
+ ImGui_ImplOpenGL3_Init("#version 410");
+ g_prev_scroll = glfwSetScrollCallback(static_cast<GLFWwindow*>(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<GLFWwindow*>(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<float>(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<float>(mx - m_last_x), static_cast<float>(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>(Shader::create("assets/shaders/Grid.glsl"));
+ m_sphere_shader = Ref<Shader>(Shader::create("assets/shaders/Sphere.glsl"));
+ m_skybox_shader = Ref<Shader>(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<glm::vec3> 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<float> sv; std::vector<unsigned int> 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<void()>& build_ui) -> void
+ {
+ auto* win = static_cast<GLFWwindow*>(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<SceneObject>& { 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<void()>& 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<SceneObject>& 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<Engine> 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<Shader> 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<SceneObject> 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;
+ };
+}