diff --git a/src/framebuffer.cpp b/src/framebuffer.cpp index b86676f..049075a 100644 --- a/src/framebuffer.cpp +++ b/src/framebuffer.cpp @@ -16,6 +16,11 @@ Rc FrameBuffer::create(GLsizei width, GLsizei height) gl_clear_errors(); + // Restored afterwards so creating a framebuffer leaves the caller's bindings intact + GLuint previous_fbo = gl_get_binding(GL_FRAMEBUFFER_BINDING); + GLuint previous_texture = gl_get_binding(GL_TEXTURE_BINDING_2D); + GLuint previous_render_buffer = gl_get_binding(GL_RENDERBUFFER_BINDING); + GLuint id, texture, render_buffer; glGenFramebuffers(1, &id); @@ -40,9 +45,9 @@ Rc FrameBuffer::create(GLsizei width, GLsizei height) GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); GLenum err = gl_pop_error(); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glBindTexture(GL_TEXTURE_2D, 0); - glBindRenderbuffer(GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, previous_fbo); + glBindTexture(GL_TEXTURE_2D, previous_texture); + glBindRenderbuffer(GL_RENDERBUFFER, previous_render_buffer); if (status != GL_FRAMEBUFFER_COMPLETE || err != GL_NO_ERROR) { glDeleteRenderbuffers(1, &render_buffer); @@ -124,6 +129,11 @@ bool FrameBuffer::resize(GLsizei width, GLsizei height) gl_clear_errors(); + // Restored afterwards so resizing leaves the caller's bindings intact + GLuint previous_fbo = gl_get_binding(GL_FRAMEBUFFER_BINDING); + GLuint previous_texture = gl_get_binding(GL_TEXTURE_BINDING_2D); + GLuint previous_render_buffer = gl_get_binding(GL_RENDERBUFFER_BINDING); + m_width = width; m_height = height; @@ -137,15 +147,12 @@ bool FrameBuffer::resize(GLsizei width, GLsizei height) glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, m_width, m_height); - glBindTexture(GL_TEXTURE_2D, 0); - glBindRenderbuffer(GL_RENDERBUFFER, 0); - - // Check completeness without disturbing the caller's framebuffer binding - GLint previous_fbo = 0; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previous_fbo); glBindFramebuffer(GL_FRAMEBUFFER, m_id); GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - glBindFramebuffer(GL_FRAMEBUFFER, static_cast(previous_fbo)); + + glBindFramebuffer(GL_FRAMEBUFFER, previous_fbo); + glBindTexture(GL_TEXTURE_2D, previous_texture); + glBindRenderbuffer(GL_RENDERBUFFER, previous_render_buffer); GLenum err = gl_pop_error(); if (status != GL_FRAMEBUFFER_COMPLETE || err != GL_NO_ERROR) { diff --git a/src/gldebug.cpp b/src/gldebug.cpp index 001aa20..8ff7785 100644 --- a/src/gldebug.cpp +++ b/src/gldebug.cpp @@ -80,6 +80,13 @@ void set_gl_label(GLenum identifier, GLuint id, const std::string& label) glObjectLabel(identifier, id, length, label.c_str()); } +GLuint gl_get_binding(GLenum binding_query) +{ + GLint id = 0; + glGetIntegerv(binding_query, &id); + return static_cast(id); +} + void gl_clear_errors() { gl_pop_error(); diff --git a/src/gldebug.hpp b/src/gldebug.hpp index a8f0637..b97be98 100644 --- a/src/gldebug.hpp +++ b/src/gldebug.hpp @@ -13,6 +13,10 @@ void enable_gl_debug_output(); // Name a GL object so debug messages and tools like RenderDoc show it void set_gl_label(GLenum identifier, GLuint id, const std::string& label); +// Object currently bound for a binding query (e.g. GL_TEXTURE_BINDING_2D), used +// to restore the caller's bindings after creating or updating an object +GLuint gl_get_binding(GLenum binding_query); + // Discard errors left over from earlier calls void gl_clear_errors(); diff --git a/src/main.cpp b/src/main.cpp index 522c680..e8ebd7e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,9 +3,9 @@ #include #include #include +#include #include #include -#include #define IMGUI_DEFINE_MATH_OPERATORS #include #include @@ -14,6 +14,20 @@ using namespace Fan; +/******************** Constants ********************/ + +// Random lights are scattered inside this box in every scene +const glm::vec3 LIGHT_BOX_MIN{-10.0f, 1.0f, -3.0f}; +const glm::vec3 LIGHT_BOX_MAX{10.0f, 5.0f, 3.0f}; + +// Overlay text in the top-left corner of the viewport +const ImVec4 OVERLAY_COLOR{0.0f, 1.0f, 0.0f, 1.0f}; +constexpr float OVERLAY_X = 20.0f; +constexpr float OVERLAY_Y = 35.0f; +constexpr float OVERLAY_LINE_HEIGHT = 17.0f; + +/******************** Application state ********************/ + // Ring buffer of per-frame measurements, for plotting and stable averages struct MetricHistory { @@ -42,47 +56,72 @@ struct MetricHistory } }; -struct UserData +// Free-fly camera controls, active while the mouse is grabbed +struct CameraInput { - Window& window; - Camera& camera; - float delta_time{}; - bool mouse_grab{}; - float speed{CAMERA_SPEED}; float sensitivity{CAMERA_SENSITIVITY}; + + bool mouse_grab{}; + bool first_mouse{true}; // Skips the jump from the cursor's position before grabbing float last_mouse_x{}; float last_mouse_y{}; - bool first_mouse{true}; +}; - bool vsync{true}; - bool show_debug{}; +// Timings of the scene pass (gpu_timer is null if timer queries are unsupported) +struct PerformanceStats +{ + CpuTimer cpu_timer{}; + Rc gpu_timer{}; - Rc logo{}; - - // Performance measurements of the scene pass (GPU timer is null if unsupported) - CpuTimer scene_cpu_timer{}; - Rc scene_gpu_timer{}; MetricHistory frame_times{}; MetricHistory scene_cpu_times{}; MetricHistory scene_gpu_times{}; +}; + +struct AppState +{ + Window& window; + Camera& camera; + + CameraInput input{}; + PerformanceStats stats{}; + + bool vsync{true}; + bool show_debug{}; + Rc logo{}; std::vector> scenes{}; size_t selected_scene_index{}; }; -void process_input(Window& window, Camera& camera, UserData& user_data); +/******************** Declarations ********************/ + +int run(Window& window); + +// Scenes +std::vector> create_scenes(const Rc& shader, const Rc& cube_mesh); +Rc create_sponza_scene(const Rc& shader); +Rc create_car_scene(const Rc& shader); +Rc create_backpack_scene(const Rc& shader, const Rc& cube_mesh); +Rc create_cubes_scene(const Rc& shader, const Rc& cube_mesh); +Rc create_textured_material(const Rc& shader, const std::string& texture_path); +void add_random_lights(Scene& scene, int point_light_count, int spot_light_count, unsigned int seed); + +// Frame +void process_input(AppState& app); void mouse_callback(double pos_x, double pos_y, void* user_data); +void render_scene(AppState& app, FrameBuffer& framebuffer, const Rc& debug_shader); + +// UI void setup_dockspace(); -void draw_panel_window(UserData& user_data); -void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data); +void draw_panel_window(AppState& app); +void draw_viewport_window(AppState& app, FrameBuffer& framebuffer); +void draw_overlay(const AppState& app); void plot_metric(const char* label, const MetricHistory& history); -void add_random_lights(Scene& scene, int point_light_count, int spot_light_count, - const glm::vec3& box_min, const glm::vec3& box_max, - unsigned int seed = std::random_device{}()); template -void overlay_text(float offset_x, float offset_y, ImVec4 color, const char* format, Args... args) +void overlay_text(float offset_x, float offset_y, const char* format, Args... args) { char buffer[256]; std::snprintf(buffer, sizeof(buffer), format, args...); @@ -92,102 +131,132 @@ void overlay_text(float offset_x, float offset_y, ImVec4 color, const char* form ImVec2 text_size = ImGui::CalcTextSize(buffer); draw_list->AddRectFilled(pos - ImVec2{4, 2}, pos + text_size + ImVec2{4, 2}, IM_COL32(0, 0, 0, 160)); - draw_list->AddText(pos, ImGui::ColorConvertFloat4ToU32(color), buffer); + draw_list->AddText(pos, ImGui::ColorConvertFloat4ToU32(OVERLAY_COLOR), buffer); } +/******************** Entry point ********************/ + int main() { - /******************** Initialization ********************/ - auto window = Window::create(WINDOW_WIDTH, WINDOW_HEIGHT, VERSION_STRING); if (!window) return EXIT_FAILURE; window->set_vsync(true); if (!Renderer::init()) return EXIT_FAILURE; + // Every GL resource created by run() is released when it returns, + // before the renderer and the window shut down + int exit_code = run(*window); + + Renderer::shutdown(); + return exit_code; +} + +int run(Window& window) +{ Camera camera{{6.0f, 3.0f, 1.0f}}; camera.fov = 60.0f; camera.look_at({0.0f, 1.0f, 0.0f}); - UserData user_data{*window, camera}; - window->set_callback_user_data(&user_data); - window->set_mouse_callback(mouse_callback); - - /******************** Resource loading ********************/ + AppState app{window, camera}; + window.set_callback_user_data(&app); + window.set_mouse_callback(mouse_callback); + // Assets never fail to load: missing ones are replaced by fallbacks auto shader = ResourceCache::load_shader("./src/shaders/default.vert", "./src/shaders/default.frag"); auto debug_shader = ResourceCache::load_shader("./src/shaders/debug.vert", "./src/shaders/debug.frag", false); - - Entity sponza = load_obj("./assets/sponza/sponza.obj", shader); - Entity car = load_obj("./assets/audi_r8/Audi_R8_2017.obj", shader); - Entity backpack = load_obj("./assets/backpack/backpack.obj", shader, true); - - auto cube_texture = ResourceCache::load_texture("./assets/textures/container.jpg"); - auto default_texture = ResourceCache::load_texture("./assets/textures/wall.jpg"); - user_data.logo = ResourceCache::load_texture("assets/logo/logo.png"); - - user_data.scene_gpu_timer = GpuTimer::create(); - - auto cube_mesh = Mesh::create_cube(); - auto framebuffer = FrameBuffer::create(window->get_width(), window->get_height()); + app.logo = ResourceCache::load_texture("assets/logo/logo.png"); + app.stats.gpu_timer = GpuTimer::create(); // Built from memory or sized from the window, failing here means OpenGL is broken - if (!cube_mesh || !framebuffer) { - Renderer::shutdown(); - return EXIT_FAILURE; + auto cube_mesh = Mesh::create_cube(); + auto framebuffer = FrameBuffer::create(window.get_width(), window.get_height()); + if (!cube_mesh || !framebuffer) return EXIT_FAILURE; + + app.scenes = create_scenes(shader, cube_mesh); + + while (!window.should_close()) { + window.begin(); + window.update(); + + app.stats.frame_times.push(window.get_delta_time() * 1000.0f); + process_input(app); + + Renderer::set_clear_color({0.0f, 0.0f, 0.0f, 1.0f}); + Renderer::clear(); + + // UI layer + setup_dockspace(); + draw_panel_window(app); + draw_viewport_window(app, *framebuffer); + + // Scene layer, shown by the viewport window + render_scene(app, *framebuffer, debug_shader); + + window.end(); } - /******************** Scene construction ********************/ + // app is about to be destroyed, don't let the window call back into it + window.set_mouse_callback(nullptr); + return EXIT_SUCCESS; +} - // Sponza scene - Rc sponza_scene = make_rc(); - user_data.scenes.push_back(sponza_scene); +/******************** Scenes ********************/ +std::vector> create_scenes(const Rc& shader, const Rc& cube_mesh) +{ + return { + create_sponza_scene(shader), + create_car_scene(shader), + create_backpack_scene(shader, cube_mesh), + create_cubes_scene(shader, cube_mesh), + }; +} + +Rc create_sponza_scene(const Rc& shader) +{ + auto scene = make_rc(); + + Entity sponza = load_obj("./assets/sponza/sponza.obj", shader); sponza.transform.position = {0.0f, -0.5f, 0.0f}; sponza.transform.scale = {0.008f, 0.008f, 0.008f}; - sponza_scene->add_entity(sponza); + scene->add_entity(sponza); - add_random_lights(*sponza_scene, 4, 4, - glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f), - 69420); + add_random_lights(*scene, 4, 4, 69420); + return scene; +} - // Car scene - Rc car_scene = make_rc(); - user_data.scenes.push_back(car_scene); +Rc create_car_scene(const Rc& shader) +{ + auto scene = make_rc(); - car_scene->add_entity(car); + scene->add_entity(load_obj("./assets/audi_r8/Audi_R8_2017.obj", shader)); - add_random_lights(*car_scene, 4, 4, - glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f), - 42); + add_random_lights(*scene, 4, 4, 42); + return scene; +} - // Backpack scene - Rc backpack_scene = make_rc(); - user_data.scenes.push_back(backpack_scene); +Rc create_backpack_scene(const Rc& shader, const Rc& cube_mesh) +{ + auto scene = make_rc(); - backpack_scene->add_entity(backpack); + scene->add_entity(load_obj("./assets/backpack/backpack.obj", shader, true)); - auto default_material = make_rc(); - default_material->shader = shader; - default_material->diffuse_map = default_texture; - Entity floor{cube_mesh, default_material}; + Entity floor{cube_mesh, create_textured_material(shader, "./assets/textures/wall.jpg")}; floor.transform.position = {0.0f, -3.0f, 0.0f}; floor.transform.scale = {20.0f, 1.0f, 20.0f}; - backpack_scene->add_entity(floor); + scene->add_entity(floor); - add_random_lights(*backpack_scene, 4, 4, - glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f), - 37); + add_random_lights(*scene, 4, 4, 37); + return scene; +} - // Cubes scene - Rc cubes_scene = make_rc(); - user_data.scenes.push_back(cubes_scene); +Rc create_cubes_scene(const Rc& shader, const Rc& cube_mesh) +{ + auto scene = make_rc(); + auto material = create_textured_material(shader, "./assets/textures/container.jpg"); - auto cube_material = make_rc(); - cube_material->shader = shader; - cube_material->diffuse_map = cube_texture; - - std::vector positions{ + const std::vector positions{ { 0.0f, 0.0f, 0.0f}, { 2.0f, 5.0f, -15.0f}, {-1.5f, -2.2f, -2.5f}, {-3.8f, -2.0f, -12.3f}, { 2.4f, -0.4f, -3.5f}, {-1.7f, 3.0f, -7.5f}, { 1.3f, -2.0f, -2.5f}, { 1.5f, 2.0f, -2.5f}, { 1.5f, 0.2f, -1.5f}, @@ -195,92 +264,110 @@ int main() }; for (size_t i = 0; i < positions.size(); ++i) { - Entity entity{cube_mesh, cube_material}; - entity.transform.position = positions[i]; - entity.transform.set_rotation(glm::radians(20.0f * i), {1.0f, 0.3f, 0.5f}); - cubes_scene->add_entity(entity); + Entity cube{cube_mesh, material}; + cube.transform.position = positions[i]; + cube.transform.set_rotation(glm::radians(20.0f * i), {1.0f, 0.3f, 0.5f}); + scene->add_entity(cube); } - add_random_lights(*cubes_scene, 4, 4, - glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f), - 152533); - - /******************** Main loop ********************/ - - while (!window->should_close()) { - window->begin(); - window->update(); - - user_data.delta_time = window->get_delta_time(); - user_data.frame_times.push(user_data.delta_time * 1000.0f); - process_input(*window, camera, user_data); - - Renderer::set_clear_color({0.0f, 0.0f, 0.0f, 1.0f}); - Renderer::clear(); - - // UI Layer - setup_dockspace(); - draw_panel_window(user_data); - draw_viewport_window(*framebuffer, user_data); - - // Scene Rendering Layer - const Scene& scene = *user_data.scenes[user_data.selected_scene_index]; - - int width = framebuffer->get_width(); - int height = framebuffer->get_height(); - framebuffer->bind(); - user_data.scene_cpu_timer.begin(); - if (user_data.scene_gpu_timer) { user_data.scene_gpu_timer->begin(); } - - Renderer::set_viewport(0, 0, width, height); - Renderer::clear(); - Renderer::draw_scene(scene, camera, width, height); - if (user_data.show_debug) { - Renderer::draw_debug_wireframes(scene, camera, width, height, - debug_shader, ResourceCache::get_fallback_mesh()); - } - - if (user_data.scene_gpu_timer) { user_data.scene_gpu_timer->end(); } - user_data.scene_cpu_timer.end(); - framebuffer->unbind(); - - user_data.scene_cpu_times.push(user_data.scene_cpu_timer.get_elapsed_ms()); - if (user_data.scene_gpu_timer) { - user_data.scene_gpu_times.push(user_data.scene_gpu_timer->get_elapsed_ms()); - } - - window->end(); - } - - Renderer::shutdown(); - return EXIT_SUCCESS; + add_random_lights(*scene, 4, 4, 152533); + return scene; } -void process_input(Window& window, Camera& camera, UserData& user_data) +Rc create_textured_material(const Rc& shader, const std::string& texture_path) { + auto material = make_rc(); + material->shader = shader; + material->diffuse_map = ResourceCache::load_texture(texture_path); + return material; +} + +void add_random_lights(Scene& scene, int point_light_count, int spot_light_count, unsigned int seed) +{ + std::mt19937 rng(seed); + + std::uniform_real_distribution random_x(LIGHT_BOX_MIN.x, LIGHT_BOX_MAX.x); + std::uniform_real_distribution random_y(LIGHT_BOX_MIN.y, LIGHT_BOX_MAX.y); + std::uniform_real_distribution random_z(LIGHT_BOX_MIN.z, LIGHT_BOX_MAX.z); + std::uniform_real_distribution random_axis(-1.0f, 1.0f); + + const glm::vec3 light_colors[] = { + glm::vec3(1.0f, 1.0f, 1.0f), // White + glm::vec3(1.0f, 0.6f, 0.2f), // Warm orange + glm::vec3(0.2f, 0.5f, 1.0f), // Cool blue + glm::vec3(1.0f, 0.2f, 0.2f), // Red + glm::vec3(0.3f, 1.0f, 0.4f), // Green + glm::vec3(0.8f, 0.3f, 1.0f) // Purple + }; + std::uniform_int_distribution random_color_index(0, std::size(light_colors) - 1); + + auto random_position = [&]() { + return glm::vec3(random_x(rng), random_y(rng), random_z(rng)); + }; + + auto random_direction = [&]() { + glm::vec3 direction; + + // Avoid generating a vector that is too close to zero + do { + direction = glm::vec3(random_axis(rng), random_axis(rng), random_axis(rng)); + } while (glm::dot(direction, direction) < 0.001f); + + return glm::normalize(direction); + }; + + auto random_color = [&]() { return light_colors[random_color_index(rng)]; }; + + for (int i = 0; i < point_light_count; ++i) { + PointLight light; + light.position = random_position(); + + glm::vec3 color = random_color(); + light.diffuse = color; + light.specular = glm::mix(color, glm::vec3(1.0f), 0.25f); + + scene.light_environment.point_lights.push_back(light); + } + + for (int i = 0; i < spot_light_count; ++i) { + SpotLight light; + light.position = random_position(); + light.direction = random_direction(); + + glm::vec3 color = random_color(); + light.diffuse = color; + light.specular = glm::mix(color, glm::vec3(1.0f), 0.25f); + + // Slightly wider soft outer cone than the default + light.outer_cut_off = glm::cos(glm::radians(17.5f)); + + scene.light_environment.spot_lights.push_back(light); + } +} + +/******************** Frame ********************/ + +void process_input(AppState& app) +{ + Window& window = app.window; + CameraInput& input = app.input; + if (window.is_key_pressed(GLFW_KEY_ESCAPE)) window.set_should_close(true); - - static bool wireframe = false; - if (window.is_key_pressed(GLFW_KEY_H)) { - wireframe = !wireframe; - Renderer::set_wireframe(wireframe); - } - - static bool fullscreen = false; - if (window.is_key_pressed(GLFW_KEY_F11)) { - fullscreen = !fullscreen; - window.set_fullscreen(fullscreen); - } + if (window.is_key_pressed(GLFW_KEY_H)) Renderer::set_wireframe(!Renderer::is_wireframe()); + if (window.is_key_pressed(GLFW_KEY_F11)) window.set_fullscreen(!window.is_fullscreen()); if (window.is_key_pressed(GLFW_KEY_INSERT)) { - user_data.mouse_grab = !user_data.mouse_grab; - user_data.first_mouse = true; - window.set_mouse_grab(user_data.mouse_grab); + input.mouse_grab = !input.mouse_grab; + input.first_mouse = true; + window.set_mouse_grab(input.mouse_grab); } - if (!user_data.mouse_grab) return; + if (!input.mouse_grab) return; + + Camera& camera = app.camera; + float boost = window.is_key_down(GLFW_KEY_LEFT_SHIFT) ? 2.0f : 1.0f; + float velocity = input.speed * window.get_delta_time() * boost; - float velocity = user_data.speed * user_data.delta_time * (window.is_key_down(GLFW_KEY_LEFT_SHIFT) ? 2.0f : 1.0f); if (window.is_key_down(GLFW_KEY_W)) camera.position += camera.get_front() * velocity; if (window.is_key_down(GLFW_KEY_S)) camera.position -= camera.get_front() * velocity; if (window.is_key_down(GLFW_KEY_A)) camera.position -= camera.get_right() * velocity; @@ -289,28 +376,60 @@ void process_input(Window& window, Camera& camera, UserData& user_data) void mouse_callback(double pos_x, double pos_y, void* user_data) { - auto* data = static_cast(user_data); - if (!data->mouse_grab) return; + AppState& app = *static_cast(user_data); + CameraInput& input = app.input; + if (!input.mouse_grab) return; - if (data->first_mouse) { - data->last_mouse_x = pos_x; - data->last_mouse_y = pos_y; - data->first_mouse = false; + if (input.first_mouse) { + input.last_mouse_x = pos_x; + input.last_mouse_y = pos_y; + input.first_mouse = false; } - float delta_x = (pos_x - data->last_mouse_x) * data->sensitivity; - float delta_y = (data->last_mouse_y - pos_y) * data->sensitivity; + float delta_x = (pos_x - input.last_mouse_x) * input.sensitivity; + float delta_y = (input.last_mouse_y - pos_y) * input.sensitivity; - data->last_mouse_x = pos_x; - data->last_mouse_y = pos_y; - data->camera.rotate(delta_x, delta_y); + input.last_mouse_x = pos_x; + input.last_mouse_y = pos_y; + app.camera.rotate(delta_x, delta_y); } +void render_scene(AppState& app, FrameBuffer& framebuffer, const Rc& debug_shader) +{ + const Scene& scene = *app.scenes[app.selected_scene_index]; + PerformanceStats& stats = app.stats; + + int width = framebuffer.get_width(); + int height = framebuffer.get_height(); + + framebuffer.bind(); + stats.cpu_timer.begin(); + if (stats.gpu_timer) { stats.gpu_timer->begin(); } + + Renderer::set_viewport(0, 0, width, height); + Renderer::clear(); + Renderer::draw_scene(scene, app.camera, width, height); + if (app.show_debug) { + Renderer::draw_debug_wireframes(scene, app.camera, width, height, + debug_shader, ResourceCache::get_fallback_mesh()); + } + + if (stats.gpu_timer) { stats.gpu_timer->end(); } + stats.cpu_timer.end(); + framebuffer.unbind(); + + stats.scene_cpu_times.push(stats.cpu_timer.get_elapsed_ms()); + if (stats.gpu_timer) { stats.scene_gpu_times.push(stats.gpu_timer->get_elapsed_ms()); } +} + +/******************** UI ********************/ + void setup_dockspace() { ImGuiID dockspace_id = ImGui::GetID("Dockspace"); ImGuiViewport* viewport = ImGui::GetMainViewport(); + // Build the default layout once: panel on the left, viewport on the right static bool first_time = true; if (first_time) { first_time = false; @@ -329,44 +448,45 @@ void setup_dockspace() ImGui::DockSpaceOverViewport(dockspace_id, viewport, ImGuiDockNodeFlags_None); } -void draw_panel_window(UserData& user_data) +void draw_panel_window(AppState& app) { ImGui::Begin("Panel"); - if (user_data.logo) ImGui::Image(user_data.logo->get_id(), {50.0f, 50.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}); + // Header: logo with the title vertically centered next to it + if (app.logo) ImGui::Image(app.logo->get_id(), {50.0f, 50.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}); ImGui::SameLine(); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (50.0f - ImGui::GetTextLineHeight()) * 0.5f); ImGui::Text("Fan Engine"); ImGui::SeparatorText("General Settings"); - if (ImGui::Checkbox("VSync", &user_data.vsync)) { - user_data.window.set_vsync(user_data.vsync); + if (ImGui::Checkbox("VSync", &app.vsync)) { + app.window.set_vsync(app.vsync); } - ImGui::Checkbox("Debug Wireframes", &user_data.show_debug); + ImGui::Checkbox("Debug Wireframes", &app.show_debug); ImGui::SeparatorText("Scene Selector"); - for (size_t i = 0; i < user_data.scenes.size(); ++i) { + for (size_t i = 0; i < app.scenes.size(); ++i) { char label[64]; ImFormatString(label, IM_ARRAYSIZE(label), "Scene %zu", i); - if (ImGui::RadioButton(label, user_data.selected_scene_index == i)) { - user_data.selected_scene_index = i; + if (ImGui::RadioButton(label, app.selected_scene_index == i)) { + app.selected_scene_index = i; } } ImGui::SeparatorText("Camera Settings"); - ImGui::DragFloat3("Position", &user_data.camera.position.x, 0.1f, -20.0f, 20.0f, "%.1f"); - ImGui::SliderFloat("FOV", &user_data.camera.fov, 30.0f, 100.0f, "%.1f"); + ImGui::DragFloat3("Position", &app.camera.position.x, 0.1f, -20.0f, 20.0f, "%.1f"); + ImGui::SliderFloat("FOV", &app.camera.fov, 30.0f, 100.0f, "%.1f"); ImGui::SeparatorText("Performance"); ImGui::TextDisabled("Disable VSync for accurate frame times"); - plot_metric("Frame", user_data.frame_times); - plot_metric("Scene CPU", user_data.scene_cpu_times); - if (user_data.scene_gpu_timer) { - plot_metric("Scene GPU", user_data.scene_gpu_times); + plot_metric("Frame", app.stats.frame_times); + plot_metric("Scene CPU", app.stats.scene_cpu_times); + if (app.stats.gpu_timer) { + plot_metric("Scene GPU", app.stats.scene_gpu_times); } else { ImGui::TextDisabled("Scene GPU: timer queries not supported"); } @@ -382,7 +502,7 @@ void draw_panel_window(UserData& user_data) ImGui::End(); } -void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data) +void draw_viewport_window(AppState& app, FrameBuffer& framebuffer) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{0.0f, 0.0f}); ImGui::Begin("Viewport"); @@ -391,32 +511,48 @@ void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data) if (size.x > 0 && size.y > 0) { // Size the framebuffer in physical pixels so HiDPI displays render at full resolution ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale; - GLsizei fb_width = static_cast(size.x * scale.x); - GLsizei fb_height = static_cast(size.y * scale.y); - if (fb_width != framebuffer.get_width() || fb_height != framebuffer.get_height()) { - framebuffer.resize(fb_width, fb_height); + GLsizei width = static_cast(size.x * scale.x); + GLsizei height = static_cast(size.y * scale.y); + if (width != framebuffer.get_width() || height != framebuffer.get_height()) { + framebuffer.resize(width, height); } + + // Flip vertically: OpenGL textures start at the bottom row ImGui::Image(framebuffer.get_texture_id(), size, ImVec2{0.0f, 1.0f}, ImVec2{1.0f, 0.0f}); } - // Overlays (averaged over the last MetricHistory::SIZE frames so they're readable) - float frame_ms = user_data.frame_times.average(); - float fps = frame_ms > 0.0f ? 1000.0f / frame_ms : 0.0f; - overlay_text(20.0f, 35.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", VERSION_STRING); - overlay_text(20.0f, 52.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "FPS: %.1f", fps); - overlay_text(20.0f, 69.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Frame time: %.2f ms", frame_ms); - overlay_text(20.0f, 86.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Scene CPU: %.2f ms", user_data.scene_cpu_times.average()); - if (user_data.scene_gpu_timer) { - overlay_text(20.0f, 103.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Scene GPU: %.2f ms", user_data.scene_gpu_times.average()); - } else { - overlay_text(20.0f, 103.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "Scene GPU: n/a"); - } - if (user_data.mouse_grab) { overlay_text(20.0f, 120.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "INPUT LOCKED"); } + draw_overlay(app); ImGui::End(); ImGui::PopStyleVar(); } +void draw_overlay(const AppState& app) +{ + const PerformanceStats& stats = app.stats; + + // Averaged over the last MetricHistory::SIZE frames so the numbers are readable + float frame_ms = stats.frame_times.average(); + float fps = frame_ms > 0.0f ? 1000.0f / frame_ms : 0.0f; + + float y = OVERLAY_Y; + auto line = [&y](const char* format, auto... args) { + overlay_text(OVERLAY_X, y, format, args...); + y += OVERLAY_LINE_HEIGHT; + }; + + line("%s", VERSION_STRING); + line("FPS: %.1f", fps); + line("Frame time: %.2f ms", frame_ms); + line("Scene CPU: %.2f ms", stats.scene_cpu_times.average()); + if (stats.gpu_timer) { + line("Scene GPU: %.2f ms", stats.scene_gpu_times.average()); + } else { + line("%s", "Scene GPU: n/a"); + } + if (app.input.mouse_grab) { line("%s", "INPUT LOCKED"); } +} + void plot_metric(const char* label, const MetricHistory& history) { char overlay[32]; @@ -426,74 +562,3 @@ void plot_metric(const char* label, const MetricHistory& history) ImGui::PlotLines(label, history.values.data(), static_cast(history.values.size()), static_cast(history.offset), overlay, 0.0f, FLT_MAX, ImVec2{0.0f, 40.0f}); } - -void add_random_lights(Scene& scene, int point_light_count, int spot_light_count, - const glm::vec3& box_min, const glm::vec3& box_max, - unsigned int seed/* = std::random_device{}()*/) -{ - std::mt19937 rng(seed); - - std::uniform_real_distribution random_x(box_min.x, box_max.x); - std::uniform_real_distribution random_y(box_min.y, box_max.y); - std::uniform_real_distribution random_z(box_min.z, box_max.z); - - std::uniform_real_distribution random_direction(-1.0f, 1.0f); - std::uniform_real_distribution random_intensity(0.5f, 1.5f); - - const glm::vec3 light_colors[] = { - glm::vec3(1.0f, 1.0f, 1.0f), // White - glm::vec3(1.0f, 0.6f, 0.2f), // Warm orange - glm::vec3(0.2f, 0.5f, 1.0f), // Cool blue - glm::vec3(1.0f, 0.2f, 0.2f), // Red - glm::vec3(0.3f, 1.0f, 0.4f), // Green - glm::vec3(0.8f, 0.3f, 1.0f) // Purple - }; - - std::uniform_int_distribution randomColor(0, std::size(light_colors) - 1); - - auto generateDirection = [&]() { - glm::vec3 direction; - - // Avoid generating a vector that is too close to zero. - do { - direction = glm::vec3(random_direction(rng), random_direction(rng), random_direction(rng)); - } while (glm::dot(direction, direction) < 0.001f); - - return glm::normalize(direction); - }; - - auto generateColor = [&]() { return light_colors[randomColor(rng)]; }; - - // Generate point lights - for (int i = 0; i < point_light_count; ++i) { - PointLight light; - - light.position = glm::vec3(random_x(rng), random_y(rng), random_z(rng)); - glm::vec3 color = generateColor(); - light.diffuse = color; - light.specular = glm::mix(color, glm::vec3(1.0f), 0.25f); - - scene.light_environment.point_lights.push_back(light); - } - - // Generate spot lights - for (int i = 0; i < spot_light_count; ++i) { - SpotLight light; - - light.position = glm::vec3(random_x(rng), random_y(rng), random_z(rng)); - light.direction = generateDirection(); - glm::vec3 color = generateColor(); - light.ambient = glm::vec3(0.0f); - light.diffuse = color; - light.specular = glm::mix(color, glm::vec3(1.0f), 0.25f); - - // Narrow inner cone and slightly wider soft outer cone - light.cut_off = glm::cos(glm::radians(12.5f)); - light.outer_cut_off = glm::cos(glm::radians(17.5f)); - light.constant = 1.0f; - light.linear = 0.09f; - light.quadratic = 0.032f; - - scene.light_environment.spot_lights.push_back(light); - } -} diff --git a/src/mesh.cpp b/src/mesh.cpp index acb4fa2..4cf1935 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -39,6 +39,11 @@ Rc Mesh::create(const std::vector& vertices, gl_clear_errors(); + // Restored afterwards so creating a mesh leaves the caller's bindings intact + // (the index buffer binding is part of the VAO, restoring the VAO covers it) + GLuint previous_vao = gl_get_binding(GL_VERTEX_ARRAY_BINDING); + GLuint previous_vbo = gl_get_binding(GL_ARRAY_BUFFER_BINDING); + GLuint VBO{}, VAO{}, EBO{}; glGenBuffers(1, &VBO); glGenVertexArrays(1, &VAO); @@ -66,8 +71,9 @@ Rc Mesh::create(const std::vector& vertices, glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); - // Unbind so later GL_ELEMENT_ARRAY_BUFFER binds can't modify this VAO - glBindVertexArray(0); + // Also stops later GL_ELEMENT_ARRAY_BUFFER binds from modifying this VAO + glBindVertexArray(previous_vao); + glBindBuffer(GL_ARRAY_BUFFER, previous_vbo); // Buffer uploads can fail with GL_OUT_OF_MEMORY on large meshes GLenum err = gl_pop_error(); diff --git a/src/renderer.cpp b/src/renderer.cpp index 0a5ba6c..8c022bc 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -11,6 +11,7 @@ namespace Fan Rc Renderer::s_camera_ubo = nullptr; Rc Renderer::s_light_ubo = nullptr; +bool Renderer::s_wireframe = false; bool Renderer::init() { @@ -52,9 +53,15 @@ void Renderer::set_clear_color(const glm::vec4& color) void Renderer::set_wireframe(bool enabled) { + s_wireframe = enabled; glPolygonMode(GL_FRONT_AND_BACK, enabled ? GL_LINE : GL_FILL); } +bool Renderer::is_wireframe() +{ + return s_wireframe; +} + void Renderer::clear() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -129,6 +136,7 @@ void Renderer::draw_debug_wireframes(const Scene& scene, const Camera& camera, debug_shader->bind(); debug_shader->set_mat4("u_view_projection", camera.get_view_projection_matrix(screen_width, screen_height)); + bool previous_wireframe = s_wireframe; set_wireframe(true); // Draw all standard entities (green) @@ -161,8 +169,7 @@ void Renderer::draw_debug_wireframes(const Scene& scene, const Camera& camera, default_mesh->draw(); } - // Restore state - set_wireframe(false); + set_wireframe(previous_wireframe); } void Renderer::update_global_data(const Scene& scene, const Camera& camera, diff --git a/src/renderer.hpp b/src/renderer.hpp index f73e157..ac5e0f9 100644 --- a/src/renderer.hpp +++ b/src/renderer.hpp @@ -19,6 +19,7 @@ public: static void set_viewport(int x, int y, int width, int height); static void set_clear_color(const glm::vec4& color); static void set_wireframe(bool enabled); // Global, stays on until disabled + static bool is_wireframe(); static void clear(); @@ -28,7 +29,8 @@ public: int screen_width, int screen_height); // debug_shader needs u_model, u_view_projection and u_color. default_mesh is - // drawn for entities without a mesh and for lights. Disables wireframe when done + // drawn for entities without a mesh and for lights. Restores the previous + // wireframe mode when done static void draw_debug_wireframes(const Scene& scene, const Camera& camera, int screen_width, int screen_height, const Rc& debug_shader, @@ -37,6 +39,7 @@ public: private: static Rc s_camera_ubo; static Rc s_light_ubo; + static bool s_wireframe; static void update_global_data(const Scene& scene, const Camera& camera, int screen_width, int screen_height); diff --git a/src/texture.cpp b/src/texture.cpp index f11d9c5..2ff6441 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -38,6 +38,8 @@ Rc Texture::create_from_pixels(GLsizei width, GLsizei height, gl_clear_errors(); + GLuint previous_texture = gl_get_binding(GL_TEXTURE_BINDING_2D); + GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); @@ -56,6 +58,7 @@ Rc Texture::create_from_pixels(GLsizei width, GLsizei height, // Upload can fail with GL_OUT_OF_MEMORY on large images GLenum err = gl_pop_error(); + glBindTexture(GL_TEXTURE_2D, previous_texture); if (err != GL_NO_ERROR) { Log::error() << "Could not upload texture " << name << ": " << gl_error_string(err); glDeleteTextures(1, &texture); @@ -97,12 +100,16 @@ void Texture::bind(GLuint slot/* = 0*/) const { glActiveTexture(GL_TEXTURE0 + slot); glBindTexture(GL_TEXTURE_2D, m_id); + + // Keep unit 0 active so later texture binds can't land on this slot + glActiveTexture(GL_TEXTURE0); } void Texture::unbind(GLuint slot/* = 0*/) { glActiveTexture(GL_TEXTURE0 + slot); glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); } } // namespace Fan diff --git a/src/texture.hpp b/src/texture.hpp index 63d5e94..2edcee8 100644 --- a/src/texture.hpp +++ b/src/texture.hpp @@ -29,7 +29,7 @@ public: GLuint get_id() const; - // slot is the texture unit, which is left as the active unit + // slot is the texture unit. The active unit is reset to 0 afterwards void bind(GLuint slot = 0) const; static void unbind(GLuint slot = 0); diff --git a/src/uniformbuffer.cpp b/src/uniformbuffer.cpp index 94d455b..bea23d8 100644 --- a/src/uniformbuffer.cpp +++ b/src/uniformbuffer.cpp @@ -26,6 +26,9 @@ Rc UniformBuffer::create(size_t size, GLuint binding_point) gl_clear_errors(); + // glBindBufferBase also changes the generic binding, restore it afterwards + GLuint previous_buffer = gl_get_binding(GL_UNIFORM_BUFFER_BINDING); + GLuint id; glGenBuffers(1, &id); @@ -33,7 +36,7 @@ Rc UniformBuffer::create(size_t size, GLuint binding_point) glBufferData(GL_UNIFORM_BUFFER, size, nullptr, GL_DYNAMIC_DRAW); glBindBufferBase(GL_UNIFORM_BUFFER, binding_point, id); - glBindBuffer(GL_UNIFORM_BUFFER, 0); + glBindBuffer(GL_UNIFORM_BUFFER, previous_buffer); GLenum err = gl_pop_error(); if (err != GL_NO_ERROR) { @@ -80,9 +83,11 @@ void UniformBuffer::update(const void* data, size_t size, size_t offset) const return; } + GLuint previous_buffer = gl_get_binding(GL_UNIFORM_BUFFER_BINDING); + glBindBuffer(GL_UNIFORM_BUFFER, m_id); glBufferSubData(GL_UNIFORM_BUFFER, offset, size, data); - glBindBuffer(GL_UNIFORM_BUFFER, 0); + glBindBuffer(GL_UNIFORM_BUFFER, previous_buffer); } } // namespace Fan