diff --git a/examples/normal_map_cornell_box b/examples/normal_map_cornell_box new file mode 100644 index 0000000..9e9afad Binary files /dev/null and b/examples/normal_map_cornell_box differ diff --git a/examples/normal_map_cornell_box.cpp b/examples/normal_map_cornell_box.cpp new file mode 100644 index 0000000..294fc4b --- /dev/null +++ b/examples/normal_map_cornell_box.cpp @@ -0,0 +1,614 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace are; + +// Window dimensions +const uint WINDOW_WIDTH = 800; +const uint WINDOW_HEIGHT = 800; + +// Global state +GLFWwindow *g_window = nullptr; +std::unique_ptr g_renderer = nullptr; +std::unique_ptr g_scene = nullptr; +std::shared_ptr g_camera = nullptr; // Keep a direct reference to camera + +// --- Camera Control State --- +Vec3 g_cameraPos = Vec3(0.0f, 0.0f, 4.5f); +Vec3 g_cameraTarget = Vec3(0.0f, 0.0f, 0.0f); +Vec3 g_cameraUp = Vec3(0.0f, 1.0f, 0.0f); +Vec3 g_worldUp = Vec3(0.0f, 1.0f, 0.0f); + +// Euler Angles +float g_yaw = -90.0f; // Initialized to look along -Z (standard OpenGL) +float g_pitch = 0.0f; + +// Control settings +float g_moveSpeed = 2.5f; +float g_mouseSensitivity = 0.1f; +bool g_firstMouse = true; +double g_lastX = WINDOW_WIDTH / 2.0; +double g_lastY = WINDOW_HEIGHT / 2.0; + +// Time +float g_deltaTime = 0.0f; +float g_lastFrame = 0.0f; + +// GLFW error callback +void glfw_error_callback(int error, const char *description) { + ARE_LOG_ERROR("GLFW Error " + std::to_string(error) + ": " + std::string(description)); +} + +/// @brief Create a quad mesh +std::shared_ptr create_quad(const Vec3 &v0, const Vec3 &v1, const Vec3 &v2, const Vec3 &v3, + const Vec3 &normal, uint material_id) { + auto mesh = std::make_shared(); + + std::vector vertices = { + { v0, normal, Vec2(0.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f) }, + { v1, normal, Vec2(1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f) }, + { v2, normal, Vec2(1.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f) }, + { v3, normal, Vec2(0.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f) } + }; + + std::vector indices = { 0, 1, 2, 0, 2, 3 }; + + mesh->set_vertices(vertices); + mesh->set_indices(indices); + mesh->set_material(material_id); + + return mesh; +} + +/// @brief Create a box mesh +std::shared_ptr create_box(const Vec3 &min, const Vec3 &max, uint material_id) { + auto mesh = std::make_shared(); + + std::vector vertices = { + // Front face + { { min.x, min.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, min.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + { { min.x, max.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + + // Back face + { { max.x, min.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f } }, + { { min.x, min.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 0.0f }, { -1.0f, 0.0f, 0.0f } }, + { { min.x, max.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } }, + + // Top face + { { min.x, max.y, max.z }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, max.z }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, min.z }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + { { min.x, max.y, min.z }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + + // Bottom face + { { min.x, min.y, min.z }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, min.y, min.z }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, min.y, max.z }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + { { min.x, min.y, max.z }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + + // Right face + { { max.x, min.y, max.z }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } }, + { { max.x, min.y, min.z }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } }, + { { max.x, max.y, min.z }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } }, + { { max.x, max.y, max.z }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } }, + + // Left face + { { min.x, min.y, min.z }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }, + { { min.x, min.y, max.z }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }, + { { min.x, max.y, max.z }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 0.0f, 1.0f } }, + { { min.x, max.y, min.z }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f } } + }; + + std::vector indices = { + 0, 1, 2, 0, 2, 3, // Front + 4, 5, 6, 4, 6, 7, // Back + 8, 9, 10, 8, 10, 11, // Top + 12, 13, 14, 12, 14, 15, // Bottom + 16, 17, 18, 16, 18, 19, // Right + 20, 21, 22, 20, 22, 23 // Left + }; + + mesh->set_vertices(vertices); + mesh->set_indices(indices); + mesh->set_material(material_id); + + return mesh; +} + +/// @brief Create a sphere mesh +std::shared_ptr create_sphere(float radius, uint segments, uint rings, uint material_id) { + auto mesh = std::make_shared(); + + std::vector vertices; + std::vector indices; + + for (uint ring = 0; ring <= rings; ++ring) { + float theta = ring * glm::pi() / rings; + float sin_theta = sin(theta); + float cos_theta = cos(theta); + + for (uint seg = 0; seg <= segments; ++seg) { + float phi = seg * 2.0f * glm::pi() / segments; + float x = cos(phi) * sin_theta; + float y = cos_theta; + float z = sin(phi) * sin_theta; + + Vec3 pos = Vec3(x, y, z) * radius; + Vec3 normal = Vec3(x, y, z); + Vec2 uv = Vec2((float)seg / segments, (float)ring / rings); + Vec3 tangent = Vec3(-sin(phi), 0.0f, cos(phi)); + + vertices.push_back({ pos, normal, uv, tangent }); + } + } + + for (uint ring = 0; ring < rings; ++ring) { + for (uint seg = 0; seg < segments; ++seg) { + uint current = ring * (segments + 1) + seg; + indices.push_back(current); + indices.push_back(current + segments + 1); + indices.push_back(current + 1); + indices.push_back(current + 1); + indices.push_back(current + segments + 1); + indices.push_back(current + segments + 2); + } + } + + mesh->set_vertices(vertices); + mesh->set_indices(indices); + mesh->set_material(material_id); + mesh->compute_tangents(); + + return mesh; +} + +/// @brief Setup Cornell Box scene +void setup_cornell_box() { + g_scene = std::make_unique(); + + // Create materials + // 0: White diffuse + auto white_material = std::make_shared(); + white_material->set_albedo(Vec3(0.73f, 0.73f, 0.73f)); + white_material->set_type(MaterialType::DIFFUSE); + uint white_id = g_scene->add_material(white_material); + + // 1: Red diffuse (left wall) + auto red_material = std::make_shared(); + red_material->set_albedo(Vec3(0.65f, 0.05f, 0.05f)); + red_material->set_type(MaterialType::DIFFUSE); + uint red_id = g_scene->add_material(red_material); + + // 2: Green diffuse (right wall) + auto green_material = std::make_shared(); + green_material->set_albedo(Vec3(0.12f, 0.45f, 0.15f)); + green_material->set_type(MaterialType::DIFFUSE); + uint green_id = g_scene->add_material(green_material); + + // 3: Light emissive + auto light_material = std::make_shared(); + light_material->set_albedo(Vec3(1.0f, 1.0f, 1.0f)); + light_material->set_emission(Vec3(15.0f, 15.0f, 15.0f)); + light_material->set_type(MaterialType::EMISSIVE); + uint light_id = g_scene->add_material(light_material); + + // 4: Metal (for one box) + auto metal_material = std::make_shared(); + metal_material->set_albedo(Vec3(0.95f, 0.93f, 0.88f)); + metal_material->set_metallic(1.0f); + metal_material->set_roughness(0.0f); + metal_material->set_type(MaterialType::METAL); + uint metal_id = g_scene->add_material(metal_material); + + // 5: Textured material with normal map (for short box) + auto textured_material = std::make_shared(); + textured_material->set_albedo(Vec3(1.0f, 1.0f, 1.0f)); + textured_material->set_metallic(0.0f); + textured_material->set_roughness(0.8f); + textured_material->set_type(MaterialType::DIFFUSE); + + // Load textures + auto albedo_tex = std::make_shared(); + if (albedo_tex->load_from_file("examples/assets/normal_map_cornell_box/albedo.png")) { + textured_material->set_albedo_texture(albedo_tex); + ARE_LOG_INFO("Loaded albedo texture"); + } else { + ARE_LOG_WARN("Failed to load albedo texture"); + } + + auto normal_tex = std::make_shared(); + if (normal_tex->load_from_file("examples/assets/normal_map_cornell_box/normal.png")) { + textured_material->set_normal_texture(normal_tex); + ARE_LOG_INFO("Loaded normal texture"); + } else { + ARE_LOG_WARN("Failed to load normal texture"); + } + + uint textured_id = g_scene->add_material(textured_material); + + // 6: Glass/Dielectric (refraction) + auto glass_material = std::make_shared(); + glass_material->set_albedo(Vec3(1.0f, 1.0f, 1.0f)); + glass_material->set_ior(1.5f); + glass_material->set_roughness(0.0f); + glass_material->set_type(MaterialType::DIELECTRIC); + uint glass_id = g_scene->add_material(glass_material); + + // 7: Yellow emissive sphere + auto emissive_sphere_mat = std::make_shared(); + emissive_sphere_mat->set_albedo(Vec3(1.0f, 0.8f, 0.2f)); + emissive_sphere_mat->set_emission(Vec3(5.0f, 4.0f, 1.0f)); + emissive_sphere_mat->set_type(MaterialType::EMISSIVE); + uint emissive_sphere_id = g_scene->add_material(emissive_sphere_mat); + + // Create room (Cornell Box) + float room_size = 2.0f; + + // Floor (white) + auto floor = create_quad( + Vec3(-room_size, -room_size, -room_size), + Vec3(room_size, -room_size, -room_size), + Vec3(room_size, -room_size, room_size), + Vec3(-room_size, -room_size, room_size), + Vec3(0.0f, 1.0f, 0.0f), + white_id); + floor->upload_to_gpu(); + g_scene->add_mesh(floor); + + // Ceiling (white) + auto ceiling = create_quad( + Vec3(-room_size, room_size, room_size), + Vec3(room_size, room_size, room_size), + Vec3(room_size, room_size, -room_size), + Vec3(-room_size, room_size, -room_size), + Vec3(0.0f, -1.0f, 0.0f), + white_id); + ceiling->upload_to_gpu(); + g_scene->add_mesh(ceiling); + + // Back wall (white) + auto back_wall = create_quad( + Vec3(-room_size, -room_size, -room_size), + Vec3(-room_size, room_size, -room_size), + Vec3(room_size, room_size, -room_size), + Vec3(room_size, -room_size, -room_size), + Vec3(0.0f, 0.0f, 1.0f), + white_id); + back_wall->upload_to_gpu(); + g_scene->add_mesh(back_wall); + + // Left wall (red) + auto left_wall = create_quad( + Vec3(-room_size, -room_size, room_size), + Vec3(-room_size, room_size, room_size), + Vec3(-room_size, room_size, -room_size), + Vec3(-room_size, -room_size, -room_size), + Vec3(1.0f, 0.0f, 0.0f), + red_id); + left_wall->upload_to_gpu(); + g_scene->add_mesh(left_wall); + + // Right wall (green) + auto right_wall = create_quad( + Vec3(room_size, -room_size, -room_size), + Vec3(room_size, room_size, -room_size), + Vec3(room_size, room_size, room_size), + Vec3(room_size, -room_size, room_size), + Vec3(-1.0f, 0.0f, 0.0f), + green_id); + right_wall->upload_to_gpu(); + g_scene->add_mesh(right_wall); + + // Area light on ceiling + float light_size = 0.5f; + auto area_light = create_quad( + Vec3(-light_size, room_size - 0.01f, -light_size), + Vec3(light_size, room_size - 0.01f, -light_size), + Vec3(light_size, room_size - 0.01f, light_size), + Vec3(-light_size, room_size - 0.01f, light_size), + Vec3(0.0f, -1.0f, 0.0f), + light_id); + area_light->upload_to_gpu(); + g_scene->add_mesh(area_light); + + // Tall box (white, left side) + auto tall_box = create_box(Vec3(-0.7f, -room_size, -0.7f), Vec3(-0.2f, 0.6f, -0.2f), white_id); + tall_box->upload_to_gpu(); + g_scene->add_mesh(tall_box); + + // Short box (textured, right side) + auto short_box = create_box(Vec3(0.2f, -room_size, 0.2f), Vec3(0.9f, -0.4f, 0.9f), textured_id); + short_box->upload_to_gpu(); + g_scene->add_mesh(short_box); + + // // Glass sphere (dielectric/refraction test) + // auto glass_sphere = create_sphere(0.4f, 32, 16, glass_id); + // glass_sphere->set_position(Vec3(-0.5f, -0.6f, 0.0f)); + // glass_sphere->upload_to_gpu(); + // g_scene->add_mesh(glass_sphere); + // + // // Yellow emissive sphere (emission test) + // auto emissive_sphere = create_sphere(0.25f, 32, 16, emissive_sphere_id); + // emissive_sphere->set_position(Vec3(0.5f, -0.75f, 0.3f)); + // emissive_sphere->upload_to_gpu(); + // g_scene->add_mesh(emissive_sphere); + + // Setup camera + g_camera = std::make_shared(); + g_camera->set_position(g_cameraPos); + g_camera->set_target(g_cameraTarget); + g_camera->set_up(g_cameraUp); + g_camera->set_perspective(45.0f, static_cast(WINDOW_WIDTH) / WINDOW_HEIGHT, 0.1f, 100.0f); + g_scene->set_camera(g_camera); + + // Add point light + auto light = std::make_shared(); + light->set_type(LightType::POINT); + light->set_position(Vec3(0.0f, 1.8f, 0.0f)); + light->set_color(Vec3(1.0f, 1.0f, 1.0f)); + light->set_intensity(10.0f); + light->set_range(10.0f); + g_scene->add_light(light); + + ARE_LOG_INFO("Cornell Box scene created"); +} + +/// @brief Initialize GLFW and create window +bool init_window() { + glfwSetErrorCallback(glfw_error_callback); + + if (!glfwInit()) { + ARE_LOG_ERROR("Failed to initialize GLFW"); + return false; + } + + ARE_LOG_INFO("GLFW initialized successfully"); + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); + glfwWindowHint(GLFW_SAMPLES, 0); + + g_window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Aurora - Cornell Box", nullptr, nullptr); + + if (!g_window) { + ARE_LOG_ERROR("Failed to create GLFW window"); + glfwTerminate(); + return false; + } + + glfwMakeContextCurrent(g_window); + glfwSwapInterval(1); + + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { + ARE_LOG_ERROR("Failed to initialize GLAD"); + return false; + } + + return true; +} + +// --- Input Processing --- +void process_input() { + // Calculate delta time + float currentFrame = glfwGetTime(); + g_deltaTime = currentFrame - g_lastFrame; + g_lastFrame = currentFrame; + + float velocity = g_moveSpeed * g_deltaTime; + bool camera_changed = false; + + // 1. Mouse Rotation (Left Button Hold) + if (glfwGetMouseButton(g_window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + double xpos, ypos; + glfwGetCursorPos(g_window, &xpos, &ypos); + + if (g_firstMouse) { + g_lastX = xpos; + g_lastY = ypos; + g_firstMouse = false; + } + + float xoffset = xpos - g_lastX; + float yoffset = g_lastY - ypos; // Reversed since y-coordinates go from bottom to top + g_lastX = xpos; + g_lastY = ypos; + + // Only update if mouse actually moved + if (xoffset != 0.0f || yoffset != 0.0f) { + xoffset *= g_mouseSensitivity; + yoffset *= g_mouseSensitivity; + + g_yaw += xoffset; + g_pitch += yoffset; + + // Constrain pitch + if (g_pitch > 89.0f) + g_pitch = 89.0f; + if (g_pitch < -89.0f) + g_pitch = -89.0f; + + camera_changed = true; + } + } else { + g_firstMouse = true; // Reset when released + } + + // 2. Calculate Direction Vectors + glm::vec3 front; + front.x = cos(glm::radians(g_yaw)) * cos(glm::radians(g_pitch)); + front.y = sin(glm::radians(g_pitch)); + front.z = sin(glm::radians(g_yaw)) * cos(glm::radians(g_pitch)); + glm::vec3 frontNorm = glm::normalize(front); + + glm::vec3 rightNorm = glm::normalize(glm::cross(frontNorm, glm::vec3(g_worldUp.x, g_worldUp.y, g_worldUp.z))); + + // 3. Keyboard Movement (WASD) + glm::vec3 pos = glm::vec3(g_cameraPos.x, g_cameraPos.y, g_cameraPos.z); + + if (glfwGetKey(g_window, GLFW_KEY_W) == GLFW_PRESS) { + pos += frontNorm * velocity; + camera_changed = true; + } + if (glfwGetKey(g_window, GLFW_KEY_S) == GLFW_PRESS) { + pos -= frontNorm * velocity; + camera_changed = true; + } + if (glfwGetKey(g_window, GLFW_KEY_A) == GLFW_PRESS) { + pos -= rightNorm * velocity; + camera_changed = true; + } + if (glfwGetKey(g_window, GLFW_KEY_D) == GLFW_PRESS) { + pos += rightNorm * velocity; + camera_changed = true; + } + + // 4. Apply changes to Scene Camera and Notify Renderer + if (camera_changed) { + g_cameraPos = Vec3(pos.x, pos.y, pos.z); + + // Target = Position + Front + Vec3 newTarget = g_cameraPos + Vec3(frontNorm.x, frontNorm.y, frontNorm.z); + + g_camera->set_position(g_cameraPos); + g_camera->set_target(newTarget); + + // CRITICAL: Notify renderer to reset accumulation + g_renderer->notify_scene_changed(*g_scene); + } +} + +/// @brief Main render loop +void render_loop() { + ARE_LOG_INFO("Entering render loop..."); + + int frame_count = 0; + double fps_time = glfwGetTime(); + g_lastFrame = glfwGetTime(); // Initialize for delta time + + while (!glfwWindowShouldClose(g_window)) { + // Process input at the start of the frame + process_input(); + + // Render + RenderStats stats = g_renderer->render(*g_scene); + + // Swap buffers + glfwSwapBuffers(g_window); + glfwPollEvents(); + + // Calculate FPS + frame_count++; + double current_time = glfwGetTime(); + double delta = current_time - fps_time; + + if (delta >= 1.0) { + double fps = frame_count / delta; + std::string title = "Aurora - Cornell Box | FPS: " + std::to_string((int)fps) + " | Frame: " + std::to_string((int)stats.frame_time_ms_) + "ms"; + glfwSetWindowTitle(g_window, title.c_str()); + + frame_count = 0; + fps_time = current_time; + } + + // ESC to exit + if (glfwGetKey(g_window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { + glfwSetWindowShouldClose(g_window, true); + } + } + + ARE_LOG_INFO("Exiting render loop"); +} + +/// @brief Cleanup +void cleanup() { + ARE_LOG_INFO("Cleaning up..."); + + if (g_renderer) { + g_renderer->shutdown(); + g_renderer.reset(); + } + + g_scene.reset(); + + if (g_window) { + glfwDestroyWindow(g_window); + glfwTerminate(); + } + + ARE_LOG_INFO("Cleanup complete"); +} + +int main() { + ARE_LOG_INFO("==========================================="); + ARE_LOG_INFO("Aurora Rendering Engine - Cornell Box Demo"); + ARE_LOG_INFO("==========================================="); + + if (!init_window()) { + cleanup(); + ARE_LOG_ERROR("Failed to initialize window"); + Logger::shutdown(); + return -1; + } + + ARE_LOG_INFO("Setting up Cornell Box scene..."); + setup_cornell_box(); + + ARE_LOG_INFO("Initializing renderer..."); + RendererConfig config; + config.width_ = WINDOW_WIDTH; + config.height_ = WINDOW_HEIGHT; + config.samples_per_pixel_ = 1; + config.max_ray_depth_ = 4; + config.enable_accumulation_ = true; + config.enable_denoising_ = false; + + g_renderer = std::make_unique(config); + if (!g_renderer->initialize()) { + ARE_LOG_ERROR("Failed to initialize renderer"); + cleanup(); + Logger::shutdown(); + return -1; + } + + ARE_LOG_INFO("==========================================="); + ARE_LOG_INFO("Renderer initialized successfully!"); + ARE_LOG_INFO("Controls:"); + ARE_LOG_INFO(" WASD - Move Camera"); + ARE_LOG_INFO(" Hold Left Mouse Button - Rotate Camera"); + ARE_LOG_INFO(" ESC - Exit"); + ARE_LOG_INFO("==========================================="); + + render_loop(); + cleanup(); + + ARE_LOG_INFO("Cornell Box demo finished"); + Logger::shutdown(); + + return 0; +}