From 783de0a217db7ccfaad22c7994084228b854697a Mon Sep 17 00:00:00 2001 From: redstrate <54911369+redstrate@users.noreply.github.com> Date: Fri, 29 May 2020 22:11:03 -0400 Subject: [PATCH] Update README and move scene functions into scene.cpp --- CMakeLists.txt | 3 +- README.md | 5 +- include/intersections.h | 4 +- include/lighting.h | 6 +- include/ray.h | 4 +- include/scene.h | 164 ++++------------------------------------ src/scene.cpp | 153 +++++++++++++++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 158 deletions(-) create mode 100644 src/scene.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 345ee9f..729b9c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,8 @@ add_executable(raytracer include/image.h include/tiny_obj_loader.h include/scene.h - src/main.cpp) + src/main.cpp + src/scene.cpp) target_include_directories(raytracer PUBLIC include PRIVATE ${GLM_INCLUDE_DIR}) target_link_libraries(raytracer PUBLIC stb SDL2::Core imgui glad) set_target_properties(raytracer PROPERTIES diff --git a/README.md b/README.md index b3d083b..faf3edc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Raytracer -A multi-threaded raytracer using glm, tinyobjloader, stb and C++17. +A multi-threaded raytracer using glm, tinyobjloader and C++17. The UI is written in imgui and image display is +rendered using OpenGL. I tried to write this to not be insanely fast or compact like other raytracers, but to be readable and understandable. ![example result](https://raw.githubusercontent.com/redstrate/raytracer/master/misc/output.png) + +The example image shown above is rendered using simple direct light computation and naive indirect light sampling. diff --git a/include/intersections.h b/include/intersections.h index 5b158d8..bf0f1c7 100644 --- a/include/intersections.h +++ b/include/intersections.h @@ -7,7 +7,7 @@ constexpr float epsilon = std::numeric_limits().epsilon(); namespace intersections { - bool ray_sphere(const Ray ray, const glm::vec4 sphere) { + inline bool ray_sphere(const Ray ray, const glm::vec4 sphere) { const glm::vec3 diff = ray.origin - glm::vec3(sphere); const float b = glm::dot(ray.direction, diff); const float c = glm::dot(diff, diff) - sphere.w * sphere.w; @@ -22,7 +22,7 @@ namespace intersections { return false; } - float ray_triangle(const Ray ray, + inline float ray_triangle(const Ray ray, const glm::vec3 v0, const glm::vec3 v1, const glm::vec3 v2, diff --git a/include/lighting.h b/include/lighting.h index 426a425..92b77f6 100644 --- a/include/lighting.h +++ b/include/lighting.h @@ -3,9 +3,9 @@ #include namespace lighting { - float point_light(const glm::vec3 pos, const glm::vec3 light, const glm::vec3 normal) { - const glm::vec3 dir = light - pos; - const float n_dot_l = glm::max(glm::dot(normal, glm::normalize(dir)), 0.0f); + inline float point_light(const glm::vec3 pos, const glm::vec3 light, const glm::vec3 normal) { + const glm::vec3 dir = glm::normalize(light - pos); + const float n_dot_l = glm::max(glm::dot(normal, dir), 0.0f); return n_dot_l; } diff --git a/include/ray.h b/include/ray.h index 730b99f..0d50e8d 100644 --- a/include/ray.h +++ b/include/ray.h @@ -3,7 +3,7 @@ #include struct Ray { - Ray(const glm::vec3 origin, const glm::vec3 direction) : origin(origin), direction(direction) {} + Ray(const glm::vec3 origin, const glm::vec3 direction) : origin(origin), direction(direction) {} - glm::vec3 origin, direction; + glm::vec3 origin, direction; }; diff --git a/include/scene.h b/include/scene.h index 88fe50d..0ad1365 100644 --- a/include/scene.h +++ b/include/scene.h @@ -1,9 +1,19 @@ #pragma once #include +#include #include +#include "ray.h" +#include "intersections.h" +#include "lighting.h" + +constexpr glm::vec3 light_position = glm::vec3(5); +constexpr float light_bias = 0.01f; +constexpr int max_depth = 2; +constexpr int num_indirect_samples = 4; + struct Object { glm::vec3 position = glm::vec3(0); glm::vec3 color = glm::vec3(1); @@ -25,164 +35,20 @@ struct Scene { } }; -inline glm::vec3 fetch_position(const Object& object, const tinyobj::mesh_t& mesh, const int32_t index, const int32_t vertex) { - const tinyobj::index_t idx = mesh.indices[(index * 3) +vertex]; - - const auto vx = object.attrib.vertices[3*idx.vertex_index+0]; - const auto vy = object.attrib.vertices[3*idx.vertex_index+1]; - const auto vz = object.attrib.vertices[3*idx.vertex_index+2]; - - return glm::vec3(vx, vy, vz); -} - -inline glm::vec3 fetch_normal(const Object& object, const tinyobj::mesh_t& mesh, const int32_t index, const int32_t vertex) { - const tinyobj::index_t idx = mesh.indices[(index * 3) + vertex]; - - const auto nx = object.attrib.normals[3*idx.normal_index+0]; - const auto ny = object.attrib.normals[3*idx.normal_index+1]; - const auto nz = object.attrib.normals[3*idx.normal_index+2]; - - return glm::vec3(nx, ny, nz); -} +glm::vec3 fetch_position(const Object& object, const tinyobj::mesh_t& mesh, const int32_t index, const int32_t vertex); +glm::vec3 fetch_normal(const Object& object, const tinyobj::mesh_t& mesh, const int32_t index, const int32_t vertex); struct HitResult { glm::vec3 position, normal; Object object; }; -std::optional test_mesh(const Ray ray, const Object& object, const tinyobj::mesh_t& mesh, float& tClosest) { - bool intersection = false; - HitResult result = {}; - - for(size_t i = 0; i < mesh.num_face_vertices.size(); i++) { - const glm::vec3 v0 = fetch_position(object, mesh, i, 0) + object.position; - const glm::vec3 v1 = fetch_position(object, mesh, i, 1) + object.position; - const glm::vec3 v2 = fetch_position(object, mesh, i, 2) + object.position; - - float t = std::numeric_limits::infinity(), u, v; - if(intersections::ray_triangle(ray, v0, v1, v2, t, u, v)) { - if(t < tClosest && t > epsilon) { - const glm::vec3 n0 = fetch_normal(object, mesh, i, 0); - const glm::vec3 n1 = fetch_normal(object, mesh, i, 1); - const glm::vec3 n2 = fetch_normal(object, mesh, i, 2); - - result.normal = (1 - u - v) * n0 + u * n1 + v * n2; - result.position = ray.origin + ray.direction * t; - - tClosest = t; - - intersection = true; - } - } - } - - if(intersection) - return result; - else - return {}; -} - -std::optional test_scene(const Ray ray, const Scene& scene, float tClosest = std::numeric_limits::infinity()) { - bool intersection = false; - HitResult result = {}; - - for(auto& object : scene.objects) { - for(uint32_t i = 0; i < object.shapes.size(); i++) { - auto mesh = object.shapes[i].mesh; - - if(const auto hit = test_mesh(ray, object, mesh, tClosest)) { - intersection = true; - result = hit.value(); - result.object = object; - } - } - } - - if(intersection) - return result; - else - return {}; -} - -constexpr glm::vec3 light_position = glm::vec3(5); -constexpr float light_bias = 0.01f; -constexpr int max_depth = 2; -constexpr int num_indirect_samples = 4; +std::optional test_mesh(const Ray ray, const Object& object, const tinyobj::mesh_t& mesh, float& tClosest); +std::optional test_scene(const Ray ray, const Scene& scene, float tClosest = std::numeric_limits::infinity()); struct SceneResult { HitResult hit; glm::vec3 color, indirect; }; -// methods adapated from https://users.cg.tuwien.ac.at/zsolnai/gfx/smallpaint/ -inline std::tuple orthogonal_system(const glm::vec3& v1) { - glm::vec3 v2; - if(glm::abs(v1.x) > glm::abs(v1.y)) { - // project to the y = 0 plane and construct a normalized orthogonal vector in this plane - const float inverse_length = 1.0f / sqrtf(v1.x * v1.x + v1.z * v1.z); - v2 = glm::vec3(-v1.z * inverse_length, 0.0f, v1.x * inverse_length); - } else { - // project to the x = 0 plane and construct a normalized orthogonal vector in this plane - const float inverse_length = 1.0f / sqrtf(v1.y * v1.y + v1.z * v1.z); - v2 = glm::vec3(0.0f, v1.z * inverse_length, -v1.y * inverse_length); - } - - return {v2, glm::cross(v1, v2)}; -} - -glm::vec3 hemisphere(const double u1, const double u2) { - const double r = sqrt(1.0 - u1 * u1); - const double phi = 2 * M_PI * u2; - - return glm::vec3(cos(phi) * r, sin(phi) * r, u1); -} - -std::optional cast_scene(const Ray ray, const Scene& scene, const int depth = 0) { - if(depth > max_depth) - return {}; - - if(auto hit = test_scene(ray, scene)) { - const float diffuse = lighting::point_light(hit->position, light_position, hit->normal); - - //shadow calculation - glm::vec3 direct(0); - if(glm::dot(light_position - hit->position, hit->normal) > 0) { - const glm::vec3 light_dir = glm::normalize(light_position - hit->position); - - const Ray shadow_ray(hit->position + (hit->normal * light_bias), light_dir); - - const float shadow = test_scene(shadow_ray, scene) ? 0.0f : 1.0f; - - direct = diffuse * shadow * glm::vec3(1); - } - - glm::vec3 indirect(0); - for(int i = 0; i < num_indirect_samples; i++) { - const float theta = drand48() * M_PI; - const float cos_theta = cos(theta); - const float sin_theta = sin(theta); - - const auto [rotX, rotY] = orthogonal_system(hit->normal); - - const glm::vec3 sampled_dir = hemisphere(cos_theta, sin_theta); - const glm::vec3 rotated_dir = { - glm::dot({rotX.x, rotY.x, hit->normal.x}, sampled_dir), - glm::dot({rotX.y, rotY.y, hit->normal.y}, sampled_dir), - glm::dot({rotX.z, rotY.z, hit->normal.z}, sampled_dir) - }; - - if(const auto indirect_result = cast_scene(Ray(ray.origin, rotated_dir), scene, depth + 1)) - indirect += indirect_result->color * cos_theta; - } - indirect /= num_indirect_samples; - - SceneResult result = {}; - result.hit = *hit; - result.color = (indirect + direct) * hit->object.color; - result.indirect = indirect; - - return result; - } else { - return {}; - } -} +std::optional cast_scene(const Ray ray, const Scene& scene, const int depth = 0); diff --git a/src/scene.cpp b/src/scene.cpp new file mode 100644 index 0000000..feac526 --- /dev/null +++ b/src/scene.cpp @@ -0,0 +1,153 @@ +#include "scene.h" + +glm::vec3 fetch_position(const Object& object, const tinyobj::mesh_t& mesh, const int32_t index, const int32_t vertex) { + const tinyobj::index_t idx = mesh.indices[(index * 3) +vertex]; + + const auto vx = object.attrib.vertices[3*idx.vertex_index+0]; + const auto vy = object.attrib.vertices[3*idx.vertex_index+1]; + const auto vz = object.attrib.vertices[3*idx.vertex_index+2]; + + return glm::vec3(vx, vy, vz); +} + +glm::vec3 fetch_normal(const Object& object, const tinyobj::mesh_t& mesh, const int32_t index, const int32_t vertex) { + const tinyobj::index_t idx = mesh.indices[(index * 3) + vertex]; + + const auto nx = object.attrib.normals[3*idx.normal_index+0]; + const auto ny = object.attrib.normals[3*idx.normal_index+1]; + const auto nz = object.attrib.normals[3*idx.normal_index+2]; + + return glm::vec3(nx, ny, nz); +} + +std::optional test_mesh(const Ray ray, const Object& object, const tinyobj::mesh_t& mesh, float& tClosest) { + bool intersection = false; + HitResult result = {}; + + for(size_t i = 0; i < mesh.num_face_vertices.size(); i++) { + const glm::vec3 v0 = fetch_position(object, mesh, i, 0) + object.position; + const glm::vec3 v1 = fetch_position(object, mesh, i, 1) + object.position; + const glm::vec3 v2 = fetch_position(object, mesh, i, 2) + object.position; + + float t = std::numeric_limits::infinity(), u, v; + if(intersections::ray_triangle(ray, v0, v1, v2, t, u, v)) { + if(t < tClosest && t > epsilon) { + const glm::vec3 n0 = fetch_normal(object, mesh, i, 0); + const glm::vec3 n1 = fetch_normal(object, mesh, i, 1); + const glm::vec3 n2 = fetch_normal(object, mesh, i, 2); + + result.normal = (1 - u - v) * n0 + u * n1 + v * n2; + result.position = ray.origin + ray.direction * t; + + tClosest = t; + + intersection = true; + } + } + } + + if(intersection) + return result; + else + return {}; +} + +std::optional test_scene(const Ray ray, const Scene& scene, float tClosest) { + bool intersection = false; + HitResult result = {}; + + for(auto& object : scene.objects) { + for(uint32_t i = 0; i < object.shapes.size(); i++) { + auto mesh = object.shapes[i].mesh; + + if(const auto hit = test_mesh(ray, object, mesh, tClosest)) { + intersection = true; + result = hit.value(); + result.object = object; + } + } + } + + if(intersection) + return result; + else + return {}; +} + +// methods adapated from https://users.cg.tuwien.ac.at/zsolnai/gfx/smallpaint/ +inline std::tuple orthogonal_system(const glm::vec3& v1) { + glm::vec3 v2; + if(glm::abs(v1.x) > glm::abs(v1.y)) { + // project to the y = 0 plane and construct a normalized orthogonal vector in this plane + const float inverse_length = 1.0f / sqrtf(v1.x * v1.x + v1.z * v1.z); + v2 = glm::vec3(-v1.z * inverse_length, 0.0f, v1.x * inverse_length); + } else { + // project to the x = 0 plane and construct a normalized orthogonal vector in this plane + const float inverse_length = 1.0f / sqrtf(v1.y * v1.y + v1.z * v1.z); + v2 = glm::vec3(0.0f, v1.z * inverse_length, -v1.y * inverse_length); + } + + return {v2, glm::cross(v1, v2)}; +} + +glm::vec3 hemisphere(const double u1, const double u2) { + const double r = sqrt(1.0 - u1 * u1); + const double phi = 2 * M_PI * u2; + + return glm::vec3(cos(phi) * r, sin(phi) * r, u1); +} + +std::optional cast_scene(const Ray ray, const Scene& scene, const int depth) { + if(depth > max_depth) + return {}; + + if(auto hit = test_scene(ray, scene)) { + const float diffuse = lighting::point_light(hit->position, light_position, hit->normal); + + // direct lighting calculation + // currently only supports only one light (directional) + glm::vec3 direct(0); + if(glm::dot(light_position - hit->position, hit->normal) > 0) { + const glm::vec3 light_dir = glm::normalize(light_position - hit->position); + + const Ray shadow_ray(hit->position + (hit->normal * light_bias), light_dir); + + const float shadow = test_scene(shadow_ray, scene) ? 0.0f : 1.0f; + + direct = diffuse * shadow * glm::vec3(1); + } + + // indirect lighting calculation + // we take a hemisphere orthogonal to the normal, and take a constant number of num_indirect_samples + // and naive monte carlo without PDF + glm::vec3 indirect(0); + for(int i = 0; i < num_indirect_samples; i++) { + const float theta = drand48() * M_PI; + const float cos_theta = cos(theta); + const float sin_theta = sin(theta); + + const auto [rotX, rotY] = orthogonal_system(hit->normal); + + const glm::vec3 sampled_dir = hemisphere(cos_theta, sin_theta); + const glm::vec3 rotated_dir = { + glm::dot({rotX.x, rotY.x, hit->normal.x}, sampled_dir), + glm::dot({rotX.y, rotY.y, hit->normal.y}, sampled_dir), + glm::dot({rotX.z, rotY.z, hit->normal.z}, sampled_dir) + }; + + if(const auto indirect_result = cast_scene(Ray(ray.origin, rotated_dir), scene, depth + 1)) + indirect += indirect_result->color * cos_theta; + } + + indirect /= num_indirect_samples; + + SceneResult result = {}; + result.hit = *hit; + result.color = (indirect + direct) * hit->object.color; + result.indirect = indirect; + + return result; + } else { + return {}; + } +}