diff --git a/CMakeLists.txt b/CMakeLists.txt index f689adc6813a84b7ea60de7836762be33b688756..7df2e9c0811fa9f81576394720f7d79a31a90a9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ endif() # define sources -set(SOURCES_SCOTTY3D_GUI +set(SOURCES_SCOTTY3D_GUI "src/gui/manager.cpp" "src/gui/manager.h" "src/gui/model.cpp" @@ -107,6 +107,7 @@ if(SCOTTY3D_BUILD_REF) "src/reference/shapes.cpp" "src/reference/bsdf.cpp" "src/reference/bbox.cpp" + "src/reference/bvh.inl" "src/reference/skeleton.cpp" "src/reference/env_light.cpp" "src/reference/tri_mesh.cpp") @@ -121,6 +122,7 @@ else() "src/student/bbox.cpp" "src/student/debug.h" "src/student/debug.cpp" + "src/student/bvh.inl" "src/student/env_light.cpp" "src/student/skeleton.cpp" "src/student/tri_mesh.cpp") @@ -232,7 +234,7 @@ include_directories(${ASSIMP_INCLUDE_DIRS}) -# link libraries +# link libraries if(WIN32) target_include_directories(Scotty3D PRIVATE "deps/win") diff --git a/docs/pathtracer/intersecting_objects.md b/docs/pathtracer/intersecting_objects.md index 197be2d7ac9d7509a2983f23bc8b9a3f4139360f..34f38ad63cb58ade03456c9e2feedd458b470ca4 100644 --- a/docs/pathtracer/intersecting_objects.md +++ b/docs/pathtracer/intersecting_objects.md @@ -18,7 +18,7 @@ In order to correctly implement `hit` you need to understand some of the fields * `dir`: represents the 3D direction of the ray (this direction will be normalized) * `time_bounds`: correspond to the minimum and maximum points on the ray with its x-component as the lower bound and y-component as the upper bound. That is, intersections that lie outside the [`ray.time_bounds.x`, `ray.time_bounds.y`] range should not be considered valid intersections with the primitive. - +One important detail of the Ray structure is that `time_bounds` is a mutable field of the Ray. This means that this fields can be modified by constant member functions such as `Triangle::hit`. When finding the first intersection of a ray and the scene, you almost certainly want to update the ray's `time_bounds` value after finding each hit with scene geometry. By bounding the ray as tightly as possible, your ray tracer will be able to avoid unnecessary tests with scene geometry that is known to not be able to result in a closest hit, resulting in higher performance. --- @@ -37,7 +37,7 @@ There are two important details you should be aware of about intersection: * `position`: the exact position of the hit point. This can be easily computed by the `time` above as with the ray's `point` and `dir`. * `normal`: the normal of the surface at the hit point. This normal should be the interpolated normal (obtained via interpolation of the per-vertex normals according to the barycentric coordinates of the hit point) -* When intersection occurs with the back-face of a triangle (the side of the triangle opposite the direction of the normal) you should flip the returned normal to point in that direction. That is, always return a normal pointing in the direction the ray came from! + Once you've successfully implemented triangle intersection, you will be able to render many of the scenes in the media directory. However, your ray tracer will be very slow! diff --git a/docs/pathtracer/ray_triangle_intersection.md b/docs/pathtracer/ray_triangle_intersection.md index 5f8bf31fb104cb6f84ae97a7482b68ea4ddd12fa..c1153b43af7c7114c8ed5148ec959e3811cb1e37 100644 --- a/docs/pathtracer/ray_triangle_intersection.md +++ b/docs/pathtracer/ray_triangle_intersection.md @@ -27,4 +27,4 @@ Of which you should notice a few common subexpressions that, if exploited in an A few final notes and thoughts: -If the denominator _dot((e1 x d), e2)_ is zero, what does that mean about the relationship of the ray and the triangle? Can a triangle with this area be hit by a ray? Given _u_ and _v_, how do you know if the ray hits the triangle? Don't forget that the intersection point on the ray should be within the ray's `time_bound`. \ No newline at end of file +If the denominator _dot((e1 x d), e2)_ is zero, what does that mean about the relationship of the ray and the triangle? Can a triangle with this area be hit by a ray? Given _u_ and _v_, how do you know if the ray hits the triangle? Don't forget that the intersection point on the ray should be within the ray's `time_bound`. diff --git a/src/gui/manager.cpp b/src/gui/manager.cpp index d085e5ba680009287ca958d2fb12884542088b67..603ebeea22e46e4e14c71d67685a77a7cf11bd02 100644 --- a/src/gui/manager.cpp +++ b/src/gui/manager.cpp @@ -293,7 +293,7 @@ Mode Manager::item_options(Undo &undo, Mode cur_mode, Scene_Item &item, Pose &ol } ImGui::SameLine(); if (ImGui::Button("Flip Normals")) { - obj.get_mesh().flip(); + obj.flip_normals(); } if (ImGui::Checkbox("Smooth Normals", &obj.opt.smooth_normals)) { update(); diff --git a/src/gui/widgets.cpp b/src/gui/widgets.cpp index 9f71799e414e98178a43c17284f670b484bcb1b6..1e1ccf4a278f73ea982ad16d381f4d82d9338288 100644 --- a/src/gui/widgets.cpp +++ b/src/gui/widgets.cpp @@ -548,6 +548,7 @@ void Widget_Render::begin(Scene &scene, Widget_Camera &cam, Camera &user_cam) { ImGui::SetNextWindowFocus(); render_window_focus = false; } + ImGui::SetNextWindowSize({675.0f, 625.0f}, ImGuiCond_Once); ImGui::Begin("Render Image", &render_window, ImGuiWindowFlags_NoCollapse); static const char *method_names[] = {"Rasterize", "Path Trace"}; diff --git a/src/lib/ray.h b/src/lib/ray.h index 9367839aa7bb861654ae7eec301b119273293853..53d6579d56c87c764566703e788e1e065ff5a925 100644 --- a/src/lib/ray.h +++ b/src/lib/ray.h @@ -33,7 +33,7 @@ struct Ray { /// The direction the ray travels in Vec3 dir; /// The minimum and maximum time/distance at which this ray should exist - Vec2 time_bounds; + mutable Vec2 time_bounds; /// Recursive depth of ray size_t depth = 0; }; diff --git a/src/rays/bsdf.h b/src/rays/bsdf.h index 3e9ae3c62bea484d74eff75993bae374667cc11b..f4bafc80d39506ea01aee9d589bd1de0b3f0a58d 100644 --- a/src/rays/bsdf.h +++ b/src/rays/bsdf.h @@ -111,6 +111,15 @@ public: underlying); } + bool is_sided() const { + return std::visit(overloaded{[](const BSDF_Lambertian &) { return false; }, + [](const BSDF_Mirror &) { return false; }, + [](const BSDF_Glass &) { return true; }, + [](const BSDF_Diffuse &) { return false; }, + [](const BSDF_Refract &) { return true; }}, + underlying); + } + private: std::variant underlying; }; diff --git a/src/rays/bvh.h b/src/rays/bvh.h index 89da0e96fdab7cb4169dcb92e4b47f5485391f54..8e2d7b7fb7cdee4422859e1be272dd4959ded35d 100644 --- a/src/rays/bvh.h +++ b/src/rays/bvh.h @@ -34,12 +34,13 @@ private: std::vector nodes; std::vector primitives; + size_t root_idx = 0; }; } // namespace PT #ifdef SCOTTY3D_BUILD_REF -#include "../reference/bvh.cpp" +#include "../reference/bvh.inl" #else -#include "../student/bvh.cpp" +#include "../student/bvh.inl" #endif diff --git a/src/rays/pathtracer.cpp b/src/rays/pathtracer.cpp index 9683fefa74a38f26363da65cf58a934b8ac939f3..e2b82cc1f121a03e3239281710739fbe6b70c3f0 100644 --- a/src/rays/pathtracer.cpp +++ b/src/rays/pathtracer.cpp @@ -131,7 +131,7 @@ void Pathtracer::build_scene(Scene &layout_scene) { obj_list.push_back( Object(std::move(shape), obj.id(), idx, obj.pose.transform())); } else { - Tri_Mesh mesh(obj.posed_mesh(), obj.get_mesh().flipped()); + Tri_Mesh mesh(obj.posed_mesh()); std::lock_guard lock(obj_mut); obj_list.push_back( Object(std::move(mesh), obj.id(), idx, obj.pose.transform())); diff --git a/src/rays/tri_mesh.h b/src/rays/tri_mesh.h index 4038b764741344f367102327885ab568d65ed1c3..999d35f88e2433fda70da8eb9b2d98650f30c824 100644 --- a/src/rays/tri_mesh.h +++ b/src/rays/tri_mesh.h @@ -32,7 +32,7 @@ private: class Tri_Mesh { public: Tri_Mesh() = default; - Tri_Mesh(const GL::Mesh &mesh, bool flip = false); + Tri_Mesh(const GL::Mesh &mesh); BBox bbox() const; Trace hit(const Ray &ray) const; @@ -44,7 +44,6 @@ public: private: std::vector verts; BVH triangles; - bool flip_normals = false; }; } // namespace PT diff --git a/src/scene/object.cpp b/src/scene/object.cpp index ee861b4a63b5ad534fb9dd8f4ff4cf617e1d30a0..125196760c55bec3259bb4f098f2354f951aa929 100644 --- a/src/scene/object.cpp +++ b/src/scene/object.cpp @@ -53,8 +53,10 @@ void Scene_Object::try_make_editable(PT::Shape_Type prev) { } std::string err = halfedge.from_mesh(_mesh); - if (err.empty()) + if (err.empty()) { editable = true; + opt.smooth_normals = true; + } mesh_dirty = true; skel_dirty = true; @@ -120,6 +122,11 @@ void Scene_Object::sync_anim_mesh() { skel_dirty = pose_dirty = false; } +void Scene_Object::flip_normals() { + halfedge.flip(); + mesh_dirty = true; +} + void Scene_Object::sync_mesh() { if (editable && mesh_dirty) { diff --git a/src/scene/object.h b/src/scene/object.h index d3cdfde2c1c22b30fa30de103e6e82a4df31e017..e51eafe6d0eebbacbc1b034c2b0ebbae839eb4a7 100644 --- a/src/scene/object.h +++ b/src/scene/object.h @@ -49,6 +49,7 @@ public: bool is_editable() const; bool is_shape() const; void try_make_editable(PT::Shape_Type prev = PT::Shape_Type::none); + void flip_normals(); void set_mesh_dirty(); void set_skel_dirty(); diff --git a/src/student/bvh.inl b/src/student/bvh.inl new file mode 100644 index 0000000000000000000000000000000000000000..4233bf696272d5b6c7ce9169e272d7e813a5e547 --- /dev/null +++ b/src/student/bvh.inl @@ -0,0 +1,155 @@ + +#include "../rays/bvh.h" +#include "debug.h" +#include + +namespace PT { + +template +void BVH::build(std::vector &&prims, size_t max_leaf_size) { + + // NOTE (PathTracer): + // This BVH is parameterized on the type of the primitive it contains. This allows + // us to build a BVH over any type that defines a certain interface. Specifically, + // we use this to both build a BVH over triangles within each Tri_Mesh, and over + // a variety of Objects (which might be Tri_Meshes, Spheres, etc.) in Pathtracer. + // + // The Primitive interface must implement these two functions: + // BBox bbox() const; + // Trace hit(const Ray& ray) const; + // Hence, you may call bbox() and hit() on any value of type Primitive. + // + // Finally, also note that while a BVH is a tree structure, our BVH nodes don't + // contain pointers to children, but rather indicies. This is because instead + // of allocating each node individually, the BVH class contains a vector that + // holds all of the nodes. Hence, to get the child of a node, you have to + // look up the child index in this vector (e.g. nodes[node.l]). Similarly, + // to create a new node, don't allocate one yourself - use BVH::new_node, which + // returns the index of a newly added node. + + // Keep these + nodes.clear(); + primitives = std::move(prims); + + // TODO (PathTracer): Task 3 + // Construct a BVH from the given vector of primitives and maximum leaf + // size configuration. The starter code builds a BVH with a + // single leaf node (which is also the root) that encloses all the + // primitives. + + // Replace these + BBox box; + for (const Primitive &prim : primitives) + box.enclose(prim.bbox()); + + new_node(box, 0, primitives.size(), 0, 0); + root_idx = 0; +} + +template Trace BVH::hit(const Ray &ray) const { + + // TODO (PathTracer): Task 3 + // Implement ray - BVH intersection test. A ray intersects + // with a BVH aggregate if and only if it intersects a primitive in + // the BVH that is not an aggregate. + + // The starter code simply iterates through all the primitives. + // Again, remember you can use hit() on any Primitive value. + + Trace ret; + for (const Primitive &prim : primitives) { + Trace hit = prim.hit(ray); + ret = Trace::min(ret, hit); + } + return ret; +} + +template +BVH::BVH(std::vector &&prims, size_t max_leaf_size) { + build(std::move(prims), max_leaf_size); +} + +template bool BVH::Node::is_leaf() const { + return l == 0 && r == 0; +} + +template +size_t BVH::new_node(BBox box, size_t start, size_t size, size_t l, size_t r) { + Node n; + n.bbox = box; + n.start = start; + n.size = size; + n.l = l; + n.r = r; + nodes.push_back(n); + return nodes.size() - 1; +} + +template BBox BVH::bbox() const { return nodes[0].bbox; } + +template std::vector BVH::destructure() { + nodes.clear(); + return std::move(primitives); +} + +template void BVH::clear() { + nodes.clear(); + primitives.clear(); +} + +template +size_t BVH::visualize(GL::Lines &lines, GL::Lines &active, size_t level, + const Mat4 &trans) const { + + std::stack> tstack; + tstack.push({root_idx, 0}); + size_t max_level = 0; + + if (nodes.empty()) + return max_level; + + while (!tstack.empty()) { + + auto [idx, lvl] = tstack.top(); + max_level = std::max(max_level, lvl); + const Node &node = nodes[idx]; + tstack.pop(); + + Vec3 color = lvl == level ? Vec3(1.0f, 0.0f, 0.0f) : Vec3(1.0f); + GL::Lines &add = lvl == level ? active : lines; + + BBox box = node.bbox; + box.transform(trans); + Vec3 min = box.min, max = box.max; + + auto edge = [&](Vec3 a, Vec3 b) { add.add(a, b, color); }; + + edge(min, Vec3{max.x, min.y, min.z}); + edge(min, Vec3{min.x, max.y, min.z}); + edge(min, Vec3{min.x, min.y, max.z}); + edge(max, Vec3{min.x, max.y, max.z}); + edge(max, Vec3{max.x, min.y, max.z}); + edge(max, Vec3{max.x, max.y, min.z}); + edge(Vec3{min.x, max.y, min.z}, Vec3{max.x, max.y, min.z}); + edge(Vec3{min.x, max.y, min.z}, Vec3{min.x, max.y, max.z}); + edge(Vec3{min.x, min.y, max.z}, Vec3{max.x, min.y, max.z}); + edge(Vec3{min.x, min.y, max.z}, Vec3{min.x, max.y, max.z}); + edge(Vec3{max.x, min.y, min.z}, Vec3{max.x, max.y, min.z}); + edge(Vec3{max.x, min.y, min.z}, Vec3{max.x, min.y, max.z}); + + if (node.l) + tstack.push({node.l, lvl + 1}); + if (node.r) + tstack.push({node.r, lvl + 1}); + + if (!node.l && !node.r) { + for (size_t i = node.start; i < node.start + node.size; i++) { + size_t c = primitives[i].visualize(lines, active, level - lvl, trans); + max_level = std::max(c, max_level); + } + } + } + return max_level; +} + +} // namespace PT diff --git a/src/student/pathtracer.cpp b/src/student/pathtracer.cpp index 061e6f8f4a437c13bfb3b096ea78ebbfd0da4392..96d3b3d99ee6484388797711209ceb8b08197c4a 100644 --- a/src/student/pathtracer.cpp +++ b/src/student/pathtracer.cpp @@ -37,13 +37,18 @@ Spectrum Pathtracer::trace_ray(const Ray &ray) { return {}; } + // If we're using a two-sided material, treat back-faces the same as front-faces + const BSDF &bsdf = materials[hit.material]; + if(!bsdf.is_sided() && dot(hit.normal, ray.dir) > 0.0f) { + hit.normal = -hit.normal; + } + // Set up a coordinate frame at the hit point, where the surface normal becomes {0, 1, 0} // This gives us out_dir and later in_dir in object space, where computations involving the // normal become much easier. For example, cos(theta) = dot(N,dir) = dir.y! Mat4 object_to_world = Mat4::rotate_to(hit.normal); Mat4 world_to_object = object_to_world.T(); Vec3 out_dir = world_to_object.rotate(ray.point - hit.position).unit(); - const BSDF &bsdf = materials[hit.material]; // Now we can compute the rendering equation at this point. // We split it into two stages: sampling lighting (i.e. directly connecting diff --git a/src/student/tri_mesh.cpp b/src/student/tri_mesh.cpp index c14fdc9de525d3e3ecab0e864cfbc276cad7c090..79aa077bcd39b6c6b15ec1d5854a7031fe9a1f13 100644 --- a/src/student/tri_mesh.cpp +++ b/src/student/tri_mesh.cpp @@ -9,6 +9,9 @@ BBox Triangle::bbox() const { // TODO (PathTracer): Task 2 // compute the bounding box of the triangle + // Beware of flat/zero-volume boxes! You may need to + // account for that here, or later on in BBox::intersect + BBox box; return box; } @@ -57,13 +60,12 @@ void Tri_Mesh::build(const GL::Mesh &mesh) { triangles.build(std::move(tris), 4); } -Tri_Mesh::Tri_Mesh(const GL::Mesh &mesh, bool flip) { build(mesh); flip_normals = flip; } +Tri_Mesh::Tri_Mesh(const GL::Mesh &mesh) { build(mesh); } BBox Tri_Mesh::bbox() const { return triangles.bbox(); } Trace Tri_Mesh::hit(const Ray &ray) const { Trace t = triangles.hit(ray); - if(flip_normals) t.normal = -t.normal; return t; }