From dbf5b8579c0a8c3bcedd8afd98cf1c30598f708f Mon Sep 17 00:00:00 2001 From: ternaryop8479 Date: Mon, 9 Feb 2026 22:16:57 +0800 Subject: [PATCH] =?UTF-8?q?Refactor=EF=BC=9A=E9=87=8D=E5=86=99=E6=89=80?= =?UTF-8?q?=E6=9C=89=E4=BB=A3=E7=A0=81=E3=80=81=E9=87=8D=E6=96=B0=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=9E=B6=E6=9E=84=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=A4=A7?= =?UTF-8?q?=E4=BD=93=E6=9E=B6=E6=9E=84=E5=92=8CG-Buffer=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 121 ++++++++ examples/cornell_box | Bin 0 -> 410304 bytes examples/cornell_box.cpp | 473 ++++++++++++++++++++++++++++++ include/basic/constants.h | 32 ++ include/basic/math.h | 59 ++++ include/basic/types.h | 71 +++++ include/core/bvh.h | 124 ++++++++ include/core/gbuffer.h | 72 +++++ include/core/raytracer.h | 106 +++++++ include/core/renderer.h | 73 +++++ include/core/screen_blit.h | 49 ++++ include/core/shader_manager.h | 70 +++++ include/extended_folders.list | 4 + include/resource/buffer.h | 84 ++++++ include/resource/model_loader.h | 58 ++++ include/resource/shader.h | 129 ++++++++ include/resource/texture.h | 127 ++++++++ include/scene/camera.h | 105 +++++++ include/scene/light.h | 114 ++++++++ include/scene/material.h | 105 +++++++ include/scene/mesh.h | 79 +++++ include/scene/scene.h | 74 +++++ include/utils/config.h | 70 +++++ include/utils/logger.h | 61 ++++ scripts/check_env.sh | 95 ++++++ shaders/gbuffer.frag | 49 ++++ shaders/gbuffer.vert | 27 ++ shaders/raytracing.comp | 503 ++++++++++++++++++++++++++++++++ shaders/screen_blit.frag | 11 + shaders/screen_blit.vert | 11 + src/basic/math.cpp | 33 +++ src/core/bvh.cpp | 297 +++++++++++++++++++ src/core/gbuffer.cpp | 221 ++++++++++++++ src/core/raytracer.cpp | 316 ++++++++++++++++++++ src/core/renderer.cpp | 205 +++++++++++++ src/core/screen_blit.cpp | 148 ++++++++++ src/core/shader_manager.cpp | 127 ++++++++ src/resource/buffer.cpp | 114 ++++++++ src/resource/model_loader.cpp | 187 ++++++++++++ src/resource/shader.cpp | 197 +++++++++++++ src/resource/texture.cpp | 274 +++++++++++++++++ src/scene/camera.cpp | 101 +++++++ src/scene/light.cpp | 49 ++++ src/scene/material.cpp | 51 ++++ src/scene/mesh.cpp | 119 ++++++++ src/scene/scene.cpp | 44 +++ src/utils/config.cpp | 144 +++++++++ src/utils/logger.cpp | 103 +++++++ 48 files changed, 5686 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 examples/cornell_box create mode 100644 examples/cornell_box.cpp create mode 100644 include/basic/constants.h create mode 100644 include/basic/math.h create mode 100644 include/basic/types.h create mode 100644 include/core/bvh.h create mode 100644 include/core/gbuffer.h create mode 100644 include/core/raytracer.h create mode 100644 include/core/renderer.h create mode 100644 include/core/screen_blit.h create mode 100644 include/core/shader_manager.h create mode 100644 include/extended_folders.list create mode 100644 include/resource/buffer.h create mode 100644 include/resource/model_loader.h create mode 100644 include/resource/shader.h create mode 100644 include/resource/texture.h create mode 100644 include/scene/camera.h create mode 100644 include/scene/light.h create mode 100644 include/scene/material.h create mode 100644 include/scene/mesh.h create mode 100644 include/scene/scene.h create mode 100644 include/utils/config.h create mode 100644 include/utils/logger.h create mode 100644 scripts/check_env.sh create mode 100644 shaders/gbuffer.frag create mode 100644 shaders/gbuffer.vert create mode 100644 shaders/raytracing.comp create mode 100644 shaders/screen_blit.frag create mode 100644 shaders/screen_blit.vert create mode 100644 src/basic/math.cpp create mode 100644 src/core/bvh.cpp create mode 100644 src/core/gbuffer.cpp create mode 100644 src/core/raytracer.cpp create mode 100644 src/core/renderer.cpp create mode 100644 src/core/screen_blit.cpp create mode 100644 src/core/shader_manager.cpp create mode 100644 src/resource/buffer.cpp create mode 100644 src/resource/model_loader.cpp create mode 100644 src/resource/shader.cpp create mode 100644 src/resource/texture.cpp create mode 100644 src/scene/camera.cpp create mode 100644 src/scene/light.cpp create mode 100644 src/scene/material.cpp create mode 100644 src/scene/mesh.cpp create mode 100644 src/scene/scene.cpp create mode 100644 src/utils/config.cpp create mode 100644 src/utils/logger.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..19a0f26 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,121 @@ +cmake_minimum_required(VERSION 3.15) +project(AuroraRenderingEngine VERSION 1.0.0 LANGUAGES CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Options +option(ARE_BUILD_TESTS "Build tests" ON) +option(ARE_BUILD_EXAMPLES "Build examples" ON) + +# Find dependencies +find_package(OpenGL REQUIRED) +find_package(glm REQUIRED) +find_package(glfw3 REQUIRED) + +# GLAD library +add_library(glad STATIC + lib/glad/src/glad.c +) +target_include_directories(glad PUBLIC lib/glad/include) + +# stb_image library +set(STB_IMAGE_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/lib/stb/stb_image.cpp) +if(NOT EXISTS ${STB_IMAGE_SOURCE}) + file(WRITE ${STB_IMAGE_SOURCE} +"#define STB_IMAGE_IMPLEMENTATION +#include \"stb_image.h\" +") +endif() + +add_library(stb_image STATIC ${STB_IMAGE_SOURCE}) +target_include_directories(stb_image PUBLIC lib/stb) + +# Collect all source files automatically +file(GLOB_RECURSE ARE_SOURCES + "src/*.cpp" + "src/*.c" +) + +# Collect all header files +file(GLOB_RECURSE ARE_HEADERS + "include/*.h" + "include/*.hpp" +) + +# Aurora Rendering Engine library +add_library(are STATIC ${ARE_SOURCES} ${ARE_HEADERS}) + +target_include_directories(are + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_link_libraries(are + PUBLIC + OpenGL::GL + glm::glm + PRIVATE + glad + stb_image +) + +# Copy shaders to build directory +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/shaders + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +# Examples +if(ARE_BUILD_EXAMPLES) + # Cornell Box example + add_executable(cornell_box examples/cornell_box.cpp) + target_link_libraries(cornell_box + PRIVATE + are + glfw + ) + + # Copy shaders to example directory + add_custom_command(TARGET cornell_box POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/shaders + $/shaders + ) +endif() + +# Tests +if(ARE_BUILD_TESTS) + enable_testing() + + # Basic test + add_executable(test_basic tests/test_basic.cpp) + target_link_libraries(test_basic PRIVATE are) + add_test(NAME BasicTest COMMAND test_basic) +endif() + +# Install +install(TARGETS are + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(DIRECTORY include/ + DESTINATION include + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" +) + +install(DIRECTORY shaders/ + DESTINATION share/are/shaders +) + +# Print configuration +message(STATUS "Aurora Rendering Engine Configuration:") +message(STATUS " Build type: ${CMAKE_BUILD_TYPE}") +message(STATUS " C++ standard: ${CMAKE_CXX_STANDARD}") +message(STATUS " Build examples: ${ARE_BUILD_EXAMPLES}") +message(STATUS " Build tests: ${ARE_BUILD_TESTS}") +message(STATUS " Install prefix: ${CMAKE_INSTALL_PREFIX}") diff --git a/examples/cornell_box b/examples/cornell_box new file mode 100644 index 0000000000000000000000000000000000000000..c8bcff203fdc3491710faa3d2209ddeab48586f2 GIT binary patch literal 410304 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 nJ6 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 +#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; + +// GLFW error callback +void glfw_error_callback(int error, const char* description) { + Logger::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 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.1f); + metal_material->set_type(MaterialType::METAL); + uint metal_id = g_scene->add_material(metal_material); + + // 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 (metal, right side) + auto short_box = create_box(Vec3(0.2f, -room_size, 0.2f), Vec3(0.9f, -0.4f, 0.9f), metal_id); + short_box->upload_to_gpu(); + g_scene->add_mesh(short_box); + + // Setup camera + auto camera = std::make_shared(); + camera->set_position(Vec3(0.0f, 0.0f, 4.5f)); + camera->set_target(Vec3(0.0f, 0.0f, 0.0f)); + camera->set_up(Vec3(0.0f, 1.0f, 0.0f)); + camera->set_perspective(45.0f, static_cast(WINDOW_WIDTH) / WINDOW_HEIGHT, 0.1f, 100.0f); + g_scene->set_camera(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); + + Logger::info("Cornell Box scene created"); +} + +/// @brief Initialize GLFW and create window +bool init_window() { + // Set error callback before init + glfwSetErrorCallback(glfw_error_callback); + + if (!glfwInit()) { + Logger::error("Failed to initialize GLFW"); + return false; + } + + Logger::info("GLFW initialized successfully"); + + // Request OpenGL 4.5 Core Profile + 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); + + // Additional hints for better compatibility + glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); + glfwWindowHint(GLFW_SAMPLES, 0); + + Logger::info("Creating window..."); + g_window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Aurora - Cornell Box", nullptr, nullptr); + + if (!g_window) { + Logger::error("Failed to create GLFW window"); + Logger::error("Possible reasons:"); + Logger::error(" 1. OpenGL 4.5 not supported by your GPU/driver"); + Logger::error(" 2. No display server running (X11/Wayland)"); + Logger::error(" 3. Insufficient GPU resources"); + + // Try to get more info + int major, minor, rev; + glfwGetVersion(&major, &minor, &rev); + Logger::info("GLFW version: " + std::to_string(major) + "." + + std::to_string(minor) + "." + std::to_string(rev)); + + glfwTerminate(); + return false; + } + + Logger::info("Window created successfully"); + + glfwMakeContextCurrent(g_window); + glfwSwapInterval(1); // Enable vsync + + // Load OpenGL functions + Logger::info("Loading OpenGL functions..."); + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { + Logger::error("Failed to initialize GLAD"); + return false; + } + + // Print OpenGL info + const char* vendor = (const char*)glGetString(GL_VENDOR); + const char* renderer = (const char*)glGetString(GL_RENDERER); + const char* version = (const char*)glGetString(GL_VERSION); + const char* glsl_version = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION); + + Logger::info("OpenGL Vendor: " + std::string(vendor ? vendor : "Unknown")); + Logger::info("OpenGL Renderer: " + std::string(renderer ? renderer : "Unknown")); + Logger::info("OpenGL Version: " + std::string(version ? version : "Unknown")); + Logger::info("GLSL Version: " + std::string(glsl_version ? glsl_version : "Unknown")); + + // Check OpenGL version + GLint major_ver, minor_ver; + glGetIntegerv(GL_MAJOR_VERSION, &major_ver); + glGetIntegerv(GL_MINOR_VERSION, &minor_ver); + + Logger::info("OpenGL Context: " + std::to_string(major_ver) + "." + std::to_string(minor_ver)); + + // if (major_ver < 4 || (major_ver == 4 && minor_ver < 5)) { + // Logger::error("OpenGL 4.5 or higher is required!"); + // Logger::error("Your system supports: OpenGL " + std::to_string(major_ver) + "." + std::to_string(minor_ver)); + // return false; + // } + + // Check compute shader support + GLint max_compute_work_group_invocations; + glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &max_compute_work_group_invocations); + Logger::info("Max compute work group invocations: " + std::to_string(max_compute_work_group_invocations)); + + return true; +} + +/// @brief Main render loop +void render_loop() { + Logger::info("Entering render loop..."); + + int frame_count = 0; + double last_time = glfwGetTime(); + double fps_time = last_time; + + while (!glfwWindowShouldClose(g_window)) { + // 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; + } + + // Print detailed stats every 60 frames + static int stat_frame_count = 0; + if (++stat_frame_count % 60 == 0) { + Logger::info("Frame time: " + std::to_string(stats.frame_time_ms_) + " ms (" + + std::to_string(1000.0f / stats.frame_time_ms_) + " FPS)"); + Logger::info(" G-Buffer: " + std::to_string(stats.gbuffer_time_ms_) + " ms"); + Logger::info(" Ray trace: " + std::to_string(stats.raytrace_time_ms_) + " ms"); + Logger::info(" Triangles: " + std::to_string(stats.triangle_count_)); + } + + // ESC to exit + if (glfwGetKey(g_window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { + glfwSetWindowShouldClose(g_window, true); + } + } + + Logger::info("Exiting render loop"); +} + +/// @brief Cleanup +void cleanup() { + Logger::info("Cleaning up..."); + + if (g_renderer) { + g_renderer->shutdown(); + g_renderer.reset(); + } + + g_scene.reset(); + + if (g_window) { + glfwDestroyWindow(g_window); + glfwTerminate(); + } + + Logger::info("Cleanup complete"); +} + +int main() { + // Initialize logger + Logger::initialize(); + Logger::info("==========================================="); + Logger::info("Aurora Rendering Engine - Cornell Box Demo"); + Logger::info("==========================================="); + + // Check environment + const char* display = getenv("DISPLAY"); + const char* wayland_display = getenv("WAYLAND_DISPLAY"); + + if (!display && !wayland_display) { + Logger::error("No display server detected!"); + Logger::error("Make sure you're running in a graphical environment (X11 or Wayland)"); + Logger::error("DISPLAY=" + std::string(display ? display : "not set")); + Logger::error("WAYLAND_DISPLAY=" + std::string(wayland_display ? wayland_display : "not set")); + return -1; + } + + Logger::info("Display server: " + std::string(display ? display : wayland_display)); + + // Initialize window + if (!init_window()) { + cleanup(); + Logger::error("Failed to initialize window"); + Logger::shutdown(); + return -1; + } + + // Setup scene + Logger::info("Setting up Cornell Box scene..."); + setup_cornell_box(); + + // Initialize renderer + Logger::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_ = false; + config.enable_denoising_ = false; + + g_renderer = std::make_unique(config); + if (!g_renderer->initialize()) { + Logger::error("Failed to initialize renderer"); + cleanup(); + Logger::shutdown(); + return -1; + } + + Logger::info("==========================================="); + Logger::info("Renderer initialized successfully!"); + Logger::info("Press ESC to exit"); + Logger::info("==========================================="); + + // Main loop + render_loop(); + + // Cleanup + cleanup(); + + Logger::info("Cornell Box demo finished"); + Logger::shutdown(); + + return 0; +} diff --git a/include/basic/constants.h b/include/basic/constants.h new file mode 100644 index 0000000..12c631d --- /dev/null +++ b/include/basic/constants.h @@ -0,0 +1,32 @@ +#ifndef ARE_INCLUDE_BASIC_CONSTANTS_H +#define ARE_INCLUDE_BASIC_CONSTANTS_H + +namespace are { + +/// @brief Maximum number of lights in scene +constexpr int MAX_LIGHTS = 16; + +/// @brief Maximum ray tracing depth +constexpr int MAX_RAY_DEPTH = 8; + +/// @brief Default samples per pixel for ray tracing +constexpr int DEFAULT_SPP = 1; + +/// @brief G-Buffer attachment indices +constexpr int GBUFFER_POSITION = 0; +constexpr int GBUFFER_NORMAL = 1; +constexpr int GBUFFER_ALBEDO = 2; +constexpr int GBUFFER_COUNT = 3; + +/// @brief Compute shader work group size +constexpr int COMPUTE_GROUP_SIZE_X = 16; +constexpr int COMPUTE_GROUP_SIZE_Y = 16; + +/// @brief Mathematical constants +constexpr float PI = 3.14159265359f; +constexpr float INV_PI = 0.31830988618f; +constexpr float EPSILON = 1e-4f; + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_CONSTANTS_H diff --git a/include/basic/math.h b/include/basic/math.h new file mode 100644 index 0000000..dc2a07d --- /dev/null +++ b/include/basic/math.h @@ -0,0 +1,59 @@ +#ifndef ARE_INCLUDE_BASIC_MATH_UTILS_H +#define ARE_INCLUDE_BASIC_MATH_UTILS_H + +#include "types.h" +#include +#include + +namespace are { + +/// @brief Math utility functions wrapping GLM +class MathUtils { +public: + /// @brief Create perspective projection matrix + /// @param fov Field of view in radians + /// @param aspect Aspect ratio + /// @param near Near plane distance + /// @param far Far plane distance + /// @return Projection matrix + static Mat4 perspective(float fov, float aspect, float near, float far); + + /// @brief Create look-at view matrix + /// @param eye Camera position + /// @param center Look-at target + /// @param up Up vector + /// @return View matrix + static Mat4 look_at(const Vec3& eye, const Vec3& center, const Vec3& up); + + /// @brief Normalize a vector + /// @param v Input vector + /// @return Normalized vector + static Vec3 normalize(const Vec3& v); + + /// @brief Calculate dot product + /// @param a First vector + /// @param b Second vector + /// @return Dot product + static float dot(const Vec3& a, const Vec3& b); + + /// @brief Calculate cross product + /// @param a First vector + /// @param b Second vector + /// @return Cross product + static Vec3 cross(const Vec3& a, const Vec3& b); + + /// @brief Reflect vector around normal + /// @param incident Incident vector + /// @param normal Surface normal + /// @return Reflected vector + static Vec3 reflect(const Vec3& incident, const Vec3& normal); + + /// @brief Get pointer to matrix data (for OpenGL) + /// @param mat Input matrix + /// @return Pointer to matrix data + static const float* value_ptr(const Mat4& mat); +}; + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_MATH_UTILS_H diff --git a/include/basic/types.h b/include/basic/types.h new file mode 100644 index 0000000..c4adb8b --- /dev/null +++ b/include/basic/types.h @@ -0,0 +1,71 @@ +#ifndef ARE_INCLUDE_BASIC_TYPES_H +#define ARE_INCLUDE_BASIC_TYPES_H + +#include +#include +#include +#include +#include + +namespace are { + +/// @brief Basic vector types using GLM +using Vec2 = glm::vec2; +using Vec3 = glm::vec3; +using Vec4 = glm::vec4; + +/// @brief Basic matrix types using GLM +using Mat3 = glm::mat3; +using Mat4 = glm::mat4; + +/// @brief Basic integer types +using uint = uint32_t; +using uchar = uint8_t; + +/// @brief Handle types for GPU resources +using TextureHandle = uint; +using BufferHandle = uint; +using ShaderHandle = uint; +using FramebufferHandle = uint; + +/// @brief Invalid handle constant +constexpr uint INVALID_HANDLE = 0; + +/// @brief Vertex structure for mesh data +struct Vertex { + Vec3 position_; + Vec3 normal_; + Vec2 texcoord_; + Vec3 tangent_; +}; + +/// @brief Ray structure for ray tracing +struct Ray { + Vec3 origin_; + Vec3 direction_; + float t_min_; + float t_max_; +}; + +/// @brief Hit information for ray-surface intersection +struct HitInfo { + bool hit_; + float t_; + Vec3 position_; + Vec3 normal_; + Vec2 texcoord_; + uint material_id_; +}; + +/// @brief Rendering statistics +struct RenderStats { + float frame_time_ms_; + uint triangle_count_; + uint ray_count_; + float gbuffer_time_ms_; + float raytrace_time_ms_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_TYPES_H diff --git a/include/core/bvh.h b/include/core/bvh.h new file mode 100644 index 0000000..e45bd07 --- /dev/null +++ b/include/core/bvh.h @@ -0,0 +1,124 @@ +#ifndef ARE_INCLUDE_CORE_BVH_H +#define ARE_INCLUDE_CORE_BVH_H + +#include "basic/types.h" +#include "scene/mesh.h" +#include "resource/buffer.h" +#include +#include + +namespace are { + +/// @brief Axis-aligned bounding box +struct AABB { + Vec3 min_; + Vec3 max_; + + /// @brief Construct AABB from min and max points + AABB(const Vec3& min = Vec3(0.0f), const Vec3& max = Vec3(0.0f)) + : min_(min), max_(max) {} + + /// @brief Expand AABB to include point + void expand(const Vec3& point); + + /// @brief Expand AABB to include another AABB + void expand(const AABB& other); + + /// @brief Get center of AABB + Vec3 center() const { return (min_ + max_) * 0.5f; } + + /// @brief Get surface area of AABB + float surface_area() const; + + /// @brief Check if AABB is valid + bool is_valid() const; +}; + +/// @brief Triangle primitive for BVH +struct Triangle { + Vec3 v0_, v1_, v2_; + Vec3 n0_, n1_, n2_; + Vec2 uv0_, uv1_, uv2_; + uint material_id_; + + /// @brief Get bounding box of triangle + AABB get_bounds() const; + + /// @brief Get centroid of triangle + Vec3 get_centroid() const; +}; + +/// @brief BVH node for GPU +struct BVHNode { + Vec3 aabb_min_; + uint left_first_; // Left child index or first primitive index + Vec3 aabb_max_; + uint count_; // 0 for interior node, >0 for leaf node +}; + +/// @brief Bounding Volume Hierarchy for ray tracing acceleration +class BVH { +public: + /// @brief Constructor + BVH(); + + /// @brief Destructor + ~BVH(); + + /// @brief Build BVH from meshes + /// @param meshes Mesh list + /// @return True if build succeeded + bool build(const std::vector>& meshes); + + /// @brief Upload BVH to GPU + /// @param node_buffer Buffer for BVH nodes + /// @param triangle_buffer Buffer for triangles + /// @return True if upload succeeded + bool upload_to_gpu(Buffer& node_buffer, Buffer& triangle_buffer); + + /// @brief Get total node count + /// @return Node count + uint get_node_count() const { return static_cast(nodes_.size()); } + + /// @brief Get total triangle count + /// @return Triangle count + uint get_triangle_count() const { return static_cast(triangles_.size()); } + + /// @brief Clear BVH data + void clear(); + +private: + std::vector nodes_; + std::vector triangles_; + std::vector triangle_indices_; + + /// @brief Recursively build BVH + /// @param node_idx Current node index + /// @param first_prim First primitive index + /// @param prim_count Primitive count + void build_recursive_(uint node_idx, uint first_prim, uint prim_count); + + /// @brief Find best split using SAH + /// @param first_prim First primitive index + /// @param prim_count Primitive count + /// @param axis Split axis (output) + /// @param split_pos Split position (output) + /// @return Split cost + float find_best_split_(uint first_prim, uint prim_count, int& axis, float& split_pos); + + /// @brief Calculate node bounds + /// @param first_prim First primitive index + /// @param prim_count Primitive count + /// @return Bounding box + AABB calculate_bounds_(uint first_prim, uint prim_count); + + /// @brief Calculate centroid bounds + /// @param first_prim First primitive index + /// @param prim_count Primitive count + /// @return Centroid bounding box + AABB calculate_centroid_bounds_(uint first_prim, uint prim_count); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_BVH_H diff --git a/include/core/gbuffer.h b/include/core/gbuffer.h new file mode 100644 index 0000000..9859cb0 --- /dev/null +++ b/include/core/gbuffer.h @@ -0,0 +1,72 @@ +#ifndef ARE_INCLUDE_CORE_GBUFFER_H +#define ARE_INCLUDE_CORE_GBUFFER_H + +#include "basic/types.h" +#include "basic/constants.h" +#include "scene/scene.h" +#include "resource/shader.h" + +namespace are { + +/// @brief G-Buffer manager for deferred rendering +class GBuffer { +public: + /// @brief Constructor + /// @param width Buffer width + /// @param height Buffer height + GBuffer(uint width, uint height); + + /// @brief Destructor + ~GBuffer(); + + /// @brief Initialize G-Buffer (create framebuffer and textures) + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release G-Buffer resources + void release(); + + /// @brief Render scene to G-Buffer + /// @param scene Scene to render + /// @param shader Shader program for G-Buffer pass + void render(const Scene& scene, const Shader& shader); + + /// @brief Resize G-Buffer + /// @param width New width + /// @param height New height + void resize(uint width, uint height); + + /// @brief Get texture handle for specific buffer + /// @param index Buffer index (GBUFFER_POSITION, GBUFFER_NORMAL, etc.) + /// @return Texture handle + TextureHandle get_texture(int index) const; + + /// @brief Get framebuffer handle + /// @return Framebuffer handle + FramebufferHandle get_framebuffer() const { return fbo_; } + + /// @brief Get buffer dimensions + /// @param width Output width + /// @param height Output height + void get_dimensions(uint& width, uint& height) const; + +private: + uint width_; + uint height_; + FramebufferHandle fbo_; + TextureHandle textures_[GBUFFER_COUNT]; + TextureHandle depth_texture_; + + bool initialized_; + + /// @brief Create texture for G-Buffer attachment + /// @param internal_format OpenGL internal format + /// @param format OpenGL format + /// @param type OpenGL type + /// @return Texture handle + TextureHandle create_texture_(uint internal_format, uint format, uint type); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_GBUFFER_H diff --git a/include/core/raytracer.h b/include/core/raytracer.h new file mode 100644 index 0000000..abe4d1d --- /dev/null +++ b/include/core/raytracer.h @@ -0,0 +1,106 @@ +#ifndef ARE_INCLUDE_CORE_RAYTRACER_H +#define ARE_INCLUDE_CORE_RAYTRACER_H + +#include "basic/types.h" +#include "core/bvh.h" // 添加 +#include "core/gbuffer.h" +#include "resource/buffer.h" +#include "resource/shader.h" +#include "scene/scene.h" + +namespace are { + +/// @brief Ray tracing configuration +struct RayTracerConfig { + uint samples_per_pixel_; + uint max_depth_; + bool enable_shadows_; + bool enable_reflections_; + bool enable_accumulation_; + bool use_bvh_; // 添加BVH开关 +}; + +/// @brief Compute shader based ray tracer +class RayTracer { +public: + /// @brief Constructor + /// @param width Output width + /// @param height Output height + /// @param config Ray tracer configuration + RayTracer(uint width, uint height, const RayTracerConfig &config); + + /// @brief Destructor + ~RayTracer(); + + /// @brief Initialize ray tracer + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release resources + void release(); + + /// @brief Trace rays using G-Buffer as input + /// @param scene Scene data + /// @param gbuffer G-Buffer containing geometry information + /// @param output_texture Output texture for ray traced result + void trace(const Scene &scene, const GBuffer &gbuffer, TextureHandle output_texture); + + /// @brief Resize output + /// @param width New width + /// @param height New height + void resize(uint width, uint height); + + /// @brief Reset accumulation buffer + void reset_accumulation(); + + /// @brief Get current configuration + /// @return Current configuration + const RayTracerConfig &get_config() const { + return config_; + } + + /// @brief Update configuration + /// @param config New configuration + void set_config(const RayTracerConfig &config); + + /// @brief Rebuild BVH from scene + /// @param scene Scene to build BVH from + /// @return True if build succeeded + bool rebuild_bvh(const Scene &scene); + + /// @brief Set compute shader (called by renderer) + /// @param shader Compute shader + void set_compute_shader(const Shader &shader); + +private: + uint width_; + uint height_; + RayTracerConfig config_; + + Shader compute_shader_; + TextureHandle accumulation_texture_; + BufferHandle scene_buffer_; + BufferHandle material_buffer_; + BufferHandle light_buffer_; + + // BVH related + std::unique_ptr bvh_; // 添加 + Buffer bvh_node_buffer_; // 添加 + Buffer bvh_triangle_buffer_; // 添加 + bool bvh_built_; // 添加 + + uint frame_count_; + bool initialized_; + + /// @brief Upload scene data to GPU buffers + /// @param scene Scene to upload + void upload_scene_data_(const Scene &scene); + + /// @brief Bind G-Buffer textures to compute shader + /// @param gbuffer G-Buffer to bind + void bind_gbuffer_(const GBuffer &gbuffer); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_RAYTRACER_H diff --git a/include/core/renderer.h b/include/core/renderer.h new file mode 100644 index 0000000..9040710 --- /dev/null +++ b/include/core/renderer.h @@ -0,0 +1,73 @@ +#ifndef ARE_INCLUDE_CORE_RENDERER_H +#define ARE_INCLUDE_CORE_RENDERER_H + +#include "basic/types.h" +#include "scene/scene.h" +#include "core/gbuffer.h" +#include "core/raytracer.h" +#include "core/screen_blit.h" +#include "core/shader_manager.h" +#include + +namespace are { + +/// @brief Main renderer configuration +struct RendererConfig { + uint width_; + uint height_; + uint samples_per_pixel_; + uint max_ray_depth_; + bool enable_denoising_; + bool enable_accumulation_; +}; + +/// @brief Main rendering engine interface +class Renderer { +public: + /// @brief Constructor + /// @param config Renderer configuration + Renderer(const RendererConfig& config); + + /// @brief Destructor + ~Renderer(); + + /// @brief Initialize renderer (OpenGL context must be current) + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Shutdown renderer and release resources + void shutdown(); + + /// @brief Render a frame + /// @param scene Scene to render + /// @param output_texture Output texture handle (0 for default framebuffer) + /// @return Rendering statistics + RenderStats render(const Scene& scene, TextureHandle output_texture = 0); + + /// @brief Resize render targets + /// @param width New width + /// @param height New height + void resize(uint width, uint height); + + /// @brief Get current configuration + /// @return Current configuration + const RendererConfig& get_config() const { return config_; } + + /// @brief Update configuration + /// @param config New configuration + void set_config(const RendererConfig& config); + +private: + RendererConfig config_; + std::unique_ptr gbuffer_; + std::unique_ptr raytracer_; + std::unique_ptr shader_manager_; + std::unique_ptr screen_blit_; + + bool initialized_; + uint frame_count_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_RENDERER_H diff --git a/include/core/screen_blit.h b/include/core/screen_blit.h new file mode 100644 index 0000000..6addbb4 --- /dev/null +++ b/include/core/screen_blit.h @@ -0,0 +1,49 @@ +#ifndef ARE_INCLUDE_CORE_SCREEN_BLIT_H +#define ARE_INCLUDE_CORE_SCREEN_BLIT_H + +#include "basic/types.h" +#include "resource/shader.h" + +namespace are { + +/// @brief Screen blit utility for rendering texture to screen +class ScreenBlit { +public: + /// @brief Constructor + ScreenBlit(); + + /// @brief Destructor + ~ScreenBlit(); + + /// @brief Initialize screen blit + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release resources + void release(); + + /// @brief Blit texture to screen + /// @param texture Texture to blit + /// @param x Screen X position + /// @param y Screen Y position + /// @param width Blit width + /// @param height Blit height + void blit(TextureHandle texture, int x, int y, uint width, uint height); + + /// @brief Blit texture to full screen + /// @param texture Texture to blit + void blit_fullscreen(TextureHandle texture); + +private: + Shader shader_; + uint vao_; + uint vbo_; + bool initialized_; + + /// @brief Create fullscreen quad + void create_quad_(); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_SCREEN_BLIT_H diff --git a/include/core/shader_manager.h b/include/core/shader_manager.h new file mode 100644 index 0000000..891dccc --- /dev/null +++ b/include/core/shader_manager.h @@ -0,0 +1,70 @@ +#ifndef ARE_INCLUDE_CORE_SHADER_MANAGER_H +#define ARE_INCLUDE_CORE_SHADER_MANAGER_H + +#include "basic/types.h" +#include "resource/shader.h" +#include +#include + +namespace are { + +/// @brief Shader manager for loading and caching shaders +class ShaderManager { +public: + /// @brief Constructor + ShaderManager(); + + /// @brief Destructor + ~ShaderManager(); + + /// @brief Initialize shader manager and load built-in shaders + /// @return True if initialization succeeded + bool initialize(); + + /// @brief Release all shaders + void release(); + + /// @brief Load shader from files + /// @param name Shader name for caching + /// @param vertex_path Vertex shader file path + /// @param fragment_path Fragment shader file path + /// @return Shader object + Shader load_shader(const std::string& name, + const std::string& vertex_path, + const std::string& fragment_path); + + /// @brief Load compute shader from file + /// @param name Shader name for caching + /// @param compute_path Compute shader file path + /// @return Shader object + Shader load_compute_shader(const std::string& name, + const std::string& compute_path); + + /// @brief Get cached shader by name + /// @param name Shader name + /// @return Shader object (invalid if not found) + Shader get_shader(const std::string& name) const; + + /// @brief Get G-Buffer shader + /// @return G-Buffer shader + const Shader& get_gbuffer_shader() const { return gbuffer_shader_; } + + /// @brief Get ray tracing compute shader + /// @return Ray tracing shader + const Shader& get_raytracing_shader() const { return raytracing_shader_; } + +private: + std::unordered_map shader_cache_; + Shader gbuffer_shader_; + Shader raytracing_shader_; + + bool initialized_; + + /// @brief Load built-in shaders + /// @return True if loading succeeded + bool load_builtin_shaders_(); +}; + +} // namespace are + +#endif // ARE_INCLUDE_CORE_SHADER_MANAGER_H diff --git a/include/extended_folders.list b/include/extended_folders.list new file mode 100644 index 0000000..6a611df --- /dev/null +++ b/include/extended_folders.list @@ -0,0 +1,4 @@ +core +scene +resource +utils diff --git a/include/resource/buffer.h b/include/resource/buffer.h new file mode 100644 index 0000000..d0ea6da --- /dev/null +++ b/include/resource/buffer.h @@ -0,0 +1,84 @@ +#ifndef ARE_INCLUDE_RESOURCE_BUFFER_H +#define ARE_INCLUDE_RESOURCE_BUFFER_H + +#include "basic/types.h" + +namespace are { + +/// @brief Buffer usage hint +enum class BufferUsage { + STATIC_DRAW, + DYNAMIC_DRAW, + STREAM_DRAW +}; + +/// @brief Buffer type +enum class BufferType { + VERTEX_BUFFER, + INDEX_BUFFER, + UNIFORM_BUFFER, + SHADER_STORAGE_BUFFER +}; + +/// @brief GPU buffer resource +class Buffer { +public: + /// @brief Constructor + Buffer(); + + /// @brief Destructor + ~Buffer(); + + /// @brief Create buffer + /// @param type Buffer type + /// @param size Buffer size in bytes + /// @param data Initial data (nullptr for empty buffer) + /// @param usage Usage hint + /// @return True if creation succeeded + bool create(BufferType type, size_t size, const void* data, BufferUsage usage); + + /// @brief Update buffer data + /// @param offset Offset in bytes + /// @param size Size in bytes + /// @param data Data to upload + void update(size_t offset, size_t size, const void* data); + + /// @brief Bind buffer + void bind() const; + + /// @brief Bind buffer to binding point (for UBO/SSBO) + /// @param binding_point Binding point index + void bind_base(uint binding_point) const; + + /// @brief Unbind buffer + void unbind() const; + + /// @brief Release buffer resources + void release(); + + /// @brief Get buffer handle + /// @return Buffer handle + BufferHandle get_handle() const { return handle_; } + + /// @brief Get buffer size + /// @return Size in bytes + size_t get_size() const { return size_; } + + /// @brief Get buffer type + /// @return Buffer type + BufferType get_type() const { return type_; } + + /// @brief Check if buffer is valid + /// @return True if valid + bool is_valid() const { return handle_ != INVALID_HANDLE; } + +private: + BufferHandle handle_; + BufferType type_; + size_t size_; + BufferUsage usage_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_BUFFER_H diff --git a/include/resource/model_loader.h b/include/resource/model_loader.h new file mode 100644 index 0000000..27e6994 --- /dev/null +++ b/include/resource/model_loader.h @@ -0,0 +1,58 @@ +#ifndef ARE_INCLUDE_RESOURCE_MODEL_LOADER_H +#define ARE_INCLUDE_RESOURCE_MODEL_LOADER_H + +#include "basic/types.h" +#include "scene/mesh.h" +#include "scene/material.h" +#include +#include +#include + +namespace are { + +/// @brief Model loader using Assimp +class ModelLoader { +public: + /// @brief Load model from file + /// @param path Model file path + /// @param meshes Output mesh list + /// @param materials Output material list + /// @param flip_uvs Flip UV coordinates vertically + /// @return True if loading succeeded + static bool load(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs = true); + + /// @brief Load model and automatically upload to GPU + /// @param path Model file path + /// @param meshes Output mesh list + /// @param materials Output material list + /// @param flip_uvs Flip UV coordinates vertically + /// @return True if loading succeeded + static bool load_and_upload(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs = true); + +private: + /// @brief Process Assimp node recursively + static void process_node_(void* node, void* scene, + std::vector>& meshes, + std::vector>& materials, + const std::string& directory); + + /// @brief Process Assimp mesh + static std::shared_ptr process_mesh_(void* mesh, void* scene, + std::vector>& materials, + const std::string& directory); + + /// @brief Load material textures + static void load_material_textures_(void* material, int type, + std::shared_ptr& mat, + const std::string& directory); +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_MODEL_LOADER_H diff --git a/include/resource/shader.h b/include/resource/shader.h new file mode 100644 index 0000000..89e8175 --- /dev/null +++ b/include/resource/shader.h @@ -0,0 +1,129 @@ +#ifndef ARE_INCLUDE_RESOURCE_SHADER_H +#define ARE_INCLUDE_RESOURCE_SHADER_H + +#include "basic/types.h" +#include +#include + +namespace are { + +/// @brief Shader program resource +class Shader { +public: + /// @brief Constructor + Shader(); + + /// @brief Destructor + ~Shader(); + + /// @brief Load and compile shader from files + /// @param vertex_path Vertex shader path + /// @param fragment_path Fragment shader path + /// @return True if compilation succeeded + bool load(const std::string& vertex_path, const std::string& fragment_path); + + /// @brief Load and compile compute shader + /// @param compute_path Compute shader path + /// @return True if compilation succeeded + bool load_compute(const std::string& compute_path); + + /// @brief Compile shader from source strings + /// @param vertex_source Vertex shader source + /// @param fragment_source Fragment shader source + /// @return True if compilation succeeded + bool compile(const std::string& vertex_source, const std::string& fragment_source); + + /// @brief Compile compute shader from source + /// @param compute_source Compute shader source + /// @return True if compilation succeeded + bool compile_compute(const std::string& compute_source); + + /// @brief Use/activate shader program + void use() const; // 改为const + + /// @brief Release shader resources + void release(); + + /// @brief Set uniform boolean + /// @param name Uniform name + /// @param value Value + void set_bool(const std::string& name, bool value) const; // 新增,const + + /// @brief Set uniform integer + /// @param name Uniform name + /// @param value Value + void set_int(const std::string& name, int value) const; // 改为const + + /// @brief Set uniform unsigned integer + /// @param name Uniform name + /// @param value Value + void set_uint(const std::string& name, uint value) const; // 改为const + + /// @brief Set uniform float + /// @param name Uniform name + /// @param value Value + void set_float(const std::string& name, float value) const; // 改为const + + /// @brief Set uniform vec2 + /// @param name Uniform name + /// @param value Value + void set_vec2(const std::string& name, const Vec2& value) const; // 改为const + + /// @brief Set uniform vec3 + /// @param name Uniform name + /// @param value Value + void set_vec3(const std::string& name, const Vec3& value) const; // 改为const + + /// @brief Set uniform vec4 + /// @param name Uniform name + /// @param value Value + void set_vec4(const std::string& name, const Vec4& value) const; // 改为const + + /// @brief Set uniform mat3 + /// @param name Uniform name + /// @param value Value + void set_mat3(const std::string& name, const Mat3& value) const; // 改为const + + /// @brief Set uniform mat4 + /// @param name Uniform name + /// @param value Value + void set_mat4(const std::string& name, const Mat4& value) const; // 改为const + + /// @brief Get shader program handle + /// @return Shader handle + ShaderHandle get_handle() const { return handle_; } + + /// @brief Check if shader is valid + /// @return True if valid + bool is_valid() const { return handle_ != INVALID_HANDLE; } + +private: + ShaderHandle handle_; + mutable std::unordered_map uniform_cache_; // 改为mutable + + /// @brief Get uniform location (with caching) + /// @param name Uniform name + /// @return Uniform location + int get_uniform_location_(const std::string& name) const; // 改为const + + /// @brief Compile shader stage + /// @param source Shader source code + /// @param type Shader type (GL_VERTEX_SHADER, etc.) + /// @return Shader object handle (0 on failure) + uint compile_shader_(const std::string& source, uint type); + + /// @brief Link shader program + /// @param shaders Array of shader object handles + /// @param count Number of shaders + /// @return True if linking succeeded + bool link_program_(const uint* shaders, uint count); + + /// @brief Read file content + /// @param path File path + /// @return File content + std::string read_file_(const std::string& path); +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_SHADER_H diff --git a/include/resource/texture.h b/include/resource/texture.h new file mode 100644 index 0000000..e195ef8 --- /dev/null +++ b/include/resource/texture.h @@ -0,0 +1,127 @@ +#ifndef ARE_INCLUDE_RESOURCE_TEXTURE_H +#define ARE_INCLUDE_RESOURCE_TEXTURE_H + +#include "basic/types.h" +#include + +namespace are { + +/// @brief Texture format enumeration +enum class TextureFormat { + R8, + RG8, + RGB8, + RGBA8, + R16F, + RG16F, + RGB16F, + RGBA16F, + R32F, + RG32F, + RGB32F, + RGBA32F, + DEPTH24_STENCIL8 +}; + +/// @brief Texture filter mode +enum class TextureFilter { + NEAREST, + LINEAR, + NEAREST_MIPMAP_NEAREST, + LINEAR_MIPMAP_NEAREST, + NEAREST_MIPMAP_LINEAR, + LINEAR_MIPMAP_LINEAR +}; + +/// @brief Texture wrap mode +enum class TextureWrap { + REPEAT, + MIRRORED_REPEAT, + CLAMP_TO_EDGE, + CLAMP_TO_BORDER +}; + +/// @brief Texture resource +class Texture { +public: + /// @brief Constructor + Texture(); + + /// @brief Destructor + ~Texture(); + + /// @brief Load texture from file + /// @param path File path + /// @param generate_mipmaps Generate mipmaps + /// @return True if loading succeeded + bool load_from_file(const std::string& path, bool generate_mipmaps = true); + + /// @brief Create empty texture + /// @param width Texture width + /// @param height Texture height + /// @param format Texture format + /// @return True if creation succeeded + bool create(uint width, uint height, TextureFormat format); + + /// @brief Upload data to texture + /// @param data Pixel data + /// @param width Data width + /// @param height Data height + /// @param format Data format + /// @return True if upload succeeded + bool upload(const void* data, uint width, uint height, TextureFormat format); + + /// @brief Set texture filter mode + /// @param min_filter Minification filter + /// @param mag_filter Magnification filter + void set_filter(TextureFilter min_filter, TextureFilter mag_filter); + + /// @brief Set texture wrap mode + /// @param wrap_s Wrap mode for S coordinate + /// @param wrap_t Wrap mode for T coordinate + void set_wrap(TextureWrap wrap_s, TextureWrap wrap_t); + + /// @brief Generate mipmaps + void generate_mipmaps(); + + /// @brief Bind texture to texture unit + /// @param unit Texture unit + void bind(uint unit) const; + + /// @brief Unbind texture + void unbind() const; + + /// @brief Release texture resources + void release(); + + /// @brief Get texture handle + /// @return Texture handle + TextureHandle get_handle() const { return handle_; } + + /// @brief Get texture width + /// @return Width + uint get_width() const { return width_; } + + /// @brief Get texture height + /// @return Height + uint get_height() const { return height_; } + + /// @brief Get texture format + /// @return Format + TextureFormat get_format() const { return format_; } + + /// @brief Check if texture is valid + /// @return True if valid + bool is_valid() const { return handle_ != INVALID_HANDLE; } + +private: + TextureHandle handle_; + uint width_; + uint height_; + TextureFormat format_; + bool has_mipmaps_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_RESOURCE_TEXTURE_H diff --git a/include/scene/camera.h b/include/scene/camera.h new file mode 100644 index 0000000..003f120 --- /dev/null +++ b/include/scene/camera.h @@ -0,0 +1,105 @@ +#ifndef ARE_INCLUDE_SCENE_CAMERA_H +#define ARE_INCLUDE_SCENE_CAMERA_H + +#include "basic/types.h" + +namespace are { + +/// @brief Camera projection type +enum class ProjectionType { + PERSPECTIVE, + ORTHOGRAPHIC +}; + +/// @brief Camera for rendering +class Camera { +public: + /// @brief Constructor + Camera(); + + /// @brief Destructor + ~Camera(); + + /// @brief Set perspective projection + /// @param fov Field of view in degrees + /// @param aspect Aspect ratio + /// @param near Near plane + /// @param far Far plane + void set_perspective(float fov, float aspect, float near, float far); + + /// @brief Set orthographic projection + /// @param left Left plane + /// @param right Right plane + /// @param bottom Bottom plane + /// @param top Top plane + /// @param near Near plane + /// @param far Far plane + void set_orthographic(float left, float right, float bottom, float top, float near, float far); + + /// @brief Set camera position + /// @param position Position + void set_position(const Vec3& position); + + /// @brief Set camera target + /// @param target Target position + void set_target(const Vec3& target); + + /// @brief Set camera up vector + /// @param up Up vector + void set_up(const Vec3& up); + + /// @brief Get view matrix + /// @return View matrix + Mat4 get_view_matrix() const; + + /// @brief Get projection matrix + /// @return Projection matrix + Mat4 get_projection_matrix() const; + + /// @brief Get view-projection matrix + /// @return View-projection matrix + Mat4 get_view_projection_matrix() const; + + /// @brief Get camera position + /// @return Position + const Vec3& get_position() const { return position_; } + + /// @brief Get camera forward direction + /// @return Forward direction + Vec3 get_forward() const; + + /// @brief Get camera right direction + /// @return Right direction + Vec3 get_right() const; + + /// @brief Get camera up direction + /// @return Up direction + Vec3 get_up() const; + +private: + Vec3 position_; + Vec3 target_; + Vec3 up_; + + ProjectionType projection_type_; + + // Perspective parameters + float fov_; + float aspect_; + + // Orthographic parameters + float left_, right_, bottom_, top_; + + // Common parameters + float near_; + float far_; + + mutable Mat4 view_matrix_; + mutable Mat4 projection_matrix_; + mutable bool view_dirty_; + mutable bool projection_dirty_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_CAMERA_H diff --git a/include/scene/light.h b/include/scene/light.h new file mode 100644 index 0000000..30a6519 --- /dev/null +++ b/include/scene/light.h @@ -0,0 +1,114 @@ +#ifndef ARE_INCLUDE_SCENE_LIGHT_H +#define ARE_INCLUDE_SCENE_LIGHT_H + +#include "basic/types.h" + +namespace are { + +/// @brief Light type enumeration +enum class LightType { + DIRECTIONAL = 0, + POINT = 1, + SPOT = 2 +}; + +/// @brief Light source +class Light { +public: + /// @brief Constructor + Light(); + + /// @brief Destructor + ~Light(); + + /// @brief Set light type + /// @param type Light type + void set_type(LightType type); + + /// @brief Set light position (for point and spot lights) + /// @param position Light position + void set_position(const Vec3 &position); + + /// @brief Set light direction (for directional and spot lights) + /// @param direction Light direction + void set_direction(const Vec3 &direction); + + /// @brief Set light color + /// @param color Light color + void set_color(const Vec3 &color); + + /// @brief Set light intensity + /// @param intensity Light intensity + void set_intensity(float intensity); + + /// @brief Set light range (for point and spot lights) + /// @param range Light range + void set_range(float range); + + /// @brief Set spot light angles + /// @param inner_angle Inner cone angle in degrees + /// @param outer_angle Outer cone angle in degrees + void set_spot_angles(float inner_angle, float outer_angle); + + /// @brief Get light type + /// @return Light type + LightType get_type() const { + return type_; + } + + /// @brief Get light position + /// @return Light position + const Vec3 &get_position() const { + return position_; + } + + /// @brief Get light direction + /// @return Light direction + const Vec3 &get_direction() const { + return direction_; + } + + /// @brief Get light color + /// @return Light color + const Vec3 &get_color() const { + return color_; + } + + /// @brief Get light intensity + /// @return Light intensity + float get_intensity() const { + return intensity_; + } + + /// @brief Get light range + /// @return Light range + float get_range() const { + return range_; + } + + /// @brief Get spot light inner angle + /// @return Inner angle in radians + float get_inner_angle() const { + return inner_angle_; + } + + /// @brief Get spot light outer angle + /// @return Outer angle in radians + float get_outer_angle() const { + return outer_angle_; + } + +private: + LightType type_; + Vec3 position_; + Vec3 direction_; + Vec3 color_; + float intensity_; + float range_; + float inner_angle_; + float outer_angle_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_LIGHT_H diff --git a/include/scene/material.h b/include/scene/material.h new file mode 100644 index 0000000..3fc933e --- /dev/null +++ b/include/scene/material.h @@ -0,0 +1,105 @@ +#ifndef ARE_INCLUDE_SCENE_MATERIAL_H +#define ARE_INCLUDE_SCENE_MATERIAL_H + +#include "basic/types.h" +#include "resource/texture.h" +#include + +namespace are { + +/// @brief Material type enumeration +enum class MaterialType { + DIFFUSE = 0, + METAL = 1, + DIELECTRIC = 2, + EMISSIVE = 3 +}; + +/// @brief Material properties +class Material { +public: + /// @brief Constructor + Material(); + + /// @brief Destructor + ~Material(); + + /// @brief Set albedo color + /// @param albedo Albedo color + void set_albedo(const Vec3& albedo); + + /// @brief Set emission color + /// @param emission Emission color + void set_emission(const Vec3& emission); + + /// @brief Set metallic value + /// @param metallic Metallic (0-1) + void set_metallic(float metallic); + + /// @brief Set roughness value + /// @param roughness Roughness (0-1) + void set_roughness(float roughness); + + /// @brief Set index of refraction + /// @param ior Index of refraction + void set_ior(float ior); + + /// @brief Set material type + /// @param type Material type + void set_type(MaterialType type); + + /// @brief Set albedo texture + /// @param texture Albedo texture + void set_albedo_texture(std::shared_ptr texture); + + /// @brief Set normal map + /// @param texture Normal map texture + void set_normal_texture(std::shared_ptr texture); + + /// @brief Get albedo color + /// @return Albedo color + const Vec3& get_albedo() const { return albedo_; } + + /// @brief Get emission color + /// @return Emission color + const Vec3& get_emission() const { return emission_; } + + /// @brief Get metallic value + /// @return Metallic + float get_metallic() const { return metallic_; } + + /// @brief Get roughness value + /// @return Roughness + float get_roughness() const { return roughness_; } + + /// @brief Get index of refraction + /// @return IOR + float get_ior() const { return ior_; } + + /// @brief Get material type + /// @return Material type + MaterialType get_type() const { return type_; } + + /// @brief Get albedo texture + /// @return Albedo texture (nullptr if none) + std::shared_ptr get_albedo_texture() const { return albedo_texture_; } + + /// @brief Get normal texture + /// @return Normal texture (nullptr if none) + std::shared_ptr get_normal_texture() const { return normal_texture_; } + +private: + Vec3 albedo_; + Vec3 emission_; + float metallic_; + float roughness_; + float ior_; + MaterialType type_; + + std::shared_ptr albedo_texture_; + std::shared_ptr normal_texture_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_MATERIAL_H diff --git a/include/scene/mesh.h b/include/scene/mesh.h new file mode 100644 index 0000000..27acdc5 --- /dev/null +++ b/include/scene/mesh.h @@ -0,0 +1,79 @@ +#ifndef ARE_INCLUDE_SCENE_MESH_H +#define ARE_INCLUDE_SCENE_MESH_H + +#include "basic/types.h" +#include + +namespace are { + +/// @brief Mesh data container +class Mesh { +public: + /// @brief Constructor + Mesh(); + + /// @brief Destructor + ~Mesh(); + + /// @brief Set vertex data + /// @param vertices Vertex array + void set_vertices(const std::vector& vertices); + + /// @brief Set index data + /// @param indices Index array + void set_indices(const std::vector& indices); + + /// @brief Set material index + /// @param material_id Material index + void set_material(uint material_id); + + /// @brief Set transform matrix + /// @param transform Transform matrix + void set_transform(const Mat4& transform); + + /// @brief Get vertices + /// @return Vertex array + const std::vector& get_vertices() const { return vertices_; } + + /// @brief Get indices + /// @return Index array + const std::vector& get_indices() const { return indices_; } + + /// @brief Get material index + /// @return Material index + uint get_material() const { return material_id_; } + + /// @brief Get transform matrix + /// @return Transform matrix + const Mat4& get_transform() const { return transform_; } + + /// @brief Upload mesh data to GPU + /// @return True if upload succeeded + bool upload_to_gpu(); + + /// @brief Release GPU resources + void release_gpu_resources(); + + /// @brief Get VAO handle + /// @return VAO handle + uint get_vao() const { return vao_; } + + /// @brief Check if mesh is uploaded to GPU + /// @return True if uploaded + bool is_uploaded() const { return uploaded_; } + +private: + std::vector vertices_; + std::vector indices_; + uint material_id_; + Mat4 transform_; + + uint vao_; + uint vbo_; + uint ebo_; + bool uploaded_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_MESH_H diff --git a/include/scene/scene.h b/include/scene/scene.h new file mode 100644 index 0000000..f5d34c2 --- /dev/null +++ b/include/scene/scene.h @@ -0,0 +1,74 @@ +#ifndef ARE_INCLUDE_SCENE_SCENE_H +#define ARE_INCLUDE_SCENE_SCENE_H + +#include "basic/types.h" +#include "scene/camera.h" +#include "scene/mesh.h" +#include "scene/material.h" +#include "scene/light.h" +#include +#include + +namespace are { + +/// @brief Scene container holding all scene objects +class Scene { +public: + /// @brief Constructor + Scene(); + + /// @brief Destructor + ~Scene(); + + /// @brief Add mesh to scene + /// @param mesh Mesh to add + /// @return Mesh index + uint add_mesh(std::shared_ptr mesh); + + /// @brief Add material to scene + /// @param material Material to add + /// @return Material index + uint add_material(std::shared_ptr material); + + /// @brief Add light to scene + /// @param light Light to add + /// @return Light index + uint add_light(std::shared_ptr light); + + /// @brief Set active camera + /// @param camera Camera to set + void set_camera(std::shared_ptr camera); + + /// @brief Get active camera + /// @return Active camera + const Camera& get_camera() const { return *camera_; } + + /// @brief Get all meshes + /// @return Mesh list + const std::vector>& get_meshes() const { return meshes_; } + + /// @brief Get all materials + /// @return Material list + const std::vector>& get_materials() const { return materials_; } + + /// @brief Get all lights + /// @return Light list + const std::vector>& get_lights() const { return lights_; } + + /// @brief Clear all scene objects + void clear(); + + /// @brief Update scene (animations, transforms, etc.) + /// @param delta_time Time since last update + void update(float delta_time); + +private: + std::shared_ptr camera_; + std::vector> meshes_; + std::vector> materials_; + std::vector> lights_; +}; + +} // namespace are + +#endif // ARE_INCLUDE_SCENE_SCENE_H diff --git a/include/utils/config.h b/include/utils/config.h new file mode 100644 index 0000000..f9a7790 --- /dev/null +++ b/include/utils/config.h @@ -0,0 +1,70 @@ +#ifndef ARE_INCLUDE_UTILS_CONFIG_H +#define ARE_INCLUDE_UTILS_CONFIG_H + +#include +#include + +namespace are { + +/// @brief Configuration manager for loading engine settings +/// @note This module should be implemented by the user +class Config { +public: + /// @brief Load configuration from file + /// @param path Configuration file path + /// @return True if loading succeeded + static bool load(const std::string& path); + + /// @brief Save configuration to file + /// @param path Configuration file path + /// @return True if saving succeeded + static bool save(const std::string& path); + + /// @brief Get string value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static std::string get_string(const std::string& key, const std::string& default_value = ""); + + /// @brief Get integer value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static int get_int(const std::string& key, int default_value = 0); + + /// @brief Get float value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static float get_float(const std::string& key, float default_value = 0.0f); + + /// @brief Get boolean value + /// @param key Configuration key + /// @param default_value Default value if key not found + /// @return Configuration value + static bool get_bool(const std::string& key, bool default_value = false); + + /// @brief Set string value + /// @param key Configuration key + /// @param value Configuration value + static void set_string(const std::string& key, const std::string& value); + + /// @brief Set integer value + /// @param key Configuration key + /// @param value Configuration value + static void set_int(const std::string& key, int value); + + /// @brief Set float value + /// @param key Configuration key + /// @param value Configuration value + static void set_float(const std::string& key, float value); + + /// @brief Set boolean value + /// @param key Configuration key + /// @param value Configuration value + static void set_bool(const std::string& key, bool value); +}; + +} // namespace are + +#endif // ARE_INCLUDE_UTILS_CONFIG_H diff --git a/include/utils/logger.h b/include/utils/logger.h new file mode 100644 index 0000000..1d8123f --- /dev/null +++ b/include/utils/logger.h @@ -0,0 +1,61 @@ +#ifndef ARE_INCLUDE_UTILS_LOGGER_H +#define ARE_INCLUDE_UTILS_LOGGER_H + +#include + +namespace are { + +/// @brief Log level enumeration +enum class LogLevel { + DEBUG, + INFO, + WARNING, + ERROR, + FATAL +}; + +/// @brief Logger interface for engine logging +/// @note This module should be implemented by the user +class Logger { +public: + /// @brief Initialize logger + /// @param log_file Log file path (empty for console only) + /// @return True if initialization succeeded + static bool initialize(const std::string& log_file = ""); + + /// @brief Shutdown logger + static void shutdown(); + + /// @brief Log message + /// @param level Log level + /// @param message Message content + static void log(LogLevel level, const std::string& message); + + /// @brief Log debug message + /// @param message Message content + static void debug(const std::string& message); + + /// @brief Log info message + /// @param message Message content + static void info(const std::string& message); + + /// @brief Log warning message + /// @param message Message content + static void warning(const std::string& message); + + /// @brief Log error message + /// @param message Message content + static void error(const std::string& message); + + /// @brief Log fatal message + /// @param message Message content + static void fatal(const std::string& message); + + /// @brief Set minimum log level + /// @param level Minimum level to log + static void set_level(LogLevel level); +}; + +} // namespace are + +#endif // ARE_INCLUDE_UTILS_LOGGER_H diff --git a/scripts/check_env.sh b/scripts/check_env.sh new file mode 100644 index 0000000..05f9201 --- /dev/null +++ b/scripts/check_env.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +echo "==========================================" +echo "Aurora Rendering Engine - System Check" +echo "==========================================" +echo "" + +# Check display server +echo "1. Display Server:" +if [ -n "$DISPLAY" ]; then + echo " ✓ X11 detected: $DISPLAY" +elif [ -n "$WAYLAND_DISPLAY" ]; then + echo " ✓ Wayland detected: $WAYLAND_DISPLAY" +else + echo " ✗ No display server detected!" + echo " Please run this in a graphical environment" +fi +echo "" + +# Check OpenGL +echo "2. OpenGL Information:" +if command -v glxinfo &> /dev/null; then + GL_VERSION=$(glxinfo | grep "OpenGL version" | cut -d: -f2) + GL_RENDERER=$(glxinfo | grep "OpenGL renderer" | cut -d: -f2) + GL_VENDOR=$(glxinfo | grep "OpenGL vendor" | cut -d: -f2) + + echo " Vendor: $GL_VENDOR" + echo " Renderer:$GL_RENDERER" + echo " Version: $GL_VERSION" + + # Check version + MAJOR=$(echo $GL_VERSION | grep -oP '\d+' | head -1) + MINOR=$(echo $GL_VERSION | grep -oP '\d+' | head -2 | tail -1) + + if [ "$MAJOR" -ge 4 ] && [ "$MINOR" -ge 5 ]; then + echo " ✓ OpenGL 4.5+ supported" + else + echo " ✗ OpenGL 4.5+ NOT supported (found $MAJOR.$MINOR)" + echo " This engine requires OpenGL 4.5 or higher" + fi +else + echo " ✗ glxinfo not found (install mesa-utils)" +fi +echo "" + +# Check GLFW +echo "3. GLFW Library:" +if ldconfig -p | grep -q libglfw; then + echo " ✓ GLFW library found" +else + echo " ✗ GLFW library not found" + echo " Install: sudo apt-get install libglfw3-dev" +fi +echo "" + +# Check GLM +echo "4. GLM Library:" +if [ -d "/usr/include/glm" ] || [ -d "/usr/local/include/glm" ]; then + echo " ✓ GLM headers found" +else + echo " ✗ GLM headers not found" + echo " Install: sudo apt-get install libglm-dev" +fi +echo "" + +# Check GPU +echo "5. GPU Information:" +if command -v lspci &> /dev/null; then + GPU=$(lspci | grep -i vga | cut -d: -f3) + echo " GPU:$GPU" +else + echo " ✗ lspci not found" +fi +echo "" + +# Check drivers +echo "6. Graphics Drivers:" +if lsmod | grep -q nvidia; then + echo " ✓ NVIDIA driver loaded" + if command -v nvidia-smi &> /dev/null; then + DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader) + echo " Driver version: $DRIVER_VERSION" + fi +elif lsmod | grep -q amdgpu; then + echo " ✓ AMD driver (amdgpu) loaded" +elif lsmod | grep -q i915; then + echo " ✓ Intel driver (i915) loaded" +else + echo " ? Unknown driver" +fi +echo "" + +echo "==========================================" +echo "System check complete" +echo "==========================================" diff --git a/shaders/gbuffer.frag b/shaders/gbuffer.frag new file mode 100644 index 0000000..61a7c4a --- /dev/null +++ b/shaders/gbuffer.frag @@ -0,0 +1,49 @@ +#version 430 core + +// Material types +const uint MATERIAL_DIFFUSE = 0u; +const uint MATERIAL_METAL = 1u; +const uint MATERIAL_DIELECTRIC = 2u; +const uint MATERIAL_EMISSIVE = 3u; + +in VS_OUT { + vec3 frag_pos; + vec3 normal; + vec2 texcoord; + vec3 tangent; +} fs_in; + +layout(location = 0) out vec4 g_position; +layout(location = 1) out vec4 g_normal; +layout(location = 2) out vec4 g_albedo; +layout(location = 3) out vec4 g_material; + +// Material uniforms +uniform vec3 u_albedo; +uniform float u_metallic; +uniform float u_roughness; +uniform float u_ior; +uniform vec3 u_emission; +uniform uint u_material_type; + +uniform bool u_has_albedo_map; +uniform sampler2D u_albedo_map; + +void main() { + // Position + g_position = vec4(fs_in.frag_pos, 1.0); + + // Normal + vec3 normal = normalize(fs_in.normal); + g_normal = vec4(normal, 0.0); + + // Albedo + vec3 albedo = u_albedo; + if (u_has_albedo_map) { + albedo *= texture(u_albedo_map, fs_in.texcoord).rgb; + } + g_albedo = vec4(albedo, 1.0); + + // Material properties + g_material = vec4(u_metallic, u_roughness, u_ior, float(u_material_type)); +} diff --git a/shaders/gbuffer.vert b/shaders/gbuffer.vert new file mode 100644 index 0000000..0556816 --- /dev/null +++ b/shaders/gbuffer.vert @@ -0,0 +1,27 @@ +#version 430 core + +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec3 a_normal; +layout(location = 2) in vec2 a_texcoord; +layout(location = 3) in vec3 a_tangent; + +out VS_OUT { + vec3 frag_pos; + vec3 normal; + vec2 texcoord; + vec3 tangent; +} vs_out; + +uniform mat4 u_model; +uniform mat4 u_view; +uniform mat4 u_projection; + +void main() { + vec4 world_pos = u_model * vec4(a_position, 1.0); + vs_out.frag_pos = world_pos.xyz; + vs_out.normal = mat3(transpose(inverse(u_model))) * a_normal; + vs_out.texcoord = a_texcoord; + vs_out.tangent = mat3(u_model) * a_tangent; + + gl_Position = u_projection * u_view * world_pos; +} diff --git a/shaders/raytracing.comp b/shaders/raytracing.comp new file mode 100644 index 0000000..28fb002 --- /dev/null +++ b/shaders/raytracing.comp @@ -0,0 +1,503 @@ +#version 430 core + +// Constants +#define PI 3.14159265359 +#define INV_PI 0.31830988618 +#define EPSILON 1e-4 +#define MAX_FLOAT 3.402823466e38 +#define MAX_RAY_DEPTH 8 +#define MAX_LIGHTS 16 + +// Material types +#define MATERIAL_DIFFUSE 0 +#define MATERIAL_METAL 1 +#define MATERIAL_DIELECTRIC 2 +#define MATERIAL_EMISSIVE 3 + +// Light types +#define LIGHT_DIRECTIONAL 0 +#define LIGHT_POINT 1 +#define LIGHT_SPOT 2 + +// Structures +struct Material { + vec3 albedo; + float metallic; + vec3 emission; + float roughness; + int type; + float ior; + vec2 padding; +}; + +struct Light { + vec3 position; + int type; + vec3 direction; + float intensity; + vec3 color; + float range; + vec2 spot_angles; // inner, outer + vec2 padding; +}; + +struct Ray { + vec3 origin; + vec3 direction; +}; + +struct HitInfo { + bool hit; + float t; + vec3 position; + vec3 normal; + vec2 texcoord; + uint material_id; +}; + +// Utility functions +float saturate(float x) { + return clamp(x, 0.0, 1.0); +} + +vec3 saturate(vec3 x) { + return clamp(x, vec3(0.0), vec3(1.0)); +} + +// Random number generation (PCG Hash) +uint pcg_hash(uint seed) { + uint state = seed * 747796405u + 2891336453u; + uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + return (word >> 22u) ^ word; +} + +float random_float(inout uint seed) { + seed = pcg_hash(seed); + return float(seed) / 4294967296.0; +} + +vec2 random_vec2(inout uint seed) { + return vec2(random_float(seed), random_float(seed)); +} + +vec3 random_vec3(inout uint seed) { + return vec3(random_float(seed), random_float(seed), random_float(seed)); +} + +// Random direction in hemisphere +vec3 random_hemisphere_direction(vec3 normal, inout uint seed) { + float z = random_float(seed); + float r = sqrt(max(0.0, 1.0 - z * z)); + float phi = 2.0 * PI * random_float(seed); + + vec3 dir = vec3(r * cos(phi), r * sin(phi), z); + + // Create coordinate system + vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, normal)); + vec3 bitangent = cross(normal, tangent); + + return normalize(tangent * dir.x + bitangent * dir.y + normal * dir.z); +} + +// Cosine-weighted hemisphere sampling +// vec3 cosine_weighted_hemisphere(vec3 normal, inout uint seed) { +// vec2 r = random_vec2(seed); +// float r1 = 2.0 * PI * r.x; +// float r2 = r.y; +// float r2s = sqrt(r2); +// +// vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); +// vec3 tangent = normalize(cross(up, normal)); +// vec3 bitangent = cross(normal, tangent); +// +// vec3 dir = tangent * cos(r1) * r2s + bitangent * sin(r1) * r2s + normal * sqrt(1.0 - r2); +// return normalize(dir); +// } + +// Schlick's approximation for Fresnel +vec3 fresnel_schlick(float cos_theta, vec3 f0) { + return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0); +} + +// GGX distribution +float distribution_ggx(vec3 N, vec3 H, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH * NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / max(denom, EPSILON); +} + +// Smith's geometry function +float geometry_smith(vec3 N, vec3 V, vec3 L, float roughness) { + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float r = roughness + 1.0; + float k = (r * r) / 8.0; + + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + + return ggx1 * ggx2; +} + +layout(local_size_x = 16, local_size_y = 16) in; + +// G-Buffer inputs +layout(binding = 0, rgba32f) uniform readonly image2D g_position; +layout(binding = 1, rgba32f) uniform readonly image2D g_normal; +layout(binding = 2, rgba8) uniform readonly image2D g_albedo; + +// Output +layout(binding = 3, rgba32f) uniform image2D output_image; +layout(binding = 4, rgba32f) uniform image2D accumulation_image; + +// Scene data +layout(std430, binding = 0) readonly buffer MaterialBuffer { + Material materials[]; +}; + +layout(std430, binding = 1) readonly buffer LightBuffer { + Light lights[]; +}; + +// Uniforms +uniform uint u_frame_count; +uniform uint u_samples_per_pixel; +uniform uint u_max_depth; +uniform uint u_light_count; +uniform vec3 u_camera_position; +uniform mat4 u_inv_view_projection; +uniform bool u_enable_accumulation; +uniform bool u_use_bvh; + +// Evaluate direct lighting +vec3 evaluate_direct_lighting(vec3 position, vec3 normal, vec3 view_dir, + Material material, inout uint seed) { + vec3 direct_light = vec3(0.0); + + for (uint i = 0u; i < u_light_count; i++) { + Light light = lights[i]; + vec3 light_dir; + float light_distance; + float attenuation = 1.0; + + if (light.type == LIGHT_POINT) { + vec3 to_light = light.position - position; + light_distance = length(to_light); + light_dir = to_light / light_distance; + attenuation = 1.0 / max(light_distance * light_distance, 0.01); + + if (light_distance > light.range) continue; + } else if (light.type == LIGHT_DIRECTIONAL) { + light_dir = normalize(-light.direction); + light_distance = MAX_FLOAT; + } else { + continue; + } + + float NdotL = max(dot(normal, light_dir), 0.0); + + if (NdotL > 0.0) { + vec3 H = normalize(view_dir + light_dir); + float NdotV = max(dot(normal, view_dir), 0.0); + + // PBR lighting + vec3 F0 = mix(vec3(0.04), material.albedo, material.metallic); + vec3 F = fresnel_schlick(max(dot(H, view_dir), 0.0), F0); + float D = distribution_ggx(normal, H, max(material.roughness, 0.04)); + float G = geometry_smith(normal, view_dir, light_dir, material.roughness); + + vec3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + EPSILON; + vec3 specular = numerator / denominator; + + vec3 kS = F; + vec3 kD = (vec3(1.0) - kS) * (1.0 - material.metallic); + + vec3 radiance = light.color * light.intensity * attenuation; + direct_light += (kD * material.albedo * INV_PI + specular) * radiance * NdotL; + } + } + + return direct_light; +} + +void main() { + ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); + ivec2 image_size = imageSize(output_image); + + if (pixel_coords.x >= image_size.x || pixel_coords.y >= image_size.y) { + return; + } + + // Read G-Buffer + vec4 position_data = imageLoad(g_position, pixel_coords); + vec4 normal_data = imageLoad(g_normal, pixel_coords); + vec4 albedo_data = imageLoad(g_albedo, pixel_coords); + + // Background + if (position_data.w < 0.5) { + vec3 background = vec3(0.1, 0.1, 0.15); + imageStore(output_image, pixel_coords, vec4(background, 1.0)); + imageStore(accumulation_image, pixel_coords, vec4(background, 1.0)); + return; + } + + vec3 position = position_data.xyz; + vec3 normal = normalize(normal_data.xyz); + vec3 albedo = albedo_data.rgb; + + // 关键修复:从G-Buffer的alpha通道读取material_id + // 注意:albedo_data是从RGBA8纹理读取的,alpha值范围是[0,1] + // 我们需要将其转换回整数ID + uint material_id = uint(albedo_data.a * 255.0 + 0.5); + + // Initialize random seed + uint seed = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x) + u_frame_count * 719393u; + + vec3 color = vec3(0.0); + + // Get material from buffer + Material material; + uint mat_count = uint(materials.length()); + + if (material_id < mat_count) { + // 从SSBO读取材质 + material = materials[material_id]; + } else { + // 使用G-Buffer中的albedo作为fallback + material.albedo = albedo; + material.metallic = 0.0; + material.roughness = 0.5; + material.emission = vec3(0.0); + material.type = MATERIAL_DIFFUSE; + material.ior = 1.5; + } + + // Add emission + color += material.emission; + + // Direct lighting + vec3 view_dir = normalize(u_camera_position - position); + + if (u_light_count > 0u) { + color += evaluate_direct_lighting(position, normal, view_dir, material, seed); + } + + // Ambient lighting + color += material.albedo * 0.05; + + // Clamp + color = clamp(color, vec3(0.0), vec3(10.0)); + + // Accumulation + if (u_enable_accumulation && u_frame_count > 0u) { + vec3 accumulated = imageLoad(accumulation_image, pixel_coords).rgb; + float weight = 1.0 / float(u_frame_count + 1u); + color = mix(accumulated, color, weight); + } + + imageStore(accumulation_image, pixel_coords, vec4(color, 1.0)); + imageStore(output_image, pixel_coords, vec4(color, 1.0)); +} + +// layout(local_size_x = 16, local_size_y = 16) in; +// +// // G-Buffer inputs +// layout(binding = 0, rgba32f) uniform readonly image2D g_position; +// layout(binding = 1, rgba32f) uniform readonly image2D g_normal; +// layout(binding = 2, rgba8) uniform readonly image2D g_albedo; +// +// // Output +// layout(binding = 3, rgba32f) uniform image2D output_image; +// layout(binding = 4, rgba32f) uniform image2D accumulation_image; +// +// // Scene data +// layout(std430, binding = 0) readonly buffer MaterialBuffer { +// Material materials[]; +// }; +// +// layout(std430, binding = 1) readonly buffer LightBuffer { +// Light lights[]; +// }; +// +// // Uniforms +// uniform uint u_frame_count; +// uniform uint u_samples_per_pixel; +// uniform uint u_max_depth; +// uniform uint u_light_count; +// uniform vec3 u_camera_position; +// uniform mat4 u_inv_view_projection; +// uniform bool u_enable_accumulation; +// uniform bool u_use_bvh; +// +// // Cosine-weighted hemisphere sampling +// vec3 cosine_weighted_hemisphere(vec3 normal, inout uint seed) { +// vec2 r = random_vec2(seed); +// float r1 = 2.0 * PI * r.x; +// float r2 = r.y; +// float r2s = sqrt(r2); +// +// vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); +// vec3 tangent = normalize(cross(up, normal)); +// vec3 bitangent = cross(normal, tangent); +// +// vec3 dir = tangent * cos(r1) * r2s + bitangent * sin(r1) * r2s + normal * sqrt(1.0 - r2); +// return normalize(dir); +// } +// +// // Evaluate direct lighting +// vec3 evaluate_direct_lighting(vec3 position, vec3 normal, vec3 view_dir, +// Material material, inout uint seed) { +// vec3 direct_light = vec3(0.0); +// +// for (uint i = 0u; i < u_light_count; i++) { +// Light light = lights[i]; +// vec3 light_dir; +// float light_distance; +// float attenuation = 1.0; +// +// if (light.type == LIGHT_POINT) { +// vec3 to_light = light.position - position; +// light_distance = length(to_light); +// light_dir = to_light / light_distance; +// attenuation = 1.0 / max(light_distance * light_distance, 0.01); +// +// if (light_distance > light.range) continue; +// } else if (light.type == LIGHT_DIRECTIONAL) { +// light_dir = normalize(-light.direction); +// light_distance = MAX_FLOAT; +// } else { +// continue; +// } +// +// float NdotL = max(dot(normal, light_dir), 0.0); +// +// if (NdotL > 0.0) { +// vec3 H = normalize(view_dir + light_dir); +// float NdotV = max(dot(normal, view_dir), 0.0); +// +// // PBR lighting +// vec3 F0 = mix(vec3(0.04), material.albedo, material.metallic); +// vec3 F = fresnel_schlick(max(dot(H, view_dir), 0.0), F0); +// float D = distribution_ggx(normal, H, max(material.roughness, 0.04)); +// float G = geometry_smith(normal, view_dir, light_dir, material.roughness); +// +// vec3 numerator = D * G * F; +// float denominator = 4.0 * NdotV * NdotL + EPSILON; +// vec3 specular = numerator / denominator; +// +// vec3 kS = F; +// vec3 kD = (vec3(1.0) - kS) * (1.0 - material.metallic); +// +// vec3 radiance = light.color * light.intensity * attenuation; +// direct_light += (kD * material.albedo * INV_PI + specular) * radiance * NdotL; +// } +// } +// +// return direct_light; +// } +// +// // Trace indirect lighting (simple path tracing) +// vec3 trace_indirect(vec3 position, vec3 normal, Material material, inout uint seed) { +// vec3 color = vec3(0.0); +// +// // Sample random direction +// vec3 ray_dir = cosine_weighted_hemisphere(normal, seed); +// +// // Sky color based on direction +// float t = 0.5 * (ray_dir.y + 1.0); +// vec3 sky_color = mix(vec3(1.0), vec3(0.5, 0.7, 1.0), t) * 0.2; +// +// color = sky_color; +// +// return color; +// } +// +// void main() { +// ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); +// ivec2 image_size = imageSize(output_image); +// +// if (pixel_coords.x >= image_size.x || pixel_coords.y >= image_size.y) { +// return; +// } +// +// // Read G-Buffer +// vec4 position_data = imageLoad(g_position, pixel_coords); +// vec4 normal_data = imageLoad(g_normal, pixel_coords); +// vec4 albedo_data = imageLoad(g_albedo, pixel_coords); +// +// // Check if this pixel has valid geometry +// if (position_data.w < 0.5) { +// vec3 background = vec3(0.1, 0.1, 0.15); +// imageStore(output_image, pixel_coords, vec4(background, 1.0)); +// imageStore(accumulation_image, pixel_coords, vec4(background, 1.0)); +// return; +// } +// +// vec3 position = position_data.xyz; +// vec3 normal = normalize(normal_data.xyz); +// vec3 albedo = albedo_data.rgb; +// uint material_id = floatBitsToUint(albedo_data.a); +// +// if (material_id >= 1000u) { +// material_id = 0u; +// } +// +// // Initialize random seed +// uint seed = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x) + u_frame_count * 719393u; +// +// vec3 color = vec3(0.0); +// +// // Get material +// Material material; +// if (material_id < uint(materials.length())) { +// material = materials[material_id]; +// } else { +// material.albedo = albedo; +// material.metallic = 0.0; +// material.roughness = 0.5; +// material.emission = vec3(0.0); +// material.type = MATERIAL_DIFFUSE; +// material.ior = 1.5; +// } +// +// // Add emission +// color += material.emission; +// +// // Direct lighting +// vec3 view_dir = normalize(u_camera_position - position); +// +// if (u_light_count > 0u) { +// color += evaluate_direct_lighting(position, normal, view_dir, material, seed); +// } +// +// // Indirect lighting (path tracing) - THIS ADDS NOISE +// for (uint samp_idx = 0u; samp_idx < u_samples_per_pixel; samp_idx++) { // 修复: sample -> samp_idx +// vec3 indirect = trace_indirect(position, normal, material, seed); +// color += indirect * material.albedo * INV_PI; +// } +// +// // Ambient +// color += material.albedo * 0.02; +// +// // Clamp +// color = clamp(color, vec3(0.0), vec3(100.0)); +// +// // Accumulation for denoising +// if (u_enable_accumulation && u_frame_count > 0u) { +// vec3 accumulated = imageLoad(accumulation_image, pixel_coords).rgb; +// float weight = 1.0 / float(u_frame_count + 1u); +// color = mix(accumulated, color, weight); +// } +// +// imageStore(accumulation_image, pixel_coords, vec4(color, 1.0)); +// imageStore(output_image, pixel_coords, vec4(color, 1.0)); +// } diff --git a/shaders/screen_blit.frag b/shaders/screen_blit.frag new file mode 100644 index 0000000..99cb1ac --- /dev/null +++ b/shaders/screen_blit.frag @@ -0,0 +1,11 @@ +#version 430 core + +in vec2 v_texcoord; + +out vec4 frag_color; + +uniform sampler2D u_texture; + +void main() { + frag_color = texture(u_texture, v_texcoord); +} diff --git a/shaders/screen_blit.vert b/shaders/screen_blit.vert new file mode 100644 index 0000000..3b3bc01 --- /dev/null +++ b/shaders/screen_blit.vert @@ -0,0 +1,11 @@ +#version 430 core + +layout(location = 0) in vec2 a_position; +layout(location = 1) in vec2 a_texcoord; + +out vec2 v_texcoord; + +void main() { + v_texcoord = a_texcoord; + gl_Position = vec4(a_position, 0.0, 1.0); +} diff --git a/src/basic/math.cpp b/src/basic/math.cpp new file mode 100644 index 0000000..d265c09 --- /dev/null +++ b/src/basic/math.cpp @@ -0,0 +1,33 @@ +#include "basic/math.h" + +namespace are { + +Mat4 MathUtils::perspective(float fov, float aspect, float near, float far) { + return glm::perspective(fov, aspect, near, far); +} + +Mat4 MathUtils::look_at(const Vec3& eye, const Vec3& center, const Vec3& up) { + return glm::lookAt(eye, center, up); +} + +Vec3 MathUtils::normalize(const Vec3& v) { + return glm::normalize(v); +} + +float MathUtils::dot(const Vec3& a, const Vec3& b) { + return glm::dot(a, b); +} + +Vec3 MathUtils::cross(const Vec3& a, const Vec3& b) { + return glm::cross(a, b); +} + +Vec3 MathUtils::reflect(const Vec3& incident, const Vec3& normal) { + return glm::reflect(incident, normal); +} + +const float* MathUtils::value_ptr(const Mat4& mat) { + return glm::value_ptr(mat); +} + +} // namespace are diff --git a/src/core/bvh.cpp b/src/core/bvh.cpp new file mode 100644 index 0000000..d8dfbf6 --- /dev/null +++ b/src/core/bvh.cpp @@ -0,0 +1,297 @@ +#include "core/bvh.h" +#include "utils/logger.h" +#include "basic/constants.h" +#include +#include + +namespace are { + +// AABB implementation +void AABB::expand(const Vec3& point) { + min_ = glm::min(min_, point); + max_ = glm::max(max_, point); +} + +void AABB::expand(const AABB& other) { + min_ = glm::min(min_, other.min_); + max_ = glm::max(max_, other.max_); +} + +float AABB::surface_area() const { + Vec3 extent = max_ - min_; + return 2.0f * (extent.x * extent.y + extent.y * extent.z + extent.z * extent.x); +} + +bool AABB::is_valid() const { + return min_.x <= max_.x && min_.y <= max_.y && min_.z <= max_.z; +} + +// Triangle implementation +AABB Triangle::get_bounds() const { + AABB bounds(v0_, v0_); + bounds.expand(v1_); + bounds.expand(v2_); + return bounds; +} + +Vec3 Triangle::get_centroid() const { + return (v0_ + v1_ + v2_) / 3.0f; +} + +// BVH implementation +BVH::BVH() { +} + +BVH::~BVH() { + clear(); +} + +bool BVH::build(const std::vector>& meshes) { + clear(); + + Logger::info("Building BVH..."); + + // Extract all triangles from meshes + for (const auto& mesh : meshes) { + const auto& vertices = mesh->get_vertices(); + const auto& indices = mesh->get_indices(); + uint material_id = mesh->get_material(); + Mat4 transform = mesh->get_transform(); + + for (size_t i = 0; i < indices.size(); i += 3) { + Triangle tri; + + // Transform vertices + Vec4 v0 = transform * Vec4(vertices[indices[i]].position_, 1.0f); + Vec4 v1 = transform * Vec4(vertices[indices[i + 1]].position_, 1.0f); + Vec4 v2 = transform * Vec4(vertices[indices[i + 2]].position_, 1.0f); + + tri.v0_ = Vec3(v0) / v0.w; + tri.v1_ = Vec3(v1) / v1.w; + tri.v2_ = Vec3(v2) / v2.w; + + // Transform normals + Mat3 normal_matrix = glm::transpose(glm::inverse(Mat3(transform))); + tri.n0_ = glm::normalize(normal_matrix * vertices[indices[i]].normal_); + tri.n1_ = glm::normalize(normal_matrix * vertices[indices[i + 1]].normal_); + tri.n2_ = glm::normalize(normal_matrix * vertices[indices[i + 2]].normal_); + + // Copy UVs + tri.uv0_ = vertices[indices[i]].texcoord_; + tri.uv1_ = vertices[indices[i + 1]].texcoord_; + tri.uv2_ = vertices[indices[i + 2]].texcoord_; + + tri.material_id_ = material_id; + + triangles_.push_back(tri); + } + } + + if (triangles_.empty()) { + Logger::warning("No triangles to build BVH"); + return false; + } + + // Initialize triangle indices + triangle_indices_.resize(triangles_.size()); + for (size_t i = 0; i < triangles_.size(); ++i) { + triangle_indices_[i] = static_cast(i); + } + + // Reserve space for nodes (estimate) + nodes_.reserve(triangles_.size() * 2); + + // Create root node + nodes_.emplace_back(); + + // Build BVH recursively + build_recursive_(0, 0, static_cast(triangles_.size())); + + Logger::info("BVH built: " + std::to_string(nodes_.size()) + " nodes, " + + std::to_string(triangles_.size()) + " triangles"); + + return true; +} + +void BVH::build_recursive_(uint node_idx, uint first_prim, uint prim_count) { + BVHNode& node = nodes_[node_idx]; + + // Calculate bounds + AABB bounds = calculate_bounds_(first_prim, prim_count); + node.aabb_min_ = bounds.min_; + node.aabb_max_ = bounds.max_; + + // Leaf node threshold + const uint LEAF_SIZE = 4; + + if (prim_count <= LEAF_SIZE) { + // Create leaf node + node.left_first_ = first_prim; + node.count_ = prim_count; + return; + } + + // Find best split + int axis; + float split_pos; + float split_cost = find_best_split_(first_prim, prim_count, axis, split_pos); + + // Check if split is beneficial + float no_split_cost = prim_count * bounds.surface_area(); + if (split_cost >= no_split_cost) { + // Create leaf node + node.left_first_ = first_prim; + node.count_ = prim_count; + return; + } + + // Partition primitives + uint mid = first_prim; + for (uint i = first_prim; i < first_prim + prim_count; ++i) { + Triangle& tri = triangles_[triangle_indices_[i]]; + float centroid = tri.get_centroid()[axis]; + + if (centroid < split_pos) { + std::swap(triangle_indices_[i], triangle_indices_[mid]); + mid++; + } + } + + // Ensure we have primitives on both sides + if (mid == first_prim || mid == first_prim + prim_count) { + mid = first_prim + prim_count / 2; + } + + // Create interior node + uint left_count = mid - first_prim; + uint right_count = prim_count - left_count; + + node.left_first_ = static_cast(nodes_.size()); + node.count_ = 0; + + // Create child nodes + nodes_.emplace_back(); + nodes_.emplace_back(); + + // Recursively build children + build_recursive_(node.left_first_, first_prim, left_count); + build_recursive_(node.left_first_ + 1, mid, right_count); +} + +float BVH::find_best_split_(uint first_prim, uint prim_count, int& axis, float& split_pos) { + float best_cost = std::numeric_limits::max(); + + AABB centroid_bounds = calculate_centroid_bounds_(first_prim, prim_count); + + // Try each axis + for (int a = 0; a < 3; ++a) { + float extent = centroid_bounds.max_[a] - centroid_bounds.min_[a]; + if (extent < EPSILON) continue; + + // Try multiple split positions + const int NUM_BINS = 16; + for (int i = 1; i < NUM_BINS; ++i) { + float t = static_cast(i) / NUM_BINS; + float pos = centroid_bounds.min_[a] + t * extent; + + // Count primitives and calculate bounds for each side + AABB left_bounds, right_bounds; + uint left_count = 0, right_count = 0; + + for (uint j = first_prim; j < first_prim + prim_count; ++j) { + Triangle& tri = triangles_[triangle_indices_[j]]; + float centroid = tri.get_centroid()[a]; + + if (centroid < pos) { + left_bounds.expand(tri.get_bounds()); + left_count++; + } else { + right_bounds.expand(tri.get_bounds()); + right_count++; + } + } + + // Calculate SAH cost + if (left_count == 0 || right_count == 0) continue; + + float cost = left_count * left_bounds.surface_area() + + right_count * right_bounds.surface_area(); + + if (cost < best_cost) { + best_cost = cost; + axis = a; + split_pos = pos; + } + } + } + + return best_cost; +} + +AABB BVH::calculate_bounds_(uint first_prim, uint prim_count) { + AABB bounds{Vec3(std::numeric_limits::max()), + Vec3(std::numeric_limits::lowest())}; + + for (uint i = first_prim; i < first_prim + prim_count; ++i) { + Triangle& tri = triangles_[triangle_indices_[i]]; + bounds.expand(tri.get_bounds()); + } + + return bounds; +} + +AABB BVH::calculate_centroid_bounds_(uint first_prim, uint prim_count) { + AABB bounds{Vec3(std::numeric_limits::max()), + Vec3(std::numeric_limits::lowest())}; + + for (uint i = first_prim; i < first_prim + prim_count; ++i) { + Triangle& tri = triangles_[triangle_indices_[i]]; + bounds.expand(tri.get_centroid()); + } + + return bounds; +} + +bool BVH::upload_to_gpu(Buffer& node_buffer, Buffer& triangle_buffer) { + if (nodes_.empty() || triangles_.empty()) { + Logger::error("Cannot upload empty BVH to GPU"); + return false; + } + + // Reorder triangles according to BVH layout + std::vector ordered_triangles; + ordered_triangles.reserve(triangles_.size()); + + for (uint idx : triangle_indices_) { + ordered_triangles.push_back(triangles_[idx]); + } + + // Upload nodes + if (!node_buffer.create(BufferType::SHADER_STORAGE_BUFFER, + nodes_.size() * sizeof(BVHNode), + nodes_.data(), + BufferUsage::STATIC_DRAW)) { + Logger::error("Failed to upload BVH nodes to GPU"); + return false; + } + + // Upload triangles + if (!triangle_buffer.create(BufferType::SHADER_STORAGE_BUFFER, + ordered_triangles.size() * sizeof(Triangle), + ordered_triangles.data(), + BufferUsage::STATIC_DRAW)) { + Logger::error("Failed to upload BVH triangles to GPU"); + return false; + } + + Logger::info("BVH uploaded to GPU successfully"); + return true; +} + +void BVH::clear() { + nodes_.clear(); + triangles_.clear(); + triangle_indices_.clear(); +} + +} // namespace are diff --git a/src/core/gbuffer.cpp b/src/core/gbuffer.cpp new file mode 100644 index 0000000..b3e01d0 --- /dev/null +++ b/src/core/gbuffer.cpp @@ -0,0 +1,221 @@ +#include "core/gbuffer.h" +#include "utils/logger.h" +#include + +namespace are { + +GBuffer::GBuffer(uint width, uint height) + : width_(width) + , height_(height) + , fbo_(INVALID_HANDLE) + , depth_texture_(INVALID_HANDLE) + , initialized_(false) { + for (int i = 0; i < GBUFFER_COUNT; ++i) { + textures_[i] = INVALID_HANDLE; + } +} + +GBuffer::~GBuffer() { + release(); +} + +bool GBuffer::initialize() { + if (initialized_) { + Logger::warning("GBuffer already initialized"); + return true; + } + + // Create framebuffer + glGenFramebuffers(1, &fbo_); + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + + // Create G-Buffer textures + textures_[GBUFFER_POSITION] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); + textures_[GBUFFER_NORMAL] = create_texture_(GL_RGBA32F, GL_RGBA, GL_FLOAT); + textures_[GBUFFER_ALBEDO] = create_texture_(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); + + // Attach textures to framebuffer + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_POSITION, + GL_TEXTURE_2D, textures_[GBUFFER_POSITION], 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_NORMAL, + GL_TEXTURE_2D, textures_[GBUFFER_NORMAL], 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO, + GL_TEXTURE_2D, textures_[GBUFFER_ALBEDO], 0); + + // Create depth texture + glGenTextures(1, &depth_texture_); + glBindTexture(GL_TEXTURE_2D, depth_texture_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width_, height_, 0, + GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_TEXTURE_2D, depth_texture_, 0); + + // Set draw buffers + GLenum draw_buffers[GBUFFER_COUNT] = { + GL_COLOR_ATTACHMENT0 + GBUFFER_POSITION, + GL_COLOR_ATTACHMENT0 + GBUFFER_NORMAL, + GL_COLOR_ATTACHMENT0 + GBUFFER_ALBEDO + }; + glDrawBuffers(GBUFFER_COUNT, draw_buffers); + + // Check framebuffer completeness + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + Logger::error("GBuffer framebuffer is not complete"); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + initialized_ = true; + Logger::info("GBuffer initialized successfully"); + return true; +} + +void GBuffer::release() { + if (!initialized_) return; + + if (fbo_ != INVALID_HANDLE) { + glDeleteFramebuffers(1, &fbo_); + fbo_ = INVALID_HANDLE; + } + + for (int i = 0; i < GBUFFER_COUNT; ++i) { + if (textures_[i] != INVALID_HANDLE) { + glDeleteTextures(1, &textures_[i]); + textures_[i] = INVALID_HANDLE; + } + } + + if (depth_texture_ != INVALID_HANDLE) { + glDeleteTextures(1, &depth_texture_); + depth_texture_ = INVALID_HANDLE; + } + + initialized_ = false; + Logger::info("GBuffer released"); +} + +void GBuffer::render(const Scene& scene, const Shader& shader) { + if (!initialized_) { + Logger::error("GBuffer not initialized"); + return; + } + + // Bind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + glViewport(0, 0, width_, height_); + + // Clear buffers + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable depth test + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + // Use shader + shader.use(); + + // Set camera matrices + const Camera& camera = scene.get_camera(); + Mat4 view = camera.get_view_matrix(); + Mat4 projection = camera.get_projection_matrix(); + + shader.set_mat4("u_view", view); + shader.set_mat4("u_projection", projection); + + // Render all meshes + const auto& meshes = scene.get_meshes(); + const auto& materials = scene.get_materials(); + + for (const auto& mesh : meshes) { + if (!mesh->is_uploaded()) { + Logger::warning("Mesh not uploaded to GPU, skipping"); + continue; + } + + // Set model matrix + Mat4 model = mesh->get_transform(); + shader.set_mat4("u_model", model); + + // Set material properties + uint material_id = mesh->get_material(); + if (material_id < materials.size()) { + const auto& material = materials[material_id]; + + shader.set_vec3("u_albedo", material->get_albedo()); + shader.set_float("u_metallic", material->get_metallic()); + shader.set_float("u_roughness", material->get_roughness()); + shader.set_uint("u_material_id", material_id); + + // Bind textures + auto albedo_tex = material->get_albedo_texture(); + if (albedo_tex && albedo_tex->is_valid()) { + albedo_tex->bind(0); + shader.set_int("u_albedo_map", 0); + shader.set_int("u_has_albedo_map", 1); + } else { + shader.set_int("u_has_albedo_map", 0); + } + + auto normal_tex = material->get_normal_texture(); + if (normal_tex && normal_tex->is_valid()) { + normal_tex->bind(1); + shader.set_int("u_normal_map", 1); + shader.set_int("u_has_normal_map", 1); + } else { + shader.set_int("u_has_normal_map", 0); + } + } + + // Draw mesh + glBindVertexArray(mesh->get_vao()); + glDrawElements(GL_TRIANGLES, mesh->get_indices().size(), GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + } + + // Unbind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void GBuffer::resize(uint width, uint height) { + if (width == width_ && height == height_) return; + + width_ = width; + height_ = height; + + if (initialized_) { + release(); + initialize(); + } +} + +TextureHandle GBuffer::get_texture(int index) const { + if (index < 0 || index >= GBUFFER_COUNT) { + Logger::error("Invalid G-Buffer texture index"); + return INVALID_HANDLE; + } + return textures_[index]; +} + +void GBuffer::get_dimensions(uint& width, uint& height) const { + width = width_; + height = height_; +} + +TextureHandle GBuffer::create_texture_(uint internal_format, uint format, uint type) { + TextureHandle texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width_, height_, 0, format, type, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + return texture; +} + +} // namespace are diff --git a/src/core/raytracer.cpp b/src/core/raytracer.cpp new file mode 100644 index 0000000..a425b58 --- /dev/null +++ b/src/core/raytracer.cpp @@ -0,0 +1,316 @@ +#include "core/raytracer.h" +#include "utils/logger.h" +#include "basic/constants.h" +#include + +namespace are { + +RayTracer::RayTracer(uint width, uint height, const RayTracerConfig& config) + : width_(width) + , height_(height) + , config_(config) + , accumulation_texture_(INVALID_HANDLE) + , scene_buffer_(INVALID_HANDLE) + , material_buffer_(INVALID_HANDLE) + , light_buffer_(INVALID_HANDLE) + , bvh_(nullptr) + , bvh_built_(false) + , frame_count_(0) + , initialized_(false) { +} + +RayTracer::~RayTracer() { + release(); +} + +bool RayTracer::initialize() { + if (initialized_) { + Logger::warning("RayTracer already initialized"); + return true; + } + + // Create accumulation texture + glGenTextures(1, &accumulation_texture_); + glBindTexture(GL_TEXTURE_2D, accumulation_texture_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width_, height_, 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Create shader storage buffers + glGenBuffers(1, &material_buffer_); + glGenBuffers(1, &light_buffer_); + + // Load compute shader + Logger::info("Loading ray tracing compute shader in RayTracer..."); + if (!compute_shader_.load_compute("shaders/raytracing.comp")) { + Logger::error("Failed to load ray tracing compute shader in RayTracer"); + return false; + } + Logger::info("Ray tracing compute shader loaded in RayTracer"); + + // Initialize BVH if enabled + if (config_.use_bvh_) { + bvh_ = std::make_unique(); + } + + initialized_ = true; + Logger::info("RayTracer initialized successfully"); + return true; +} + +void RayTracer::release() { + if (!initialized_) return; + + if (accumulation_texture_ != INVALID_HANDLE) { + glDeleteTextures(1, &accumulation_texture_); + accumulation_texture_ = INVALID_HANDLE; + } + + if (material_buffer_ != INVALID_HANDLE) { + glDeleteBuffers(1, &material_buffer_); + material_buffer_ = INVALID_HANDLE; + } + + if (light_buffer_ != INVALID_HANDLE) { + glDeleteBuffers(1, &light_buffer_); + light_buffer_ = INVALID_HANDLE; + } + + bvh_node_buffer_.release(); + bvh_triangle_buffer_.release(); + + compute_shader_.release(); + + bvh_.reset(); + bvh_built_ = false; + + initialized_ = false; + Logger::info("RayTracer released"); +} + +bool RayTracer::rebuild_bvh(const Scene& scene) { + if (!config_.use_bvh_) { + Logger::warning("BVH is disabled in configuration"); + return false; + } + + if (!bvh_) { + bvh_ = std::make_unique(); + } + + Logger::info("Building BVH for ray tracing..."); + + if (!bvh_->build(scene.get_meshes())) { + Logger::error("Failed to build BVH"); + return false; + } + + if (!bvh_->upload_to_gpu(bvh_node_buffer_, bvh_triangle_buffer_)) { + Logger::error("Failed to upload BVH to GPU"); + return false; + } + + bvh_built_ = true; + Logger::info("BVH built and uploaded successfully"); + return true; +} + +void RayTracer::trace(const Scene& scene, const GBuffer& gbuffer, TextureHandle output_texture) { + if (!initialized_) { + Logger::error("RayTracer not initialized"); + return; + } + + if (!compute_shader_.is_valid()) { + Logger::error("Ray tracing compute shader not loaded"); + return; + } + + // Build BVH if enabled and not built yet + if (config_.use_bvh_ && !bvh_built_) { + rebuild_bvh(scene); + } + + // Upload scene data + upload_scene_data_(scene); + + // Use compute shader + compute_shader_.use(); + + // Bind G-Buffer textures + bind_gbuffer_(gbuffer); + + // Bind output and accumulation textures + glBindImageTexture(3, output_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); + glBindImageTexture(4, accumulation_texture_, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F); + + // Bind BVH buffers if enabled + if (config_.use_bvh_ && bvh_built_) { + bvh_node_buffer_.bind_base(2); + bvh_triangle_buffer_.bind_base(3); + compute_shader_.set_bool("u_use_bvh", true); + compute_shader_.set_uint("u_bvh_node_count", bvh_->get_node_count()); + } else { + compute_shader_.set_bool("u_use_bvh", false); + } + + // Set uniforms + compute_shader_.set_uint("u_frame_count", frame_count_); + compute_shader_.set_uint("u_samples_per_pixel", config_.samples_per_pixel_); + compute_shader_.set_uint("u_max_depth", config_.max_depth_); + compute_shader_.set_uint("u_light_count", static_cast(scene.get_lights().size())); + compute_shader_.set_bool("u_enable_accumulation", config_.enable_accumulation_); + + // Set camera data + const Camera& camera = scene.get_camera(); + compute_shader_.set_vec3("u_camera_position", camera.get_position()); + + Mat4 inv_vp = glm::inverse(camera.get_view_projection_matrix()); + compute_shader_.set_mat4("u_inv_view_projection", inv_vp); + + // Dispatch compute shader + uint num_groups_x = (width_ + COMPUTE_GROUP_SIZE_X - 1) / COMPUTE_GROUP_SIZE_X; + uint num_groups_y = (height_ + COMPUTE_GROUP_SIZE_Y - 1) / COMPUTE_GROUP_SIZE_Y; + + glDispatchCompute(num_groups_x, num_groups_y, 1); + + // Memory barrier + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + // Increment frame count for accumulation + if (config_.enable_accumulation_) { + frame_count_++; + } +} + +void RayTracer::resize(uint width, uint height) { + if (width == width_ && height == height_) return; + + width_ = width; + height_ = height; + + if (initialized_) { + // Recreate accumulation texture + if (accumulation_texture_ != INVALID_HANDLE) { + glDeleteTextures(1, &accumulation_texture_); + } + + glGenTextures(1, &accumulation_texture_); + glBindTexture(GL_TEXTURE_2D, accumulation_texture_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width_, height_, 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + reset_accumulation(); + } +} + +void RayTracer::reset_accumulation() { + frame_count_ = 0; +} + +void RayTracer::set_config(const RayTracerConfig& config) { + bool bvh_changed = (config.use_bvh_ != config_.use_bvh_); + + config_ = config; + reset_accumulation(); + + if (bvh_changed) { + if (config_.use_bvh_ && !bvh_) { + bvh_ = std::make_unique(); + bvh_built_ = false; + } else if (!config_.use_bvh_) { + bvh_.reset(); + bvh_built_ = false; + } + } +} + +void RayTracer::upload_scene_data_(const Scene& scene) { + // Upload materials + const auto& materials = scene.get_materials(); + if (!materials.empty()) { + struct MaterialData { + Vec3 albedo; + float metallic; + Vec3 emission; + float roughness; + int type; + float ior; + Vec2 padding; + }; + + std::vector material_data; + material_data.reserve(materials.size()); + + for (const auto& mat : materials) { + MaterialData data; + data.albedo = mat->get_albedo(); + data.metallic = mat->get_metallic(); + data.emission = mat->get_emission(); + data.roughness = mat->get_roughness(); + data.type = static_cast(mat->get_type()); + data.ior = mat->get_ior(); + material_data.push_back(data); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, material_buffer_); + glBufferData(GL_SHADER_STORAGE_BUFFER, + material_data.size() * sizeof(MaterialData), + material_data.data(), GL_DYNAMIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, material_buffer_); + } + + // Upload lights + const auto& lights = scene.get_lights(); + if (!lights.empty()) { + struct LightData { + Vec3 position; + int type; + Vec3 direction; + float intensity; + Vec3 color; + float range; + Vec2 spot_angles; + Vec2 padding; + }; + + std::vector light_data; + light_data.reserve(lights.size()); + + for (const auto& light : lights) { + LightData data; + data.position = light->get_position(); + data.type = static_cast(light->get_type()); + data.direction = light->get_direction(); + data.intensity = light->get_intensity(); + data.color = light->get_color(); + data.range = light->get_range(); + data.spot_angles = Vec2(light->get_inner_angle(), light->get_outer_angle()); + light_data.push_back(data); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, light_buffer_); + glBufferData(GL_SHADER_STORAGE_BUFFER, + light_data.size() * sizeof(LightData), + light_data.data(), GL_DYNAMIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, light_buffer_); + } +} + +void RayTracer::bind_gbuffer_(const GBuffer& gbuffer) { + glBindImageTexture(0, gbuffer.get_texture(GBUFFER_POSITION), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + glBindImageTexture(1, gbuffer.get_texture(GBUFFER_NORMAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + glBindImageTexture(2, gbuffer.get_texture(GBUFFER_ALBEDO), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); +} + +void RayTracer::set_compute_shader(const Shader& shader) { + compute_shader_ = shader; + Logger::info("Compute shader set for RayTracer"); +} + +} // namespace are diff --git a/src/core/renderer.cpp b/src/core/renderer.cpp new file mode 100644 index 0000000..056c6d3 --- /dev/null +++ b/src/core/renderer.cpp @@ -0,0 +1,205 @@ +#include "core/renderer.h" +#include "utils/logger.h" +#include +#include + +namespace are { + +Renderer::Renderer(const RendererConfig &config) + : config_(config) + , initialized_(false) + , frame_count_(0) { +} + +Renderer::~Renderer() { + shutdown(); +} + +bool Renderer::initialize() { + if (initialized_) { + Logger::warning("Renderer already initialized"); + return true; + } + + Logger::info("Initializing Aurora Rendering Engine..."); + + // Initialize shader manager + shader_manager_ = std::make_unique(); + if (!shader_manager_->initialize()) { + Logger::error("Failed to initialize shader manager"); + return false; + } + + // Initialize G-Buffer + gbuffer_ = std::make_unique(config_.width_, config_.height_); + if (!gbuffer_->initialize()) { + Logger::error("Failed to initialize G-Buffer"); + return false; + } + + // Initialize ray tracer + RayTracerConfig rt_config; + rt_config.samples_per_pixel_ = config_.samples_per_pixel_; + rt_config.max_depth_ = config_.max_ray_depth_; + rt_config.enable_shadows_ = true; + rt_config.enable_reflections_ = true; + rt_config.enable_accumulation_ = config_.enable_accumulation_; + rt_config.use_bvh_ = true; + + raytracer_ = std::make_unique(config_.width_, config_.height_, rt_config); + if (!raytracer_->initialize()) { + Logger::error("Failed to initialize ray tracer"); + return false; + } + + // Pass compute shader to ray tracer + const Shader& rt_shader = shader_manager_->get_raytracing_shader(); + if (!rt_shader.is_valid()) { + Logger::error("Ray tracing shader is invalid"); + return false; + } + raytracer_->set_compute_shader(rt_shader); + + // Initialize screen blit + screen_blit_ = std::make_unique(); + if (!screen_blit_->initialize()) { + Logger::error("Failed to initialize screen blit"); + return false; + } + + initialized_ = true; + Logger::info("Aurora Rendering Engine initialized successfully"); + return true; +} + +void Renderer::shutdown() { + if (!initialized_) + return; + + Logger::info("Shutting down Aurora Rendering Engine..."); + + if (screen_blit_) { + screen_blit_->release(); + screen_blit_.reset(); + } + + if (raytracer_) { + raytracer_->release(); + raytracer_.reset(); + } + + if (gbuffer_) { + gbuffer_->release(); + gbuffer_.reset(); + } + + if (shader_manager_) { + shader_manager_->release(); + shader_manager_.reset(); + } + + initialized_ = false; + Logger::info("Aurora Rendering Engine shut down"); +} + +RenderStats Renderer::render(const Scene& scene, TextureHandle output_texture) { + RenderStats stats = {}; + + if (!initialized_) { + Logger::error("Renderer not initialized"); + return stats; + } + + // Start timing + auto start_time = std::chrono::high_resolution_clock::now(); + + // Phase 1: G-Buffer pass + auto gbuffer_start = std::chrono::high_resolution_clock::now(); + + const Shader& gbuffer_shader = shader_manager_->get_gbuffer_shader(); + gbuffer_->render(scene, gbuffer_shader); + + auto gbuffer_end = std::chrono::high_resolution_clock::now(); + stats.gbuffer_time_ms_ = std::chrono::duration(gbuffer_end - gbuffer_start).count(); + + // Phase 2: Ray tracing pass + auto raytrace_start = std::chrono::high_resolution_clock::now(); + + // Create output texture if not provided + TextureHandle rt_output = output_texture; + bool created_temp_texture = false; + + if (rt_output == 0) { + glGenTextures(1, &rt_output); + glBindTexture(GL_TEXTURE_2D, rt_output); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, config_.width_, config_.height_, + 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + created_temp_texture = true; + } + + raytracer_->trace(scene, *gbuffer_, rt_output); + + auto raytrace_end = std::chrono::high_resolution_clock::now(); + stats.raytrace_time_ms_ = std::chrono::duration(raytrace_end - raytrace_start).count(); + + // Phase 3: Blit to screen if output is default framebuffer + if (created_temp_texture && output_texture == 0) { + screen_blit_->blit_fullscreen(rt_output); + glDeleteTextures(1, &rt_output); + } + + // Calculate total frame time + auto end_time = std::chrono::high_resolution_clock::now(); + stats.frame_time_ms_ = std::chrono::duration(end_time - start_time).count(); + + // Count triangles + const auto& meshes = scene.get_meshes(); + for (const auto& mesh : meshes) { + stats.triangle_count_ += mesh->get_indices().size() / 3; + } + + // Estimate ray count (very rough) + stats.ray_count_ = config_.width_ * config_.height_ * config_.samples_per_pixel_ * config_.max_ray_depth_; + + frame_count_++; + + return stats; +} + +void Renderer::resize(uint width, uint height) { + if (width == config_.width_ && height == config_.height_) + return; + + config_.width_ = width; + config_.height_ = height; + + if (initialized_) { + gbuffer_->resize(width, height); + raytracer_->resize(width, height); + + Logger::info("Renderer resized to " + std::to_string(width) + "x" + std::to_string(height)); + } +} + +void Renderer::set_config(const RendererConfig &config) { + bool size_changed = (config.width_ != config_.width_ || config.height_ != config_.height_); + + config_ = config; + + if (initialized_) { + if (size_changed) { + resize(config_.width_, config_.height_); + } + + // Update ray tracer config + RayTracerConfig rt_config = raytracer_->get_config(); + rt_config.samples_per_pixel_ = config_.samples_per_pixel_; + rt_config.max_depth_ = config_.max_ray_depth_; + rt_config.enable_accumulation_ = config_.enable_accumulation_; + raytracer_->set_config(rt_config); + } +} + +} // namespace are diff --git a/src/core/screen_blit.cpp b/src/core/screen_blit.cpp new file mode 100644 index 0000000..011326f --- /dev/null +++ b/src/core/screen_blit.cpp @@ -0,0 +1,148 @@ +#include "core/screen_blit.h" +#include "utils/logger.h" +#include + +namespace are { + +namespace { + const char* VERTEX_SHADER_SOURCE = R"( + #version 430 core + layout(location = 0) in vec2 a_position; + layout(location = 1) in vec2 a_texcoord; + + out vec2 v_texcoord; + + void main() { + v_texcoord = a_texcoord; + gl_Position = vec4(a_position, 0.0, 1.0); + } + )"; + + const char* FRAGMENT_SHADER_SOURCE = R"( + #version 430 core + in vec2 v_texcoord; + out vec4 frag_color; + + uniform sampler2D u_texture; + + void main() { + frag_color = texture(u_texture, v_texcoord); + } + )"; +} + +ScreenBlit::ScreenBlit() + : vao_(0) + , vbo_(0) + , initialized_(false) { +} + +ScreenBlit::~ScreenBlit() { + release(); +} + +bool ScreenBlit::initialize() { + if (initialized_) { + Logger::warning("ScreenBlit already initialized"); + return true; + } + + // Compile shader + if (!shader_.compile(VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)) { + Logger::error("Failed to compile screen blit shader"); + return false; + } + + // Create fullscreen quad + create_quad_(); + + initialized_ = true; + Logger::info("ScreenBlit initialized successfully"); + return true; +} + +void ScreenBlit::release() { + if (!initialized_) return; + + shader_.release(); + + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } + + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } + + initialized_ = false; +} + +void ScreenBlit::blit(TextureHandle texture, int x, int y, uint width, uint height) { + if (!initialized_) { + Logger::error("ScreenBlit not initialized"); + return; + } + + // Set viewport + glViewport(x, y, width, height); + + // Disable depth test + glDisable(GL_DEPTH_TEST); + + // Use shader + shader_.use(); + shader_.set_int("u_texture", 0); + + // Bind texture + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + + // Draw quad + glBindVertexArray(vao_); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); + + // Re-enable depth test + glEnable(GL_DEPTH_TEST); +} + +void ScreenBlit::blit_fullscreen(TextureHandle texture) { + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + blit(texture, viewport[0], viewport[1], viewport[2], viewport[3]); +} + +void ScreenBlit::create_quad_() { + // Fullscreen quad vertices (position + texcoord) + float vertices[] = { + // Position // TexCoord + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f + }; + + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); + + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + // Position attribute + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); + + // TexCoord attribute + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); + + glBindVertexArray(0); +} + +} // namespace are diff --git a/src/core/shader_manager.cpp b/src/core/shader_manager.cpp new file mode 100644 index 0000000..15c41a9 --- /dev/null +++ b/src/core/shader_manager.cpp @@ -0,0 +1,127 @@ +#include "core/shader_manager.h" +#include "utils/logger.h" + +namespace are { + +ShaderManager::ShaderManager() + : initialized_(false) { +} + +ShaderManager::~ShaderManager() { + release(); +} + +bool ShaderManager::initialize() { + if (initialized_) { + Logger::warning("ShaderManager already initialized"); + return true; + } + + Logger::info("Loading built-in shaders..."); + + if (!load_builtin_shaders_()) { + Logger::error("Failed to load built-in shaders"); + return false; + } + + initialized_ = true; + Logger::info("ShaderManager initialized successfully"); + return true; +} + +void ShaderManager::release() { + if (!initialized_) return; + + gbuffer_shader_.release(); + raytracing_shader_.release(); + + for (auto& pair : shader_cache_) { + pair.second.release(); + } + shader_cache_.clear(); + + initialized_ = false; + Logger::info("ShaderManager released"); +} + +Shader ShaderManager::load_shader(const std::string& name, + const std::string& vertex_path, + const std::string& fragment_path) { + // Check cache + auto it = shader_cache_.find(name); + if (it != shader_cache_.end()) { + Logger::info("Shader '" + name + "' loaded from cache"); + return it->second; + } + + // Load shader + Shader shader; + if (!shader.load(vertex_path, fragment_path)) { + Logger::error("Failed to load shader '" + name + "'"); + return Shader(); + } + + shader_cache_[name] = shader; + Logger::info("Shader '" + name + "' loaded successfully"); + return shader; +} + +Shader ShaderManager::load_compute_shader(const std::string& name, + const std::string& compute_path) { + // Check cache + auto it = shader_cache_.find(name); + if (it != shader_cache_.end()) { + Logger::info("Compute shader '" + name + "' loaded from cache"); + return it->second; + } + + // Load shader + Shader shader; + if (!shader.load_compute(compute_path)) { + Logger::error("Failed to load compute shader '" + name + "'"); + return Shader(); + } + + shader_cache_[name] = shader; + Logger::info("Compute shader '" + name + "' loaded successfully"); + return shader; +} + +Shader ShaderManager::get_shader(const std::string& name) const { + auto it = shader_cache_.find(name); + if (it != shader_cache_.end()) { + return it->second; + } + + Logger::warning("Shader '" + name + "' not found in cache"); + return Shader(); +} + +bool ShaderManager::load_builtin_shaders_() { + // Load G-Buffer shader + if (!gbuffer_shader_.load("shaders/gbuffer.vert", "shaders/gbuffer.frag")) { + Logger::error("Failed to load G-Buffer shader"); + return false; + } + shader_cache_["gbuffer"] = gbuffer_shader_; + + // Load ray tracing compute shader + if (!raytracing_shader_.load_compute("shaders/raytracing.comp")) { + Logger::error("Failed to load ray tracing shader"); + return false; + } + shader_cache_["raytracing"] = raytracing_shader_; + + // Load ray tracing compute shader + Logger::info("Loading ray tracing compute shader..."); + if (!raytracing_shader_.load_compute("shaders/raytracing.comp")) { + Logger::error("Failed to load ray tracing shader"); + return false; + } + shader_cache_["raytracing"] = raytracing_shader_; + Logger::info("Ray tracing shader loaded successfully"); + + return true; +} + +} // namespace are diff --git a/src/resource/buffer.cpp b/src/resource/buffer.cpp new file mode 100644 index 0000000..3172908 --- /dev/null +++ b/src/resource/buffer.cpp @@ -0,0 +1,114 @@ +#include "resource/buffer.h" +#include "utils/logger.h" +#include + +namespace are { + +namespace { + GLenum get_gl_buffer_type(BufferType type) { + switch (type) { + case BufferType::VERTEX_BUFFER: return GL_ARRAY_BUFFER; + case BufferType::INDEX_BUFFER: return GL_ELEMENT_ARRAY_BUFFER; + case BufferType::UNIFORM_BUFFER: return GL_UNIFORM_BUFFER; + case BufferType::SHADER_STORAGE_BUFFER: return GL_SHADER_STORAGE_BUFFER; + default: return GL_ARRAY_BUFFER; + } + } + + GLenum get_gl_usage(BufferUsage usage) { + switch (usage) { + case BufferUsage::STATIC_DRAW: return GL_STATIC_DRAW; + case BufferUsage::DYNAMIC_DRAW: return GL_DYNAMIC_DRAW; + case BufferUsage::STREAM_DRAW: return GL_STREAM_DRAW; + default: return GL_STATIC_DRAW; + } + } +} + +Buffer::Buffer() + : handle_(INVALID_HANDLE) + , type_(BufferType::VERTEX_BUFFER) + , size_(0) + , usage_(BufferUsage::STATIC_DRAW) { +} + +Buffer::~Buffer() { + // Don't auto-release, let user control lifetime +} + +bool Buffer::create(BufferType type, size_t size, const void* data, BufferUsage usage) { + if (handle_ != INVALID_HANDLE) { + Logger::warning("Buffer already created, releasing old buffer"); + release(); + } + + type_ = type; + size_ = size; + usage_ = usage; + + glGenBuffers(1, &handle_); + + GLenum gl_type = get_gl_buffer_type(type); + GLenum gl_usage = get_gl_usage(usage); + + glBindBuffer(gl_type, handle_); + glBufferData(gl_type, size, data, gl_usage); + glBindBuffer(gl_type, 0); + + Logger::info("Buffer created successfully"); + return true; +} + +void Buffer::update(size_t offset, size_t size, const void* data) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot update invalid buffer"); + return; + } + + if (offset + size > size_) { + Logger::error("Buffer update out of bounds"); + return; + } + + GLenum gl_type = get_gl_buffer_type(type_); + + glBindBuffer(gl_type, handle_); + glBufferSubData(gl_type, offset, size, data); + glBindBuffer(gl_type, 0); +} + +void Buffer::bind() const { + if (handle_ == INVALID_HANDLE) { + Logger::warning("Attempting to bind invalid buffer"); + return; + } + + GLenum gl_type = get_gl_buffer_type(type_); + glBindBuffer(gl_type, handle_); +} + +void Buffer::bind_base(uint binding_point) const { + if (handle_ == INVALID_HANDLE) { + Logger::warning("Attempting to bind invalid buffer"); + return; + } + + GLenum gl_type = get_gl_buffer_type(type_); + glBindBufferBase(gl_type, binding_point, handle_); +} + +void Buffer::unbind() const { + GLenum gl_type = get_gl_buffer_type(type_); + glBindBuffer(gl_type, 0); +} + +void Buffer::release() { + if (handle_ != INVALID_HANDLE) { + glDeleteBuffers(1, &handle_); + handle_ = INVALID_HANDLE; + } + + size_ = 0; +} + +} // namespace are diff --git a/src/resource/model_loader.cpp b/src/resource/model_loader.cpp new file mode 100644 index 0000000..b6c0424 --- /dev/null +++ b/src/resource/model_loader.cpp @@ -0,0 +1,187 @@ +#include "resource/model_loader.h" +#include "utils/logger.h" +#include "resource/texture.h" + +// Note: This is a simplified implementation without Assimp +// For full implementation, include Assimp and implement properly + +namespace are { + +bool ModelLoader::load(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs) { + Logger::error("ModelLoader requires Assimp library (not implemented in this version)"); + Logger::info("To implement: include , , "); + + // Placeholder implementation + // TODO: Implement with Assimp + /* + Assimp::Importer importer; + const aiScene* scene = importer.ReadFile(path, + aiProcess_Triangulate | + aiProcess_GenNormals | + aiProcess_CalcTangentSpace | + (flip_uvs ? aiProcess_FlipUVs : 0)); + + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + Logger::error("Failed to load model: " + std::string(importer.GetErrorString())); + return false; + } + + std::string directory = path.substr(0, path.find_last_of('/')); + process_node_(scene->mRootNode, scene, meshes, materials, directory); + + Logger::info("Model loaded: " + path); + return true; + */ + + return false; +} + +bool ModelLoader::load_and_upload(const std::string& path, + std::vector>& meshes, + std::vector>& materials, + bool flip_uvs) { + if (!load(path, meshes, materials, flip_uvs)) { + return false; + } + + // Upload all meshes to GPU + for (auto& mesh : meshes) { + if (!mesh->upload_to_gpu()) { + Logger::error("Failed to upload mesh to GPU"); + return false; + } + } + + return true; +} + +void ModelLoader::process_node_(void* node, void* scene, + std::vector>& meshes, + std::vector>& materials, + const std::string& directory) { + // TODO: Implement with Assimp + /* + aiNode* ai_node = static_cast(node); + const aiScene* ai_scene = static_cast(scene); + + // Process all meshes in this node + for (uint i = 0; i < ai_node->mNumMeshes; ++i) { + aiMesh* ai_mesh = ai_scene->mMeshes[ai_node->mMeshes[i]]; + meshes.push_back(process_mesh_(ai_mesh, ai_scene, materials, directory)); + } + + // Process children recursively + for (uint i = 0; i < ai_node->mNumChildren; ++i) { + process_node_(ai_node->mChildren[i], ai_scene, meshes, materials, directory); + } + */ +} + +std::shared_ptr ModelLoader::process_mesh_(void* mesh, void* scene, + std::vector>& materials, + const std::string& directory) { + // TODO: Implement with Assimp + /* + aiMesh* ai_mesh = static_cast(mesh); + const aiScene* ai_scene = static_cast(scene); + + std::vector vertices; + std::vector indices; + + // Process vertices + for (uint i = 0; i < ai_mesh->mNumVertices; ++i) { + Vertex vertex; + + vertex.position_ = Vec3(ai_mesh->mVertices[i].x, + ai_mesh->mVertices[i].y, + ai_mesh->mVertices[i].z); + + if (ai_mesh->HasNormals()) { + vertex.normal_ = Vec3(ai_mesh->mNormals[i].x, + ai_mesh->mNormals[i].y, + ai_mesh->mNormals[i].z); + } + + if (ai_mesh->mTextureCoords[0]) { + vertex.texcoord_ = Vec2(ai_mesh->mTextureCoords[0][i].x, + ai_mesh->mTextureCoords[0][i].y); + } + + if (ai_mesh->HasTangentsAndBitangents()) { + vertex.tangent_ = Vec3(ai_mesh->mTangents[i].x, + ai_mesh->mTangents[i].y, + ai_mesh->mTangents[i].z); + } + + vertices.push_back(vertex); + } + + // Process indices + for (uint i = 0; i < ai_mesh->mNumFaces; ++i) { + aiFace face = ai_mesh->mFaces[i]; + for (uint j = 0; j < face.mNumIndices; ++j) { + indices.push_back(face.mIndices[j]); + } + } + + // Process material + uint material_id = materials.size(); + if (ai_mesh->mMaterialIndex >= 0) { + aiMaterial* ai_material = ai_scene->mMaterials[ai_mesh->mMaterialIndex]; + + auto material = std::make_shared(); + + // Load diffuse color + aiColor3D color; + if (ai_material->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) { + material->set_albedo(Vec3(color.r, color.g, color.b)); + } + + // Load textures + load_material_textures_(ai_material, aiTextureType_DIFFUSE, material, directory); + load_material_textures_(ai_material, aiTextureType_NORMALS, material, directory); + + materials.push_back(material); + } + + auto mesh_obj = std::make_shared(); + mesh_obj->set_vertices(vertices); + mesh_obj->set_indices(indices); + mesh_obj->set_material(material_id); + + return mesh_obj; + */ + + return nullptr; +} + +void ModelLoader::load_material_textures_(void* material, int type, + std::shared_ptr& mat, + const std::string& directory) { + // TODO: Implement with Assimp + /* + aiMaterial* ai_material = static_cast(material); + aiTextureType ai_type = static_cast(type); + + for (uint i = 0; i < ai_material->GetTextureCount(ai_type); ++i) { + aiString str; + ai_material->GetTexture(ai_type, i, &str); + + std::string filename = directory + "/" + std::string(str.C_Str()); + + auto texture = std::make_shared(); + if (texture->load_from_file(filename)) { + if (ai_type == aiTextureType_DIFFUSE) { + mat->set_albedo_texture(texture); + } else if (ai_type == aiTextureType_NORMALS) { + mat->set_normal_texture(texture); + } + } + } + */ +} + +} // namespace are diff --git a/src/resource/shader.cpp b/src/resource/shader.cpp new file mode 100644 index 0000000..73aa0f5 --- /dev/null +++ b/src/resource/shader.cpp @@ -0,0 +1,197 @@ +#include "resource/shader.h" +#include "utils/logger.h" +#include "basic/math.h" // 修改为math.h +#include +#include +#include + +namespace are { + +Shader::Shader() + : handle_(INVALID_HANDLE) { +} + +Shader::~Shader() { + // Don't auto-release, let user control lifetime +} + +bool Shader::load(const std::string& vertex_path, const std::string& fragment_path) { + std::string vertex_source = read_file_(vertex_path); + std::string fragment_source = read_file_(fragment_path); + + if (vertex_source.empty() || fragment_source.empty()) { + Logger::error("Failed to read shader files"); + return false; + } + + return compile(vertex_source, fragment_source); +} + +bool Shader::load_compute(const std::string& compute_path) { + std::string compute_source = read_file_(compute_path); + + if (compute_source.empty()) { + Logger::error("Failed to read compute shader file"); + return false; + } + + return compile_compute(compute_source); +} + +bool Shader::compile(const std::string& vertex_source, const std::string& fragment_source) { + uint vertex_shader = compile_shader_(vertex_source, GL_VERTEX_SHADER); + if (vertex_shader == 0) return false; + + uint fragment_shader = compile_shader_(fragment_source, GL_FRAGMENT_SHADER); + if (fragment_shader == 0) { + glDeleteShader(vertex_shader); + return false; + } + + uint shaders[] = { vertex_shader, fragment_shader }; + bool success = link_program_(shaders, 2); + + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + return success; +} + +bool Shader::compile_compute(const std::string& compute_source) { + uint compute_shader = compile_shader_(compute_source, GL_COMPUTE_SHADER); + if (compute_shader == 0) return false; + + uint shaders[] = { compute_shader }; + bool success = link_program_(shaders, 1); + + glDeleteShader(compute_shader); + + return success; +} + +void Shader::use() const { // 改为const + if (handle_ != INVALID_HANDLE) { + glUseProgram(handle_); + } +} + +void Shader::release() { + if (handle_ != INVALID_HANDLE) { + glDeleteProgram(handle_); + handle_ = INVALID_HANDLE; + } + uniform_cache_.clear(); +} + +void Shader::set_bool(const std::string& name, bool value) const { // 新增 + glUniform1i(get_uniform_location_(name), static_cast(value)); +} + +void Shader::set_int(const std::string& name, int value) const { // 改为const + glUniform1i(get_uniform_location_(name), value); +} + +void Shader::set_uint(const std::string& name, uint value) const { // 改为const + glUniform1ui(get_uniform_location_(name), value); +} + +void Shader::set_float(const std::string& name, float value) const { // 改为const + glUniform1f(get_uniform_location_(name), value); +} + +void Shader::set_vec2(const std::string& name, const Vec2& value) const { // 改为const + glUniform2fv(get_uniform_location_(name), 1, &value[0]); +} + +void Shader::set_vec3(const std::string& name, const Vec3& value) const { // 改为const + glUniform3fv(get_uniform_location_(name), 1, &value[0]); +} + +void Shader::set_vec4(const std::string& name, const Vec4& value) const { // 改为const + glUniform4fv(get_uniform_location_(name), 1, &value[0]); +} + +void Shader::set_mat3(const std::string& name, const Mat3& value) const { // 改为const + glUniformMatrix3fv(get_uniform_location_(name), 1, GL_FALSE, &value[0][0]); +} + +void Shader::set_mat4(const std::string& name, const Mat4& value) const { // 改为const + glUniformMatrix4fv(get_uniform_location_(name), 1, GL_FALSE, MathUtils::value_ptr(value)); +} + +int Shader::get_uniform_location_(const std::string& name) const { // 改为const + auto it = uniform_cache_.find(name); + if (it != uniform_cache_.end()) { + return it->second; + } + + int location = glGetUniformLocation(handle_, name.c_str()); + uniform_cache_[name] = location; // mutable允许修改 + + if (location == -1) { + Logger::warning("Uniform '" + name + "' not found in shader"); + } + + return location; +} + +uint Shader::compile_shader_(const std::string& source, uint type) { + uint shader = glCreateShader(type); + const char* source_cstr = source.c_str(); + glShaderSource(shader, 1, &source_cstr, nullptr); + glCompileShader(shader); + + int success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { + char info_log[512]; + glGetShaderInfoLog(shader, 512, nullptr, info_log); + + std::string type_str = (type == GL_VERTEX_SHADER) ? "VERTEX" : + (type == GL_FRAGMENT_SHADER) ? "FRAGMENT" : "COMPUTE"; + Logger::error("Shader compilation failed (" + type_str + "): " + std::string(info_log)); + + glDeleteShader(shader); + return 0; + } + + return shader; +} + +bool Shader::link_program_(const uint* shaders, uint count) { + handle_ = glCreateProgram(); + + for (uint i = 0; i < count; ++i) { + glAttachShader(handle_, shaders[i]); + } + + glLinkProgram(handle_); + + int success; + glGetProgramiv(handle_, GL_LINK_STATUS, &success); + if (!success) { + char info_log[512]; + glGetProgramInfoLog(handle_, 512, nullptr, info_log); + Logger::error("Shader linking failed: " + std::string(info_log)); + + glDeleteProgram(handle_); + handle_ = INVALID_HANDLE; + return false; + } + + return true; +} + +std::string Shader::read_file_(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) { + Logger::error("Failed to open file: " + path); + return ""; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +} // namespace are diff --git a/src/resource/texture.cpp b/src/resource/texture.cpp new file mode 100644 index 0000000..521e955 --- /dev/null +++ b/src/resource/texture.cpp @@ -0,0 +1,274 @@ +#include "resource/texture.h" +#include "utils/logger.h" +#include +#include + +namespace are { + +namespace { + GLenum get_gl_internal_format(TextureFormat format) { + switch (format) { + case TextureFormat::R8: return GL_R8; + case TextureFormat::RG8: return GL_RG8; + case TextureFormat::RGB8: return GL_RGB8; + case TextureFormat::RGBA8: return GL_RGBA8; + case TextureFormat::R16F: return GL_R16F; + case TextureFormat::RG16F: return GL_RG16F; + case TextureFormat::RGB16F: return GL_RGB16F; + case TextureFormat::RGBA16F: return GL_RGBA16F; + case TextureFormat::R32F: return GL_R32F; + case TextureFormat::RG32F: return GL_RG32F; + case TextureFormat::RGB32F: return GL_RGB32F; + case TextureFormat::RGBA32F: return GL_RGBA32F; + case TextureFormat::DEPTH24_STENCIL8: return GL_DEPTH24_STENCIL8; + default: return GL_RGBA8; + } + } + + GLenum get_gl_format(TextureFormat format) { + switch (format) { + case TextureFormat::R8: + case TextureFormat::R16F: + case TextureFormat::R32F: + return GL_RED; + case TextureFormat::RG8: + case TextureFormat::RG16F: + case TextureFormat::RG32F: + return GL_RG; + case TextureFormat::RGB8: + case TextureFormat::RGB16F: + case TextureFormat::RGB32F: + return GL_RGB; + case TextureFormat::RGBA8: + case TextureFormat::RGBA16F: + case TextureFormat::RGBA32F: + return GL_RGBA; + case TextureFormat::DEPTH24_STENCIL8: + return GL_DEPTH_STENCIL; + default: + return GL_RGBA; + } + } + + GLenum get_gl_type(TextureFormat format) { + switch (format) { + case TextureFormat::R8: + case TextureFormat::RG8: + case TextureFormat::RGB8: + case TextureFormat::RGBA8: + return GL_UNSIGNED_BYTE; + case TextureFormat::R16F: + case TextureFormat::RG16F: + case TextureFormat::RGB16F: + case TextureFormat::RGBA16F: + case TextureFormat::R32F: + case TextureFormat::RG32F: + case TextureFormat::RGB32F: + case TextureFormat::RGBA32F: + return GL_FLOAT; + case TextureFormat::DEPTH24_STENCIL8: + return GL_UNSIGNED_INT_24_8; + default: + return GL_UNSIGNED_BYTE; + } + } + + GLenum get_gl_filter(TextureFilter filter) { + switch (filter) { + case TextureFilter::NEAREST: return GL_NEAREST; + case TextureFilter::LINEAR: return GL_LINEAR; + case TextureFilter::NEAREST_MIPMAP_NEAREST: return GL_NEAREST_MIPMAP_NEAREST; + case TextureFilter::LINEAR_MIPMAP_NEAREST: return GL_LINEAR_MIPMAP_NEAREST; + case TextureFilter::NEAREST_MIPMAP_LINEAR: return GL_NEAREST_MIPMAP_LINEAR; + case TextureFilter::LINEAR_MIPMAP_LINEAR: return GL_LINEAR_MIPMAP_LINEAR; + default: return GL_LINEAR; + } + } + + GLenum get_gl_wrap(TextureWrap wrap) { + switch (wrap) { + case TextureWrap::REPEAT: return GL_REPEAT; + case TextureWrap::MIRRORED_REPEAT: return GL_MIRRORED_REPEAT; + case TextureWrap::CLAMP_TO_EDGE: return GL_CLAMP_TO_EDGE; + case TextureWrap::CLAMP_TO_BORDER: return GL_CLAMP_TO_BORDER; + default: return GL_REPEAT; + } + } +} + +Texture::Texture() + : handle_(INVALID_HANDLE) + , width_(0) + , height_(0) + , format_(TextureFormat::RGBA8) + , has_mipmaps_(false) { +} + +Texture::~Texture() { + // Don't auto-release, let user control lifetime +} + +bool Texture::load_from_file(const std::string& path, bool generate_mipmaps) { + // Load image using stb_image + int width, height, channels; + stbi_set_flip_vertically_on_load(true); + unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 0); + + if (!data) { + Logger::error("Failed to load texture: " + path); + return false; + } + + // Determine format based on channels + TextureFormat format; + switch (channels) { + case 1: format = TextureFormat::R8; break; + case 2: format = TextureFormat::RG8; break; + case 3: format = TextureFormat::RGB8; break; + case 4: format = TextureFormat::RGBA8; break; + default: + Logger::error("Unsupported channel count: " + std::to_string(channels)); + stbi_image_free(data); + return false; + } + + // Create texture + bool success = create(width, height, format); + if (!success) { + stbi_image_free(data); + return false; + } + + // Upload data + success = upload(data, width, height, format); + stbi_image_free(data); + + if (!success) { + return false; + } + + // Generate mipmaps if requested + if (generate_mipmaps) { + this->generate_mipmaps(); + } + + Logger::info("Texture loaded successfully: " + path); + return true; +} + +bool Texture::create(uint width, uint height, TextureFormat format) { + if (handle_ != INVALID_HANDLE) { + Logger::warning("Texture already created, releasing old texture"); + release(); + } + + width_ = width; + height_ = height; + format_ = format; + + glGenTextures(1, &handle_); + glBindTexture(GL_TEXTURE_2D, handle_); + + GLenum internal_format = get_gl_internal_format(format); + GLenum gl_format = get_gl_format(format); + GLenum type = get_gl_type(format); + + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, gl_format, type, nullptr); + + // Set default parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + glBindTexture(GL_TEXTURE_2D, 0); + + return true; +} + +bool Texture::upload(const void* data, uint width, uint height, TextureFormat format) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot upload to invalid texture"); + return false; + } + + if (width != width_ || height != height_ || format != format_) { + Logger::warning("Upload parameters differ from texture creation, recreating texture"); + create(width, height, format); + } + + glBindTexture(GL_TEXTURE_2D, handle_); + + GLenum gl_format = get_gl_format(format); + GLenum type = get_gl_type(format); + + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, gl_format, type, data); + + glBindTexture(GL_TEXTURE_2D, 0); + + return true; +} + +void Texture::set_filter(TextureFilter min_filter, TextureFilter mag_filter) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot set filter on invalid texture"); + return; + } + + glBindTexture(GL_TEXTURE_2D, handle_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, get_gl_filter(min_filter)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, get_gl_filter(mag_filter)); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Texture::set_wrap(TextureWrap wrap_s, TextureWrap wrap_t) { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot set wrap mode on invalid texture"); + return; + } + + glBindTexture(GL_TEXTURE_2D, handle_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, get_gl_wrap(wrap_s)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, get_gl_wrap(wrap_t)); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Texture::generate_mipmaps() { + if (handle_ == INVALID_HANDLE) { + Logger::error("Cannot generate mipmaps for invalid texture"); + return; + } + + glBindTexture(GL_TEXTURE_2D, handle_); + glGenerateMipmap(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + + has_mipmaps_ = true; +} + +void Texture::bind(uint unit) const { + if (handle_ == INVALID_HANDLE) { + Logger::warning("Attempting to bind invalid texture"); + return; + } + + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, handle_); +} + +void Texture::unbind() const { + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Texture::release() { + if (handle_ != INVALID_HANDLE) { + glDeleteTextures(1, &handle_); + handle_ = INVALID_HANDLE; + } + + width_ = 0; + height_ = 0; + has_mipmaps_ = false; +} + +} // namespace are diff --git a/src/scene/camera.cpp b/src/scene/camera.cpp new file mode 100644 index 0000000..02821da --- /dev/null +++ b/src/scene/camera.cpp @@ -0,0 +1,101 @@ +#include "scene/camera.h" +#include "basic/math.h" +#include + +namespace are { + +Camera::Camera() + : position_(0.0f, 0.0f, 5.0f) + , target_(0.0f, 0.0f, 0.0f) + , up_(0.0f, 1.0f, 0.0f) + , projection_type_(ProjectionType::PERSPECTIVE) + , fov_(glm::radians(45.0f)) + , aspect_(16.0f / 9.0f) + , left_(-1.0f) + , right_(1.0f) + , bottom_(-1.0f) + , top_(1.0f) + , near_(0.1f) + , far_(100.0f) + , view_dirty_(true) + , projection_dirty_(true) { +} + +Camera::~Camera() { +} + +void Camera::set_perspective(float fov, float aspect, float near, float far) { + projection_type_ = ProjectionType::PERSPECTIVE; + fov_ = glm::radians(fov); + aspect_ = aspect; + near_ = near; + far_ = far; + projection_dirty_ = true; +} + +void Camera::set_orthographic(float left, float right, float bottom, float top, float near, float far) { + projection_type_ = ProjectionType::ORTHOGRAPHIC; + left_ = left; + right_ = right; + bottom_ = bottom; + top_ = top; + near_ = near; + far_ = far; + projection_dirty_ = true; +} + +void Camera::set_position(const Vec3& position) { + position_ = position; + view_dirty_ = true; +} + +void Camera::set_target(const Vec3& target) { + target_ = target; + view_dirty_ = true; +} + +void Camera::set_up(const Vec3& up) { + up_ = up; + view_dirty_ = true; +} + +Mat4 Camera::get_view_matrix() const { + if (view_dirty_) { + view_matrix_ = MathUtils::look_at(position_, target_, up_); + view_dirty_ = false; + } + return view_matrix_; +} + +Mat4 Camera::get_projection_matrix() const { + if (projection_dirty_) { + if (projection_type_ == ProjectionType::PERSPECTIVE) { + projection_matrix_ = MathUtils::perspective(fov_, aspect_, near_, far_); + } else { + projection_matrix_ = glm::ortho(left_, right_, bottom_, top_, near_, far_); + } + projection_dirty_ = false; + } + return projection_matrix_; +} + +Mat4 Camera::get_view_projection_matrix() const { + return get_projection_matrix() * get_view_matrix(); +} + +Vec3 Camera::get_forward() const { + return MathUtils::normalize(target_ - position_); +} + +Vec3 Camera::get_right() const { + Vec3 forward = get_forward(); + return MathUtils::normalize(MathUtils::cross(forward, up_)); +} + +Vec3 Camera::get_up() const { + Vec3 forward = get_forward(); + Vec3 right = get_right(); + return MathUtils::cross(right, forward); +} + +} // namespace are diff --git a/src/scene/light.cpp b/src/scene/light.cpp new file mode 100644 index 0000000..00d76bb --- /dev/null +++ b/src/scene/light.cpp @@ -0,0 +1,49 @@ +#include "scene/light.h" +#include + +namespace are { + +Light::Light() + : type_(LightType::POINT) + , position_(0.0f, 5.0f, 0.0f) + , direction_(0.0f, -1.0f, 0.0f) + , color_(1.0f, 1.0f, 1.0f) + , intensity_(1.0f) + , range_(10.0f) + , inner_angle_(glm::radians(30.0f)) + , outer_angle_(glm::radians(45.0f)) { +} + +Light::~Light() { +} + +void Light::set_type(LightType type) { + type_ = type; +} + +void Light::set_position(const Vec3& position) { + position_ = position; +} + +void Light::set_direction(const Vec3& direction) { + direction_ = glm::normalize(direction); +} + +void Light::set_color(const Vec3& color) { + color_ = color; +} + +void Light::set_intensity(float intensity) { + intensity_ = intensity; +} + +void Light::set_range(float range) { + range_ = range; +} + +void Light::set_spot_angles(float inner_angle, float outer_angle) { + inner_angle_ = glm::radians(inner_angle); + outer_angle_ = glm::radians(outer_angle); +} + +} // namespace are diff --git a/src/scene/material.cpp b/src/scene/material.cpp new file mode 100644 index 0000000..9e805de --- /dev/null +++ b/src/scene/material.cpp @@ -0,0 +1,51 @@ +#include "scene/material.h" + +namespace are { + +Material::Material() + : albedo_(1.0f, 1.0f, 1.0f) + , emission_(0.0f, 0.0f, 0.0f) + , metallic_(0.0f) + , roughness_(0.5f) + , ior_(1.5f) + , type_(MaterialType::DIFFUSE) + , albedo_texture_(nullptr) + , normal_texture_(nullptr) { +} + +Material::~Material() { +} + +void Material::set_albedo(const Vec3& albedo) { + albedo_ = albedo; +} + +void Material::set_emission(const Vec3& emission) { + emission_ = emission; +} + +void Material::set_metallic(float metallic) { + metallic_ = glm::clamp(metallic, 0.0f, 1.0f); +} + +void Material::set_roughness(float roughness) { + roughness_ = glm::clamp(roughness, 0.0f, 1.0f); +} + +void Material::set_ior(float ior) { + ior_ = ior; +} + +void Material::set_type(MaterialType type) { + type_ = type; +} + +void Material::set_albedo_texture(std::shared_ptr texture) { + albedo_texture_ = texture; +} + +void Material::set_normal_texture(std::shared_ptr texture) { + normal_texture_ = texture; +} + +} // namespace are diff --git a/src/scene/mesh.cpp b/src/scene/mesh.cpp new file mode 100644 index 0000000..610e75a --- /dev/null +++ b/src/scene/mesh.cpp @@ -0,0 +1,119 @@ +#include "scene/mesh.h" +#include "utils/logger.h" +#include + +namespace are { + +Mesh::Mesh() + : material_id_(0) + , transform_(1.0f) + , vao_(0) + , vbo_(0) + , ebo_(0) + , uploaded_(false) { +} + +Mesh::~Mesh() { + release_gpu_resources(); +} + +void Mesh::set_vertices(const std::vector& vertices) { + vertices_ = vertices; + uploaded_ = false; +} + +void Mesh::set_indices(const std::vector& indices) { + indices_ = indices; + uploaded_ = false; +} + +void Mesh::set_material(uint material_id) { + material_id_ = material_id; +} + +void Mesh::set_transform(const Mat4& transform) { + transform_ = transform; +} + +bool Mesh::upload_to_gpu() { + if (uploaded_) { + Logger::warning("Mesh already uploaded to GPU"); + return true; + } + + if (vertices_.empty()) { + Logger::error("Cannot upload mesh: no vertices"); + return false; + } + + if (indices_.empty()) { + Logger::error("Cannot upload mesh: no indices"); + return false; + } + + // Generate VAO + glGenVertexArrays(1, &vao_); + glBindVertexArray(vao_); + + // Generate and upload VBO + glGenBuffers(1, &vbo_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(Vertex), + vertices_.data(), GL_STATIC_DRAW); + + // Generate and upload EBO + glGenBuffers(1, &ebo_); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices_.size() * sizeof(uint), + indices_.data(), GL_STATIC_DRAW); + + // Set vertex attributes + // Location 0: Position + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, position_)); + + // Location 1: Normal + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, normal_)); + + // Location 2: TexCoord + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, texcoord_)); + + // Location 3: Tangent + glEnableVertexAttribArray(3); + glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)offsetof(Vertex, tangent_)); + + glBindVertexArray(0); + + uploaded_ = true; + Logger::info("Mesh uploaded to GPU successfully"); + return true; +} + +void Mesh::release_gpu_resources() { + if (!uploaded_) return; + + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } + + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } + + if (ebo_ != 0) { + glDeleteBuffers(1, &ebo_); + ebo_ = 0; + } + + uploaded_ = false; +} + +} // namespace are diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp new file mode 100644 index 0000000..7e27f3b --- /dev/null +++ b/src/scene/scene.cpp @@ -0,0 +1,44 @@ +#include "scene/scene.h" + +namespace are { + +Scene::Scene() { + // Create default camera + camera_ = std::make_shared(); +} + +Scene::~Scene() { + clear(); +} + +uint Scene::add_mesh(std::shared_ptr mesh) { + meshes_.push_back(mesh); + return static_cast(meshes_.size() - 1); +} + +uint Scene::add_material(std::shared_ptr material) { + materials_.push_back(material); + return static_cast(materials_.size() - 1); +} + +uint Scene::add_light(std::shared_ptr light) { + lights_.push_back(light); + return static_cast(lights_.size() - 1); +} + +void Scene::set_camera(std::shared_ptr camera) { + camera_ = camera; +} + +void Scene::clear() { + meshes_.clear(); + materials_.clear(); + lights_.clear(); +} + +void Scene::update(float delta_time) { + // Reserved for future animation/physics updates + (void)delta_time; // Suppress unused parameter warning +} + +} // namespace are diff --git a/src/utils/config.cpp b/src/utils/config.cpp new file mode 100644 index 0000000..23b61af --- /dev/null +++ b/src/utils/config.cpp @@ -0,0 +1,144 @@ +#include "utils/config.h" +#include "utils/logger.h" +#include +#include +#include + +namespace are { + +// Static storage +static std::unordered_map g_config_map; + +// Helper function to trim whitespace +static std::string trim(const std::string& str) { + size_t first = str.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return ""; + size_t last = str.find_last_not_of(" \t\r\n"); + return str.substr(first, last - first + 1); +} + +bool Config::load(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) { + Logger::error("Failed to open config file: " + path); + return false; + } + + g_config_map.clear(); + + std::string line; + std::string current_section; + + while (std::getline(file, line)) { + line = trim(line); + + // Skip empty lines and comments + if (line.empty() || line[0] == '#' || line[0] == ';') { + continue; + } + + // Section header + if (line[0] == '[' && line.back() == ']') { + current_section = line.substr(1, line.length() - 2); + continue; + } + + // Key-value pair + size_t pos = line.find('='); + if (pos != std::string::npos) { + std::string key = trim(line.substr(0, pos)); + std::string value = trim(line.substr(pos + 1)); + + // Add section prefix if in a section + if (!current_section.empty()) { + key = current_section + "." + key; + } + + g_config_map[key] = value; + } + } + + Logger::info("Config loaded: " + path + " (" + std::to_string(g_config_map.size()) + " entries)"); + return true; +} + +bool Config::save(const std::string& path) { + std::ofstream file(path); + if (!file.is_open()) { + Logger::error("Failed to open config file for writing: " + path); + return false; + } + + for (const auto& pair : g_config_map) { + file << pair.first << "=" << pair.second << std::endl; + } + + Logger::info("Config saved: " + path); + return true; +} + +std::string Config::get_string(const std::string& key, const std::string& default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + return it->second; + } + return default_value; +} + +int Config::get_int(const std::string& key, int default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + try { + return std::stoi(it->second); + } catch (...) { + Logger::warning("Failed to parse int for key: " + key); + } + } + return default_value; +} + +float Config::get_float(const std::string& key, float default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + try { + return std::stof(it->second); + } catch (...) { + Logger::warning("Failed to parse float for key: " + key); + } + } + return default_value; +} + +bool Config::get_bool(const std::string& key, bool default_value) { + auto it = g_config_map.find(key); + if (it != g_config_map.end()) { + std::string value = it->second; + std::transform(value.begin(), value.end(), value.begin(), ::tolower); + + if (value == "true" || value == "1" || value == "yes" || value == "on") { + return true; + } + if (value == "false" || value == "0" || value == "no" || value == "off") { + return false; + } + } + return default_value; +} + +void Config::set_string(const std::string& key, const std::string& value) { + g_config_map[key] = value; +} + +void Config::set_int(const std::string& key, int value) { + g_config_map[key] = std::to_string(value); +} + +void Config::set_float(const std::string& key, float value) { + g_config_map[key] = std::to_string(value); +} + +void Config::set_bool(const std::string& key, bool value) { + g_config_map[key] = value ? "true" : "false"; +} + +} // namespace are diff --git a/src/utils/logger.cpp b/src/utils/logger.cpp new file mode 100644 index 0000000..708e317 --- /dev/null +++ b/src/utils/logger.cpp @@ -0,0 +1,103 @@ +#include "utils/logger.h" +#include +#include +#include +#include +#include + +namespace are { + +// Static members +static LogLevel g_min_level = LogLevel::DEBUG; +static std::ofstream g_log_file; +static bool g_initialized = false; + +bool Logger::initialize(const std::string& log_file) { + if (g_initialized) { + return true; + } + + if (!log_file.empty()) { + g_log_file.open(log_file, std::ios::out | std::ios::app); + if (!g_log_file.is_open()) { + std::cerr << "Failed to open log file: " << log_file << std::endl; + return false; + } + } + + g_initialized = true; + return true; +} + +void Logger::shutdown() { + if (g_log_file.is_open()) { + g_log_file.close(); + } + g_initialized = false; +} + +static std::string get_current_time() { + auto now = std::time(nullptr); + auto tm = *std::localtime(&now); + std::ostringstream oss; + oss << std::put_time(&tm, "%H:%M:%S"); + return oss.str(); +} + +static std::string level_to_string(LogLevel level) { + switch (level) { + case LogLevel::DEBUG: return "DEBUG"; + case LogLevel::INFO: return "INFO"; + case LogLevel::WARNING: return "WARN"; + case LogLevel::ERROR: return "ERROR"; + case LogLevel::FATAL: return "FATAL"; + default: return "UNKNOWN"; + } +} + +void Logger::log(LogLevel level, const std::string& message) { + if (level < g_min_level) return; + + std::string time_str = get_current_time(); + std::string level_str = level_to_string(level); + std::string formatted = "[" + time_str + "] [" + level_str + "] " + message; + + // Console output + if (level >= LogLevel::ERROR) { + std::cerr << formatted << std::endl; + } else { + std::cout << formatted << std::endl; + } + + // File output + if (g_log_file.is_open()) { + g_log_file << formatted << std::endl; + g_log_file.flush(); + } +} + +void Logger::debug(const std::string& message) { + log(LogLevel::DEBUG, message); +} + +void Logger::info(const std::string& message) { + log(LogLevel::INFO, message); +} + +void Logger::warning(const std::string& message) { + log(LogLevel::WARNING, message); +} + +void Logger::error(const std::string& message) { + log(LogLevel::ERROR, message); +} + +void Logger::fatal(const std::string& message) { + log(LogLevel::FATAL, message); +} + +void Logger::set_level(LogLevel level) { + g_min_level = level; +} + +} // namespace are