diff options
Diffstat (limited to 'src/engine')
| -rw-r--r-- | src/engine/engine.cpp | 614 | ||||
| -rw-r--r-- | src/engine/engine.h | 184 | ||||
| -rw-r--r-- | src/engine/object.h | 76 | ||||
| -rw-r--r-- | src/engine/scene.h | 70 |
4 files changed, 944 insertions, 0 deletions
diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp new file mode 100644 index 0000000..059fdda --- /dev/null +++ b/src/engine/engine.cpp @@ -0,0 +1,614 @@ +#include <iostream> +#include <fstream> +#include <sstream> + +#include <GLFW/glfw3.h> +#include <glad/glad.h> + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" + +#include "engine.h" +#include "core/log.h" +#include "core/hdri_manager.h" +#include "rendering/vertex_buffer.h" +#include "rendering/index_buffer.h" + +namespace Donut +{ + Engine::Engine() + : m_sag_a(glm::vec3(0.0f, 0.0f, 0.0f), 8.54e36f) + { + m_width = 1280; + m_height = 720; + + m_camera.set_camera_mode(CameraMode::Orbital); + m_camera.set_orbital_radius(1e11); + m_camera.set_orbital_limits(1e9, 1e13); + m_camera.set_orbital_speed(0.01f); + m_camera.set_zoom_speed(1e10f); + + m_objects = + { + { glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), glm::vec4(0, 0, 0, 1), static_cast<float>(m_sag_a.m_mass) } + }; + + // The geodesic ray tracer used to be a compute shader; it is now a + // fullscreen vertex+fragment pass (see dispatch_compute) so it runs on + // macOS OpenGL 4.1, which has no compute shaders. + m_compute_program = Ref<Shader>(Shader::create("assets/shaders/Geodesic.glsl")); + m_shader_program = Ref<Shader>(Shader::create("assets/shaders/TexturedQuad.glsl")); + m_blur_shader = Ref<Shader>(Shader::create("assets/shaders/Blur.glsl")); + + auto& hdri_manager = HDRIManager::get(); + m_hdri_environment = hdri_manager.get_current_hdri(); + if (!m_hdri_environment) + { + hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr"); + m_hdri_environment = hdri_manager.get_current_hdri(); + if (!m_hdri_environment) + DONUT_WARN("Failed to load default HDRI, using fallback"); + } + + m_camera_ubo = UniformBuffer::create(128, 1); + m_disk_ubo = UniformBuffer::create(sizeof(float) * 5, 2); + + uint32_t obj_ubo_size = sizeof(int) + 3 * sizeof(float) + + 16 * (sizeof(glm::vec4) + sizeof(glm::vec4)) + + 16 * sizeof(float); + m_objects_ubo = UniformBuffer::create(obj_ubo_size, 3); + + m_simulation_ubo = UniformBuffer::create(sizeof(int) * 2 + sizeof(float) * 2, 4); + + auto result = QuadVAO(); + m_quad_vao = result.first; + m_texture = result.second; + + // GLSL 4.10 forbids explicit binding qualifiers on uniform blocks, so + // associate the geodesic shader's blocks with their UBO binding points + // from the host side instead. + if (m_compute_program) + { + uint32_t prog = m_compute_program->get_renderer_id(); + struct { const char* name; uint32_t point; } blocks[] = + { + { "Camera", 1 }, { "Disk", 2 }, { "Objects", 3 }, { "Simulation", 4 } + }; + for (const auto& b : blocks) + { + uint32_t idx = glGetUniformBlockIndex(prog, b.name); + if (idx != GL_INVALID_INDEX) + glUniformBlockBinding(prog, idx, b.point); + } + } + + glGenFramebuffers(1, &m_geodesic_fbo); + } + + auto Engine::update_window_dimensions() -> void + { + update_compute_dimensions(); + } + + auto Engine::set_window_dimensions(int width, int height) -> void + { + int old_width = m_width; + int old_height = m_height; + int old_compute_height = m_compute_height; + + m_width = width; + m_height = height; + + if (old_width != m_width || + old_height != m_height || + old_compute_height != m_compute_height) + update_compute_dimensions(); + } + + auto Engine::update_performance(float delta_time) -> void + { + if (delta_time > 0.0f) + m_current_fps = 1.0f / delta_time; + } + + auto Engine::update_compute_dimensions() -> void + { + m_texture = Texture2D::create(get_compute_width(), m_compute_height); + } + + auto Engine::draw_full_screen_quad() -> void + { + RenderCommand::set_viewport(0, 0, m_width, m_height); + + m_shader_program->bind(); + m_quad_vao->bind(); + + m_texture->bind(0); + m_shader_program->set_int("u_ScreenTexture", 0); + + RenderCommand::disable_depth_test(); + RenderCommand::draw_arrays(6); + RenderCommand::enable_depth_test(); + } + + auto Engine::draw_blur_pass() -> void + { + RenderCommand::set_viewport(0, 0, m_width, m_height); + + m_blur_shader->bind(); + m_quad_vao->bind(); + + m_texture->bind(0); + m_blur_shader->set_int("u_ScreenTexture", 0); + m_blur_shader->set_float2("u_Resolution", glm::vec2(m_width, m_height)); + m_blur_shader->set_float("u_BlurStrength", m_blur_strength); + m_blur_shader->set_float("u_GlowIntensity", m_glow_intensity); + + RenderCommand::disable_depth_test(); + RenderCommand::draw_arrays(6); + RenderCommand::enable_depth_test(); + } + + auto Engine::draw_geodesic_pass(int cw, int ch) -> void + { + m_quad_vao->bind(); + RenderCommand::disable_depth_test(); + +#ifdef __APPLE__ + // macOS aborts any GPU submission that runs longer than a couple of + // seconds ("GPU Hang"). The geodesic ray-marcher can far exceed that in + // a single fullscreen draw, so render it in scissored tiles and flush + // after each, keeping every submission short enough to survive the + // watchdog. Compute-capable platforms draw it in one pass. + // Largest tile that keeps a tile's worst-case work (tile^2 * step_cap) + // inside the safe watchdog zone measured on this GPU (~25M pixel-steps + // per submission); bigger tiles mean fewer glFinish stalls. + const int tile = 64; + glEnable(GL_SCISSOR_TEST); + for (int y = 0; y < ch; y += tile) + { + int th = std::min(tile, ch - y); + for (int x = 0; x < cw; x += tile) + { + int tw = std::min(tile, cw - x); + glScissor(x, y, tw, th); + RenderCommand::draw_arrays(6); + glFinish(); + } + } + glDisable(GL_SCISSOR_TEST); +#else + RenderCommand::draw_arrays(6); +#endif + + RenderCommand::enable_depth_test(); + } + + auto Engine::dispatch_compute(const Camera& cam) -> void + { + auto& hdri_manager = HDRIManager::get(); + m_hdri_environment = hdri_manager.get_current_hdri(); + + int cw = get_compute_width(); + int ch = m_compute_height; + + // Render the geodesic pass into m_texture through an FBO. This replaces + // the old compute dispatch + image_store path, which relied on OpenGL + // 4.3 compute and 4.2 image load/store that macOS does not provide. + glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture->get_renderer_id(), 0); + glViewport(0, 0, cw, ch); + + m_compute_program->bind(); + upload_camera_ubo(cam); + upload_disk_ubo(); + upload_objects_ubo(m_objects); + upload_simulation_ubo(); + m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast<float>(cw), static_cast<float>(ch))); + + if (m_hdri_environment) + { + m_hdri_environment->bind(5); + m_compute_program->set_int("u_HDRIEnvironment", 5); + } + + draw_geodesic_pass(cw, ch); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + auto Engine::upload_camera_ubo(const Camera& cam) -> void + { + struct UBOData + { + glm::vec3 pos; float _pad0; + glm::vec3 right; float _pad1; + glm::vec3 up; float _pad2; + glm::vec3 forward; float _pad3; + float tan_half_fov; + float aspect; + bool moving; + int _pad4; + } data; + + glm::vec3 fwd = glm::normalize(cam.get_orbital_target() - cam.get_orbital_position()); + glm::vec3 up = glm::vec3(0, 1, 0); + glm::vec3 right = glm::normalize(glm::cross(fwd, up)); + up = glm::cross(right, fwd); + + data.pos = cam.get_orbital_position(); + data.right = right; + data.up = up; + data.forward = fwd; + data.tan_half_fov = static_cast<float>(tan(glm::radians(60.0f * 0.5f))); + data.aspect = static_cast<float>(get_compute_width()) / static_cast<float>(m_compute_height); + data.moving = cam.is_dragging() || cam.is_panning(); + + m_camera_ubo->set_data(&data, sizeof(UBOData)); + m_camera_ubo->bind(1); + } + + auto Engine::upload_objects_ubo(const std::vector<ObjectData>& objs) -> void + { + struct UBOData + { + int num_objects; + float _pad0, _pad1, _pad2; + glm::vec4 pos_radius[16]; + glm::vec4 color[16]; + float mass[16]; + } data; + + size_t count = std::min(objs.size(), size_t(16)); + data.num_objects = static_cast<int>(count); + + for (size_t i = 0; i < count; ++i) + { + data.pos_radius[i] = objs[i].m_pos_radius; + data.color[i] = objs[i].m_color; + data.mass[i] = objs[i].m_mass; + } + + m_objects_ubo->set_data(&data, sizeof(data)); + m_objects_ubo->bind(3); + } + + auto Engine::upload_disk_ubo() -> void + { + float r1 = static_cast<float>(m_sag_a.m_rs * 2.2); + float r2 = static_cast<float>(m_sag_a.m_rs * 5.2); + float num = 2.0f; + float thickness = static_cast<float>(m_sag_a.m_rs * m_disk_thickness); + float disk_data[5] = { r1, r2, num, thickness, m_disk_density }; + + m_disk_ubo->set_data(disk_data, sizeof(disk_data)); + m_disk_ubo->bind(2); + } + + auto Engine::upload_simulation_ubo() -> void + { + struct UBOData + { + int max_steps_moving; + int max_steps_static; + float early_exit_distance; + float time; + } data; + + data.max_steps_moving = m_max_steps_moving; + data.max_steps_static = m_max_steps_static; + data.early_exit_distance = m_early_exit_distance; + data.time = static_cast<float>(glfwGetTime()) * m_rotation_speed; + +#ifdef __APPLE__ + // macOS has no compute shaders, so the geodesic pass runs as a tiled + // fragment shader under the OS GPU watchdog. The stock step counts + // (up to 30000) make a single tile exceed the watchdog and hang the + // GPU, so cap them here. Windows/Linux keep the full step count. + // While the camera moves, render cheaply so interaction stays smooth; + // when it settles, spend more steps for a cleaner image. Both stay well + // under the per-tile GPU-watchdog budget (see draw_geodesic_pass). + data.max_steps_moving = std::min(data.max_steps_moving, 4000); + data.max_steps_static = std::min(data.max_steps_static, 6000); +#endif + + m_simulation_ubo->set_data(&data, sizeof(data)); + m_simulation_ubo->bind(4); + } + + auto Engine::update_physics(float delta_time) -> void + { + for (auto& obj : m_objects) + { + for (auto& obj2 : m_objects) + { + if (&obj == &obj2) continue; + float dx = obj2.m_pos_radius.x - obj.m_pos_radius.x; + float dy = obj2.m_pos_radius.y - obj.m_pos_radius.y; + float dz = obj2.m_pos_radius.z - obj.m_pos_radius.z; + float distance = sqrt(dx * dx + dy * dy + dz * dz); + if (distance > 0) + { + std::vector<double> direction = {dx / distance, dy / distance, dz / distance}; + double Gforce = (G * obj.m_mass * obj2.m_mass) / (distance * distance); + double acc1 = Gforce / obj.m_mass; + std::vector<double> acc = {direction[0] * acc1, direction[1] * acc1, direction[2] * acc1}; + + if (m_gravity) + { + obj.m_velocity.x += static_cast<float>(acc[0]); + obj.m_velocity.y += static_cast<float>(acc[1]); + obj.m_velocity.z += static_cast<float>(acc[2]); + + obj.m_pos_radius.x += static_cast<float>(obj.m_velocity.x); + obj.m_pos_radius.y += static_cast<float>(obj.m_velocity.y); + obj.m_pos_radius.z += static_cast<float>(obj.m_velocity.z); + } + } + } + } + } + + auto Engine::render_scene() -> void + { + RenderCommand::clear(); + m_shader_program->bind(); + m_quad_vao->bind(); + m_texture->bind(0); + RenderCommand::draw_arrays(6); + } + + auto Engine::create_compute_program(const char* path) -> Ref<Shader> + { + std::ifstream in(path); + if(!in.is_open()) + { + std::cerr << "Failed to open compute shader: " << path << "\n"; + return nullptr; + } + std::stringstream ss; + ss << in.rdbuf(); + std::string src_str = ss.str(); + return Ref<Shader>(Shader::create_compute("ComputeShader", src_str)); + } + + auto Engine::QuadVAO() -> std::pair<Ref<VertexArray>, Ref<Texture2D>> + { + float quad_vertices[] = + { + // Positions // TexCoords + -1.0f, 1.0f, 0.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f + }; + + auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(quad_vertices, static_cast<uint32_t>(sizeof(quad_vertices)))); + VertexBufferLayout layout; + layout.push<float>(2); // Position (x, y) + layout.push<float>(2); // TexCoord (u, v) + vertex_buffer->set_layout(layout); + + auto vertex_array = Ref<VertexArray>(VertexArray::create()); + vertex_array->add_vertex_buffer(vertex_buffer); + auto texture = Texture2D::create(get_compute_width(), m_compute_height); + + return { vertex_array, texture }; + } + + auto Engine::load_objects_from_scene(const std::vector<Donut::Object>& objects) -> void + { + m_objects.clear(); + m_objects.push_back( + { + glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), + glm::vec4(0, 0, 0, 1), + static_cast<float>(m_sag_a.m_mass) + }); + + for (const auto& obj : objects) + { + ObjectData engine_obj; + + float scale_factor = 1e10f; + engine_obj.m_pos_radius = glm::vec4 + ( + obj.m_centre.x * scale_factor, + obj.m_centre.y * scale_factor, + obj.m_centre.z * scale_factor, + obj.m_radius * scale_factor + ); + + engine_obj.m_color = glm::vec4(obj.m_material.m_color, 1.0f); + + float volume = (4.0f / 3.0f) * 3.14159f * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w; + float density = 1e12f; + engine_obj.m_mass = volume * density; + engine_obj.m_velocity = glm::vec3(0.0f, 0.0f, 0.0f); + + m_objects.push_back(engine_obj); + } + + DONUT_INFO("Loaded {} objects from WorldBuilder scene (scaled up by {})", objects.size(), 1e10f); + print_object_info(); + } + + auto Engine::print_object_info() const -> void + { + DONUT_INFO("=== Object Information ==="); + DONUT_INFO("Total objects: {}", m_objects.size()); + + for (size_t i = 0; i < m_objects.size(); ++i) + { + const auto& obj = m_objects[i]; + DONUT_INFO("Object {}: Pos=({}, {}, {}), Radius={}, Mass={}, Color=({}, {}, {})", + i, + obj.m_pos_radius.x, obj.m_pos_radius.y, obj.m_pos_radius.z, + obj.m_pos_radius.w, + obj.m_mass, + obj.m_color.x, obj.m_color.y, obj.m_color.z + ); + } + DONUT_INFO("Camera position: ({}, {}, {})", + m_camera.get_orbital_position().x, + m_camera.get_orbital_position().y, + m_camera.get_orbital_position().z + ); + DONUT_INFO("Camera radius: {}", m_camera.get_orbital_radius()); + DONUT_INFO("========================"); + } + + auto Engine::export_high_res_frame(const std::string& filename, int width, int height) -> void + { + DONUT_INFO("Exporting high-resolution frame: {}x{} to {}", width, height, filename); + + if (width <= 0 || height <= 0) + { + DONUT_ERROR("Invalid dimensions for export: {}x{}", width, height); + return; + } + + if (filename.empty()) + { + DONUT_ERROR("Invalid filename for export"); + return; + } + + int original_width = m_width; + int original_height = m_height; + int original_compute_height = m_compute_height; + + m_width = width; + m_height = height; + m_compute_height = height; + + int compute_height = height; + int compute_width = (width * compute_height) / height; + + if (compute_width <= 0 || compute_height <= 0) + { + DONUT_ERROR("Invalid compute dimensions: {}x{}", compute_width, compute_height); + return; + } + + FramebufferSpecification fb_spec; + fb_spec.Width = width; + fb_spec.Height = height; + fb_spec.attachments = { FramebufferTextureFormat::RGBA8 }; + + auto high_res_framebuffer = Framebuffer::create(fb_spec); + if (!high_res_framebuffer) + { + DONUT_ERROR("Failed to create high-resolution framebuffer"); + return; + } + + auto high_res_texture = Texture2D::create(compute_width, compute_height); + if (!high_res_texture) + { + DONUT_ERROR("Failed to create high-resolution texture"); + return; + } + + // Render the geodesic pass into high_res_texture through the geodesic FBO. + glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, high_res_texture->get_renderer_id(), 0); + glViewport(0, 0, compute_width, compute_height); + + m_compute_program->bind(); + + struct UBOData + { + glm::vec3 pos; float _pad0; + glm::vec3 right; float _pad1; + glm::vec3 up; float _pad2; + glm::vec3 forward; float _pad3; + float tan_half_fov; + float aspect; + bool moving; + int _pad4; + } data; + + glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - m_camera.get_orbital_position()); + glm::vec3 up = glm::vec3(0, 1, 0); + glm::vec3 right = glm::normalize(glm::cross(fwd, up)); + up = glm::cross(right, fwd); + + data.pos = m_camera.get_orbital_position(); + data.right = right; + data.up = up; + data.forward = fwd; + data.tan_half_fov = static_cast<float>(tan(glm::radians(60.0f * 0.5f))); + data.aspect = static_cast<float>(compute_width) / static_cast<float>(compute_height); + data.moving = m_camera.is_dragging() || m_camera.is_panning(); + + m_camera_ubo->set_data(&data, sizeof(UBOData)); + m_camera_ubo->bind(1); + + upload_disk_ubo(); + upload_objects_ubo(m_objects); + upload_simulation_ubo(); + m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast<float>(compute_width), static_cast<float>(compute_height))); + + if (m_hdri_environment) + { + m_hdri_environment->bind(5); + m_compute_program->set_int("u_HDRIEnvironment", 5); + } + + draw_geodesic_pass(compute_width, compute_height); + + // Display the rendered frame into the high-res framebuffer for read-back. + high_res_framebuffer->bind(); + RenderCommand::set_viewport(0, 0, width, height); + RenderCommand::clear(); + + m_shader_program->bind(); + m_quad_vao->bind(); + + high_res_texture->bind(0); + m_shader_program->set_int("u_ScreenTexture", 0); + + RenderCommand::disable_depth_test(); + RenderCommand::draw_arrays(6); + RenderCommand::enable_depth_test(); + + std::vector<unsigned char> pixels(width * height * 4); + DONUT_INFO("Reading {} pixels from framebuffer...", width * height); + RenderCommand::read_pixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + + DONUT_INFO("Flipping image vertically..."); + std::vector<unsigned char> flipped_pixels(width * height * 4); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + int src_index = (y * width + x) * 4; + int dst_index = ((height - 1 - y) * width + x) * 4; + flipped_pixels[dst_index + 0] = pixels[src_index + 0]; // R + flipped_pixels[dst_index + 1] = pixels[src_index + 1]; // G + flipped_pixels[dst_index + 2] = pixels[src_index + 2]; // B + flipped_pixels[dst_index + 3] = pixels[src_index + 3]; // A + } + } + + DONUT_INFO("Saving PNG file: {}...", filename); + int result = stbi_write_png(filename.c_str(), width, height, 4, flipped_pixels.data(), width * 4); + + if (result) + DONUT_INFO("Successfully exported high-resolution frame to: {}", filename); + else + DONUT_ERROR("Failed to export high-resolution frame to: {}", filename); + + high_res_framebuffer->unbind(); + + m_width = original_width; + m_height = original_height; + m_compute_height = original_compute_height; + + RenderCommand::set_viewport(0, 0, original_width, original_height); + } + +} diff --git a/src/engine/engine.h b/src/engine/engine.h new file mode 100644 index 0000000..c7d02b5 --- /dev/null +++ b/src/engine/engine.h @@ -0,0 +1,184 @@ +#pragma once + +#include <vector> +#include <numbers> +#include <iostream> +#include <fstream> +#include <sstream> +#include <chrono> +#include <cmath> + +#include "core/camera.h" +#include "object.h" + +#include "rendering/renderer.h" +#include "rendering/shader.h" +#include "rendering/vertex_array.h" +#include "rendering/texture.h" +#include "rendering/uniform_buffer.h" +#include "rendering/texture_manager.h" + +#include <GLFW/glfw3.h> +#include <glm/glm.hpp> +#include <glm/gtc/matrix_transform.hpp> +#include <glm/gtc/type_ptr.hpp> + +namespace Donut +{ + const double c = 299792458.0; + const double G = 6.67430e-11; + + struct BlackHole + { + glm::vec3 m_position; + double m_mass; + double m_radius; + double m_rs; + + BlackHole(glm::vec3 pos, float mass) + : m_position(pos), m_mass(mass) + { + m_rs = 2.0 * G * m_mass / (c * c); + } + + bool intercept(float px, float py, float pz) const + { + double dx = double(px) - double(m_position.x); + double dy = double(py) - double(m_position.y); + double dz = double(pz) - double(m_position.z); + double dist2 = dx * dx + dy * dy + dz * dz; + return dist2 < m_rs * m_rs; + } + }; + + struct ObjectData + { + glm::vec4 m_pos_radius; + glm::vec4 m_color; + float m_mass; + glm::vec3 m_velocity = glm::vec3(0.0f, 0.0f, 0.0f); + }; + + class Engine + { + public: + Engine(); + ~Engine() = default; + + auto draw_full_screen_quad() -> void; + auto draw_blur_pass() -> void; + auto dispatch_compute(const Camera& cam) -> void; + auto upload_camera_ubo(const Camera& cam) -> void; + auto upload_objects_ubo(const std::vector<ObjectData>& objs) -> void; + auto upload_disk_ubo() -> void; + auto upload_simulation_ubo() -> void; + auto render_scene() -> void; + auto update_physics(float delta_time) -> void; + auto update_window_dimensions() -> void; + auto set_window_dimensions(int width, int height) -> void; + + auto get_width() const -> int { return m_width; } + auto get_height() const -> int { return m_height; } + + auto get_objects() -> std::vector<ObjectData>& { return m_objects; } + auto get_sag_a() -> BlackHole& { return m_sag_a; } + auto get_camera() -> Camera& { return m_camera; } + auto get_gravity() -> bool& { return m_gravity; } + + auto update_performance(float delta_time) -> void; + auto set_target_fps(int fps) -> void { m_target_fps = fps; } + auto get_target_fps() const -> int { return m_target_fps; } + auto get_current_fps() const -> float { return m_current_fps; } + void set_compute_height(int height) + { +#ifdef __APPLE__ + // Without compute shaders the geodesic pass runs as a tiled + // fragment shader (see draw_geodesic_pass), so very high working + // resolutions make each frame take many seconds. Cap it on macOS. + if (height > 256) height = 256; +#endif + m_compute_height = height; + } + auto get_compute_height() const -> int { return m_compute_height; } + auto get_compute_width() const -> int { return (m_width * m_compute_height) / m_height; } + auto update_compute_dimensions() -> void; + + auto get_max_steps_moving() const -> int { return m_max_steps_moving; } + auto get_max_steps_static() const -> int { return m_max_steps_static; } + auto get_early_exit_distance() const -> float { return m_early_exit_distance; } + auto set_max_steps_moving(int steps) -> void { m_max_steps_moving = steps; } + auto set_max_steps_static(int steps) -> void { m_max_steps_static = steps; } + auto set_early_exit_distance(float distance) -> void { m_early_exit_distance = distance; } + + auto get_disk_thickness() const -> float { return m_disk_thickness; } + auto set_disk_thickness(float thickness) -> void { m_disk_thickness = thickness; } + + auto get_disk_density() const -> float { return m_disk_density; } + auto set_disk_density(float density) -> void { m_disk_density = density; } + + auto get_rotation_speed() const -> float { return m_rotation_speed; } + auto set_rotation_speed(float speed) -> void { m_rotation_speed = speed; } + + auto get_blur_strength() const -> float { return m_blur_strength; } + auto set_blur_strength(float strength) -> void { m_blur_strength = strength; } + + auto get_glow_intensity() const -> float { return m_glow_intensity; } + auto set_glow_intensity(float intensity) -> void { m_glow_intensity = intensity; } + + auto load_objects_from_scene(const std::vector<Donut::Object>& objects) -> void; + auto export_high_res_frame(const std::string& filename, int width = 4096, int height = 3072) -> void; + auto print_object_info() const -> void; + + auto set_hdri_environment(Ref<CubemapTexture> hdri) -> void { m_hdri_environment = hdri; } + auto get_hdri_environment() const -> Ref<CubemapTexture> { return m_hdri_environment; } + private: + auto create_compute_program(const char* path) -> Ref<Shader>; + std::pair<Ref<VertexArray>, Ref<Texture2D>> QuadVAO(); + + // Draws the bound geodesic shader over a cw x ch target. On macOS this + // is split into scissored tiles (with a flush each) so no single GPU + // submission trips the OS watchdog; elsewhere it is one fast draw. + auto draw_geodesic_pass(int cw, int ch) -> void; + private: + Ref<VertexArray> m_quad_vao; + Ref<Texture2D> m_texture; + Ref<CubemapTexture> m_hdri_environment; + Ref<Shader> m_shader_program; + Ref<Shader> m_compute_program; + Ref<Shader> m_blur_shader; + Ref<UniformBuffer> m_camera_ubo; + Ref<UniformBuffer> m_disk_ubo; + Ref<UniformBuffer> m_objects_ubo; + Ref<UniformBuffer> m_simulation_ubo; + + // FBO used to render the geodesic pass into m_texture. The geodesic + // shader is a fragment shader (macOS has no compute), so it draws a + // fullscreen quad into this framebuffer instead of dispatching compute. + uint32_t m_geodesic_fbo = 0; + + int m_width; + int m_height; + float m_width_f = 100.0f*1e10f; + float m_height_f = 75.0f*1e10f; + + int m_target_fps = 60; + float m_current_fps = 60.0f; + float m_last_frame_time = 0.0f; + int m_compute_height = 150; + + std::vector<ObjectData> m_objects; + BlackHole m_sag_a; + Camera m_camera; + bool m_gravity = false; + + int m_max_steps_moving = 60000; + int m_max_steps_static = 30000; + float m_early_exit_distance = 5.0e11f; + + float m_disk_thickness = 0.1f; + float m_disk_density = 0.1f; + float m_rotation_speed = 1.0f; + float m_blur_strength = 2.0f; + float m_glow_intensity = 0.1f; + }; +}; diff --git a/src/engine/object.h b/src/engine/object.h new file mode 100644 index 0000000..8101540 --- /dev/null +++ b/src/engine/object.h @@ -0,0 +1,76 @@ +#pragma once + +#include <glm/glm.hpp> + +namespace Donut +{ + class Ray + { + public: + Ray(glm::vec3 o, glm::vec3 d) + : m_origin(o), + m_direction(glm::normalize(d)) { } + + public: + glm::vec3 m_direction; + glm::vec3 m_origin; + }; + + class Material + { + public: + Material() : m_color(1.0f, 1.0f, 1.0f), m_specular(0.5f), m_emission(0.0f) { } + Material(glm::vec3 c, float s, float e) + : m_color(c), + m_specular(s), + m_emission(e) { } + + public: + glm::vec3 m_color; + float m_specular; + float m_emission; + }; + + class Object + { + public: + Object() : m_centre(0.0f, 0.0f, 0.0f), m_radius(1.0f), m_material() { } + Object(glm::vec3 c, float r, Material m) + : m_centre(c), + m_radius(r), + m_material(m) { } + + auto intersect(Ray& ray, float& t) -> bool + { + glm::vec3 oc = ray.m_origin - m_centre; + float a = glm::dot(ray.m_direction, ray.m_direction); + float b = 2.0f * glm::dot(oc, ray.m_direction); + float c = glm::dot(oc, oc) - m_radius * m_radius; + float discriminant = static_cast<float>(b*b - 4*a*c); + + if (discriminant < 0) + return false; + + float intercept = (-b - sqrt(discriminant)) / (2.0f*a); + if (intercept < 0) + { + intercept = (-b + sqrt(discriminant)) / (2.0f*a); + if (intercept < 0) + return false; + } + + t = intercept; + return true; + } + + auto get_normal(glm::vec3& point) const -> glm::vec3 + { + return glm::normalize(point - m_centre); + } + + public: + glm::vec3 m_centre; + float m_radius; + Material m_material; + }; +}; diff --git a/src/engine/scene.h b/src/engine/scene.h new file mode 100644 index 0000000..4b41203 --- /dev/null +++ b/src/engine/scene.h @@ -0,0 +1,70 @@ +#pragma once + +#include <vector> +#include <limits> + +#include "object.h" + +namespace Donut +{ + class Scene + { + public: + Scene() + : m_light_pos(5.0f, 5.0f, 5.0f) { } + + auto trace(Ray& ray) -> glm::vec3 + { + float closest = std::numeric_limits<float>::infinity(); + const Object* hit_obj = nullptr; + + for (auto& obj : objs) + { + float t; + + if (obj.intersect(ray, t)) + if (t < closest) + { + closest = t; + hit_obj = &obj; + } + } + + if (hit_obj) + { + glm::vec3 hit_point = ray.m_origin + ray.m_direction * closest; + glm::vec3 normal = hit_obj->get_normal(hit_point); + glm::vec3 light_dir = glm::normalize(m_light_pos - hit_point); + + float diff = std::max(glm::dot(normal, light_dir), 0.0f); + + Ray shadow_ray(hit_point + normal * 0.001f, light_dir); + bool in_shadow = false; + + for (auto& obj : objs) + { + float t; + + if (obj.intersect(shadow_ray, t)) + { + in_shadow = true; + break; + } + } + + glm::vec3 color = hit_obj->m_material.m_color; + float ambient = 0.1f; + + if (in_shadow) + return color * ambient; + return color * (ambient + diff * 0.9f); + } + + return glm::vec3(0.0f, 0.0f, 0.1f); + } + + public: + std::vector<Object> objs; + glm::vec3 m_light_pos; + }; +}; |
