feat: 实现纹理缓存、GGX BRDF、Sobol采样、时域降噪

### 纹理数组内容哈希缓存
- feat: 添加纹理配置哈希追踪,避免每帧重建纹理数组
- feat: 实现增量更新,只重建变化的纹理槽位
- fix: 消除 O(n²) 重复纹理线性搜索

### GGX 微表面 BRDF
- feat: 实现 GGX/Trowbridge-Reitz 法线分布函数
- feat: 添加 GGX 重要性采样替代简单扰动反射
- fix: 修复金属材质物理计算,提升收敛速度

### GBuffer 八面体法线编码
- feat: 法线从 RGBA32F 压缩到 RG32F,带宽减少 50%
- feat: 添加八面体编码/解码函数 (encoding.h)
- fix: 更新 GBuffer 着色器和绑定格式

### Sobol 低差异序列采样
- feat: 实现 8 维 Sobol 序列 + Owen 置乱
- feat: 收敛速度从 O(1/√n) 提升到 O(1/n)
- fix: 改进 PCG 种子策略,减少帧间相关性

### 降噪器时域累积
- feat: 添加历史帧纹理和 EMA 混合
- fix: 场景变化时自动重置历史
- fix: 显著减少闪烁,提升视觉稳定性
master
ternaryop8479 2026-04-04 22:21:42 +08:00
parent 58d6184085
commit 6d9d95ddad
22 changed files with 1312 additions and 106 deletions

View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,562 @@
#include <core/renderer.h>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <iostream>
#include <memory>
#include <scene/camera.h>
#include <scene/light.h>
#include <scene/material.h>
#include <scene/mesh.h>
#include <scene/scene.h>
#include <utils/logger.h>
using namespace are;
// Window dimensions
const uint WINDOW_WIDTH = 800;
const uint WINDOW_HEIGHT = 800;
// Global state
GLFWwindow *g_window = nullptr;
std::unique_ptr<Renderer> g_renderer = nullptr;
std::unique_ptr<Scene> g_scene = nullptr;
std::shared_ptr<Camera> 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<Mesh> 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<Mesh>();
std::vector<Vertex> 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<uint> 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<Mesh> create_box(const Vec3 &min, const Vec3 &max, uint material_id) {
auto mesh = std::make_shared<Mesh>();
std::vector<Vertex> 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<uint> 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<Mesh> create_sphere(float radius, uint segments, uint rings, uint material_id) {
auto mesh = std::make_shared<Mesh>();
std::vector<Vertex> vertices;
std::vector<uint> indices;
for (uint ring = 0; ring <= rings; ++ring) {
float theta = ring * glm::pi<float>() / 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<float>() / 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<Scene>();
// Create materials
// 0: White diffuse
auto white_material = std::make_shared<Material>();
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<Material>();
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<Material>();
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<Material>();
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<Material>();
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<Material>();
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<Camera>();
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<float>(WINDOW_WIDTH) / WINDOW_HEIGHT, 0.1f, 100.0f);
g_scene->set_camera(g_camera);
// Add point light
auto light = std::make_shared<Light>();
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<Renderer>(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;
}

Binary file not shown.

46
include/basic/encoding.h Normal file
View File

@ -0,0 +1,46 @@
#ifndef ARE_INCLUDE_BASIC_ENCODING_H
#define ARE_INCLUDE_BASIC_ENCODING_H
#include "basic/types.h"
#include <cmath>
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

View File

@ -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> 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

View File

@ -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<Shader> 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_;

View File

@ -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
# 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
fi
# Convert to absolute path using readlink or realpath
if command -v realpath >/dev/null 2>&1; then
# 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
elif command -v readlink >/dev/null 2>&1 && readlink -f / >/dev/null 2>&1; then
target_dir="$(readlink -f "$scan_dir")"
else
else
# Fallback: use cd + pwd
target_dir="$(cd "$scan_dir" && pwd)"
fi
fi
if [[ -z "$target_dir" || ! -d "$target_dir" ]]; then
if [[ -z "$target_dir" || ! -d "$target_dir" ]]; then
printf "Error: Cannot resolve directory: %s\n" "$scan_dir" >&2
exit 1
fi
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,7 +180,8 @@ 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
for target_dir in "${target_dirs[@]}"; do
while IFS= read -r -d '' file; do
# Skip empty
[[ -z "$file" ]] && continue
@ -193,13 +203,14 @@ while IFS= read -r -d '' file; do
ALL_FILES+=("$file")
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

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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

154
shaders/include/sobol.glsl Normal file
View File

@ -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

View File

@ -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 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));
}
}

View File

@ -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);

View File

@ -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,7 +103,35 @@ 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_() {

View File

@ -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;

View File

@ -16,21 +16,39 @@ namespace {
}
return h;
}
// Compute hash of texture pointers for a specific slot
uint compute_slot_texture_hash(const std::vector<std::shared_ptr<Texture>> &textures) {
if (textures.empty())
return 0u;
// Hash the raw pointers to detect texture set changes
std::vector<const void *> 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<TextureSlot>(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

View File

@ -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

View File

@ -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];