diff --git a/CMakeLists.txt b/CMakeLists.txt index a595dc2..86d31da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,8 +69,11 @@ set(SOURCES src/scene.cpp src/shapes/objmesh.cpp src/shapes/plymesh.cpp + src/shapes/triangle_mesh.cpp + src/shapes/inline_triangle_mesh.cpp src/shapes/shape.cpp src/shapes/triangle.cpp + src/shapes/instance.cpp src/scene_parser.cpp src/rayintersectinfo.cpp src/textures/image_texture.cpp diff --git a/caramel-scenes b/caramel-scenes index 8ac172a..090fd3a 160000 --- a/caramel-scenes +++ b/caramel-scenes @@ -1 +1 @@ -Subproject commit 8ac172a93b7432d50350b1b6d98e10b62af09cde +Subproject commit 090fd3a19e636a3a74b79ff10ba291fea7136690 diff --git a/include/scene_parser.h b/include/scene_parser.h index 0dabacd..48805bf 100644 --- a/include/scene_parser.h +++ b/include/scene_parser.h @@ -63,6 +63,10 @@ namespace Caramel{ private: Shape* parse_shape(const Json &shape_json) const; + // Expands a {"type":"instance", "shapes":[...], "instances":[...]} entry into one + // Instance per (template sub-shape x placement); templates are loaded once and shared. + void parse_instanced_shapes(const Json &shape_json, std::vector &out) const; + Light* parse_light(const Json &light_json) const; // Other lights are handled in `parse_light()` diff --git a/include/shape.h b/include/shape.h index dcd90a4..bf2a4be 100644 --- a/include/shape.h +++ b/include/shape.h @@ -123,23 +123,10 @@ namespace Caramel{ const bool is_tx_exists; }; - // Interface for triangle mesh shapes (used by acceleration structures) class TriangleMesh : public Shape{ public: - using Shape::Shape; - virtual Index get_triangle_num() const = 0; - virtual std::pair get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const = 0; - virtual AABB get_triangle_aabb(Index i) const = 0; - virtual Float get_triangle_area(Index i) const = 0; - virtual std::tuple get_triangle_sample_point(Index i, Sampler &sampler) const = 0; - virtual std::tuple get_triangle_vertices(Index i) const = 0; - virtual Index sample_triangle_index(Float u) const = 0; - virtual Float triangle_select_pdf(Index i) const = 0; - }; - - class OBJMesh final : public TriangleMesh{ - public: - OBJMesh(const std::filesystem::path &path, BSDF *bsdf, AreaLight *arealight = nullptr, const Matrix44f &transform = Matrix44f::identity()); + TriangleMesh(BSDF *bsdf, AreaLight *arealight); + ~TriangleMesh() override; std::pair ray_intersect(const Ray &ray, Float maxt) const override; AABB get_aabb() const override; @@ -147,35 +134,31 @@ namespace Caramel{ // point, normal, probability std::tuple sample_point(Sampler &sampler) const override; Float pdf_solidangle(const Vector3f &hitpos_world, const Vector3f &shapepos_world, const Vector3f &shape_normal_world) const override; - - std::pair get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const override; - AABB get_triangle_aabb(Index i) const override; - Float get_triangle_area(Index i) const override; - std::tuple get_triangle_sample_point(Index i, Sampler &sampler) const override; - std::tuple get_triangle_vertices(Index i) const override; - Index sample_triangle_index(Float u) const override; - Float triangle_select_pdf(Index i) const override; - - Index get_triangle_num() const override { - return m_vertex_indices.size(); - } - bool is_solid_angle_sampling_possible() const override; const std::vector& get_polygon_vertices() const override; - private: + Index get_triangle_num() const { return m_face_indices.size(); } + std::pair get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const; + AABB get_triangle_aabb(Index i) const; + Float get_triangle_area(Index i) const; + std::tuple get_triangle_sample_point(Index i, Sampler &sampler) const; + std::tuple get_triangle_vertices(Index i) const; + Index sample_triangle_index(Float u) const; + Float triangle_select_pdf(Index i) const; + + protected: + void finalize(AreaLight *arealight, const std::string &name); + Distrib1D m_triangle_pdf; - Float m_area; + Float m_area = Float0; AABB m_aabb; - bool is_vn_exists; - bool is_tx_exists; + bool is_vn_exists = false; + bool is_tx_exists = false; std::unique_ptr m_accel; std::vector m_vertices; std::vector m_normals; std::vector m_tex_coords; - std::vector m_vertex_indices; - std::vector m_normal_indices; - std::vector m_tex_coord_indices; + std::vector m_face_indices; // for solid angle sampling std::vector m_polygon_vertices; @@ -185,42 +168,51 @@ namespace Caramel{ class PLYMesh final : public TriangleMesh{ public: PLYMesh(const std::filesystem::path &path, BSDF *bsdf, AreaLight *arealight = nullptr, const Matrix44f &transform = Matrix44f::identity()); + }; + + class OBJMesh final : public TriangleMesh{ + public: + OBJMesh(const std::filesystem::path &path, BSDF *bsdf, AreaLight *arealight = nullptr, const Matrix44f &transform = Matrix44f::identity()); + }; + + class InlineTriangleMesh final : public TriangleMesh{ + public: + InlineTriangleMesh(std::vector positions, + std::vector indices, + std::vector normals, + BSDF *bsdf, AreaLight *arealight = nullptr, + const Matrix44f &transform = Matrix44f::identity()); + }; + + // Instanced geometry. Shares a template Shape (kept in LOCAL space) and + // applies a per-placement transform at intersection time, so one template's + // geometry can be reused across many placements without copying vertices. + class Instance final : public Shape{ + public: + // bsdf belongs to this placement: the scene BVH overwrites info.shape with + // the top-level Shape* (this Instance), so the integrator reads get_bsdf() + // from here, not from the template. + Instance(const Shape *geometry, const Matrix44f &to_world, BSDF *bsdf, AreaLight *arealight = nullptr); std::pair ray_intersect(const Ray &ray, Float maxt) const override; AABB get_aabb() const override; Float get_area() const override; - // point, normal, probability std::tuple sample_point(Sampler &sampler) const override; Float pdf_solidangle(const Vector3f &hitpos_world, const Vector3f &shapepos_world, const Vector3f &shape_normal_world) const override; - - std::pair get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const override; - AABB get_triangle_aabb(Index i) const override; - Float get_triangle_area(Index i) const override; - std::tuple get_triangle_sample_point(Index i, Sampler &sampler) const override; - std::tuple get_triangle_vertices(Index i) const override; - Index sample_triangle_index(Float u) const override; - Float triangle_select_pdf(Index i) const override; - - Index get_triangle_num() const override { - return m_face_indices.size(); - } - bool is_solid_angle_sampling_possible() const override; const std::vector& get_polygon_vertices() const override; - private: - Distrib1D m_triangle_pdf; - Float m_area; - AABB m_aabb; - bool is_vn_exists; - std::unique_ptr m_accel; - std::vector m_vertices; - std::vector m_normals; - std::vector m_face_indices; + // The shared template geometry pointer (test accessor for sharing checks). + const Shape *geometry() const { return m_geometry; } - // for solid angle sampling - std::vector m_polygon_vertices; - bool m_is_solid_angle_sampling_possible = false; + private: + const Shape *m_geometry; // shared template, LOCAL space (intersection only) + Matrix44f m_to_world; + Matrix44f m_to_local; // Inverse(to_world) + AABB m_world_aabb; + Float m_world_area = Float0; + Distrib1D m_world_triangle_pdf; + std::vector m_world_polygon_vertices; }; // u, v, t diff --git a/src/scene_parser.cpp b/src/scene_parser.cpp index bca6018..d93e3c9 100644 --- a/src/scene_parser.cpp +++ b/src/scene_parser.cpp @@ -148,12 +148,62 @@ namespace Caramel{ const Json child = get_unique_first_elem(m_scene_json, "shape"); if(child.is_array()){ for(const auto &ch : child){ - shapes.emplace_back(parse_shape(ch)); + if(parse_string(ch, "type") == "instance"){ + parse_instanced_shapes(ch, shapes); + } + else{ + shapes.emplace_back(parse_shape(ch)); + } } } return shapes; } + void SceneParser::parse_instanced_shapes(const SceneParser::Json &shape_json, std::vector &out) const { + // Build the template once (group-local space): parallel geometry + bsdf arrays. + // shapes-form : many sub-shapes, each carrying its own geometry + bsdf. + std::vector geometries; + std::vector bsdfs; + std::vector radiances; + + const Json sub_shapes = get_unique_first_elem(shape_json, "shapes"); + if(!sub_shapes.is_array() || sub_shapes.empty()){ + CRM_ERROR("instance 'shapes' must be a non-empty array"); + } + + // One Instance per (template sub-shape x placement); geometry shared across placements. + const Json instance_list = get_unique_first_elem(shape_json, "instances"); + if(!instance_list.is_array() || instance_list.empty()){ + CRM_ERROR("instance 'instances' must be a non-empty array"); + } + + for(const auto &s : sub_shapes){ + Json geom = s; + geom.erase("arealight"); + Shape *geometry = parse_shape(geom); + geometries.emplace_back(geometry); + bsdfs.emplace_back(geometry->get_bsdf()); + + if(s.contains("arealight")){ + const Json al_child = get_unique_first_elem(s, "arealight"); + radiances.emplace_back(parse_vector3f(al_child, "radiance")); + } else { + radiances.emplace_back(Vector3f{-1.0f, -1.0f, -1.0f}); + } + } + + for(const auto &inst : instance_list){ + const Matrix44f to_world = parse_matrix44f(inst, "to_world"); + for(std::size_t i = 0; i < geometries.size(); ++i){ + AreaLight *al = nullptr; + if(radiances[i][0] >= 0.0f){ + al = AreaLight::Create(radiances[i]); + } + out.emplace_back(Shape::Create(geometries[i], to_world, bsdfs[i], al)); + } + } + } + std::vector SceneParser::parse_lights() const{ std::vector lights; @@ -212,6 +262,45 @@ namespace Caramel{ parse_bsdf(shape_json)); } } + else if(type=="trianglemesh"){ + const Json Pj = get_unique_first_elem(shape_json, "P"); + const Json Ij = get_unique_first_elem(shape_json, "indices"); + if(!Pj.is_array() || Pj.size() % 3 != 0){ + CRM_ERROR("trianglemesh 'P' must be a flat array of 3*N numbers"); + } + if(!Ij.is_array() || Ij.size() % 3 != 0){ + CRM_ERROR("trianglemesh 'indices' must be a flat array of 3*M integers"); + } + std::vector positions; + for(std::size_t i = 0; i + 2 < Pj.size(); i += 3){ + positions.push_back(Vector3f{static_cast(Pj[i]), static_cast(Pj[i+1]), static_cast(Pj[i+2])}); + } + std::vector indices; + for(std::size_t i = 0; i + 2 < Ij.size(); i += 3){ + indices.push_back(Vector3i{static_cast(Ij[i]), static_cast(Ij[i+1]), static_cast(Ij[i+2])}); + } + for(const auto &t : indices){ + for(int k = 0; k < 3; ++k){ + if(t[k] < 0 || static_cast(t[k]) >= positions.size()){ + CRM_ERROR("trianglemesh index out of range"); + } + } + } + std::vector normals; + if(shape_json.contains("N")){ + const Json Nj = get_unique_first_elem(shape_json, "N"); + if(!Nj.is_array() || Nj.size() != Pj.size()){ + CRM_ERROR("trianglemesh 'N' must be a flat per-vertex array matching 'P' length"); + } + for(std::size_t i = 0; i + 2 < Nj.size(); i += 3){ + normals.push_back(Vector3f{static_cast(Nj[i]), static_cast(Nj[i+1]), static_cast(Nj[i+2])}); + } + } + return Shape::Create(positions, indices, normals, + parse_bsdf(shape_json), + shape_json.contains("arealight") ? parse_arealight(shape_json) : nullptr, + shape_json.contains("to_world") ? parse_matrix44f(shape_json, "to_world") : Matrix44f::identity()); + } CRM_ERROR("Unsupported shape type : " + type); return nullptr; diff --git a/src/shapes/inline_triangle_mesh.cpp b/src/shapes/inline_triangle_mesh.cpp new file mode 100644 index 0000000..767de80 --- /dev/null +++ b/src/shapes/inline_triangle_mesh.cpp @@ -0,0 +1,57 @@ +// +// This software is released under the MIT license. +// +// Copyright (c) 2022-2026 Jino Park +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include + +#include + +#include +#include + +namespace Caramel { + InlineTriangleMesh::InlineTriangleMesh(std::vector positions, + std::vector indices, + std::vector normals, + BSDF *bsdf, AreaLight *arealight, + const Matrix44f &transform) + : TriangleMesh(bsdf, arealight) { + is_vn_exists = !normals.empty(); + + m_vertices.reserve(positions.size()); + for (const auto &p : positions) { + m_vertices.emplace_back(transform_point(p, transform)); + } + + if (is_vn_exists) { + m_normals.reserve(normals.size()); + for (const auto &n : normals) { + m_normals.emplace_back(transform_normal(n, transform)); + } + } + + m_face_indices = std::move(indices); + + finalize(arealight, "trianglemesh"); + } +} diff --git a/src/shapes/instance.cpp b/src/shapes/instance.cpp new file mode 100644 index 0000000..260afa4 --- /dev/null +++ b/src/shapes/instance.cpp @@ -0,0 +1,157 @@ +// +// This software is released under the MIT license. +// +// Copyright (c) 2022-2026 Jino Park +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include + +#include + +#include +#include +#include +#include +#include + +namespace Caramel{ + namespace { + // World AABB = tight box around the 8 transformed corners of the local AABB. + AABB transform_aabb(const AABB &local, const Matrix44f &to_world){ + Vector3f mn{INF, INF, INF}; + Vector3f mx{-INF, -INF, -INF}; + for(Index i = 0; i < 8; ++i){ + const Vector3f c = transform_point(local.corner(i), to_world); + for(int a = 0; a < 3; ++a){ + mn[a] = std::min(mn[a], c[a]); + mx[a] = std::max(mx[a], c[a]); + } + } + return {mn, mx}; + } + + Float transformed_triangle_area(const Vector3f &a, const Vector3f &b, const Vector3f &c, const Matrix44f &m){ + const Vector3f w0 = transform_point(a, m); + const Vector3f w1 = transform_point(b, m); + const Vector3f w2 = transform_point(c, m); + return Vector3f::cross(w1 - w0, w2 - w0).length() * Float0_5; + } + + Float polygon_world_area(const std::vector &poly, const Matrix44f &to_world){ + Float area = Float0; + for(std::size_t i = 1; i + 1 < poly.size(); ++i){ + area += transformed_triangle_area(poly[0], poly[i], poly[i + 1], to_world); + } + return area; + } + } + + Instance::Instance(const Shape *geometry, const Matrix44f &to_world, BSDF *bsdf, AreaLight *arealight) + : Shape{bsdf, arealight}, m_geometry{geometry}, m_to_world{to_world}, + m_to_local{Inverse(to_world)}, + m_world_aabb{transform_aabb(geometry->get_aabb(), to_world)} { + if(arealight != nullptr){ + // World area must be exact under an arbitrary (non-uniform / sheared) to_world, + // which no single scalar captures, so a mesh emitter is measured per-triangle in + // world space. Templates carry no arealight, so finalize() built no boundary + // polygon to reuse -- hence the type check. m_world_triangle_pdf weights triangles + // by world area so sample_point()'s 1/m_world_area pdf is exact. + if(const auto *mesh = dynamic_cast(geometry)){ + std::vector world_tri_areas(mesh->get_triangle_num()); + m_world_area = Float0; + for(Index i = 0; i < mesh->get_triangle_num(); ++i){ + const auto [a, b, c] = mesh->get_triangle_vertices(i); + world_tri_areas[i] = transformed_triangle_area(a, b, c, to_world); + m_world_area += world_tri_areas[i]; + } + m_world_triangle_pdf = Distrib1D(world_tri_areas); + } + else{ + m_world_area = polygon_world_area(m_geometry->get_polygon_vertices(), to_world); + } + if(m_geometry->is_solid_angle_sampling_possible()){ + for(const auto &v : m_geometry->get_polygon_vertices()){ + m_world_polygon_vertices.push_back(transform_point(v, to_world)); + } + } + } + } + + std::pair Instance::ray_intersect(const Ray &ray, Float maxt) const{ + // World ray -> local space. Ray's ctor renormalizes the direction, so the + // local t is measured along a unit local dir; k = |M_inv * d| converts + // between world and local distance (handles non-uniform scale + rotation). + const Vector3f d_local = transform_vector(ray.m_d, m_to_local); + const Float k = d_local.length(); + const Ray local{transform_point(ray.m_o, m_to_local), d_local}; + + auto [hit, info] = m_geometry->ray_intersect(local, maxt * k); + if(!hit){ + return {false, info}; + } + + info.p = transform_point(info.p, m_to_world); + // Normal transform = inverse-transpose of to_world = transpose(m_to_local). + // Using the transpose avoids a per-ray matrix inverse. + const Matrix44f normal_mat{T(m_to_local)}; + info.sh_coord = Coordinate{transform_vector(info.sh_coord.m_world_n, normal_mat).normalize()}; + info.t /= k; + return {true, info}; + // info.shape is overwritten by the scene BVH with this Instance*, so the + // integrator reads the BSDF carried by this Instance (gotcha 3 in the design doc). + } + + AABB Instance::get_aabb() const{ + return m_world_aabb; + } + + Float Instance::get_area() const{ + return m_world_area; + } + + std::tuple Instance::sample_point(Sampler &sampler) const{ + // Pick a triangle by its WORLD area, then sample uniformly within it, so the + // world-space density is exactly 1/m_world_area even under non-uniform scale. + const TriangleMesh *mesh = dynamic_cast(m_geometry); + const auto [local_p, local_n, local_pdf] = mesh + ? mesh->get_triangle_sample_point(m_world_triangle_pdf.sample(sampler.sample_1d()), sampler) + : m_geometry->sample_point(sampler); + const Vector3f world_p = transform_point(local_p, m_to_world); + const Vector3f world_n = transform_vector(local_n, T(m_to_local)).normalize(); + return {world_p, world_n, Float1 / m_world_area}; + } + + Float Instance::pdf_solidangle(const Vector3f &hitpos_world, const Vector3f &shapepos_world, const Vector3f &shape_normal_world) const{ + const Vector3f shape_to_hitpos_world = hitpos_world - shapepos_world; + using std::abs; + const Float cos = abs(shape_normal_world.dot(shape_to_hitpos_world.normalize())); + const Float dist_squared = shape_to_hitpos_world.dot(shape_to_hitpos_world); + return dist_squared / (cos * get_area()); + } + + bool Instance::is_solid_angle_sampling_possible() const{ + return m_geometry->is_solid_angle_sampling_possible(); + } + + const std::vector& Instance::get_polygon_vertices() const{ + return m_world_polygon_vertices; + } +} diff --git a/src/shapes/objmesh.cpp b/src/shapes/objmesh.cpp index 85a51fe..a7736ce 100644 --- a/src/shapes/objmesh.cpp +++ b/src/shapes/objmesh.cpp @@ -28,12 +28,8 @@ #include -#include #include #include -#include -#include -#include #include #define TINYOBJLOADER_IMPLEMENTATION @@ -41,14 +37,10 @@ namespace Caramel { OBJMesh::OBJMesh(const std::filesystem::path &path, BSDF *bsdf, AreaLight *arealight, const Matrix44f &transform) - : TriangleMesh(bsdf, arealight){ - if (!m_vertices.empty()) { - CRM_ERROR("This mesh already loaded obj file"); - } + : TriangleMesh(bsdf, arealight) { if (!std::filesystem::exists(path)) { CRM_ERROR(path.string() + " is not exists"); } - std::string err; tinyobj::ObjReader reader; // `triangulate` option is true by default @@ -61,7 +53,6 @@ namespace Caramel { const auto &attrib = reader.GetAttrib(); const auto &shapes = reader.GetShapes(); - const auto &mats = reader.GetMaterials(); CRM_LOG("Loading obj : " + path.string()); if (shapes.size() != 1) { @@ -71,315 +62,40 @@ namespace Caramel { is_vn_exists = !attrib.normals.empty(); is_tx_exists = !attrib.texcoords.empty(); - // Used for aabb - Float min_x = INF, min_y = INF, min_z = INF, - max_x = -INF, max_y = -INF, max_z = -INF; - - for (int i = 0; i < attrib.vertices.size(); i += 3) { - const Vector3f transformed_point = transform_point({attrib.vertices[i], attrib.vertices[i+1], attrib.vertices[i+2]}, transform); - - min_x = min_x > transformed_point[0] ? transformed_point[0] : min_x; - min_y = min_y > transformed_point[1] ? transformed_point[1] : min_y; - min_z = min_z > transformed_point[2] ? transformed_point[2] : min_z; - - max_x = max_x < transformed_point[0] ? transformed_point[0] : max_x; - max_y = max_y < transformed_point[1] ? transformed_point[1] : max_y; - max_z = max_z < transformed_point[2] ? transformed_point[2] : max_z; - - m_vertices.emplace_back(transformed_point[0], - transformed_point[1], - transformed_point[2]); - } - - for (int i = 0; i < attrib.normals.size(); i += 3) { - const Vector3f transformed_normal = transform_normal({attrib.normals[i], attrib.normals[i + 1], attrib.normals[i + 2]}, transform); - m_normals.emplace_back(transformed_normal[0], transformed_normal[1], transformed_normal[2]); - } - - for (int i = 0; i < attrib.texcoords.size(); i += 2) { - m_tex_coords.emplace_back(attrib.texcoords[i], - attrib.texcoords[i + 1]); - } - const auto &indices = shapes[0].mesh.indices; - - for (int i = 0; i < indices.size(); i += 3) { - m_vertex_indices.emplace_back(indices[i].vertex_index, - indices[i + 1].vertex_index, - indices[i + 2].vertex_index); - - m_normal_indices.emplace_back(indices[i].normal_index, - indices[i + 1].normal_index, - indices[i + 2].normal_index); - - m_tex_coord_indices.emplace_back(indices[i].texcoord_index, - indices[i + 1].texcoord_index, - indices[i + 2].texcoord_index); - } - - std::vector triangle_area_vec; - triangle_area_vec.resize(m_vertex_indices.size()); - - m_area = Float0; - // Initialize m_triangle_pdf used in sampling triangle. - for(int i=0;i(*this, Float1, Float1, 32, 1); - m_accel->build(); - - if (AreaLight::TRY_SOLID_ANGLE_SAMPLING && arealight != nullptr) { - // Check coplanarity of all triangles - const auto& idx0 = m_vertex_indices[0]; - const Vector3f ref_normal = Vector3f::cross( - m_vertices[idx0[1]] - m_vertices[idx0[0]], - m_vertices[idx0[2]] - m_vertices[idx0[0]]).normalize(); - - bool coplanar = true; - constexpr Float coplanar_eps = Float(1e-4); - - for (Index i = 0; i < m_vertex_indices.size() && coplanar; ++i) { - const auto& idx = m_vertex_indices[i]; - const Vector3f tri_normal = Vector3f::cross( - m_vertices[idx[1]] - m_vertices[idx[0]], - m_vertices[idx[2]] - m_vertices[idx[0]]).normalize(); - if (ref_normal.dot(tri_normal) <= Float1 - coplanar_eps) { - coplanar = false; - break; - } - - for (int j = 0; j < 3; ++j) { - using std::abs; - if (abs(ref_normal.dot(m_vertices[idx[j]] - m_vertices[idx0[0]])) > coplanar_eps) { - coplanar = false; - break; + std::map, Int> welded; + for (size_t i = 0; i + 2 < indices.size(); i += 3) { + Int tri[3]; + for (int k = 0; k < 3; ++k) { + const auto &idx = indices[i + k]; + const auto key = std::make_tuple(idx.vertex_index, + is_vn_exists ? idx.normal_index : 0, + is_tx_exists ? idx.texcoord_index : 0); + auto [it, inserted] = welded.try_emplace(key, static_cast(m_vertices.size())); + if (inserted) { + const int v = idx.vertex_index; + m_vertices.emplace_back(transform_point( + Vector3f{attrib.vertices[3 * v], attrib.vertices[3 * v + 1], attrib.vertices[3 * v + 2]}, transform)); + if (is_vn_exists) { + const int n = idx.normal_index; + m_normals.emplace_back(transform_normal( + Vector3f{attrib.normals[3 * n], attrib.normals[3 * n + 1], attrib.normals[3 * n + 2]}, transform)); + } + if (is_tx_exists) { + const int t = idx.texcoord_index; + m_tex_coords.emplace_back(attrib.texcoords[2 * t], attrib.texcoords[2 * t + 1]); } } + tri[k] = it->second; } - - if (!coplanar) { - return; - } - - // Build edge map: (min_idx, max_idx) -> count - std::map, int> edge_count; - for (Index i = 0; i < m_vertex_indices.size(); ++i) { - const auto& idx = m_vertex_indices[i]; - for (int e = 0; e < 3; ++e) { - Int a = idx[e]; - Int b = idx[(e + 1) % 3]; - auto edge = std::make_pair(std::min(a, b), std::max(a, b)); - edge_count[edge]++; - } - } - - // Find boundary edges (count == 1) and build adjacency - std::map> adjacency; - for (const auto& [edge, count] : edge_count) { - if (count == 1) { - adjacency[edge.first].push_back(edge.second); - adjacency[edge.second].push_back(edge.first); - } - } - - if (adjacency.empty()) { - return; - } - - // Walk boundary edges to get ordered vertex indices - std::vector boundary_indices; - Int start = adjacency.begin()->first; - Int current = start; - Int prev = -1; - do { - boundary_indices.push_back(current); - const auto& neighbors = adjacency[current]; - Int next = (neighbors[0] != prev) ? neighbors[0] : neighbors[1]; - prev = current; - current = next; - } while (current != start); - - if (boundary_indices.size() > MAX_POLYGON_VERTEX_COUNT) { - return; - } - - // Ensure boundary winding matches original triangle winding - const Vector3f boundary_normal = Vector3f::cross( - m_vertices[boundary_indices[1]] - m_vertices[boundary_indices[0]], - m_vertices[boundary_indices[2]] - m_vertices[boundary_indices[0]]); - if (ref_normal.dot(boundary_normal) < Float0) { - std::reverse(boundary_indices.begin(), boundary_indices.end()); - } - - m_is_solid_angle_sampling_possible = true; - CRM_LOG("Area light solid angle sampling enabled : " + path.string()) - m_polygon_vertices.resize(boundary_indices.size()); - for (Index i = 0; i < boundary_indices.size(); ++i) { - m_polygon_vertices[i] = m_vertices[boundary_indices[i]]; - } - } - } - - std::pair OBJMesh::ray_intersect(const Ray &ray, Float maxt) const { - return m_accel->ray_intersect(ray, maxt); - } - - AABB OBJMesh::get_aabb() const { - return m_aabb; - } - - Float OBJMesh::get_area() const{ - return m_area; - } - - std::tuple OBJMesh::sample_point(Sampler &sampler) const{ - // Sample triangle considering area - const Index i = m_triangle_pdf.sample(sampler.sample_1d()); - - // Sample point in chosen triangle - const auto [pos, normal, _] = get_triangle_sample_point(i, sampler); - - return {pos, normal, Float1 / m_area}; - } - - // Similar with `Triangle::pdf_solidangle()` - // This implementation assumes that two given points are visible to each other - Float OBJMesh::pdf_solidangle(const Vector3f &hitpos_world, const Vector3f &shapepos_world, const Vector3f &shape_normal_world) const{ - const Vector3f shape_to_hitpos_world = hitpos_world - shapepos_world; - using std::abs; - const Float cos = abs(shape_normal_world.dot(shape_to_hitpos_world.normalize())); - const Float dist_squared = shape_to_hitpos_world.dot(shape_to_hitpos_world); - return dist_squared / (cos * m_area); - } - - Float OBJMesh::get_triangle_area(Index i) const { - const auto &v_idx = m_vertex_indices[i]; - const Vector3f &p0 = m_vertices[v_idx[0]]; - const Vector3f &p1 = m_vertices[v_idx[1]]; - const Vector3f &p2 = m_vertices[v_idx[2]]; - - return Vector3f::cross(p1 - p0, p2 - p0).length() * Float0_5; - } - - std::tuple OBJMesh::get_triangle_sample_point(Index i, Sampler &sampler) const { - const auto &v_idx = m_vertex_indices[i]; - const Vector3f &p0 = m_vertices[v_idx[0]]; - const Vector3f &p1 = m_vertices[v_idx[1]]; - const Vector3f &p2 = m_vertices[v_idx[2]]; - - const Float u = sampler.sample_1d(); - const Float v = sampler.sample_1d(); - using std::sqrt; - const Float x = Float1 - sqrt(Float1 - u); - const Float y = v * sqrt(Float1 - u); - - Vector3f normal_vec; - if (is_vn_exists) { - const auto &n_idx = m_normal_indices[i]; - const Vector3f &n0 = m_normals[n_idx[0]]; - const Vector3f &n1 = m_normals[n_idx[1]]; - const Vector3f &n2 = m_normals[n_idx[2]]; - normal_vec = interpolate(n0, n1, n2, x, y).normalize(); - } else { - normal_vec = Vector3f::cross(p1 - p0, p2 - p0).normalize(); - } - - return {interpolate(p0, p1, p2, x, y), - normal_vec, - Float1 / get_triangle_area(i)}; - } - - std::pair OBJMesh::get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const { - const auto &v_idx = m_vertex_indices[i]; - const Vector3f &p0 = m_vertices[v_idx[0]]; - const Vector3f &p1 = m_vertices[v_idx[1]]; - const Vector3f &p2 = m_vertices[v_idx[2]]; - -#ifdef USE_MOLLER_TRUMBORE - auto [u, v, t] = moller_trumbore(ray, p0, p1, p2, maxt); -#else - // Default: watertight intersection (more robust at triangle edges) - // Reference: https://jcgt.org/published/0002/01/05/paper.pdf - auto [u, v, t] = watertight_intersection(ray, p0, p1, p2, maxt); -#endif - - if(u==-Float1 && v==-Float1 && t==-Float1){ - return {false, RayIntersectInfo()}; - } - - RayIntersectInfo ret; - ret.t = t; - ret.tri_index = i; - - if (is_tx_exists) { - const auto &uv_idx = m_tex_coord_indices[i]; - const Vector2f &uv0 = m_tex_coords[uv_idx[0]]; - const Vector2f &uv1 = m_tex_coords[uv_idx[1]]; - const Vector2f &uv2 = m_tex_coords[uv_idx[2]]; - ret.tex_uv = interpolate(uv0, uv1, uv2, u, v); - } else { - ret.tex_uv = Vector2f{u, v}; + m_face_indices.emplace_back(tri[0], tri[1], tri[2]); } - ret.tex_uv[0] -= floor(ret.tex_uv[0]); - ret.tex_uv[1] -= floor(ret.tex_uv[1]); - - ret.p = interpolate(p0, p1, p2, u, v); - - Vector3f n; - if (is_vn_exists) { - const auto &n_idx = m_normal_indices[i]; - const Vector3f &n0 = m_normals[n_idx[0]]; - const Vector3f &n1 = m_normals[n_idx[1]]; - const Vector3f &n2 = m_normals[n_idx[2]]; - n = interpolate(n0, n1, n2, u, v).normalize(); - } else { - n = Vector3f::cross(p1 - p0, p2 - p0).normalize(); - } - ret.sh_coord = Coordinate(n); - - return {true, ret}; - } - - AABB OBJMesh::get_triangle_aabb(Index i) const { - const auto &v_idx = m_vertex_indices[i]; - const Vector3f &p0 = m_vertices[v_idx[0]]; - const Vector3f &p1 = m_vertices[v_idx[1]]; - const Vector3f &p2 = m_vertices[v_idx[2]]; - - return AABB(Vector3f{std::min({p0[0], p1[0], p2[0]}), - std::min({p0[1], p1[1], p2[1]}), - std::min({p0[2], p1[2], p2[2]})}, - Vector3f{std::max({p0[0], p1[0], p2[0]}), - std::max({p0[1], p1[1], p2[1]}), - std::max({p0[2], p1[2], p2[2]})}); - } - - std::tuple OBJMesh::get_triangle_vertices(Index i) const { - const auto &v_idx = m_vertex_indices[i]; - return {m_vertices[v_idx[0]], m_vertices[v_idx[1]], m_vertices[v_idx[2]]}; - } - - Index OBJMesh::sample_triangle_index(Float u) const { - return m_triangle_pdf.sample(u); - } - - Float OBJMesh::triangle_select_pdf(Index i) const { - return m_triangle_pdf.pdf(i); - } - - bool OBJMesh::is_solid_angle_sampling_possible() const { - return m_is_solid_angle_sampling_possible; - } - - const std::vector& OBJMesh::get_polygon_vertices() const { - return m_polygon_vertices; + finalize(arealight, path.string()); } } diff --git a/src/shapes/plymesh.cpp b/src/shapes/plymesh.cpp index 8a0042a..c05b781 100644 --- a/src/shapes/plymesh.cpp +++ b/src/shapes/plymesh.cpp @@ -23,17 +23,11 @@ // #include -#include -#include #include -#include #include #include -#include -#include -#include #include #include "happly.h" @@ -67,10 +61,6 @@ namespace Caramel { // Get face indices std::vector> faces = plyData.getFaceIndices(); - // Used for aabb - Float min_x = INF, min_y = INF, min_z = INF, - max_x = -INF, max_y = -INF, max_z = -INF; - // Process vertices m_vertices.reserve(vertices.size()); for (size_t i = 0; i < vertices.size(); ++i) { @@ -79,15 +69,6 @@ namespace Caramel { static_cast(vertices[i][1]), static_cast(vertices[i][2])}, transform); - - min_x = std::min(min_x, transformed_point[0]); - min_y = std::min(min_y, transformed_point[1]); - min_z = std::min(min_z, transformed_point[2]); - - max_x = std::max(max_x, transformed_point[0]); - max_y = std::max(max_y, transformed_point[1]); - max_z = std::max(max_z, transformed_point[2]); - m_vertices.emplace_back(transformed_point[0], transformed_point[1], transformed_point[2]); @@ -108,14 +89,12 @@ namespace Caramel { } } - // Process faces - triangulate if necessary + // Process faces - triangulate polygons via fan triangulation for (const auto& face : faces) { if (face.size() < 3) { CRM_WARNING("Skipping degenerate face with less than 3 vertices"); continue; } - - // Triangulate polygon using fan triangulation for (size_t i = 1; i + 1 < face.size(); ++i) { m_face_indices.emplace_back( static_cast(face[0]), @@ -124,258 +103,6 @@ namespace Caramel { } } - // Calculate triangle areas for sampling - std::vector triangle_area_vec; - triangle_area_vec.resize(m_face_indices.size()); - - m_area = Float0; - for (size_t i = 0; i < m_face_indices.size(); ++i) { - const Float ith_tri_area = get_triangle_area(i); - triangle_area_vec[i] = ith_tri_area; - m_area += ith_tri_area; - } - m_triangle_pdf = Distrib1D(triangle_area_vec); - - m_aabb = AABB({min_x, min_y, min_z}, {max_x, max_y, max_z}); - m_accel = std::make_unique(*this, Float1, Float1, 32, 1); - m_accel->build(); - - if (AreaLight::TRY_SOLID_ANGLE_SAMPLING && arealight != nullptr) { - // Check coplanarity of all triangles - const auto& idx0 = m_face_indices[0]; - const Vector3f ref_normal = Vector3f::cross( - m_vertices[idx0[1]] - m_vertices[idx0[0]], - m_vertices[idx0[2]] - m_vertices[idx0[0]]).normalize(); - - bool coplanar = true; - constexpr Float coplanar_eps = Float(1e-4); - - for (Index i = 0; i < m_face_indices.size() && coplanar; ++i) { - const auto& idx = m_face_indices[i]; - const Vector3f tri_normal = Vector3f::cross( - m_vertices[idx[1]] - m_vertices[idx[0]], - m_vertices[idx[2]] - m_vertices[idx[0]]).normalize(); - - if (ref_normal.dot(tri_normal) <= Float1 - coplanar_eps) { - coplanar = false; - break; - } - - for (int j = 0; j < 3; ++j) { - using std::abs; - if (abs(ref_normal.dot(m_vertices[idx[j]] - m_vertices[idx0[0]])) > coplanar_eps) { - coplanar = false; - break; - } - } - } - - if (!coplanar) { - return; - } - - // Build edge map: (min_idx, max_idx) -> count - std::map, int> edge_count; - for (Index i = 0; i < m_face_indices.size(); ++i) { - const auto& idx = m_face_indices[i]; - for (int e = 0; e < 3; ++e) { - Int a = idx[e]; - Int b = idx[(e + 1) % 3]; - auto edge = std::make_pair(std::min(a, b), std::max(a, b)); - edge_count[edge]++; - } - } - - // Find boundary edges (count == 1) and build adjacency - std::map> adjacency; - for (const auto& [edge, count] : edge_count) { - if (count == 1) { - adjacency[edge.first].push_back(edge.second); - adjacency[edge.second].push_back(edge.first); - } - } - - if (adjacency.empty()) { - return; - } - - // Walk boundary edges to get ordered vertex indices - std::vector boundary_indices; - Int start = adjacency.begin()->first; - Int current = start; - Int prev = -1; - do { - boundary_indices.push_back(current); - const auto& neighbors = adjacency[current]; - Int next = (neighbors[0] != prev) ? neighbors[0] : neighbors[1]; - prev = current; - current = next; - } while (current != start); - - if (boundary_indices.size() > MAX_POLYGON_VERTEX_COUNT) { - return; - } - - // Ensure boundary winding matches original triangle winding - const Vector3f boundary_normal = Vector3f::cross( - m_vertices[boundary_indices[1]] - m_vertices[boundary_indices[0]], - m_vertices[boundary_indices[2]] - m_vertices[boundary_indices[0]]); - if (ref_normal.dot(boundary_normal) < Float0) { - std::reverse(boundary_indices.begin(), boundary_indices.end()); - } - - m_is_solid_angle_sampling_possible = true; - CRM_LOG("Area light solid angle sampling enabled : " + path.string()) - m_polygon_vertices.resize(boundary_indices.size()); - for (Index i = 0; i < boundary_indices.size(); ++i) { - m_polygon_vertices[i] = m_vertices[boundary_indices[i]]; - } - - } - } - - std::pair PLYMesh::ray_intersect(const Ray &ray, Float maxt) const { - return m_accel->ray_intersect(ray, maxt); - } - - AABB PLYMesh::get_aabb() const { - return m_aabb; - } - - Float PLYMesh::get_area() const { - return m_area; - } - - std::tuple PLYMesh::sample_point(Sampler &sampler) const { - // Sample triangle considering area - const Index i = m_triangle_pdf.sample(sampler.sample_1d()); - - // Sample point in chosen triangle - const auto [pos, normal, _] = get_triangle_sample_point(i, sampler); - - return {pos, normal, Float1 / m_area}; - } - - Float PLYMesh::pdf_solidangle(const Vector3f &hitpos_world, const Vector3f &shapepos_world, const Vector3f &shape_normal_world) const { - const Vector3f shape_to_hitpos_world = hitpos_world - shapepos_world; - using std::abs; - const Float cos = abs(shape_normal_world.dot(shape_to_hitpos_world.normalize())); - const Float dist_squared = shape_to_hitpos_world.dot(shape_to_hitpos_world); - return dist_squared / (cos * m_area); - } - - Float PLYMesh::get_triangle_area(Index i) const { - const Vector3i& idx = m_face_indices[i]; - const Vector3f &p0 = m_vertices[idx[0]]; - const Vector3f &p1 = m_vertices[idx[1]]; - const Vector3f &p2 = m_vertices[idx[2]]; - - return Vector3f::cross(p1 - p0, p2 - p0).length() * Float0_5; - } - - std::tuple PLYMesh::get_triangle_sample_point(Index i, Sampler &sampler) const { - const Vector3i& idx = m_face_indices[i]; - const Vector3f &p0 = m_vertices[idx[0]]; - const Vector3f &p1 = m_vertices[idx[1]]; - const Vector3f &p2 = m_vertices[idx[2]]; - - const Float u = sampler.sample_1d(); - const Float v = sampler.sample_1d(); - using std::sqrt; - const Float x = Float1 - sqrt(Float1 - u); - const Float y = v * sqrt(Float1 - u); - - Vector3f n; - if (is_vn_exists) { - const Vector3f &n0 = m_normals[idx[0]]; - const Vector3f &n1 = m_normals[idx[1]]; - const Vector3f &n2 = m_normals[idx[2]]; - n = interpolate(n0, n1, n2, x, y).normalize(); - } else { - n = Vector3f::cross(p1 - p0, p2 - p0).normalize(); - } - - return {interpolate(p0, p1, p2, x, y), - n, - Float1 / get_triangle_area(i)}; - } - - std::pair PLYMesh::get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const { - const Vector3i& idx = m_face_indices[i]; - const Vector3f &p0 = m_vertices[idx[0]]; - const Vector3f &p1 = m_vertices[idx[1]]; - const Vector3f &p2 = m_vertices[idx[2]]; - -#ifdef USE_MOLLER_TRUMBORE - auto [u, v, t] = moller_trumbore(ray, p0, p1, p2, maxt); -#else - // Default: watertight intersection (more robust at triangle edges) - // Reference: https://jcgt.org/published/0002/01/05/paper.pdf - auto [u, v, t] = watertight_intersection(ray, p0, p1, p2, maxt); -#endif - - if(u==-Float1 && v==-Float1 && t==-Float1){ - return {false, RayIntersectInfo()}; - } - - RayIntersectInfo ret; - ret.t = t; - ret.tri_index = i; - - // PLYMesh doesn't have UVs in this implementation - ret.tex_uv = Vector2f{u, v}; - - ret.tex_uv[0] -= floor(ret.tex_uv[0]); - ret.tex_uv[1] -= floor(ret.tex_uv[1]); - - ret.p = interpolate(p0, p1, p2, u, v); - - Vector3f n; - if (is_vn_exists) { - const Vector3f &n0 = m_normals[idx[0]]; - const Vector3f &n1 = m_normals[idx[1]]; - const Vector3f &n2 = m_normals[idx[2]]; - n = interpolate(n0, n1, n2, u, v).normalize(); - } else { - n = Vector3f::cross(p1 - p0, p2 - p0).normalize(); - } - ret.sh_coord = Coordinate(n); - - return {true, ret}; - } - - AABB PLYMesh::get_triangle_aabb(Index i) const { - const Vector3i& idx = m_face_indices[i]; - const Vector3f &p0 = m_vertices[idx[0]]; - const Vector3f &p1 = m_vertices[idx[1]]; - const Vector3f &p2 = m_vertices[idx[2]]; - - return AABB(Vector3f{std::min({p0[0], p1[0], p2[0]}), - std::min({p0[1], p1[1], p2[1]}), - std::min({p0[2], p1[2], p2[2]})}, - Vector3f{std::max({p0[0], p1[0], p2[0]}), - std::max({p0[1], p1[1], p2[1]}), - std::max({p0[2], p1[2], p2[2]})}); - } - - std::tuple PLYMesh::get_triangle_vertices(Index i) const { - const Vector3i& idx = m_face_indices[i]; - return {m_vertices[idx[0]], m_vertices[idx[1]], m_vertices[idx[2]]}; - } - - Index PLYMesh::sample_triangle_index(Float u) const { - return m_triangle_pdf.sample(u); - } - - Float PLYMesh::triangle_select_pdf(Index i) const { - return m_triangle_pdf.pdf(i); - } - - bool PLYMesh::is_solid_angle_sampling_possible() const { - return m_is_solid_angle_sampling_possible; - } - - const std::vector& PLYMesh::get_polygon_vertices() const { - return m_polygon_vertices; + finalize(arealight, path.string()); } } diff --git a/src/shapes/triangle_mesh.cpp b/src/shapes/triangle_mesh.cpp new file mode 100644 index 0000000..0c7e726 --- /dev/null +++ b/src/shapes/triangle_mesh.cpp @@ -0,0 +1,321 @@ +// +// This software is released under the MIT license. +// +// Copyright (c) 2022-2026 Jino Park +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Caramel { + TriangleMesh::TriangleMesh(BSDF *bsdf, AreaLight *arealight) : Shape{bsdf, arealight} {} + + TriangleMesh::~TriangleMesh() = default; + + void TriangleMesh::finalize(AreaLight *arealight, const std::string &name) { + Vector3f mn{INF, INF, INF}; + Vector3f mx{-INF, -INF, -INF}; + for (const auto &v : m_vertices) { + for (int a = 0; a < 3; ++a) { + mn[a] = std::min(mn[a], v[a]); + mx[a] = std::max(mx[a], v[a]); + } + } + m_aabb = AABB(mn, mx); + + // Calculate triangle areas for sampling + std::vector triangle_area_vec; + triangle_area_vec.resize(m_face_indices.size()); + + m_area = Float0; + for (size_t i = 0; i < m_face_indices.size(); ++i) { + const Float ith_tri_area = get_triangle_area(i); + triangle_area_vec[i] = ith_tri_area; + m_area += ith_tri_area; + } + + if(m_face_indices.empty() || m_area <= Float0){ + CRM_ERROR(name + " : mesh has no triangles or zero surface area"); + } + + m_triangle_pdf = Distrib1D(triangle_area_vec); + + m_accel = std::make_unique(*this, Float1, Float1, 32, 1); + m_accel->build(); + + if (AreaLight::TRY_SOLID_ANGLE_SAMPLING && arealight != nullptr && !m_face_indices.empty()) { + // Check coplanarity of all triangles + const auto& idx0 = m_face_indices[0]; + const Vector3f ref_normal = Vector3f::cross( + m_vertices[idx0[1]] - m_vertices[idx0[0]], + m_vertices[idx0[2]] - m_vertices[idx0[0]]).normalize(); + + bool coplanar = true; + constexpr Float coplanar_eps = Float(1e-4); + + for (Index i = 0; i < m_face_indices.size() && coplanar; ++i) { + const auto& idx = m_face_indices[i]; + const Vector3f tri_normal = Vector3f::cross( + m_vertices[idx[1]] - m_vertices[idx[0]], + m_vertices[idx[2]] - m_vertices[idx[0]]).normalize(); + + if (ref_normal.dot(tri_normal) <= Float1 - coplanar_eps) { + coplanar = false; + break; + } + + for (int j = 0; j < 3; ++j) { + using std::abs; + if (abs(ref_normal.dot(m_vertices[idx[j]] - m_vertices[idx0[0]])) > coplanar_eps) { + coplanar = false; + break; + } + } + } + + if (!coplanar) { + return; + } + + // Build edge map: (min_idx, max_idx) -> count + std::map, int> edge_count; + for (Index i = 0; i < m_face_indices.size(); ++i) { + const auto& idx = m_face_indices[i]; + for (int e = 0; e < 3; ++e) { + Int a = idx[e]; + Int b = idx[(e + 1) % 3]; + auto edge = std::make_pair(std::min(a, b), std::max(a, b)); + edge_count[edge]++; + } + } + + // Find boundary edges (count == 1) and build adjacency + std::map> adjacency; + for (const auto& [edge, count] : edge_count) { + if (count == 1) { + adjacency[edge.first].push_back(edge.second); + adjacency[edge.second].push_back(edge.first); + } + } + + if (adjacency.empty()) { + return; + } + for (const auto& kv : adjacency) { + if (kv.second.size() != 2) { + return; + } + } + + // Walk boundary edges to get ordered vertex indices + std::vector boundary_indices; + Int start = adjacency.begin()->first; + Int current = start; + Int prev = -1; + do { + boundary_indices.push_back(current); + const auto& neighbors = adjacency[current]; + Int next = (neighbors[0] != prev) ? neighbors[0] : neighbors[1]; + prev = current; + current = next; + } while (current != start); + + if (boundary_indices.size() > MAX_POLYGON_VERTEX_COUNT) { + return; + } + + // Ensure boundary winding matches original triangle winding + const Vector3f boundary_normal = Vector3f::cross( + m_vertices[boundary_indices[1]] - m_vertices[boundary_indices[0]], + m_vertices[boundary_indices[2]] - m_vertices[boundary_indices[0]]); + if (ref_normal.dot(boundary_normal) < Float0) { + std::reverse(boundary_indices.begin(), boundary_indices.end()); + } + + m_is_solid_angle_sampling_possible = true; + CRM_LOG("Area light solid angle sampling enabled : " + name) + m_polygon_vertices.resize(boundary_indices.size()); + for (Index i = 0; i < boundary_indices.size(); ++i) { + m_polygon_vertices[i] = m_vertices[boundary_indices[i]]; + } + } + } + + std::pair TriangleMesh::ray_intersect(const Ray &ray, Float maxt) const { + return m_accel->ray_intersect(ray, maxt); + } + + AABB TriangleMesh::get_aabb() const { + return m_aabb; + } + + Float TriangleMesh::get_area() const { + return m_area; + } + + std::tuple TriangleMesh::sample_point(Sampler &sampler) const { + // Sample triangle considering area + const Index i = m_triangle_pdf.sample(sampler.sample_1d()); + + // Sample point in chosen triangle + const auto [pos, normal, _] = get_triangle_sample_point(i, sampler); + + return {pos, normal, Float1 / m_area}; + } + + Float TriangleMesh::pdf_solidangle(const Vector3f &hitpos_world, const Vector3f &shapepos_world, const Vector3f &shape_normal_world) const { + const Vector3f shape_to_hitpos_world = hitpos_world - shapepos_world; + using std::abs; + const Float cos = abs(shape_normal_world.dot(shape_to_hitpos_world.normalize())); + const Float dist_squared = shape_to_hitpos_world.dot(shape_to_hitpos_world); + return dist_squared / (cos * m_area); + } + + Float TriangleMesh::get_triangle_area(Index i) const { + const Vector3i& idx = m_face_indices[i]; + const Vector3f &p0 = m_vertices[idx[0]]; + const Vector3f &p1 = m_vertices[idx[1]]; + const Vector3f &p2 = m_vertices[idx[2]]; + + return Vector3f::cross(p1 - p0, p2 - p0).length() * Float0_5; + } + + std::tuple TriangleMesh::get_triangle_sample_point(Index i, Sampler &sampler) const { + const Vector3i& idx = m_face_indices[i]; + const Vector3f &p0 = m_vertices[idx[0]]; + const Vector3f &p1 = m_vertices[idx[1]]; + const Vector3f &p2 = m_vertices[idx[2]]; + + const Float u = sampler.sample_1d(); + const Float v = sampler.sample_1d(); + using std::sqrt; + const Float x = Float1 - sqrt(Float1 - u); + const Float y = v * sqrt(Float1 - u); + + Vector3f n; + if (is_vn_exists) { + const Vector3f &n0 = m_normals[idx[0]]; + const Vector3f &n1 = m_normals[idx[1]]; + const Vector3f &n2 = m_normals[idx[2]]; + n = interpolate(n0, n1, n2, x, y).normalize(); + } else { + n = Vector3f::cross(p1 - p0, p2 - p0).normalize(); + } + + return {interpolate(p0, p1, p2, x, y), + n, + Float1 / get_triangle_area(i)}; + } + + std::pair TriangleMesh::get_triangle_ray_intersect(Index i, const Ray &ray, Float maxt) const { + const Vector3i& idx = m_face_indices[i]; + const Vector3f &p0 = m_vertices[idx[0]]; + const Vector3f &p1 = m_vertices[idx[1]]; + const Vector3f &p2 = m_vertices[idx[2]]; + +#ifdef USE_MOLLER_TRUMBORE + auto [u, v, t] = moller_trumbore(ray, p0, p1, p2, maxt); +#else + // Default: watertight intersection (more robust at triangle edges) + // Reference: https://jcgt.org/published/0002/01/05/paper.pdf + auto [u, v, t] = watertight_intersection(ray, p0, p1, p2, maxt); +#endif + + if(u==-Float1 && v==-Float1 && t==-Float1){ + return {false, RayIntersectInfo()}; + } + + RayIntersectInfo ret; + ret.t = t; + ret.tri_index = i; + + if (is_tx_exists) { + const Vector2f &uv0 = m_tex_coords[idx[0]]; + const Vector2f &uv1 = m_tex_coords[idx[1]]; + const Vector2f &uv2 = m_tex_coords[idx[2]]; + ret.tex_uv = interpolate(uv0, uv1, uv2, u, v); + } else { + ret.tex_uv = Vector2f{u, v}; + } + + ret.tex_uv[0] -= floor(ret.tex_uv[0]); + ret.tex_uv[1] -= floor(ret.tex_uv[1]); + + ret.p = interpolate(p0, p1, p2, u, v); + + Vector3f n; + if (is_vn_exists) { + const Vector3f &n0 = m_normals[idx[0]]; + const Vector3f &n1 = m_normals[idx[1]]; + const Vector3f &n2 = m_normals[idx[2]]; + n = interpolate(n0, n1, n2, u, v).normalize(); + } else { + n = Vector3f::cross(p1 - p0, p2 - p0).normalize(); + } + ret.sh_coord = Coordinate(n); + + return {true, ret}; + } + + AABB TriangleMesh::get_triangle_aabb(Index i) const { + const Vector3i& idx = m_face_indices[i]; + const Vector3f &p0 = m_vertices[idx[0]]; + const Vector3f &p1 = m_vertices[idx[1]]; + const Vector3f &p2 = m_vertices[idx[2]]; + + return AABB(Vector3f{std::min({p0[0], p1[0], p2[0]}), + std::min({p0[1], p1[1], p2[1]}), + std::min({p0[2], p1[2], p2[2]})}, + Vector3f{std::max({p0[0], p1[0], p2[0]}), + std::max({p0[1], p1[1], p2[1]}), + std::max({p0[2], p1[2], p2[2]})}); + } + + std::tuple TriangleMesh::get_triangle_vertices(Index i) const { + const Vector3i& idx = m_face_indices[i]; + return {m_vertices[idx[0]], m_vertices[idx[1]], m_vertices[idx[2]]}; + } + + Index TriangleMesh::sample_triangle_index(Float u) const { + return m_triangle_pdf.sample(u); + } + + Float TriangleMesh::triangle_select_pdf(Index i) const { + return m_triangle_pdf.pdf(i); + } + + bool TriangleMesh::is_solid_angle_sampling_possible() const { + return m_is_solid_angle_sampling_possible; + } + + const std::vector& TriangleMesh::get_polygon_vertices() const { + return m_polygon_vertices; + } +} diff --git a/test/simple_render_test.cpp b/test/simple_render_test.cpp index ff83233..b167eda 100644 --- a/test/simple_render_test.cpp +++ b/test/simple_render_test.cpp @@ -28,12 +28,16 @@ #include #include #include +#include +#include #include // Dependencies headers #include "catch_amalgamated.hpp" +#include + using namespace Caramel; TEST_CASE("test1 render test", "[RenderTest]") { @@ -163,7 +167,16 @@ TEST_CASE("test6 render test", "[RenderTest]") { CHECK(Catch::Approx(0.9995) <= avg(rendered)/avg(ref)); } +TEST_CASE("test7 render test", "[RenderTest]") { + std::string scene_path = std::string(TEST_SCENE_PATH) + "test_scenes/test7/scene.json"; + Image ref(std::string(TEST_SCENE_PATH) + "test_scenes/test7/gt.exr"); + auto [_s, _i] = build_scene(scene_path); + Image rendered = render(_s, _i); + if (SAVE_RENDERED_IMAGES) { + rendered.write_exr((std::filesystem::path(scene_path).parent_path() / "test7_rendered.exr").string()); + } - - + CHECK(avg(rendered)/avg(ref) <= Catch::Approx(1.002)); + CHECK(Catch::Approx(0.998) <= avg(rendered)/avg(ref)); +} diff --git a/test/unit_tests.cpp b/test/unit_tests.cpp index e914cb6..dacaa9d 100644 --- a/test/unit_tests.cpp +++ b/test/unit_tests.cpp @@ -32,6 +32,8 @@ #include #include #include +#include +#include // Dependencies headers #include "catch_amalgamated.hpp" @@ -1777,3 +1779,82 @@ TEST_CASE("Scene::is_visible", "[UnitTest]") { delete tri2; } +// ============================================================================= +// Instance::ray_intersect Tests +// ============================================================================= + +TEST_CASE("Instance matches baked-transform geometry", "[UnitTest]") { + // Template triangle in LOCAL space. + const Vector3f p0{0.0f, 0.0f, 0.0f}; + const Vector3f p1{1.0f, 0.0f, 0.0f}; + const Vector3f p2{0.0f, 1.0f, 0.0f}; + Triangle tri_local(p0, p1, p2, nullptr); + + // Non-reflecting affine transform (det > 0): scale, then rotate_y, then translate. + const Matrix44f to_world = + translate(2.0f, -1.0f, 0.5f) * rotate_y(30.0f) * scale(2.0f, 2.0f, 2.0f); + + Instance inst(&tri_local, to_world, nullptr); + + // Reference: the SAME triangle with the transform baked into its vertices. + const Vector3f w0 = transform_point(p0, to_world); + const Vector3f w1 = transform_point(p1, to_world); + const Vector3f w2 = transform_point(p2, to_world); + Triangle tri_world(w0, w1, w2, nullptr); + + // Ray from +Z toward the world-space centroid. + const Vector3f centroid = (w0 + w1 + w2) * (Float1 / 3.0f); + Ray ray = RayTestHelper::create({centroid[0], centroid[1], centroid[2] + 10.0f}, + {0.0f, 0.0f, -1.0f}); + + auto [hit_ref, info_ref ] = tri_world.ray_intersect(ray, INF); + auto [hit_inst, info_inst] = inst.ray_intersect(ray, INF); + + REQUIRE(hit_ref); + CHECK(hit_inst == hit_ref); + CHECK(is_approx(info_inst.t, info_ref.t)); + CHECK(is_approx(info_inst.p[0], info_ref.p[0])); + CHECK(is_approx(info_inst.p[1], info_ref.p[1])); + CHECK(is_approx(info_inst.p[2], info_ref.p[2])); + + // World-space shading normals point the same way. + const Float ndot = info_inst.sh_coord.m_world_n.dot(info_ref.sh_coord.m_world_n); + CHECK(is_approx(ndot, Float1)); + + // World AABB contains the (baked) world-space geometry. + CHECK(inst.get_aabb().is_contain(centroid)); +} + +// ============================================================================= +// InlineTriangleMesh (inline "trianglemesh") Tests +// ============================================================================= + +TEST_CASE("InlineTriangleMesh intersects an inline quad and reports area", "[UnitTest]") { + // Unit square in z=0 plane: two triangles. + std::vector P = { Vector3f{0.f,0.f,0.f}, Vector3f{1.f,0.f,0.f}, Vector3f{1.f,1.f,0.f}, Vector3f{0.f,1.f,0.f} }; + std::vector idx = { Vector3i{0,1,2}, Vector3i{0,2,3} }; + InlineTriangleMesh mesh(P, idx, {}, nullptr, nullptr, Matrix44f::identity()); + + CHECK(mesh.get_triangle_num() == 2); + CHECK(is_approx(mesh.get_area(), 1.0f)); // unit square -> area 1 + + Ray ray = RayTestHelper::create({0.5f, 0.5f, 5.0f}, {0.0f, 0.0f, -1.0f}); + auto [hit, info] = mesh.ray_intersect(ray, INF); + CHECK(hit); + CHECK(is_approx(info.p[0], 0.5f)); + CHECK(is_approx(info.p[1], 0.5f)); + CHECK(std::abs(info.p[2]) < 1e-3f); + CHECK(is_approx(std::abs(info.sh_coord.m_world_n[2]), 1.0f)); // normal faces +/-z +} + +TEST_CASE("instanced area-light get_area is exact under non-uniform scale", "[UnitTest]") { + // xz-plane unit quad under scale(2,3,5): true area = sx*sz = 10 (old sx*sy gave 6). + std::vector P = { Vector3f{0.f,0.f,0.f}, Vector3f{1.f,0.f,0.f}, Vector3f{1.f,0.f,1.f}, Vector3f{0.f,0.f,1.f} }; + std::vector idx = { Vector3i{0,1,2}, Vector3i{0,2,3} }; + InlineTriangleMesh quad(P, idx, {}, nullptr); + + AreaLight *al = AreaLight::Create(Vector3f{1.f, 1.f, 1.f}); + Instance inst(&quad, scale(2.f, 3.f, 5.f), nullptr, al); + CHECK(is_approx(inst.get_area(), 10.0f)); +} +