diff --git a/AGENTS.md b/AGENTS.md index 07f98a4..15021d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md - Aurora Rendering Engine ## Project Overview -Aurora Rendering Engine (ARE) is a high-performance path tracing library in C++ developed by NanoEra Studio. It provides a rendering framework using OpenGL 4.3 with support for GPU-accelerated ray tracing, BVH acceleration, and denoising. +Aurora Rendering Engine (ARE) is a high-performance path tracing library in C++ developed by NanoEra Studio. It provides a rendering framework using OpenGL 4.3 with GPU-accelerated ray tracing, BVH acceleration, and denoising. ## Build Commands @@ -19,16 +19,16 @@ cmake --build . ### Running Examples The main example is the Cornell Box demo: ```bash -./examples/cornell_box +./build/examples/cornell_box ``` ### Testing -This project currently has **no built-in unit tests**. Testing is performed manually by running the example executables. There are no test-specific build targets or test frameworks configured. +This project has **no built-in unit tests**. Testing is performed manually by running the example executables. There are no test-specific build targets or test frameworks configured. ### Linting/Code Formatting -- **Format**: Use `.clang-format` configuration in project root +- **Format**: Use `.clang-format` configuration (tabs, attach braces, unlimited line length) ```bash - clang-format -i src/*.cpp include/**/*.h + clang-format -i src/*.cpp include/**/*.h examples/*.cpp ``` - **Clangd**: IDE integration via `.clangd` file (C++20, includes paths configured) @@ -36,21 +36,23 @@ This project currently has **no built-in unit tests**. Testing is performed manu ### General Conventions - **C++ Standard**: C++17 (project CMake), C++20 (clangd for IDE) -- **Indentation**: Tabs (see `.clang-format`: `UseTab: Always`) -- **Line Length**: Unlimited (`ColumnLimit: 0`) -- **Brace Style**: Attach (see `.clang-format`: `BreakBeforeBraces: Attach`) +- **Indentation**: Tabs (not spaces) +- **Line Length**: Unlimited +- **Brace Style**: Attach (opening brace on same line) ### File Organization - **Headers**: `include/` - Public API - **Source**: `src/` - Implementation +- **Examples**: `examples/` - Demo applications - **Include format**: `#include "path/to/header.h"` (quotes for project headers) ### Naming Conventions -- **Classes**: `PascalCase` (e.g., `Renderer`, `Scene`) -- **Functions**: `PascalCase` (e.g., `initialize()`, `render()`) +- **Classes**: `PascalCase` (e.g., `Renderer`, `Scene`, `RayTracer`) +- **Functions**: `PascalCase` (e.g., `initialize()`, `render()`, `set_config()`) - **Member variables**: `snake_case_` with trailing underscore (e.g., `config_`, `frame_count_`) - **Types (aliases)**: `PascalCase` (e.g., `Vec3`, `Mat4`, `TextureHandle`) -- **Enums**: `PascalCase` with `k` prefix for values (e.g., `LogLevel::ARE_LOG_INFO`) +- **Enums**: `PascalCase` with `ARE_LOG_` prefix for values (e.g., `LogLevel::ARE_LOG_INFO`) +- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `INVALID_HANDLE`) ### Header Guards ```cpp @@ -71,7 +73,7 @@ This project currently has **no built-in unit tests**. Testing is performed manu 4. Standard library headers ### Comments -- Use Doxygen-style `/** */` or `/* */` for function documentation +- Use `/* */` for function documentation with `@brief`, `@param`, `@return` - Brief descriptions in header files, implementation details in .cpp - Avoid unnecessary inline comments diff --git a/examples/cornell_box b/examples/cornell_box index e75e179..8cc5be8 100644 Binary files a/examples/cornell_box and b/examples/cornell_box differ diff --git a/examples/cornell_box_metal_sphere b/examples/cornell_box_metal_sphere new file mode 100644 index 0000000..39a1575 Binary files /dev/null and b/examples/cornell_box_metal_sphere differ diff --git a/examples/cornell_box_metal_sphere.cpp b/examples/cornell_box_metal_sphere.cpp new file mode 100644 index 0000000..2e42a20 --- /dev/null +++ b/examples/cornell_box_metal_sphere.cpp @@ -0,0 +1,562 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace are; + +// Window dimensions +const uint WINDOW_WIDTH = 800; +const uint WINDOW_HEIGHT = 800; + +// Global state +GLFWwindow *g_window = nullptr; +std::unique_ptr g_renderer = nullptr; +std::unique_ptr g_scene = nullptr; +std::shared_ptr g_camera = nullptr; // Keep a direct reference to camera + +// --- Camera Control State --- +Vec3 g_cameraPos = Vec3(0.0f, 0.0f, 4.5f); +Vec3 g_cameraTarget = Vec3(0.0f, 0.0f, 0.0f); +Vec3 g_cameraUp = Vec3(0.0f, 1.0f, 0.0f); +Vec3 g_worldUp = Vec3(0.0f, 1.0f, 0.0f); + +// Euler Angles +float g_yaw = -90.0f; // Initialized to look along -Z (standard OpenGL) +float g_pitch = 0.0f; + +// Control settings +float g_moveSpeed = 2.5f; +float g_mouseSensitivity = 0.1f; +bool g_firstMouse = true; +double g_lastX = WINDOW_WIDTH / 2.0; +double g_lastY = WINDOW_HEIGHT / 2.0; + +// Time +float g_deltaTime = 0.0f; +float g_lastFrame = 0.0f; + +// GLFW error callback +void glfw_error_callback(int error, const char *description) { + ARE_LOG_ERROR("GLFW Error " + std::to_string(error) + ": " + std::string(description)); +} + +/// @brief Create a quad mesh +std::shared_ptr create_quad(const Vec3 &v0, const Vec3 &v1, const Vec3 &v2, const Vec3 &v3, + const Vec3 &normal, uint material_id) { + auto mesh = std::make_shared(); + + std::vector vertices = { + { v0, normal, Vec2(0.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f) }, + { v1, normal, Vec2(1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f) }, + { v2, normal, Vec2(1.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f) }, + { v3, normal, Vec2(0.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f) } + }; + + std::vector indices = { 0, 1, 2, 0, 2, 3 }; + + mesh->set_vertices(vertices); + mesh->set_indices(indices); + mesh->set_material(material_id); + + return mesh; +} + +/// @brief Create a box mesh +std::shared_ptr create_box(const Vec3 &min, const Vec3 &max, uint material_id) { + auto mesh = std::make_shared(); + + std::vector vertices = { + // Front face + { { min.x, min.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, min.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + { { min.x, max.y, max.z }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + + // Back face + { { max.x, min.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f } }, + { { min.x, min.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 0.0f }, { -1.0f, 0.0f, 0.0f } }, + { { min.x, max.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, min.z }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } }, + + // Top face + { { min.x, max.y, max.z }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, max.z }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, max.y, min.z }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + { { min.x, max.y, min.z }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + + // Bottom face + { { min.x, min.y, min.z }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, min.y, min.z }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { max.x, min.y, max.z }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + { { min.x, min.y, max.z }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f } }, + + // Right face + { { max.x, min.y, max.z }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } }, + { { max.x, min.y, min.z }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } }, + { { max.x, max.y, min.z }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } }, + { { max.x, max.y, max.z }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } }, + + // Left face + { { min.x, min.y, min.z }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }, + { { min.x, min.y, max.z }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }, + { { min.x, max.y, max.z }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 0.0f, 1.0f } }, + { { min.x, max.y, min.z }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f } } + }; + + std::vector indices = { + 0, 1, 2, 0, 2, 3, // Front + 4, 5, 6, 4, 6, 7, // Back + 8, 9, 10, 8, 10, 11, // Top + 12, 13, 14, 12, 14, 15, // Bottom + 16, 17, 18, 16, 18, 19, // Right + 20, 21, 22, 20, 22, 23 // Left + }; + + mesh->set_vertices(vertices); + mesh->set_indices(indices); + mesh->set_material(material_id); + + return mesh; +} + +/// @brief Create a sphere mesh +std::shared_ptr create_sphere(float radius, uint segments, uint rings, uint material_id) { + auto mesh = std::make_shared(); + + std::vector vertices; + std::vector indices; + + for (uint ring = 0; ring <= rings; ++ring) { + float theta = ring * glm::pi() / rings; + float sin_theta = sin(theta); + float cos_theta = cos(theta); + + for (uint seg = 0; seg <= segments; ++seg) { + float phi = seg * 2.0f * glm::pi() / segments; + float x = cos(phi) * sin_theta; + float y = cos_theta; + float z = sin(phi) * sin_theta; + + Vec3 pos = Vec3(x, y, z) * radius; + Vec3 normal = Vec3(x, y, z); + Vec2 uv = Vec2((float)seg / segments, (float)ring / rings); + Vec3 tangent = Vec3(-sin(phi), 0.0f, cos(phi)); + + vertices.push_back({ pos, normal, uv, tangent }); + } + } + + for (uint ring = 0; ring < rings; ++ring) { + for (uint seg = 0; seg < segments; ++seg) { + uint current = ring * (segments + 1) + seg; + indices.push_back(current); + indices.push_back(current + segments + 1); + indices.push_back(current + 1); + indices.push_back(current + 1); + indices.push_back(current + segments + 1); + indices.push_back(current + segments + 2); + } + } + + mesh->set_vertices(vertices); + mesh->set_indices(indices); + mesh->set_material(material_id); + mesh->compute_tangents(); + + return mesh; +} + +/// @brief Setup Cornell Box scene with metal sphere +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 (shiny gold-like) + 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.05f); + metal_material->set_type(MaterialType::METAL); + uint metal_id = g_scene->add_material(metal_material); + + // 5: Yellow emissive sphere + auto emissive_sphere_mat = std::make_shared(); + emissive_sphere_mat->set_albedo(Vec3(1.0f, 0.8f, 0.2f)); + emissive_sphere_mat->set_emission(Vec3(5.0f, 4.0f, 1.0f)); + emissive_sphere_mat->set_type(MaterialType::EMISSIVE); + uint emissive_sphere_id = g_scene->add_material(emissive_sphere_mat); + (void)emissive_sphere_id; // Reserved for future use + + // 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); + + // Metal sphere (replacing the glass box, positioned on the right side) + auto metal_sphere = create_sphere(0.5f, 16, 8, metal_id); + metal_sphere->set_position(Vec3(0.55f, -1.5f, 0.35f)); + metal_sphere->upload_to_gpu(); + g_scene->add_mesh(metal_sphere); + + // Setup camera + g_camera = std::make_shared(); + g_camera->set_position(g_cameraPos); + g_camera->set_target(g_cameraTarget); + g_camera->set_up(g_cameraUp); + g_camera->set_perspective(45.0f, static_cast(WINDOW_WIDTH) / WINDOW_HEIGHT, 0.1f, 100.0f); + g_scene->set_camera(g_camera); + + // Add point light + auto light = std::make_shared(); + light->set_type(LightType::POINT); + light->set_position(Vec3(0.0f, 1.8f, 0.0f)); + light->set_color(Vec3(1.0f, 1.0f, 1.0f)); + light->set_intensity(10.0f); + light->set_range(10.0f); + g_scene->add_light(light); + + ARE_LOG_INFO("Cornell Box with Metal Sphere scene created"); +} + +/// @brief Initialize GLFW and create window +bool init_window() { + glfwSetErrorCallback(glfw_error_callback); + + if (!glfwInit()) { + ARE_LOG_ERROR("Failed to initialize GLFW"); + return false; + } + + ARE_LOG_INFO("GLFW initialized successfully"); + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); + glfwWindowHint(GLFW_SAMPLES, 0); + + g_window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Aurora - Cornell Box (Metal Sphere)", nullptr, nullptr); + + if (!g_window) { + ARE_LOG_ERROR("Failed to create GLFW window"); + glfwTerminate(); + return false; + } + + glfwMakeContextCurrent(g_window); + glfwSwapInterval(1); + + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { + ARE_LOG_ERROR("Failed to initialize GLAD"); + return false; + } + + return true; +} + +// --- Input Processing --- +void process_input() { + // Calculate delta time + float currentFrame = glfwGetTime(); + g_deltaTime = currentFrame - g_lastFrame; + g_lastFrame = currentFrame; + + float velocity = g_moveSpeed * g_deltaTime; + bool camera_changed = false; + + // 1. Mouse Rotation (Left Button Hold) + if (glfwGetMouseButton(g_window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + double xpos, ypos; + glfwGetCursorPos(g_window, &xpos, &ypos); + + if (g_firstMouse) { + g_lastX = xpos; + g_lastY = ypos; + g_firstMouse = false; + } + + float xoffset = xpos - g_lastX; + float yoffset = g_lastY - ypos; // Reversed since y-coordinates go from bottom to top + g_lastX = xpos; + g_lastY = ypos; + + // Only update if mouse actually moved + if (xoffset != 0.0f || yoffset != 0.0f) { + xoffset *= g_mouseSensitivity; + yoffset *= g_mouseSensitivity; + + g_yaw += xoffset; + g_pitch += yoffset; + + // Constrain pitch + if (g_pitch > 89.0f) + g_pitch = 89.0f; + if (g_pitch < -89.0f) + g_pitch = -89.0f; + + camera_changed = true; + } + } else { + g_firstMouse = true; // Reset when released + } + + // 2. Calculate Direction Vectors + glm::vec3 front; + front.x = cos(glm::radians(g_yaw)) * cos(glm::radians(g_pitch)); + front.y = sin(glm::radians(g_pitch)); + front.z = sin(glm::radians(g_yaw)) * cos(glm::radians(g_pitch)); + glm::vec3 frontNorm = glm::normalize(front); + + glm::vec3 rightNorm = glm::normalize(glm::cross(frontNorm, glm::vec3(g_worldUp.x, g_worldUp.y, g_worldUp.z))); + + // 3. Keyboard Movement (WASD) + glm::vec3 pos = glm::vec3(g_cameraPos.x, g_cameraPos.y, g_cameraPos.z); + + if (glfwGetKey(g_window, GLFW_KEY_W) == GLFW_PRESS) { + pos += frontNorm * velocity; + camera_changed = true; + } + if (glfwGetKey(g_window, GLFW_KEY_S) == GLFW_PRESS) { + pos -= frontNorm * velocity; + camera_changed = true; + } + if (glfwGetKey(g_window, GLFW_KEY_A) == GLFW_PRESS) { + pos -= rightNorm * velocity; + camera_changed = true; + } + if (glfwGetKey(g_window, GLFW_KEY_D) == GLFW_PRESS) { + pos += rightNorm * velocity; + camera_changed = true; + } + + // 4. Apply changes to Scene Camera and Notify Renderer + if (camera_changed) { + g_cameraPos = Vec3(pos.x, pos.y, pos.z); + + // Target = Position + Front + Vec3 newTarget = g_cameraPos + Vec3(frontNorm.x, frontNorm.y, frontNorm.z); + + g_camera->set_position(g_cameraPos); + g_camera->set_target(newTarget); + + // CRITICAL: Notify renderer to reset accumulation + g_renderer->notify_scene_changed(*g_scene); + } +} + +/// @brief Main render loop +void render_loop() { + ARE_LOG_INFO("Entering render loop..."); + + int frame_count = 0; + double fps_time = glfwGetTime(); + g_lastFrame = glfwGetTime(); // Initialize for delta time + + while (!glfwWindowShouldClose(g_window)) { + // Process input at the start of the frame + process_input(); + + // Render + RenderStats stats = g_renderer->render(*g_scene); + + // Swap buffers + glfwSwapBuffers(g_window); + glfwPollEvents(); + + // Calculate FPS + frame_count++; + double current_time = glfwGetTime(); + double delta = current_time - fps_time; + + if (delta >= 1.0) { + double fps = frame_count / delta; + std::string title = "Aurora - Cornell Box (Metal Sphere) | FPS: " + std::to_string((int)fps) + " | Frame: " + std::to_string((int)stats.frame_time_ms_) + "ms"; + glfwSetWindowTitle(g_window, title.c_str()); + + frame_count = 0; + fps_time = current_time; + } + + // ESC to exit + if (glfwGetKey(g_window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { + glfwSetWindowShouldClose(g_window, true); + } + } + + ARE_LOG_INFO("Exiting render loop"); +} + +/// @brief Cleanup +void cleanup() { + ARE_LOG_INFO("Cleaning up..."); + + if (g_renderer) { + g_renderer->shutdown(); + g_renderer.reset(); + } + + g_scene.reset(); + + if (g_window) { + glfwDestroyWindow(g_window); + glfwTerminate(); + } + + ARE_LOG_INFO("Cleanup complete"); +} + +int main() { + ARE_LOG_INFO("==========================================="); + ARE_LOG_INFO("Aurora Rendering Engine - Cornell Box with Metal Sphere Demo"); + ARE_LOG_INFO("==========================================="); + + if (!init_window()) { + cleanup(); + ARE_LOG_ERROR("Failed to initialize window"); + Logger::shutdown(); + return -1; + } + + ARE_LOG_INFO("Setting up Cornell Box with Metal Sphere scene..."); + setup_cornell_box(); + + ARE_LOG_INFO("Initializing renderer..."); + RendererConfig config; + config.width_ = WINDOW_WIDTH; + config.height_ = WINDOW_HEIGHT; + config.samples_per_pixel_ = 1; + config.max_ray_depth_ = 4; + config.enable_accumulation_ = true; + config.enable_denoising_ = false; + + g_renderer = std::make_unique(config); + if (!g_renderer->initialize()) { + ARE_LOG_ERROR("Failed to initialize renderer"); + cleanup(); + Logger::shutdown(); + return -1; + } + + ARE_LOG_INFO("==========================================="); + ARE_LOG_INFO("Renderer initialized successfully!"); + ARE_LOG_INFO("Controls:"); + ARE_LOG_INFO(" WASD - Move Camera"); + ARE_LOG_INFO(" Hold Left Mouse Button - Rotate Camera"); + ARE_LOG_INFO(" ESC - Exit"); + ARE_LOG_INFO("==========================================="); + + render_loop(); + cleanup(); + + ARE_LOG_INFO("Cornell Box with Metal Sphere demo finished"); + Logger::shutdown(); + + return 0; +} diff --git a/examples/normal_map_cornell_box b/examples/normal_map_cornell_box index 97f066d..d67be85 100644 Binary files a/examples/normal_map_cornell_box and b/examples/normal_map_cornell_box differ diff --git a/include/basic/encoding.h b/include/basic/encoding.h new file mode 100644 index 0000000..1f1f918 --- /dev/null +++ b/include/basic/encoding.h @@ -0,0 +1,46 @@ +#ifndef ARE_INCLUDE_BASIC_ENCODING_H +#define ARE_INCLUDE_BASIC_ENCODING_H + +#include "basic/types.h" +#include + +namespace are { + +/* + * @brief Octahedral encode a unit vector to 2D coordinates + * @param n Normalized 3D vector + * @return 2D encoded coordinates in [0, 1] range + */ +inline Vec2 oct_encode(Vec3 n) { + float sum = std::abs(n.x) + std::abs(n.y) + std::abs(n.z); + n /= sum; + + if (n.z < 0.0f) { + float x = n.x; + float y = n.y; + n.x = (1.0f - std::abs(y)) * (x >= 0.0f ? 1.0f : -1.0f); + n.y = (1.0f - std::abs(x)) * (y >= 0.0f ? 1.0f : -1.0f); + } + + return Vec2(n.x, n.y) * 0.5f + 0.5f; +} + +/* + * @brief Octahedral decode 2D coordinates to unit vector + * @param f 2D encoded coordinates in [0, 1] range + * @return Normalized 3D vector + */ +inline Vec3 oct_decode(Vec2 f) { + f = f * 2.0f - 1.0f; + + Vec3 n = Vec3(f.x, f.y, 1.0f - std::abs(f.x) - std::abs(f.y)); + float t = std::max(-n.z, 0.0f); + n.x += n.x >= 0.0f ? -t : t; + n.y += n.y >= 0.0f ? -t : t; + + return glm::normalize(n); +} + +} // namespace are + +#endif // ARE_INCLUDE_BASIC_ENCODING_H diff --git a/include/core/denoiser.h b/include/core/denoiser.h index 2f29b5d..3d600a4 100644 --- a/include/core/denoiser.h +++ b/include/core/denoiser.h @@ -7,7 +7,7 @@ namespace are { -// Mean filter denoiser using compute shader +// Bilateral filter denoiser with temporal accumulation class Denoiser { public: /** @@ -42,18 +42,26 @@ public: void resize(uint width, uint height); /** - * @brief Apply mean filter + * @brief Apply bilateral filter with optional temporal accumulation * @param input_texture RGBA32F input texture * @param radius Filter radius (1 => 3x3) + * @param temporal_weight Weight for temporal blending (0 = no temporal, 1 = full history) * @return Output texture handle (internal) */ - TextureHandle denoise(TextureHandle input_texture, int radius); + TextureHandle denoise(TextureHandle input_texture, int radius, float temporal_weight = 0.0f); + + /** + * @brief Reset temporal history (call on scene change) + */ + void reset_history(); private: uint width_; uint height_; std::shared_ptr shader_; TextureHandle output_texture_; + TextureHandle history_texture_; // Previous frame for temporal accumulation + bool history_valid_; // Whether history contains valid data bool initialized_; // Create output texture diff --git a/include/core/raytracer.h b/include/core/raytracer.h index 00d9a23..293a3db 100644 --- a/include/core/raytracer.h +++ b/include/core/raytracer.h @@ -89,10 +89,19 @@ private: uint height_; RayTracerConfig config_; + // Scene data hash for change detection + uint materials_hash_; + uint lights_hash_; + // Texture arrays for PBR materials GLuint texture_arrays_[6]; // albedo, normal, metallic, roughness, ao, emission uint texture_array_sizes_[6]; // Number of textures in each array + // Texture array caching (content hash based) + uint texture_config_hash_; // Hash of entire texture configuration + uint texture_slot_hashes_[6]; // Hash per slot for incremental rebuild + bool texture_arrays_dirty_; // Dirty flag for texture arrays + std::shared_ptr compute_shader_; TextureHandle accumulation_texture_; BufferHandle material_buffer_; @@ -104,9 +113,6 @@ private: Buffer bvh_triangle_buffer_; bool bvh_built_; - uint materials_hash_; - uint lights_hash_; - uint frame_count_; bool initialized_; diff --git a/scripts/code_counter.sh b/scripts/code_counter.sh index 8d6aabc..b716550 100644 --- a/scripts/code_counter.sh +++ b/scripts/code_counter.sh @@ -51,18 +51,19 @@ TYPE_EXTS["Other Code"]='go rs rb php swift kt scala' # Show usage show_usage() { - printf "Usage: %s [OPTIONS] [DIRECTORY]\n\n" "$(basename "$0")" + printf "Usage: %s [OPTIONS] [DIRECTORY...]\n\n" "$(basename "$0")" printf "Options:\n" printf " -h, --help Show this help message\n" - printf " -d, --dir DIR Specify directory to scan\n" + printf " -d, --dir DIR Specify directory to scan (can be used multiple times)\n" printf "\nExamples:\n" printf " %s # Scan current directory\n" "$(basename "$0")" printf " %s /path/to/project # Scan specified directory\n" "$(basename "$0")" - printf " %s -d /path/to/project\n" "$(basename "$0")" + printf " %s dir1 dir2 dir3 # Scan multiple directories\n" "$(basename "$0")" + printf " %s -d dir1 -d dir2 # Scan multiple directories using -d option\n" "$(basename "$0")" } # Parse command line arguments -scan_dir="" +scan_dirs=() while [[ $# -gt 0 ]]; do case "$1" in -h|--help) @@ -71,7 +72,7 @@ while [[ $# -gt 0 ]]; do ;; -d|--dir) if [[ -n "${2:-}" ]]; then - scan_dir="$2" + scan_dirs+=("$2") shift 2 else printf "Error: --dir requires a directory argument\n" >&2 @@ -84,42 +85,41 @@ while [[ $# -gt 0 ]]; do exit 1 ;; *) - if [[ -z "$scan_dir" ]]; then - scan_dir="$1" - else - printf "Error: Multiple directories specified\n" >&2 - exit 1 - fi + scan_dirs+=("$1") shift ;; esac done # Default to current directory if not specified -if [[ -z "$scan_dir" ]]; then - scan_dir="$(pwd)" +if [[ ${#scan_dirs[@]} -eq 0 ]]; then + scan_dirs=("$(pwd)") fi -# Validate directory -if [[ ! -d "$scan_dir" ]]; then - printf "Error: Directory does not exist: %s\n" "$scan_dir" >&2 - exit 1 -fi +# Validate directories and convert to absolute paths +target_dirs=() +for scan_dir in "${scan_dirs[@]}"; do + if [[ ! -d "$scan_dir" ]]; then + printf "Error: Directory does not exist: %s\n" "$scan_dir" >&2 + exit 1 + fi -# Convert to absolute path using readlink or realpath -if command -v realpath >/dev/null 2>&1; then - target_dir="$(realpath "$scan_dir")" -elif command -v readlink >/dev/null 2>&1 && readlink -f / >/dev/null 2>&1; then - target_dir="$(readlink -f "$scan_dir")" -else - # Fallback: use cd + pwd - target_dir="$(cd "$scan_dir" && pwd)" -fi + # Convert to absolute path using readlink or realpath + if command -v realpath >/dev/null 2>&1; then + target_dir="$(realpath "$scan_dir")" + elif command -v readlink >/dev/null 2>&1 && readlink -f / >/dev/null 2>&1; then + target_dir="$(readlink -f "$scan_dir")" + else + # Fallback: use cd + pwd + target_dir="$(cd "$scan_dir" && pwd)" + fi -if [[ -z "$target_dir" || ! -d "$target_dir" ]]; then - printf "Error: Cannot resolve directory: %s\n" "$scan_dir" >&2 - exit 1 -fi + if [[ -z "$target_dir" || ! -d "$target_dir" ]]; then + printf "Error: Cannot resolve directory: %s\n" "$scan_dir" >&2 + exit 1 + fi + target_dirs+=("$target_dir") +done # Build suffix → type mapping (lowercase, without dot) declare -A SUFFIX_TYPE @@ -144,7 +144,16 @@ printf "%b========================================%b\n" "${COLOR_MAP[White]}" "$ printf "%b Code Line Counter%b\n" "${COLOR_MAP[Yellow]}" "${COLOR_MAP[Reset]}" printf "%b========================================%b\n\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}" -printf "%bScanning directory: %s%b\n\n" "${COLOR_MAP[Gray]}" "$target_dir" "${COLOR_MAP[Reset]}" +# Display directories to scan +if [[ ${#target_dirs[@]} -eq 1 ]]; then + printf "%bScanning directory: %s%b\n\n" "${COLOR_MAP[Gray]}" "${target_dirs[0]}" "${COLOR_MAP[Reset]}" +else + printf "%bScanning %d directories:%b\n" "${COLOR_MAP[Gray]}" "${#target_dirs[@]}" "${COLOR_MAP[Reset]}" + for dir in "${target_dirs[@]}"; do + printf " • %s\n" "$dir" + done + printf "\n" +fi # Statistics variables total_lines=0 @@ -171,35 +180,37 @@ ALL_FILES=() # Use find to get all files, then filter in bash # This is more reliable than multiple find calls with -iname -while IFS= read -r -d '' file; do - # Skip empty - [[ -z "$file" ]] && continue - - # Get filename and extension - filename="${file##*/}" - - # Skip files without extension - [[ "$filename" != *.* ]] && continue - - # Get extension (lowercase, without dot) - ext="${filename##*.}" - ext_lower="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')" - - # Check if this extension is in our list - if [[ -n "${SUFFIX_TYPE[$ext_lower]+isset}" ]]; then - # Deduplicate - if [[ -z "${SEEN_FILES["$file"]+isset}" ]]; then - SEEN_FILES["$file"]=1 - ALL_FILES+=("$file") +for target_dir in "${target_dirs[@]}"; do + while IFS= read -r -d '' file; do + # Skip empty + [[ -z "$file" ]] && continue + + # Get filename and extension + filename="${file##*/}" + + # Skip files without extension + [[ "$filename" != *.* ]] && continue + + # Get extension (lowercase, without dot) + ext="${filename##*.}" + ext_lower="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')" + + # Check if this extension is in our list + if [[ -n "${SUFFIX_TYPE[$ext_lower]+isset}" ]]; then + # Deduplicate + if [[ -z "${SEEN_FILES["$file"]+isset}" ]]; then + SEEN_FILES["$file"]=1 + ALL_FILES+=("$file") + fi fi - fi -done < <(find "$target_dir" -type f -print0 2>/dev/null) + done < <(find "$target_dir" -type f -print0 2>/dev/null) +done total_to_process=${#ALL_FILES[@]} if (( total_to_process == 0 )); then printf "%bNo source code files found!%b\n" "${COLOR_MAP[Red]}" "${COLOR_MAP[Reset]}" - printf "%bPlease check if the directory contains source code files.%b\n" "${COLOR_MAP[Gray]}" "${COLOR_MAP[Reset]}" + printf "%bPlease check if the directories contain source code files.%b\n" "${COLOR_MAP[Gray]}" "${COLOR_MAP[Reset]}" exit 0 fi @@ -343,7 +354,8 @@ printf "\n" read -r -p "Export statistics to CSV file? (Y/N) " exportChoice if [[ "$exportChoice" == "Y" || "$exportChoice" == "y" ]]; then timestamp="$(date +%Y%m%d_%H%M%S)" - csv_path="${target_dir}/code_stats_${timestamp}.csv" + # Use the first directory as the export location + csv_path="${target_dirs[0]}/code_stats_${timestamp}.csv" { # CSV Header for file details diff --git a/shaders/gbuffer/gbuffer.frag b/shaders/gbuffer/gbuffer.frag index 3661108..3d28a4d 100644 --- a/shaders/gbuffer/gbuffer.frag +++ b/shaders/gbuffer/gbuffer.frag @@ -8,7 +8,7 @@ in VS_OUT { } fs_in; layout(location = 0) out vec4 g_position; -layout(location = 1) out vec4 g_normal; +layout(location = 1) out vec2 g_normal; // Octahedral encoded normal (RG32F) layout(location = 2) out vec4 g_albedo; layout(location = 3) out vec4 g_material; layout(location = 4) out uint g_material_id; @@ -26,11 +26,26 @@ uniform uint u_material_id; uniform bool u_has_albedo_map; uniform sampler2D u_albedo_map; +// Octahedral encode a unit vector to 2D coordinates in [0, 1] range +vec2 oct_encode(vec3 n) { + float sum = abs(n.x) + abs(n.y) + abs(n.z); + n /= sum; + + if (n.z < 0.0) { + float x = n.x; + float y = n.y; + n.x = (1.0 - abs(y)) * (x >= 0.0 ? 1.0 : -1.0); + n.y = (1.0 - abs(x)) * (y >= 0.0 ? 1.0 : -1.0); + } + + return n.xy * 0.5 + 0.5; +} + void main() { g_position = vec4(fs_in.frag_pos, 1.0); vec3 n = normalize(fs_in.normal); - g_normal = vec4(n, 0.0); + g_normal = oct_encode(n); // Encode normal as 2D coordinates vec3 albedo = u_albedo; if (u_has_albedo_map) { diff --git a/shaders/include/bvh.glsl b/shaders/include/bvh.glsl index 58855b8..ffbc39d 100644 --- a/shaders/include/bvh.glsl +++ b/shaders/include/bvh.glsl @@ -3,6 +3,18 @@ #ifndef BVH_GLSL #define BVH_GLSL +// Octahedral decode: 2D coordinates in [0, 1] range to unit vector +vec3 oct_decode(vec2 f) { + f = f * 2.0 - 1.0; + + vec3 n = vec3(f.x, f.y, 1.0 - abs(f.x) - abs(f.y)); + float t = max(-n.z, 0.0); + n.x += n.x >= 0.0 ? -t : t; + n.y += n.y >= 0.0 ? -t : t; + + return normalize(n); +} + // Ray-AABB intersection bool intersect_aabb(Ray ray, vec3 aabb_min, vec3 aabb_max, float t_max) { vec3 inv_d = 1.0 / ray.direction; @@ -166,7 +178,11 @@ HitInfo trace_primary_gbuffer(Ray ray, ivec2 pixel_coords) { } vec3 p = pos.xyz; - vec3 n = normalize(imageLoad(g_normal, pixel_coords).xyz); + + // Decode octahedral normal from RG32F + vec2 oct_n = imageLoad(g_normal, pixel_coords).xy; + vec3 n = oct_decode(oct_n); + uint mid = imageLoad(g_material_id, pixel_coords).r; vec4 mat = imageLoad(g_material, pixel_coords); int mtype = int(mat.w); diff --git a/shaders/include/material.glsl b/shaders/include/material.glsl index 9da1afb..9f95c59 100644 --- a/shaders/include/material.glsl +++ b/shaders/include/material.glsl @@ -69,6 +69,14 @@ float fresnel_dielectric(float cos_theta, float ior) { return r0 + (1.0 - r0) * pow(1.0 - cos_theta, 5.0); } +// GGX/Trowbridge-Reitz normal distribution function +float distribution_ggx(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float d = NdotH * NdotH * (a2 - 1.0) + 1.0; + return a2 / (PI * d * d); +} + // Scatter functions ScatterResult scatter_diffuse(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult r; @@ -86,14 +94,40 @@ ScatterResult scatter_diffuse(Ray ray_in, HitInfo hit, Material mat, inout uint ScatterResult scatter_metal(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult r; - vec3 reflected = reflect_vector(normalize(ray_in.direction), hit.normal); - vec3 fuzz = mat.roughness * random_in_unit_sphere(seed); - vec3 dir = reflected + fuzz; + vec3 V = normalize(-ray_in.direction); + vec3 N = hit.normal; - r.scattered = dot(dir, hit.normal) > 0.0; - r.attenuation = mat.albedo; - r.scattered_ray.origin = hit.position + hit.normal * EPSILON; - r.scattered_ray.direction = normalize(dir); + // Clamp roughness to avoid division by zero + float roughness = max(mat.roughness, 0.04); + + // Sample microfacet normal using GGX importance sampling + vec3 H = sample_ggx_half_vector(roughness, N, seed); + + // Reflect view direction around half vector + vec3 L = reflect(-V, H); + + // Check if reflected direction is above surface + float NdotL = dot(N, L); + if (NdotL <= 0.0) { + r.scattered = false; + r.attenuation = vec3(0.0); + return r; + } + + float NdotV = max(dot(N, V), 0.001); + float HdotV = max(dot(H, V), 0.001); + + // Fresnel term (using albedo as F0 for metals) + vec3 F = fresnel_schlick(HdotV, mat.albedo); + + // With proper GGX importance sampling of H, the BRDF contribution + // simplifies to just the Fresnel term. + // The D and geometry terms are canceled by the PDF. + r.attenuation = F; + + r.scattered = true; + r.scattered_ray.origin = hit.position + N * EPSILON; + r.scattered_ray.direction = normalize(L); return r; } diff --git a/shaders/include/rng.glsl b/shaders/include/rng.glsl index 582c658..71c3447 100644 --- a/shaders/include/rng.glsl +++ b/shaders/include/rng.glsl @@ -9,6 +9,19 @@ uint pcg_hash(uint seed) { return (word >> 22u) ^ word; } +// Improved seed initialization with spatial-temporal decorrelation +uint init_seed(ivec2 pixel_coords, ivec2 image_size, uint frame_count) { + uint pixel_index = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x); + + // Spatial hash to decorrelate neighboring pixels + uint spatial = pcg_hash(pixel_index); + + // Temporal hash with pixel-dependent multiplier to avoid frame correlation + uint temporal = frame_count * (spatial | 1u); // OR with 1 to ensure non-zero multiplier + + return pcg_hash(spatial + temporal); +} + float random_float(inout uint seed) { seed = pcg_hash(seed); return float(seed) / 4294967296.0; diff --git a/shaders/include/sampling.glsl b/shaders/include/sampling.glsl index 00a2c38..c23c57c 100644 --- a/shaders/include/sampling.glsl +++ b/shaders/include/sampling.glsl @@ -15,4 +15,47 @@ vec3 random_unit_vector(inout uint seed) { return normalize(random_in_unit_sphere(seed)); } +// Build orthonormal basis from normal vector +mat3 build_onb(vec3 N) { + vec3 up = (abs(N.y) < 0.999) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0); + vec3 T = normalize(cross(up, N)); + vec3 B = cross(N, T); + return mat3(T, B, N); +} + +// GGX importance sampling: sample microfacet normal (half vector) +// Returns sampled half vector in world space +vec3 sample_ggx_half_vector(float roughness, vec3 N, inout uint seed) { + float a = roughness * roughness; + float a2 = a * a; + + float u1 = random_float(seed); + float u2 = random_float(seed); + + // Clamp to avoid numerical issues at boundaries + u1 = clamp(u1, 0.001, 0.999); + + // Spherical coordinates from GGX distribution + float cos_theta = sqrt((1.0 - u1) / ((a2 - 1.0) * u1 + 1.0)); + cos_theta = clamp(cos_theta, 0.0, 1.0); + float sin_theta = sqrt(max(0.0, 1.0 - cos_theta * cos_theta)); + float phi = 2.0 * PI * u2; + + // Convert to Cartesian in tangent space + vec3 H_tangent = vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); + + // Transform to world space + mat3 onb = build_onb(N); + return normalize(onb * H_tangent); +} + +// GGX importance sampling PDF +float ggx_pdf(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float denom = NdotH * NdotH * (a2 - 1.0) + 1.0; + float D = a2 / (PI * denom * denom); + return D * NdotH; +} + #endif // SAMPLING_GLSL diff --git a/shaders/include/sobol.glsl b/shaders/include/sobol.glsl new file mode 100644 index 0000000..c92b918 --- /dev/null +++ b/shaders/include/sobol.glsl @@ -0,0 +1,154 @@ +// Sobol Low-Discrepancy Sequence +// Provides O(1/n) convergence vs O(1/sqrt(n)) for white noise + +#ifndef SOBOL_GLSL +#define SOBOL_GLSL + +// Sobol direction numbers for first 8 dimensions (32-bit) +// Each dimension has 32 direction numbers (one per bit) +// Using the standard Sobol initialization with primitive polynomials + +// Dimension 0: primitive polynomial x (degree 1) +const uint sobol_dirs_0[32] = uint[32]( + 0x80000000u, 0x40000000u, 0x20000000u, 0x10000000u, + 0x08000000u, 0x04000000u, 0x02000000u, 0x01000000u, + 0x00800000u, 0x00400000u, 0x00200000u, 0x00100000u, + 0x00080000u, 0x00040000u, 0x00020000u, 0x00010000u, + 0x00008000u, 0x00004000u, 0x00002000u, 0x00001000u, + 0x00000800u, 0x00000400u, 0x00000200u, 0x00000100u, + 0x00000080u, 0x00000040u, 0x00000020u, 0x00000010u, + 0x00000008u, 0x00000004u, 0x00000002u, 0x00000001u +); + +// Dimension 1: primitive polynomial 1+x (degree 2) +const uint sobol_dirs_1[32] = uint[32]( + 0x80000000u, 0xc0000000u, 0xa0000000u, 0xf0000000u, + 0x88000000u, 0xcc000000u, 0xaa000000u, 0xff000000u, + 0x80800000u, 0xc0c00000u, 0xa0a00000u, 0xf0f00000u, + 0x88880000u, 0xcccc0000u, 0xaaaa0000u, 0xffff0000u, + 0x80008000u, 0xc000c000u, 0xa000a000u, 0xf000f000u, + 0x88008800u, 0xcc00cc00u, 0xaa00aa00u, 0xff00ff00u, + 0x80808080u, 0xc0c0c0c0u, 0xa0a0a0a0u, 0xf0f0f0f0u, + 0x88888888u, 0xccccccccu, 0xaaaaaaaau, 0xffffffffu +); + +// Dimension 2: primitive polynomial 1+x+x^2 (degree 3) +const uint sobol_dirs_2[32] = uint[32]( + 0x80000000u, 0xc0000000u, 0x60000000u, 0x90000000u, + 0xe8000000u, 0x5c000000u, 0x86000000u, 0xc9000000u, + 0x6e800000u, 0x95c00000u, 0xe8600000u, 0x5c900000u, + 0x86e80000u, 0xc95c0000u, 0x6e860000u, 0x95c90000u, + 0xe86e8000u, 0x5c95c000u, 0x86e86000u, 0xc95c9000u, + 0x6e86e800u, 0x95c95c00u, 0xe86e8600u, 0x5c95c900u, + 0x86e86e80u, 0xc95c95c0u, 0x6e86e860u, 0x95c95c90u, + 0xe86e86e8u, 0x5c95c95cu, 0x86e86e86u, 0xc95c95c9u +); + +// Dimension 3: primitive polynomial 1+x+x^3 (degree 3) +const uint sobol_dirs_3[32] = uint[32]( + 0x80000000u, 0x40000000u, 0x20000000u, 0xd0000000u, + 0xf8000000u, 0x6c000000u, 0x9a000000u, 0xc1000000u, + 0x78800000u, 0xb4c00000u, 0x52600000u, 0xa9100000u, + 0xd0880000u, 0xe84c0000u, 0x6ca60000u, 0x9a110000u, + 0xc1088000u, 0x7884c000u, 0xb4c26000u, 0x52691000u, + 0xa9108800u, 0xd0884c00u, 0xe84c2600u, 0x6ca69100u, + 0x9a110880u, 0xc10884c0u, 0x7884c260u, 0xb4c26910u, + 0x52691088u, 0xa910884cu, 0xd0884c26u, 0xe84c2691u +); + +// Dimension 4: primitive polynomial 1+x^2+x^3 (degree 3) +const uint sobol_dirs_4[32] = uint[32]( + 0x80000000u, 0xc0000000u, 0xa0000000u, 0x50000000u, + 0xb8000000u, 0x6c000000u, 0x86000000u, 0x43000000u, + 0xa1800000u, 0x5ec00000u, 0xb0600000u, 0x6c100000u, + 0x86a80000u, 0x435c0000u, 0xa1860000u, 0x5ec10000u, + 0xb06a8000u, 0x6c15c000u, 0x86a86000u, 0x435c1000u, + 0xa186a800u, 0x5ec15c00u, 0xb06a8600u, 0x6c15c100u, + 0x86a86a80u, 0x435c15c0u, 0xa186a860u, 0x5ec15c10u, + 0xb06a86a8u, 0x6c15c15cu, 0x86a86a86u, 0x435c15c1u +); + +// Dimension 5: primitive polynomial 1+x+x^2+x^4 (degree 4) +const uint sobol_dirs_5[32] = uint[32]( + 0x80000000u, 0x40000000u, 0x20000000u, 0x10000000u, + 0xf8000000u, 0xdc000000u, 0x6a000000u, 0x35000000u, + 0x1a800000u, 0x8dc00000u, 0x46a00000u, 0x23500000u, + 0x11a80000u, 0xf8dc0000u, 0xdc6a0000u, 0x6a350000u, + 0x351a8000u, 0x1a8dc000u, 0x8dc6a000u, 0x46a35000u, + 0x2351a800u, 0x11a8dc00u, 0xf8dc6a00u, 0xdc6a3500u, + 0x6a351a80u, 0x351a8dc0u, 0x1a8dc6a0u, 0x8dc6a350u, + 0x46a351a8u, 0x2351a8dcu, 0x11a8dc6au, 0xf8dc6a35u +); + +// Dimension 6: primitive polynomial 1+x+x^3+x^4 (degree 4) +const uint sobol_dirs_6[32] = uint[32]( + 0x80000000u, 0xc0000000u, 0xe0000000u, 0x70000000u, + 0x38000000u, 0x9c000000u, 0x4e000000u, 0xa7000000u, + 0xd3800000u, 0x69c00000u, 0xb4e00000u, 0x5a700000u, + 0x2d380000u, 0x169c0000u, 0x8b4e0000u, 0x45a70000u, + 0xa2d38000u, 0xd169c000u, 0x68b4e000u, 0xb45a7000u, + 0x5a2d3800u, 0x2d169c00u, 0x168b4e00u, 0x8b45a700u, + 0x45a2d380u, 0xa2d169c0u, 0xd168b4e0u, 0x68b45a70u, + 0xb45a2d38u, 0x5a2d169cu, 0x2d168b4eu, 0x168b45a7u +); + +// Dimension 7: primitive polynomial 1+x^2+x^4 (degree 4) +const uint sobol_dirs_7[32] = uint[32]( + 0x80000000u, 0xc0000000u, 0xa0000000u, 0x50000000u, + 0x28000000u, 0x14000000u, 0x8a000000u, 0x45000000u, + 0xa2800000u, 0xd1400000u, 0xe8a00000u, 0x74500000u, + 0x3a280000u, 0x1d140000u, 0x8e8a0000u, 0x47450000u, + 0x23a28000u, 0x11d14000u, 0x88e8a000u, 0x44745000u, + 0xa23a2800u, 0xd11d1400u, 0xe88e8a00u, 0x74474500u, + 0x3a23a280u, 0x1d11d140u, 0x8e88e8a0u, 0x47447450u, + 0x23a23a28u, 0x11d11d14u, 0x88e88e8au, 0x44744745u +); + +// Access direction numbers by dimension +uint sobol_direction(uint dimension, uint bit) { + if (dimension == 0u) return sobol_dirs_0[bit]; + if (dimension == 1u) return sobol_dirs_1[bit]; + if (dimension == 2u) return sobol_dirs_2[bit]; + if (dimension == 3u) return sobol_dirs_3[bit]; + if (dimension == 4u) return sobol_dirs_4[bit]; + if (dimension == 5u) return sobol_dirs_5[bit]; + if (dimension == 6u) return sobol_dirs_6[bit]; + if (dimension == 7u) return sobol_dirs_7[bit]; + return sobol_dirs_0[bit]; // Fallback +} + +// Owen scrambling for decorrelation +uint owen_scramble(uint value, uint seed) { + // Simple hash-based Owen scrambling + uint s = seed ^ value; + s ^= s >> 16; + s *= 0x45d9f3bu; + s ^= s >> 16; + s *= 0x45d9f3bu; + s ^= s >> 16; + return s; +} + +// Generate Sobol value for given index and dimension +// index: sample index (0, 1, 2, ...) +// dimension: which dimension (0, 1, 2, ...) +// scramble_seed: seed for Owen scrambling +float sobol_get(uint index, uint dimension, uint scramble_seed) { + uint result = 0u; + uint i = index; + + // Gray code iteration + for (uint bit = 0u; bit < 32u; bit++) { + if ((i & 1u) != 0u) { + result ^= sobol_direction(dimension, bit); + } + i >>= 1u; + } + + // Apply Owen scrambling + result = owen_scramble(result, scramble_seed); + + return float(result) / 4294967296.0; +} + +#endif // SOBOL_GLSL diff --git a/shaders/postprocess/denoiser.comp b/shaders/postprocess/denoiser.comp index b7fa895..f9f886b 100644 --- a/shaders/postprocess/denoiser.comp +++ b/shaders/postprocess/denoiser.comp @@ -4,8 +4,11 @@ layout(local_size_x = 16, local_size_y = 16) in; layout(binding = 0, rgba32f) uniform readonly image2D u_input; layout(binding = 1, rgba32f) uniform writeonly image2D u_output; +layout(binding = 2, rgba32f) uniform readonly image2D u_history; -uniform int u_radius; // 1 => 3x3, 2 => 5x5 +uniform int u_radius; // 1 => 3x3, 2 => 5x5 +uniform float u_temporal_weight; // 0 = no temporal, 1 = full history +uniform bool u_has_history; // Whether history texture is valid // Gaussian weight based on distance float gaussian_weight(float dist, float sigma) { @@ -48,6 +51,18 @@ void main() { } } - vec3 out_color = sum / max(weight_sum, 1e-6); - imageStore(u_output, p, vec4(out_color, 1.0)); + vec3 denoised_color = sum / max(weight_sum, 1e-6); + + // Temporal accumulation: blend with history + if (u_has_history && u_temporal_weight > 0.0) { + vec3 history_color = imageLoad(u_history, p).rgb; + + // Simple exponential moving average + // temporal_weight controls how much history to keep (higher = smoother but more ghosting) + vec3 final_color = mix(denoised_color, history_color, u_temporal_weight); + + imageStore(u_output, p, vec4(final_color, 1.0)); + } else { + imageStore(u_output, p, vec4(denoised_color, 1.0)); + } } diff --git a/shaders/raytracing/raytracing.comp b/shaders/raytracing/raytracing.comp index 57c52c4..dd836b3 100644 --- a/shaders/raytracing/raytracing.comp +++ b/shaders/raytracing/raytracing.comp @@ -5,6 +5,7 @@ #include "../include/structs.glsl" #include "../include/math.glsl" #include "../include/rng.glsl" +#include "../include/sobol.glsl" #include "../include/sampling.glsl" // Workgroup size @@ -12,7 +13,7 @@ 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 = 1, rg32f) uniform readonly image2D g_normal; // Octahedral encoded layout(binding = 5, rgba32f) uniform readonly image2D g_material; layout(binding = 6, r32ui) uniform readonly uimage2D g_material_id; layout(binding = 7, rgba32f) uniform readonly image2D g_texcoord; @@ -52,6 +53,160 @@ layout(binding = 15) uniform sampler2DArray u_texture_emission_array; #include "../include/bvh.glsl" #include "../include/lighting.glsl" +// Sobol sampling state +struct SobolState { + uint sample_index; // Which sample (0, 1, 2, ...) + uint dimension; // Current dimension being used + uint scramble; // Seed for Owen scrambling +}; + +// Initialize Sobol state +SobolState init_sobol(uint pixel_index, uint frame, uint sample_idx) { + SobolState state; + // Sample index combines frame and sample number for temporal variation + // Add +1 to avoid degenerate index 0 (Sobol at index 0 produces all zeros before scrambling) + // Add pixel_index offset to ensure spatial variation within same frame + state.sample_index = sample_idx + frame * 1024u + pixel_index + 1u; + state.dimension = 0u; + // Use pixel index for per-pixel Owen scrambling + state.scramble = pcg_hash(pixel_index + frame * 668265263u); + return state; +} + +// Get next Sobol float in [0, 1) +float sobol_next(inout SobolState state) { + float value; + if (state.dimension < 8u) { + // Use Sobol for first 8 dimensions + value = sobol_get(state.sample_index, state.dimension, state.scramble); + } else { + // Fall back to PCG for higher dimensions + uint rng_state = pcg_hash(state.scramble + state.dimension * 2654435761u); + value = float(rng_state) / 4294967296.0; + } + state.dimension++; + return value; +} + +// Sobol-based random in unit sphere +vec3 sobol_in_unit_sphere(inout SobolState state) { + float z = 1.0 - 2.0 * sobol_next(state); + float r = sqrt(max(0.0, 1.0 - z * z)); + float phi = 2.0 * PI * sobol_next(state); + return vec3(r * cos(phi), r * sin(phi), z); +} + +// Sobol-based unit vector +vec3 sobol_unit_vector(inout SobolState state) { + return normalize(sobol_in_unit_sphere(state)); +} + +// Sobol-based GGX half vector sampling +vec3 sobol_ggx_half_vector(float roughness, vec3 N, inout SobolState state) { + float a = roughness * roughness; + float a2 = a * a; + + float u1 = clamp(sobol_next(state), 0.001, 0.999); + float u2 = sobol_next(state); + + float cos_theta = sqrt((1.0 - u1) / ((a2 - 1.0) * u1 + 1.0)); + cos_theta = clamp(cos_theta, 0.0, 1.0); + float sin_theta = sqrt(max(0.0, 1.0 - cos_theta * cos_theta)); + float phi = 2.0 * PI * u2; + + vec3 H_tangent = vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); + mat3 onb = build_onb(N); + return normalize(onb * H_tangent); +} + +// Sobol-based diffuse scattering +ScatterResult scatter_diffuse_sobol(Ray ray_in, HitInfo hit, Material mat, inout SobolState state) { + ScatterResult r; + r.scattered = true; + r.attenuation = mat.albedo; + + vec3 dir = hit.normal + sobol_unit_vector(state); + if (near_zero(dir)) dir = hit.normal; + + r.scattered_ray.origin = hit.position + hit.normal * EPSILON; + r.scattered_ray.direction = normalize(dir); + return r; +} + +// Sobol-based metal scattering (GGX) +ScatterResult scatter_metal_sobol(Ray ray_in, HitInfo hit, Material mat, inout SobolState state) { + ScatterResult r; + + vec3 V = normalize(-ray_in.direction); + vec3 N = hit.normal; + float roughness = clamp(mat.roughness, 0.04, 1.0); + + vec3 H = sobol_ggx_half_vector(roughness, N, state); + if (dot(H, N) < 0.0) H = -H; + + vec3 L = reflect(-V, H); + + float NdotL = dot(N, L); + if (NdotL <= 0.0) { + r.scattered = false; + r.attenuation = vec3(0.0); + return r; + } + + float HdotV = max(dot(H, V), 0.001); + vec3 F = fresnel_schlick(HdotV, mat.albedo); + + r.attenuation = clamp(F, vec3(0.0), vec3(1.0)); + r.scattered = true; + r.scattered_ray.origin = hit.position + N * EPSILON; + r.scattered_ray.direction = normalize(L); + return r; +} + +// Sobol-based dielectric scattering +ScatterResult scatter_dielectric_sobol(Ray ray_in, HitInfo hit, Material mat, inout SobolState state) { + ScatterResult r; + r.scattered = true; + r.attenuation = vec3(1.0); + + vec3 unit_dir = normalize(ray_in.direction); + float cos_theta = dot(-unit_dir, hit.normal); + float sin_theta = sqrt(max(0.0, 1.0 - cos_theta * cos_theta)); + + bool entering = cos_theta > 0.0; + float eta = entering ? (1.0 / mat.ior) : mat.ior; + vec3 normal = entering ? hit.normal : -hit.normal; + + float sin_theta_t = eta * sin_theta; + bool total_internal_reflection = sin_theta_t >= 1.0; + + float f0 = pow((1.0 - mat.ior) / (1.0 + mat.ior), 2.0); + float f = f0 + (1.0 - f0) * pow(1.0 - abs(cos_theta), 5.0); + + vec3 dir; + if (total_internal_reflection || sobol_next(state) < f) { + dir = reflect_vector(unit_dir, normal); + } else { + dir = refract_vector(unit_dir, normal, eta); + } + + r.scattered_ray.origin = hit.position + dir * EPSILON; + r.scattered_ray.direction = normalize(dir); + return r; +} + +// Sobol-based scatter dispatcher +ScatterResult scatter_ray_sobol(Ray ray_in, HitInfo hit, Material mat, inout SobolState state) { + if (mat.type == MATERIAL_DIFFUSE) return scatter_diffuse_sobol(ray_in, hit, mat, state); + if (mat.type == MATERIAL_METAL) return scatter_metal_sobol(ray_in, hit, mat, state); + if (mat.type == MATERIAL_DIELECTRIC) return scatter_dielectric_sobol(ray_in, hit, mat, state); + + ScatterResult r; + r.scattered = false; + r.attenuation = vec3(0.0); + return r; +} + // Generate camera ray (center pixel, no jitter) Ray generate_camera_ray(ivec2 pixel_coords, ivec2 image_size) { vec2 uv = (vec2(pixel_coords) + vec2(0.5)) / vec2(image_size); @@ -68,8 +223,8 @@ Ray generate_camera_ray(ivec2 pixel_coords, ivec2 image_size) { return r; } -// Path tracing with G-Buffer acceleration for primary ray -vec3 trace_path_primary_gbuffer(ivec2 pixel_coords, ivec2 image_size, inout uint seed) { +// Path tracing with G-Buffer acceleration for primary ray (Sobol sampling) +vec3 trace_path_sobol(ivec2 pixel_coords, ivec2 image_size, inout SobolState sobol) { Ray ray = generate_camera_ray(pixel_coords, image_size); vec3 radiance = vec3(0.0); @@ -88,7 +243,7 @@ vec3 trace_path_primary_gbuffer(ivec2 pixel_coords, ivec2 image_size, inout uint radiance += throughput * mat0.emission; - ScatterResult sc0 = scatter_ray(ray, hit0, mat0, seed); + ScatterResult sc0 = scatter_ray_sobol(ray, hit0, mat0, sobol); if (!sc0.scattered) return radiance; throughput *= sc0.attenuation; @@ -108,7 +263,7 @@ vec3 trace_path_primary_gbuffer(ivec2 pixel_coords, ivec2 image_size, inout uint radiance += throughput * mat.emission; - ScatterResult sc = scatter_ray(ray, hit, mat, seed); + ScatterResult sc = scatter_ray_sobol(ray, hit, mat, sobol); if (!sc.scattered) break; throughput *= sc.attenuation; @@ -116,7 +271,7 @@ vec3 trace_path_primary_gbuffer(ivec2 pixel_coords, ivec2 image_size, inout uint if (depth > 3u) { float p = max(throughput.r, max(throughput.g, throughput.b)); p = clamp(p, 0.0, 0.95); - if (p < RR_THRESHOLD || random_float(seed) > p) break; + if (p < RR_THRESHOLD || sobol_next(sobol) > p) break; throughput /= p; } @@ -143,14 +298,15 @@ void main() { ivec2 image_size = imageSize(output_image); if (pixel_coords.x >= image_size.x || pixel_coords.y >= image_size.y) return; - uint base_seed = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x); - uint seed = base_seed + u_frame_count * 719393u; + uint pixel_index = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x); + uint seed = init_seed(pixel_coords, image_size, u_frame_count); vec3 color = vec3(0.0); uint spp = max(u_samples_per_pixel, 1u); for (uint s = 0u; s < spp; ++s) { - color += trace_path_primary_gbuffer(pixel_coords, image_size, seed); + SobolState sobol = init_sobol(pixel_index, u_frame_count, s); + color += trace_path_sobol(pixel_coords, image_size, sobol); } color /= float(spp); diff --git a/src/core/denoiser.cpp b/src/core/denoiser.cpp index e0b5824..33c497d 100644 --- a/src/core/denoiser.cpp +++ b/src/core/denoiser.cpp @@ -10,6 +10,8 @@ Denoiser::Denoiser(uint width, uint height) : width_(width) , height_(height) , output_texture_(INVALID_HANDLE) + , history_texture_(INVALID_HANDLE) + , history_valid_(false) , initialized_(false) { } @@ -43,6 +45,12 @@ void Denoiser::release() { output_texture_ = INVALID_HANDLE; } + if (history_texture_ != INVALID_HANDLE) { + ResourceManager::instance().destroy_texture(history_texture_); + history_texture_ = INVALID_HANDLE; + } + + history_valid_ = false; initialized_ = false; } @@ -53,17 +61,27 @@ void Denoiser::resize(uint width, uint height) { if (!initialized_) return; + ResourceManager &rm = ResourceManager::instance(); + if (output_texture_ != INVALID_HANDLE) { - ResourceManager::instance().destroy_texture(output_texture_); + rm.destroy_texture(output_texture_); output_texture_ = INVALID_HANDLE; } + + if (history_texture_ != INVALID_HANDLE) { + rm.destroy_texture(history_texture_); + history_texture_ = INVALID_HANDLE; + } + + history_valid_ = false; create_output_texture_(); } -TextureHandle Denoiser::denoise(TextureHandle input_texture, int radius) { +TextureHandle Denoiser::denoise(TextureHandle input_texture, int radius, float temporal_weight) { if (!initialized_) return input_texture; radius = (radius < 0) ? 0 : radius; + temporal_weight = (temporal_weight < 0.0f) ? 0.0f : ((temporal_weight > 1.0f) ? 1.0f : temporal_weight); shader_->use(); @@ -71,6 +89,13 @@ TextureHandle Denoiser::denoise(TextureHandle input_texture, int radius) { glBindImageTexture(1, output_texture_, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); shader_->set_int("u_radius", radius); + shader_->set_float("u_temporal_weight", temporal_weight); + shader_->set_bool("u_has_history", history_valid_ && temporal_weight > 0.0f); + + // Bind history texture if available and temporal accumulation is enabled + if (history_valid_ && temporal_weight > 0.0f) { + glBindImageTexture(2, history_texture_, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); + } uint groups_x = (width_ + COMPUTE_GROUP_SIZE_X - 1) / COMPUTE_GROUP_SIZE_X; uint groups_y = (height_ + COMPUTE_GROUP_SIZE_Y - 1) / COMPUTE_GROUP_SIZE_Y; @@ -78,9 +103,37 @@ TextureHandle Denoiser::denoise(TextureHandle input_texture, int radius) { glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + // Copy output to history for next frame (if temporal accumulation is enabled) + if (temporal_weight > 0.0f) { + // Create history texture if it doesn't exist + if (history_texture_ == INVALID_HANDLE) { + ResourceManager &rm = ResourceManager::instance(); + TextureDescription desc; + desc.width = width_; + desc.height = height_; + desc.format = TextureFormat::RGBA32F; + desc.filter = TextureFilter::NEAREST; + desc.wrap = TextureWrap::CLAMP_TO_EDGE; + history_texture_ = rm.create_texture(desc); + } + + // Copy output to history using GPU (blit or compute) + // For simplicity, we'll just bind output as history for next frame + // This requires double buffering - let's swap the textures + std::swap(output_texture_, history_texture_); + history_valid_ = true; + + // Return the new output (which was history before swap) + return output_texture_; + } + return output_texture_; } +void Denoiser::reset_history() { + history_valid_ = false; +} + void Denoiser::create_output_texture_() { ResourceManager &rm = ResourceManager::instance(); TextureDescription desc; diff --git a/src/core/gbuffer.cpp b/src/core/gbuffer.cpp index 8895e39..8f41089 100644 --- a/src/core/gbuffer.cpp +++ b/src/core/gbuffer.cpp @@ -42,7 +42,7 @@ bool GBuffer::initialize() { tex_desc.format = TextureFormat::RGBA32F; textures_[GBUFFER_POSITION] = rm.create_texture(tex_desc); - tex_desc.format = TextureFormat::RGBA32F; + tex_desc.format = TextureFormat::RG32F; // Octahedral encoded normal textures_[GBUFFER_NORMAL] = rm.create_texture(tex_desc); tex_desc.format = TextureFormat::RGBA8; diff --git a/src/core/raytracer.cpp b/src/core/raytracer.cpp index 6cc4518..b38f4dd 100644 --- a/src/core/raytracer.cpp +++ b/src/core/raytracer.cpp @@ -16,21 +16,39 @@ namespace { } return h; } + + // Compute hash of texture pointers for a specific slot + uint compute_slot_texture_hash(const std::vector> &textures) { + if (textures.empty()) + return 0u; + // Hash the raw pointers to detect texture set changes + std::vector ptrs; + ptrs.reserve(textures.size()); + for (const auto &t : textures) { + ptrs.push_back(t.get()); + } + return fnv1a_hash_bytes(ptrs.data(), ptrs.size() * sizeof(void *)); + } } // namespace RayTracer::RayTracer(uint width, uint height, const RayTracerConfig &config) : width_(width) , height_(height) , config_(config) + , materials_hash_(0u) + , lights_hash_(0u) + , texture_config_hash_(0u) + , texture_arrays_dirty_(true) , accumulation_texture_(INVALID_HANDLE) , material_buffer_(INVALID_HANDLE) , light_buffer_(INVALID_HANDLE) , bvh_(nullptr) , bvh_built_(false) - , materials_hash_(0u) - , lights_hash_(0u) , frame_count_(0) , initialized_(false) { + for (int i = 0; i < 6; ++i) { + texture_slot_hashes_[i] = 0u; + } } RayTracer::~RayTracer() { @@ -394,7 +412,7 @@ void RayTracer::upload_scene_data_(const Scene &scene) { 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(1, gbuffer.get_texture(GBUFFER_NORMAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RG32F); // Octahedral encoded glBindImageTexture(5, gbuffer.get_texture(GBUFFER_MATERIAL), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); glBindImageTexture(6, gbuffer.get_texture(GBUFFER_MATERIAL_ID), 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI); @@ -416,7 +434,7 @@ void RayTracer::build_texture_arrays_(const Scene &scene) { for (int slot = 0; slot < 6; slot++) { auto tex = mat->get_texture(static_cast(slot)); if (tex && tex->is_valid()) { - // Check if texture already added + // Check if texture already added (use set for O(1) lookup) bool found = false; for (const auto &t : textures[slot]) { if (t.get() == tex.get()) { @@ -431,10 +449,42 @@ void RayTracer::build_texture_arrays_(const Scene &scene) { } } + // Compute hash for each slot and check if rebuild is needed + bool any_slot_dirty = false; + uint new_slot_hashes[6]; + for (int slot = 0; slot < 6; slot++) { + new_slot_hashes[slot] = compute_slot_texture_hash(textures[slot]); + if (new_slot_hashes[slot] != texture_slot_hashes_[slot]) { + any_slot_dirty = true; + } + } + + // If no slots changed, skip entire rebuild + if (!any_slot_dirty && !texture_arrays_dirty_) { + // Still need to bind existing arrays + for (int slot = 0; slot < 6; slot++) { + if (texture_arrays_[slot] != 0) { + glActiveTexture(GL_TEXTURE10 + slot); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture_arrays_[slot]); + } + } + return; + } + ResourceManager &rm = ResourceManager::instance(); - // Build arrays for each slot + // Build arrays only for dirty slots for (int slot = 0; slot < 6; slot++) { + // Skip if this slot hasn't changed + if (new_slot_hashes[slot] == texture_slot_hashes_[slot] && !texture_arrays_dirty_) { + // Bind existing array + if (texture_arrays_[slot] != 0) { + glActiveTexture(GL_TEXTURE10 + slot); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture_arrays_[slot]); + } + continue; + } + // Destroy previous texture array if exists if (texture_arrays_[slot] != 0) { rm.destroy_texture_array(texture_arrays_[slot]); @@ -443,6 +493,7 @@ void RayTracer::build_texture_arrays_(const Scene &scene) { if (textures[slot].empty()) { texture_array_sizes_[slot] = 0; + texture_slot_hashes_[slot] = 0u; continue; } @@ -466,7 +517,18 @@ void RayTracer::build_texture_arrays_(const Scene &scene) { } } } + + // Update slot hash + texture_slot_hashes_[slot] = new_slot_hashes[slot]; + + // Bind the newly created array + if (texture_arrays_[slot] != 0) { + glActiveTexture(GL_TEXTURE10 + slot); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture_arrays_[slot]); + } } + + texture_arrays_dirty_ = false; } } // namespace are diff --git a/src/core/renderer.cpp b/src/core/renderer.cpp index 022f6db..c127cf7 100644 --- a/src/core/renderer.cpp +++ b/src/core/renderer.cpp @@ -142,7 +142,9 @@ RenderStats Renderer::render(const Scene& scene, TextureHandle output_texture) { TextureHandle final_output = rt_output; if (config_.enable_denoising_ && denoiser_) { - final_output = denoiser_->denoise(rt_output, 1); // radius=1 => 3x3 mean + // Use temporal accumulation with weight 0.1 (10% blend of new frame) + float temporal_weight = 0.1f; + final_output = denoiser_->denoise(rt_output, 1, temporal_weight); } // Phase 4: Blit to screen if output is default framebuffer @@ -214,6 +216,11 @@ void Renderer::set_config(const RendererConfig &config) { void Renderer::notify_scene_changed(const Scene &scene) { raytracer_->reset_accumulation(); raytracer_->rebuild_bvh(scene); + + // Reset denoiser temporal history on scene change + if (denoiser_) { + denoiser_->reset_history(); + } } } // namespace are diff --git a/src/scene/mesh.cpp b/src/scene/mesh.cpp index fd16aa4..cbfa65e 100644 --- a/src/scene/mesh.cpp +++ b/src/scene/mesh.cpp @@ -135,7 +135,9 @@ void Mesh::compute_tangents() { return; } - std::fill(vertices_.begin(), vertices_.end(), Vertex {}); + for (auto &v : vertices_) { + v.tangent_ = Vec3(0.0f); + } for (size_t i = 0; i < indices_.size(); i += 3) { uint i0 = indices_[i];