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
parent
58d6184085
commit
6d9d95ddad
26
AGENTS.md
26
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
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in New Issue