From 0f83ddfb9d74d6a13d904dc53fba6c072da66de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Dumas?= Date: Wed, 8 Feb 2023 15:16:56 -0800 Subject: [PATCH] Sync with dbfbeaa (v6.8.0) (#5) --- CMakeLists.txt | 27 +- LagrangeOptions.cmake.sample | 2 + NOTICE.txt | 49 ++ VERSION | 2 +- cmake/lagrange/lagrangeMklModules.txt | 1 + cmake/lagrange/lagrange_add_module.cmake | 2 + cmake/lagrange/lagrange_download_data.cmake | 2 +- cmake/lagrange/lagrange_has_onetbb.cmake | 2 +- cmake/recipes/external/assimp.cmake | 2 +- cmake/recipes/external/blas.cmake | 11 + cmake/recipes/external/embree.cmake | 2 +- cmake/recipes/external/filesystem.cmake | 5 +- cmake/recipes/external/happly.cmake | 42 ++ cmake/recipes/external/lapack.cmake | 11 + cmake/recipes/external/mikktspace.cmake | 2 + cmake/recipes/external/nanobind.cmake | 2 +- cmake/recipes/external/opensubdiv.cmake | 2 +- cmake/recipes/external/openvdb.cmake | 18 +- cmake/recipes/external/spdlog.cmake | 2 +- cmake/recipes/external/tinygltf.cmake | 36 ++ cmake/recipes/external/winding_number.cmake | 2 +- .../include/lagrange/bvh/EdgeAABBTree.impl.h | 52 +- modules/core/CMakeLists.txt | 4 +- modules/core/include/lagrange/AttributeFwd.h | 2 +- modules/core/include/lagrange/Components.h | 2 +- modules/core/include/lagrange/DisjointSets.h | 2 +- modules/core/include/lagrange/Edge.h | 10 +- modules/core/include/lagrange/MeshTopology.h | 1 - .../core/include/lagrange/attribute_names.h | 78 +++ .../attributes/condense_indexed_attribute.h | 2 +- modules/core/include/lagrange/chain_edges.h | 75 ++- .../core/include/lagrange/combine_mesh_list.h | 2 +- modules/core/include/lagrange/compute_area.h | 128 ++++ .../core/include/lagrange/compute_centroid.h | 92 +++ .../include/lagrange/compute_facet_area.h | 214 +------ .../include/lagrange/compute_mesh_centroid.h | 64 +- .../lagrange/compute_tangent_bitangent.h | 2 +- .../include/lagrange/compute_vertex_valence.h | 2 +- .../core/include/lagrange/get_opposite_edge.h | 1 + .../internal/attribute_string_utils.h | 11 + .../lagrange/internal/find_attribute_utils.h | 34 +- .../core/include/lagrange/internal/skinning.h | 280 ++++++++ .../lagrange/legacy/combine_mesh_list.h | 2 +- .../lagrange/legacy/compute_corner_normal.h | 2 +- .../lagrange/legacy/compute_facet_area.h | 227 +++++++ .../lagrange/legacy/compute_mesh_centroid.h | 76 +++ .../include/lagrange/legacy/compute_normal.h | 2 +- .../lagrange/legacy/compute_triangle_normal.h | 2 +- .../lagrange/legacy/compute_vertex_normal.h | 2 +- .../lagrange/legacy/normalize_meshes.h | 2 +- .../lagrange/legacy/reorder_mesh_vertices.h | 117 ++++ .../mesh_cleanup/is_vertex_manifold.h | 1 - .../resolve_vertex_nonmanifoldness.h | 18 +- .../core/include/lagrange/mesh_convert.impl.h | 8 +- .../core/include/lagrange/permute_vertices.h | 45 ++ .../core/include/lagrange/point_on_segment.h | 60 +- .../lagrange/point_segment_squared_distance.h | 137 +--- .../point_triangle_squared_distance.h | 331 +--------- modules/core/include/lagrange/quad_to_tri.h | 2 +- .../core/include/lagrange/remap_vertices.h | 76 +++ .../include/lagrange/reorder_mesh_vertices.h | 103 +-- .../segment_segment_squared_distance.h | 139 ++++ .../include/lagrange/utils/DisjointSets.h | 2 +- .../core/include/lagrange/utils/StackSet.h | 2 +- .../core/include/lagrange/utils/StackVector.h | 2 +- modules/core/include/lagrange/utils/hash.h | 69 ++ .../include/lagrange/utils/point_on_segment.h | 71 ++ .../utils/point_segment_squared_distance.h | 125 ++++ .../utils/point_triangle_squared_distance.h | 319 +++++++++ modules/core/include/lagrange/utils/warnoff.h | 1 + modules/core/include/lagrange/views.h | 4 +- modules/core/performance/marquee.cpp | 1 - .../lagrange/python/utils/StackVector.h | 35 + modules/core/python/src/bind_surface_mesh.h | 24 +- modules/core/python/src/bind_utilities.h | 80 +++ modules/core/python/src/core.cpp | 3 + modules/core/python/tests/assets.py | 5 - .../python/tests/test_compute_centroid.py | 35 + .../python/tests/test_compute_facet_area.py | 71 ++ .../python/tests/test_permute_vertices.py | 74 +++ .../core/python/tests/test_remap_vertices.py | 92 +++ modules/core/src/SurfaceMesh.cpp | 36 +- modules/core/src/combine_meshes.cpp | 20 +- modules/core/src/compute_area.cpp | 399 ++++++++++++ modules/core/src/compute_centroid.cpp | 141 ++++ modules/core/src/compute_components.cpp | 2 + modules/core/src/compute_normal.cpp | 2 +- modules/core/src/compute_vertex_valence.cpp | 2 +- .../src/compute_vertex_vertex_adjacency.cpp | 2 +- .../src/internal/attribute_string_utils.cpp | 14 +- .../src/internal/find_attribute_utils.cpp | 98 ++- modules/core/src/map_attribute.cpp | 24 +- modules/core/src/mapbox/earcut.h | 2 - modules/core/src/permute_vertices.cpp | 71 ++ modules/core/src/remap_vertices.cpp | 214 +++++++ .../core/src/triangulate_polygonal_facets.cpp | 8 +- modules/core/src/unify_index_buffer.cpp | 10 +- modules/core/src/utils/DisjointSets.cpp | 8 +- .../core/src/{ => utils}/point_on_segment.cpp | 2 +- .../compute_tangent_bitangent_mikktspace.h | 11 + modules/core/tests/test_compute_centroid.cpp | 132 ++++ .../core/tests/test_compute_components.cpp | 2 +- .../core/tests/test_compute_facet_area.cpp | 203 +++++- .../core/tests/test_compute_mesh_centroid.cpp | 4 +- .../tests/test_compute_tangent_bitangent.cpp | 5 +- modules/core/tests/test_map_attribute.cpp | 9 +- modules/core/tests/test_permute_vertices.cpp | 154 +++++ modules/core/tests/test_remap_vertices.cpp | 204 ++++++ modules/core/tests/test_surface_mesh.cpp | 604 +++++------------- .../test_triangulate_polygonal_facets.cpp | 33 +- modules/core/tests/test_utils_hash.cpp | 112 ++++ modules/fs/CMakeLists.txt | 4 +- modules/io/CMakeLists.txt | 11 +- modules/io/examples/CMakeLists.txt | 15 + modules/io/examples/mesh_convert.cpp | 67 ++ .../lagrange/io/internal/load_assimp.h | 54 ++ .../include/lagrange/io/internal/load_gltf.h | 48 ++ .../include/lagrange/io/internal/load_obj.h | 61 ++ .../io/include/lagrange/io/legacy/load_mesh.h | 49 ++ .../lagrange/io/legacy/load_mesh.impl.h | 103 +++ .../lagrange/io/legacy/load_mesh_assimp.h | 207 ++++++ .../lagrange/io/legacy/load_mesh_ext.h | 476 ++++++++++++++ .../lagrange/io/legacy/load_mesh_ply.h | 120 ++++ .../io/include/lagrange/io/legacy/save_mesh.h | 292 +++++++++ .../lagrange/io/legacy/save_mesh_ply.h | 101 +++ modules/io/include/lagrange/io/load_mesh.h | 43 +- .../io/include/lagrange/io/load_mesh.impl.h | 87 +-- .../io/include/lagrange/io/load_mesh_assimp.h | 199 +----- .../io/include/lagrange/io/load_mesh_ext.h | 466 +------------- .../io/include/lagrange/io/load_mesh_gltf.h | 34 + .../io/include/lagrange/io/load_mesh_msh.h | 28 +- .../io/include/lagrange/io/load_mesh_obj.h | 86 +-- .../io/include/lagrange/io/load_mesh_ply.h | 130 +--- .../include/lagrange/io/load_simple_scene.h | 32 + .../lagrange/io/load_simple_scene_assimp.h | 35 + .../lagrange/io/load_simple_scene_gltf.h | 31 + modules/io/include/lagrange/io/save_mesh.h | 291 +-------- .../io/include/lagrange/io/save_mesh_gltf.h | 37 ++ .../io/include/lagrange/io/save_mesh_msh.h | 42 +- .../io/include/lagrange/io/save_mesh_obj.h | 31 +- .../io/include/lagrange/io/save_mesh_ply.h | 118 ++-- .../include/lagrange/io/save_simple_scene.h | 33 + .../lagrange/io/save_simple_scene_gltf.h | 34 + modules/io/include/lagrange/io/types.h | 69 ++ modules/io/src/internal/load_obj.cpp | 241 +++++++ modules/io/src/legacy_load_mesh.cpp | 33 + modules/io/src/load_assimp.cpp | 322 ++++++++++ modules/io/src/load_gltf.cpp | 432 +++++++++++++ modules/io/src/load_mesh.cpp | 57 +- modules/io/src/load_mesh_msh.cpp | 62 +- modules/io/src/load_mesh_obj.cpp | 231 +------ modules/io/src/load_mesh_ply.cpp | 92 +++ modules/io/src/load_simple_scene.cpp | 46 ++ modules/io/src/save_gltf.cpp | 391 ++++++++++++ modules/io/src/save_mesh.cpp | 47 ++ modules/io/src/save_mesh_msh.cpp | 46 +- modules/io/src/save_mesh_obj.cpp | 54 +- modules/io/src/save_mesh_ply.cpp | 184 ++++++ modules/io/src/save_simple_scene.cpp | 53 ++ modules/io/tests/io_common.cpp | 34 + modules/io/tests/io_common.h | 23 + modules/io/tests/test_io.cpp | 73 ++- ...p.cpp => test_legacy_load_mesh_assimp.cpp} | 13 +- ..._ext.cpp => test_legacy_load_mesh_ext.cpp} | 20 +- ...st_mesh_io.cpp => test_legacy_mesh_io.cpp} | 6 +- modules/io/tests/test_load_assimp.cpp | 97 +++ modules/io/tests/test_load_gltf.cpp | 80 +++ modules/io/tests/test_load_mesh_data.h | 6 - modules/io/tests/test_load_mesh_obj.cpp | 15 + modules/io/tests/test_load_mesh_ply.cpp | 21 + modules/io/tests/test_load_simple_scene.cpp | 0 modules/io/tests/test_msh.cpp | 15 +- modules/io/tests/test_save_mesh.cpp | 35 + modules/io/tests/test_save_simple_scene.cpp | 47 ++ modules/python/CMakeLists.txt | 28 +- .../lagrange/raycasting/EmbreeRayCaster.h | 67 +- .../raycasting/embree_closest_point.h | 2 +- modules/scene/CMakeLists.txt | 42 ++ .../include/lagrange/scene/SimpleScene.h | 198 ++++++ .../include/lagrange/scene/SimpleSceneTypes.h | 48 ++ modules/scene/python/CMakeLists.txt | 12 + .../python/include/lagrange/python/scene.h | 22 + modules/scene/python/src/bind_simple_scene.h | 134 ++++ modules/scene/python/src/scene.cpp | 32 + .../scene/python/tests/test_mesh_instance.py | 31 + .../scene/python/tests/test_simple_scene.py | 69 ++ modules/scene/src/SimpleScene.cpp | 82 +++ modules/scene/tests/CMakeLists.txt | 12 + modules/scene/tests/test_scene.cpp | 78 +++ modules/testing/CMakeLists.txt | 1 + .../include/lagrange/testing/check_mesh.h | 319 +++++++++ .../testing/include/lagrange/testing/common.h | 9 +- modules/testing/src/common.cpp | 2 +- modules/ui/examples/103_MeshFromMemory.cpp | 4 +- modules/ui/examples/108_FileDialog.cpp | 2 +- modules/ui/examples/ui_callbacks/main.cpp | 12 +- modules/ui/include/lagrange/ui/Viewer.h | 5 +- .../include/lagrange/ui/components/Common.h | 6 +- .../ui/include/lagrange/ui/default_tools.h | 2 +- modules/volume/CMakeLists.txt | 9 +- modules/volume/examples/voxelize_mesh.cpp | 57 +- .../include/lagrange/volume/GridTypes.h | 47 ++ .../lagrange/volume/fill_with_spheres.h | 10 +- .../lagrange/volume/legacy/mesh_to_volume.h | 113 ++++ .../lagrange/volume/legacy/volume_to_mesh.h | 114 ++++ .../include/lagrange/volume/mesh_to_volume.h | 99 +-- .../volume/include/lagrange/volume/types.h | 46 ++ .../include/lagrange/volume/volume_to_mesh.h | 113 +--- modules/volume/src/mesh_to_volume.cpp | 196 ++++++ modules/volume/src/volume_to_mesh.cpp | 82 +++ modules/volume/tests/test_voxelization.cpp | 48 +- modules/winding/CMakeLists.txt | 32 + modules/winding/examples/CMakeLists.txt | 16 + .../examples/sample_points_in_mesh.cpp | 87 +++ .../lagrange/winding/FastWindingNumber.h | 111 ++++ modules/winding/src/FastWindingNumber.cpp | 120 ++++ modules/winding/tests/CMakeLists.txt | 13 + .../tests/test_fast_winding_number.cpp | 126 ++++ 218 files changed, 11628 insertions(+), 3417 deletions(-) create mode 100644 cmake/lagrange/lagrangeMklModules.txt create mode 100644 cmake/recipes/external/happly.cmake create mode 100644 cmake/recipes/external/tinygltf.cmake create mode 100644 modules/core/include/lagrange/attribute_names.h create mode 100644 modules/core/include/lagrange/compute_area.h create mode 100644 modules/core/include/lagrange/compute_centroid.h create mode 100644 modules/core/include/lagrange/internal/skinning.h create mode 100644 modules/core/include/lagrange/legacy/compute_facet_area.h create mode 100644 modules/core/include/lagrange/legacy/compute_mesh_centroid.h create mode 100644 modules/core/include/lagrange/legacy/reorder_mesh_vertices.h create mode 100644 modules/core/include/lagrange/permute_vertices.h create mode 100644 modules/core/include/lagrange/remap_vertices.h create mode 100644 modules/core/include/lagrange/segment_segment_squared_distance.h create mode 100644 modules/core/include/lagrange/utils/hash.h create mode 100644 modules/core/include/lagrange/utils/point_on_segment.h create mode 100644 modules/core/include/lagrange/utils/point_segment_squared_distance.h create mode 100644 modules/core/include/lagrange/utils/point_triangle_squared_distance.h create mode 100644 modules/core/python/include/lagrange/python/utils/StackVector.h create mode 100644 modules/core/python/tests/test_compute_centroid.py create mode 100644 modules/core/python/tests/test_compute_facet_area.py create mode 100644 modules/core/python/tests/test_permute_vertices.py create mode 100644 modules/core/python/tests/test_remap_vertices.py create mode 100644 modules/core/src/compute_area.cpp create mode 100644 modules/core/src/compute_centroid.cpp create mode 100644 modules/core/src/permute_vertices.cpp create mode 100644 modules/core/src/remap_vertices.cpp rename modules/core/src/{ => utils}/point_on_segment.cpp (97%) create mode 100644 modules/core/tests/test_compute_centroid.cpp create mode 100644 modules/core/tests/test_permute_vertices.cpp create mode 100644 modules/core/tests/test_remap_vertices.cpp create mode 100644 modules/core/tests/test_utils_hash.cpp create mode 100644 modules/io/examples/CMakeLists.txt create mode 100644 modules/io/examples/mesh_convert.cpp create mode 100644 modules/io/include/lagrange/io/internal/load_assimp.h create mode 100644 modules/io/include/lagrange/io/internal/load_gltf.h create mode 100644 modules/io/include/lagrange/io/internal/load_obj.h create mode 100644 modules/io/include/lagrange/io/legacy/load_mesh.h create mode 100644 modules/io/include/lagrange/io/legacy/load_mesh.impl.h create mode 100644 modules/io/include/lagrange/io/legacy/load_mesh_assimp.h create mode 100644 modules/io/include/lagrange/io/legacy/load_mesh_ext.h create mode 100644 modules/io/include/lagrange/io/legacy/load_mesh_ply.h create mode 100644 modules/io/include/lagrange/io/legacy/save_mesh.h create mode 100644 modules/io/include/lagrange/io/legacy/save_mesh_ply.h create mode 100644 modules/io/include/lagrange/io/load_mesh_gltf.h create mode 100644 modules/io/include/lagrange/io/load_simple_scene.h create mode 100644 modules/io/include/lagrange/io/load_simple_scene_assimp.h create mode 100644 modules/io/include/lagrange/io/load_simple_scene_gltf.h create mode 100644 modules/io/include/lagrange/io/save_mesh_gltf.h create mode 100644 modules/io/include/lagrange/io/save_simple_scene.h create mode 100644 modules/io/include/lagrange/io/save_simple_scene_gltf.h create mode 100644 modules/io/src/internal/load_obj.cpp create mode 100644 modules/io/src/legacy_load_mesh.cpp create mode 100644 modules/io/src/load_assimp.cpp create mode 100644 modules/io/src/load_gltf.cpp create mode 100644 modules/io/src/load_mesh_ply.cpp create mode 100644 modules/io/src/load_simple_scene.cpp create mode 100644 modules/io/src/save_gltf.cpp create mode 100644 modules/io/src/save_mesh.cpp create mode 100644 modules/io/src/save_mesh_ply.cpp create mode 100644 modules/io/src/save_simple_scene.cpp create mode 100644 modules/io/tests/io_common.cpp create mode 100644 modules/io/tests/io_common.h rename modules/io/tests/{test_load_mesh_assimp.cpp => test_legacy_load_mesh_assimp.cpp} (83%) rename modules/io/tests/{test_load_mesh_ext.cpp => test_legacy_load_mesh_ext.cpp} (95%) rename modules/io/tests/{test_mesh_io.cpp => test_legacy_mesh_io.cpp} (89%) create mode 100644 modules/io/tests/test_load_assimp.cpp create mode 100644 modules/io/tests/test_load_gltf.cpp create mode 100644 modules/io/tests/test_load_mesh_obj.cpp create mode 100644 modules/io/tests/test_load_mesh_ply.cpp create mode 100644 modules/io/tests/test_load_simple_scene.cpp create mode 100644 modules/io/tests/test_save_mesh.cpp create mode 100644 modules/io/tests/test_save_simple_scene.cpp create mode 100644 modules/scene/CMakeLists.txt create mode 100644 modules/scene/include/lagrange/scene/SimpleScene.h create mode 100644 modules/scene/include/lagrange/scene/SimpleSceneTypes.h create mode 100644 modules/scene/python/CMakeLists.txt create mode 100644 modules/scene/python/include/lagrange/python/scene.h create mode 100644 modules/scene/python/src/bind_simple_scene.h create mode 100644 modules/scene/python/src/scene.cpp create mode 100644 modules/scene/python/tests/test_mesh_instance.py create mode 100644 modules/scene/python/tests/test_simple_scene.py create mode 100644 modules/scene/src/SimpleScene.cpp create mode 100644 modules/scene/tests/CMakeLists.txt create mode 100644 modules/scene/tests/test_scene.cpp create mode 100644 modules/testing/include/lagrange/testing/check_mesh.h create mode 100644 modules/volume/include/lagrange/volume/GridTypes.h create mode 100644 modules/volume/include/lagrange/volume/legacy/mesh_to_volume.h create mode 100644 modules/volume/include/lagrange/volume/legacy/volume_to_mesh.h create mode 100644 modules/volume/include/lagrange/volume/types.h create mode 100644 modules/volume/src/mesh_to_volume.cpp create mode 100644 modules/volume/src/volume_to_mesh.cpp create mode 100644 modules/winding/CMakeLists.txt create mode 100644 modules/winding/examples/CMakeLists.txt create mode 100644 modules/winding/examples/sample_points_in_mesh.cpp create mode 100644 modules/winding/include/lagrange/winding/FastWindingNumber.h create mode 100644 modules/winding/src/FastWindingNumber.cpp create mode 100644 modules/winding/tests/CMakeLists.txt create mode 100644 modules/winding/tests/test_fast_winding_number.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88a2c2c9..eaad8473 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ else() endif() # Check required CMake version -set(REQUIRED_CMAKE_VERSION "3.20.0") +set(REQUIRED_CMAKE_VERSION "3.24.0") if(LAGRANGE_TOPLEVEL_PROJECT) cmake_minimum_required(VERSION ${REQUIRED_CMAKE_VERSION}) else() @@ -32,6 +32,7 @@ endif() cmake_policy(SET CMP0054 NEW) # Only interpret if() arguments as variables or keywords when unquoted. cmake_policy(SET CMP0076 NEW) # target_sources() command converts relative paths to absolute. set(CMAKE_POLICY_DEFAULT_CMP0091 NEW) # MSVC runtime library flags are selected by an abstraction. +set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) # Set the timestamps of all extracted contents to the time of the extraction. # Include user-provided default options if available. We do that before the main # `project()` so that we can define the C/C++ compilers from the option file. @@ -66,7 +67,7 @@ endif() # Set default macOS deployment target if(LAGRANGE_TOPLEVEL_PROJECT) - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14" CACHE STRING "Minimum OS X deployment version") + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") endif() ################################################################################ @@ -82,7 +83,7 @@ else() endif() # Meta target: ALL includes all optional modules and UI. -option(LAGRANGE_ALL "Build all lagrange modules" OFF) +option(LAGRANGE_ALL "Build all lagrange modules" OFF) # General CMake options option(LAGRANGE_ASSERT_DEBUG_BREAK "Assert will break into debugger on failure" ${LAGRANGE_TOPLEVEL_PROJECT}) @@ -159,6 +160,11 @@ else() endif() set(LAGRANGE_IDE_PREFIX ${LAGRANGE_IDE_PREFIX_DEFAULT} CACHE STRING "Folder prefix for Lagrange targets in MSVC/Xcode") +# When building python module, compile TBB as a shared library +if(LAGRANGE_MODULE_PYTHON OR LAGRANGE_ALL) + option(TBB_PREFER_STATIC "Build with static TBB" OFF) +endif() + # When building anorigami module, defaults to pre-built Arpack and dynamic MKL/TBB if(LAGRANGE_MODULE_ANORIGAMI OR (LAGRANGE_ALL AND NOT LAGRANGE_NO_INTERNAL)) set(MKL_LINKING "dynamic" CACHE STRING "Linking strategy to use with MKL (static, dynamic or sdl)") @@ -168,10 +174,21 @@ endif() # On Linux & Windows we use MKL to provide BLAS/LAPACK. Since it comes precompiled with /MD on Windows, # we need to use the MSVC runtime library flag globally for the whole project. -if(LAGRANGE_TOPLEVEL_PROJECT AND NOT APPLE) - if(LAGRANGE_ALL OR LAGRANGE_MODULE_ANORIGAMI OR LAGRANGE_MODULE_DEFORMERS) +file(READ "cmake/lagrange/lagrangeMklModules.txt" LAGRANGE_MKL_MODULES) +if(LAGRANGE_TOPLEVEL_PROJECT) + if(LAGRANGE_ALL AND NOT LAGRANGE_NO_INTERNAL) # Set MSVC runtime library globally for all targets + message(STATUS "Forcing /MD globally due to LAGRANGE_ALL requiring MKL") set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "Select the MSVC runtime library") + else() + foreach(mkl_module_lc IN ITEMS ${LAGRANGE_MKL_MODULES}) + string(TOUPPER ${mkl_module_lc} mkl_module_uc) + if(LAGRANGE_MODULE_${mkl_module_uc}) + # Set MSVC runtime library globally for all targets + message(STATUS "Forcing /MD globally due to lagrange::${mkl_module_lc} requiring MKL") + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "Select the MSVC runtime library") + endif() + endforeach() endif() endif() diff --git a/LagrangeOptions.cmake.sample b/LagrangeOptions.cmake.sample index 437f7da7..4c8d04ed 100644 --- a/LagrangeOptions.cmake.sample +++ b/LagrangeOptions.cmake.sample @@ -51,9 +51,11 @@ # option(LAGRANGE_MODULE_PARTITIONING "Build module lagrange::partitioning" ON) # option(LAGRANGE_MODULE_PYTHON "Build module lagrange::python" ON) # option(LAGRANGE_MODULE_RAYCASTING "Build module lagrange::raycasting" ON) +# option(LAGRANGE_MODULE_SCENE "Build module lagrange::scene" ON) # option(LAGRANGE_MODULE_SUBDIVISION "Build module lagrange::subdivision" ON) # option(LAGRANGE_MODULE_UI "Build module lagrange::ui" ON) # option(LAGRANGE_MODULE_VOLUME "Build module lagrange::volume" ON) +# option(LAGRANGE_MODULE_WINDING "Build module lagrange::winding" ON) # General options # option(LAGRANGE_DOCUMENTATION "Build Doxygen documentation" ON) diff --git a/NOTICE.txt b/NOTICE.txt index 679060d1..8676e17e 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2,6 +2,55 @@ Lagrange ======== The following is a list of sources from which code was used/modified in this codebase. +------------------------------------------------------------------------------- +This codebase contains a modified portion of code from Boost.Functional/Hash which can be obtained at: + * SOURCE: + * https://www.boost.org/doc/libs/1_64_0/boost/functional/hash/hash.hpp + + * LICENSE: + * https://www.boost.org/LICENSE_1_0.txt + +------------------------------------------------------------------------------- +This codebase contains a modified portion of code from Christer Ericson, Real-Time Collision Detection which can be obtained at: + * SOURCE: + * https://doi.org/10.1201/b14581 + + * LICENSE: + This project contains code adapted from Real-Time Collision Detection by Christer + Ericson, published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc. Usage is + governed by the following license. + + Software License Agreement + + This Software License Agreement is a legal agreement between the Author and any person + or legal entity using or accepting any software governed by this Agreement. The software + is available on the CD-ROM in the Book, Real-Time Collision Detection, which is + published by Morgan Kaufmann Publishers. "The software" is comprised of all code + (fragments and pseudocode) presented in the book. No additional files are on the CD-ROM. + + By installing, copying, or otherwise using the software, you agree to be bound by the + terms of this Agreement. + + The parties agree as follows: + + 1. Grant of License. We grant you a nonexclusive license to use the software for any + purpose, commercial or non-commercial, as long as the following credit is included + identifying the original source of the software: "from Real-Time Collision Detection by + Christer Ericson, published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc". + + 2. Disclaimer of Warranty. We make no warranties at all.The software is transferred to + you on an "as is" basis. You use the software at your own peril. You assume all risk of + loss for all claims or controversies, now existing or hereafter, arising out of use of + the software. We shall have no liability based on a claim that your use or combination + of the software with products or data not supplied by us infringes any patent, + copyright, or proprietary right. All other warranties, expressed or implied, including, + without limitation, any warranty of merchantability or fitness for a particular purpose + are hereby excluded. + + 3. Limitation of Liability. We will have no liability for special, incidental or + consequential damages even if advised of the possibility of such damages. We will not be + liable for any other damages or loss in any way connected with the software. + ------------------------------------------------------------------------------- This codebase contains a modified portion of code from CMake Scripts which can be obtained at: * SOURCE: diff --git a/VERSION b/VERSION index f0e13c50..e029aa99 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.7.0 +6.8.0 diff --git a/cmake/lagrange/lagrangeMklModules.txt b/cmake/lagrange/lagrangeMklModules.txt new file mode 100644 index 00000000..24f444ad --- /dev/null +++ b/cmake/lagrange/lagrangeMklModules.txt @@ -0,0 +1 @@ +anorigami;deformers \ No newline at end of file diff --git a/cmake/lagrange/lagrange_add_module.cmake b/cmake/lagrange/lagrange_add_module.cmake index a877a8c5..d1cdc1ea 100644 --- a/cmake/lagrange/lagrange_add_module.cmake +++ b/cmake/lagrange/lagrange_add_module.cmake @@ -37,6 +37,8 @@ function(lagrange_add_module) $ ) + target_compile_features(lagrange_${module_name} ${module_scope} cxx_std_17) + # Target sources file(GLOB_RECURSE INC_FILES "include/*.h") file(GLOB_RECURSE SRC_FILES "src/*.cpp") diff --git a/cmake/lagrange/lagrange_download_data.cmake b/cmake/lagrange/lagrange_download_data.cmake index c889616d..806d7198 100644 --- a/cmake/lagrange/lagrange_download_data.cmake +++ b/cmake/lagrange/lagrange_download_data.cmake @@ -29,7 +29,7 @@ function(lagrange_download_data) PREFIX "${FETCHCONTENT_BASE_DIR}/lagrange-test-data" SOURCE_DIR ${LAGRANGE_DATA_FOLDER} GIT_REPOSITORY https://github.com/adobe/lagrange-test-data.git - GIT_TAG 8f90b3ec4c9616d6d92a0df397ee01aa7882fe0d + GIT_TAG d78861e85da84b341e86bbdc3fb0d4d2ddde6394 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/lagrange/lagrange_has_onetbb.cmake b/cmake/lagrange/lagrange_has_onetbb.cmake index 8470a10e..711bf17f 100644 --- a/cmake/lagrange/lagrange_has_onetbb.cmake +++ b/cmake/lagrange/lagrange_has_onetbb.cmake @@ -1,5 +1,5 @@ # -# Copyright 2020 Adobe. All rights reserved. +# Copyright 2022 Adobe. All rights reserved. # This file is licensed to you under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. You may obtain a copy # of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/cmake/recipes/external/assimp.cmake b/cmake/recipes/external/assimp.cmake index 3b1b4741..a774bea6 100644 --- a/cmake/recipes/external/assimp.cmake +++ b/cmake/recipes/external/assimp.cmake @@ -19,7 +19,7 @@ include(FetchContent) FetchContent_Declare( assimp GIT_REPOSITORY https://github.com/assimp/assimp.git - GIT_TAG ed8612ea356021080898ea2378f0b545431ccae3 + GIT_TAG 0fdae2879d78864693ee730610dcf8ee10707875 ) option(BUILD_SHARED_LIBS "Build package with shared libraries." OFF) diff --git a/cmake/recipes/external/blas.cmake b/cmake/recipes/external/blas.cmake index 7942a0ec..760d70a1 100644 --- a/cmake/recipes/external/blas.cmake +++ b/cmake/recipes/external/blas.cmake @@ -1,3 +1,14 @@ +# +# Copyright 2022 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# if(TARGET BLAS::BLAS) return() endif() diff --git a/cmake/recipes/external/embree.cmake b/cmake/recipes/external/embree.cmake index cd09f55c..c4730969 100644 --- a/cmake/recipes/external/embree.cmake +++ b/cmake/recipes/external/embree.cmake @@ -1,5 +1,5 @@ # -# Copyright 2022 Adobe. All rights reserved. +# Copyright 2019 Adobe. All rights reserved. # This file is licensed to you under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. You may obtain a copy # of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/cmake/recipes/external/filesystem.cmake b/cmake/recipes/external/filesystem.cmake index 2cdc406d..dd4d02ad 100644 --- a/cmake/recipes/external/filesystem.cmake +++ b/cmake/recipes/external/filesystem.cmake @@ -9,11 +9,11 @@ # OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. # -if(TARGET filesystem::filesystem) +if(TARGET ghcFilesystem::ghc_filesystem) return() endif() -message(STATUS "Third-party (external): creating target 'filesystem::filesystem'") +message(STATUS "Third-party (external): creating target 'ghcFilesystem::ghc_filesystem'") include(FetchContent) FetchContent_Declare( @@ -24,4 +24,3 @@ FetchContent_Declare( FetchContent_MakeAvailable(filesystem) target_compile_definitions(ghc_filesystem INTERFACE GHC_WIN_WSTRING_STRING_TYPE) -add_library(filesystem::filesystem ALIAS ghc_filesystem) diff --git a/cmake/recipes/external/happly.cmake b/cmake/recipes/external/happly.cmake new file mode 100644 index 00000000..7b89717b --- /dev/null +++ b/cmake/recipes/external/happly.cmake @@ -0,0 +1,42 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +if(TARGET happly::happly) + return() +endif() + +message(STATUS "Third-party (external): creating target 'happly::happly'") + +include(FetchContent) +FetchContent_Declare( + happly + GIT_REPOSITORY https://github.com/nmwsharp/happly.git + GIT_TAG cfa2611550bc7da65855a78af0574b65deb81766 +) +FetchContent_MakeAvailable(happly) + +# Define happly library +add_library(happly INTERFACE ${happly_SOURCE_DIR}/happly.h) +add_library(happly::happly ALIAS happly) + +include(GNUInstallDirs) +target_include_directories(happly INTERFACE + $ + $ +) + +set_target_properties(happly PROPERTIES FOLDER third_party) + +# Install rules +set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME happly) +install(DIRECTORY ${happly_SOURCE_DIR} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(TARGETS happly EXPORT Happly_Targets) +install(EXPORT Happly_Targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/happly NAMESPACE happly::) diff --git a/cmake/recipes/external/lapack.cmake b/cmake/recipes/external/lapack.cmake index e6e23ece..c2723b32 100644 --- a/cmake/recipes/external/lapack.cmake +++ b/cmake/recipes/external/lapack.cmake @@ -1,3 +1,14 @@ +# +# Copyright 2022 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# if(TARGET LAPACK::LAPACK) return() endif() diff --git a/cmake/recipes/external/mikktspace.cmake b/cmake/recipes/external/mikktspace.cmake index cf9b4213..46b5b23e 100644 --- a/cmake/recipes/external/mikktspace.cmake +++ b/cmake/recipes/external/mikktspace.cmake @@ -24,3 +24,5 @@ FetchContent_MakeAvailable(mikktspace) add_library(mikktspace ${mikktspace_SOURCE_DIR}/mikktspace.c) add_library(mikktspace::mikktspace ALIAS mikktspace) target_include_directories(mikktspace PUBLIC ${mikktspace_SOURCE_DIR}) + +set_target_properties(mikktspace PROPERTIES FOLDER third_party) diff --git a/cmake/recipes/external/nanobind.cmake b/cmake/recipes/external/nanobind.cmake index bd6a4e75..4a7298b3 100644 --- a/cmake/recipes/external/nanobind.cmake +++ b/cmake/recipes/external/nanobind.cmake @@ -19,7 +19,7 @@ include(FetchContent) FetchContent_Declare( nanobind GIT_REPOSITORY https://github.com/wjakob/nanobind.git - GIT_TAG 42db2fdb3292c6851d68d4a56f48e2031666173f + GIT_TAG v0.1.0 ) include(python) diff --git a/cmake/recipes/external/opensubdiv.cmake b/cmake/recipes/external/opensubdiv.cmake index c5eb8d7c..33926b6e 100644 --- a/cmake/recipes/external/opensubdiv.cmake +++ b/cmake/recipes/external/opensubdiv.cmake @@ -19,7 +19,7 @@ include(FetchContent) FetchContent_Declare( opensubdiv GIT_REPOSITORY https://github.com/PixarAnimationStudios/OpenSubdiv.git - GIT_TAG tags/v3_4_0 + GIT_TAG tags/v3_4_4 GIT_SHALLOW TRUE ) diff --git a/cmake/recipes/external/openvdb.cmake b/cmake/recipes/external/openvdb.cmake index d2585aaa..6df3b46d 100644 --- a/cmake/recipes/external/openvdb.cmake +++ b/cmake/recipes/external/openvdb.cmake @@ -22,8 +22,15 @@ FetchContent_Declare( GIT_TAG v10.0.0 ) -option(OPENVDB_CORE_SHARED "" OFF) -option(OPENVDB_CORE_STATIC "" ON) +if(WIN32) + # On Windows, prefer shared lib for OpenVDB, otherwise we can run into + # linking error LNK1248 in Debug mode (due to lib size exceeding 4GB). + option(OPENVDB_CORE_SHARED "" ON) + option(OPENVDB_CORE_STATIC "" OFF) +else() + option(OPENVDB_CORE_SHARED "" OFF) + option(OPENVDB_CORE_STATIC "" ON) +endif() option(OPENVDB_BUILD_CORE "" ON) option(OPENVDB_BUILD_BINARIES "" OF) option(OPENVDB_ENABLE_RPATH "" OFF) @@ -135,4 +142,9 @@ endfunction() # Call via a proper function in order to scope variables such as CMAKE_FIND_PACKAGE_PREFER_CONFIG and TBB_DIR openvdb_import_target() -set_target_properties(openvdb_static PROPERTIES FOLDER third_party) +# Set folders for MSVC +foreach(name IN ITEMS openvdb_static openvdb_shared) + if(TARGET ${name}) + set_target_properties(${name} PROPERTIES FOLDER third_party) + endif() +endforeach() diff --git a/cmake/recipes/external/spdlog.cmake b/cmake/recipes/external/spdlog.cmake index 232b3f35..d9bd13ce 100644 --- a/cmake/recipes/external/spdlog.cmake +++ b/cmake/recipes/external/spdlog.cmake @@ -19,7 +19,7 @@ include(FetchContent) FetchContent_Declare( spdlog GIT_REPOSITORY https://github.com/gabime/spdlog.git - GIT_TAG v1.9.2 + GIT_TAG v1.10.0 ) option(SPDLOG_INSTALL "Generate the install target" ON) diff --git a/cmake/recipes/external/tinygltf.cmake b/cmake/recipes/external/tinygltf.cmake new file mode 100644 index 00000000..beb608be --- /dev/null +++ b/cmake/recipes/external/tinygltf.cmake @@ -0,0 +1,36 @@ +# +# Copyright 2022 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +if(TARGET tinygltf::tinygltf) + return() +endif() + +message(STATUS "Third-party (external): creating target 'tinygltf::tinygltf'") + +include(FetchContent) +FetchContent_Declare( + tinygltf + GIT_REPOSITORY https://github.com/syoyo/tinygltf.git + GIT_TAG v2.7.0 + GIT_SHALLOW TRUE +) + +option(TINYGLTF_BUILD_LOADER_EXAMPLE "Build loader_example(load glTF and dump infos)" OFF) +option(TINYGLTF_BUILD_GL_EXAMPLES "Build GL exampels(requires glfw, OpenGL, etc)" OFF) +option(TINYGLTF_BUILD_VALIDATOR_EXAMPLE "Build validator exampe" OFF) +option(TINYGLTF_BUILD_BUILDER_EXAMPLE "Build glTF builder example" OFF) +option(TINYGLTF_HEADER_ONLY "On: header-only mode. Off: create tinygltf library(No TINYGLTF_IMPLEMENTATION required in your project)" OFF) +option(TINYGLTF_INSTALL "Install tinygltf files during install step. Usually set to OFF if you include tinygltf through add_subdirectory()" ON) +FetchContent_MakeAvailable(tinygltf) + +set_target_properties(tinygltf PROPERTIES FOLDER third_party) + +add_library(tinygltf::tinygltf ALIAS tinygltf) diff --git a/cmake/recipes/external/winding_number.cmake b/cmake/recipes/external/winding_number.cmake index 17bfc239..cf15eb15 100644 --- a/cmake/recipes/external/winding_number.cmake +++ b/cmake/recipes/external/winding_number.cmake @@ -1,5 +1,5 @@ # -# Copyright 2022 Adobe. All rights reserved. +# Copyright 2021 Adobe. All rights reserved. # This file is licensed to you under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. You may obtain a copy # of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/bvh/include/lagrange/bvh/EdgeAABBTree.impl.h b/modules/bvh/include/lagrange/bvh/EdgeAABBTree.impl.h index 3442ee64..c0cf5561 100644 --- a/modules/bvh/include/lagrange/bvh/EdgeAABBTree.impl.h +++ b/modules/bvh/include/lagrange/bvh/EdgeAABBTree.impl.h @@ -12,9 +12,9 @@ #pragma once #include -#include #include -#include +#include +#include // clang-format off #include @@ -52,8 +52,8 @@ Scalar sqr(Scalar x) /// template Scalar inner_point_box_squared_distance( - const Eigen::Matrix &p, - const Eigen::AlignedBox &B) + const Eigen::Matrix& p, + const Eigen::AlignedBox& B) { assert(B.contains(p)); Scalar result = sqr(p[0] - B.min()[0]); @@ -78,8 +78,8 @@ Scalar inner_point_box_squared_distance( /// template Scalar point_box_signed_squared_distance( - const Eigen::Matrix &p, - const Eigen::AlignedBox &B) + const Eigen::Matrix& p, + const Eigen::AlignedBox& B) { bool inside = true; Scalar result = 0; @@ -108,8 +108,8 @@ Scalar point_box_signed_squared_distance( /// template Eigen::AlignedBox bbox_edge( - const Eigen::Matrix &a, - const Eigen::Matrix &b) + const Eigen::Matrix& a, + const Eigen::Matrix& b) { Eigen::AlignedBox bbox; bbox.extend(a.transpose()); @@ -122,7 +122,7 @@ Eigen::AlignedBox bbox_edge( //////////////////////////////////////////////////////////////////////////////// template -EdgeAABBTree::EdgeAABBTree(const VertexArray &V, const EdgeArray &E) +EdgeAABBTree::EdgeAABBTree(const VertexArray& V, const EdgeArray& E) { la_runtime_assert(DIM == V.cols(), "Dimension mismatch in EdgeAABBTree!"); // Compute the centroids of all the edges in the input mesh @@ -177,7 +177,7 @@ EdgeAABBTree::EdgeAABBTree(const VertexArray &V, co Index midpoint = (i + j) / 2; Index left = top_down(i, midpoint, current); Index right = top_down(midpoint, j, current); - Node &node = m_nodes[current]; + Node& node = m_nodes[current]; node.left = left; node.right = right; node.parent = parent; @@ -196,10 +196,10 @@ EdgeAABBTree::EdgeAABBTree(const VertexArray &V, co template void EdgeAABBTree::get_element_closest_point( - const RowVectorType &p, + const RowVectorType& p, Index element_id, - RowVectorType &closest_point, - Scalar &closest_sq_dist) const + RowVectorType& closest_point, + Scalar& closest_sq_dist) const { RowVectorType v0 = m_vertices->row((*m_edges)(element_id, 0)); RowVectorType v1 = m_vertices->row((*m_edges)(element_id, 1)); @@ -215,21 +215,21 @@ void EdgeAABBTree::get_element_closest_point( template void EdgeAABBTree::foreach_element_in_radius( - const RowVectorType &p, + const RowVectorType& p, Scalar sq_dist, - std::function func) const + std::function func) const { foreach_element_in_radius_recursive(p, sq_dist, (Index)m_root, func); } template void EdgeAABBTree::foreach_element_in_radius_recursive( - const RowVectorType &p, + const RowVectorType& p, Scalar sq_dist, Index node_id, ActionCallback func) const { - const auto &node = m_nodes[node_id]; + const auto& node = m_nodes[node_id]; if (node.is_leaf()) { RowVectorType closest_point; Scalar closest_sq_dist; @@ -257,19 +257,19 @@ void EdgeAABBTree::foreach_element_in_radius_recurs template void EdgeAABBTree::foreach_element_containing( - const RowVectorType &p, - std::function func) const + const RowVectorType& p, + std::function func) const { foreach_element_containing_recursive(p, (Index)m_root, func); } template void EdgeAABBTree::foreach_element_containing_recursive( - const RowVectorType &p, + const RowVectorType& p, Index node_id, ActionCallback func) const { - const auto &node = m_nodes[node_id]; + const auto& node = m_nodes[node_id]; if (node.is_leaf()) { RowVectorType p0 = m_vertices->row((*m_edges)(node.index, 0)); RowVectorType p1 = m_vertices->row((*m_edges)(node.index, 1)); @@ -296,10 +296,10 @@ void EdgeAABBTree::foreach_element_containing_recur template void EdgeAABBTree::get_closest_point( - const RowVectorType &query_pt, - Index &element_id, - RowVectorType &closest_point, - Scalar &closest_sq_dist, + const RowVectorType& query_pt, + Index& element_id, + RowVectorType& closest_point, + Scalar& closest_sq_dist, std::function filter_func) const { la_runtime_assert(!empty()); @@ -309,7 +309,7 @@ void EdgeAABBTree::get_closest_point( std::function traverse_aabb_tree; traverse_aabb_tree = [&](Index node_id) { - const auto &node = m_nodes[node_id]; + const auto& node = m_nodes[node_id]; if (node.is_leaf()) { if (!filter_func || filter_func(node.index)) { RowVectorType _closest_point; diff --git a/modules/core/CMakeLists.txt b/modules/core/CMakeLists.txt index b4b906e5..0268839b 100644 --- a/modules/core/CMakeLists.txt +++ b/modules/core/CMakeLists.txt @@ -12,9 +12,11 @@ # 1. define module lagrange_add_module() -target_compile_features(lagrange_core PUBLIC cxx_std_17) set_target_properties(lagrange_core PROPERTIES POSITION_INDEPENDENT_CODE ON) +if(LAGRANGE_TOPLEVEL_PROJECT) + set_target_properties(lagrange_core PROPERTIES COMPILE_WARNING_AS_ERROR ON) +endif() if(LAGRANGE_DISABLE_FPE) target_compile_definitions(lagrange_core PRIVATE -DLA_DISABLE_FPE) diff --git a/modules/core/include/lagrange/AttributeFwd.h b/modules/core/include/lagrange/AttributeFwd.h index 2530cecb..04ecff0f 100644 --- a/modules/core/include/lagrange/AttributeFwd.h +++ b/modules/core/include/lagrange/AttributeFwd.h @@ -48,7 +48,7 @@ enum AttributeElement : int { /// Usage tag indicating how the attribute should behave under mesh transformations. This tag mostly /// serves as a hint, and does not impact how the attribute is stored/loaded. /// -/// @todo Add Tangent, Bitangent + others? +/// @todo Add skinning weights + others? /// enum class AttributeUsage { Vector, ///< Mesh attribute can have any number of channels (including 1 channel). diff --git a/modules/core/include/lagrange/Components.h b/modules/core/include/lagrange/Components.h index 56a01118..21cbb40c 100644 --- a/modules/core/include/lagrange/Components.h +++ b/modules/core/include/lagrange/Components.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2017 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/DisjointSets.h b/modules/core/include/lagrange/DisjointSets.h index 768554ff..ed104374 100644 --- a/modules/core/include/lagrange/DisjointSets.h +++ b/modules/core/include/lagrange/DisjointSets.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/Edge.h b/modules/core/include/lagrange/Edge.h index 896b7716..f6f9f300 100644 --- a/modules/core/include/lagrange/Edge.h +++ b/modules/core/include/lagrange/Edge.h @@ -95,8 +95,16 @@ class EdgeType // std::set. Use std::unordered_set instead. // allows: for (Index v : edge) { ... } - class iterator : std::iterator> + class iterator { + public: + // Types needed to make a compatible iterator. + using iterator_category = std::input_iterator_tag; + using value_type = EdgeType; + using difference_type = std::ptrdiff_t; + using pointer = EdgeType*; + using reference = EdgeType&; + private: int m_i; const EdgeType& m_edge; diff --git a/modules/core/include/lagrange/MeshTopology.h b/modules/core/include/lagrange/MeshTopology.h index da3d02b0..ba01a978 100644 --- a/modules/core/include/lagrange/MeshTopology.h +++ b/modules/core/include/lagrange/MeshTopology.h @@ -21,7 +21,6 @@ #include #include -#include #include namespace lagrange { diff --git a/modules/core/include/lagrange/attribute_names.h b/modules/core/include/lagrange/attribute_names.h new file mode 100644 index 00000000..7a42cd2f --- /dev/null +++ b/modules/core/include/lagrange/attribute_names.h @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +namespace lagrange { + +// Valid attribute semantic properties are listed below. +// Attribute semantic property names can be in the form [semantic]_[set_index], e.g. texcoord_0, +// texcoord_1, etc. + +struct AttributeName +{ + /** + * Normal. Paired with AttributeUsage::Normal. + */ + static constexpr std::string_view normal = "normal"; + + /** + * Color. Paired with AttributeUsage::Color. + */ + static constexpr std::string_view color = "color"; + + /** + * Texture coordinates. Paired with AttributeUsage::UV. Other common names are UV, UVs. + */ + static constexpr std::string_view texcoord = "texcoord"; + + /** + * Tangent. Paired with AttributeUsage::Vector. + */ + static constexpr std::string_view tangent = "tangent"; + + /** + * Bitangent. Paired with AttributeUsage::Vector. + */ + static constexpr std::string_view bitangent = "bitangent"; + + /** + * Material ID. Paired with AttributeUsage::Scalar. + */ + static constexpr std::string_view material_id = "material_id"; + + /** + * Object ID (in obj files). Paired with AttributeUsage::Scalar. + */ + static constexpr std::string_view object_id = "object_id"; + + /** + * Skinning weights, with all joints specified for each vertex. Paired with + * AttributeUsage::Vector. + */ + static constexpr std::string_view weight = "weight"; + + /** + * Indexed skinning weights, with a fixed number (typically 4) specified for each vertex. Paired + * with AttributeUsage::Vector. + */ + static constexpr std::string_view indexed_weight = "indexed_weight"; + + /** + * Indexed skinning index, with a fixed number (typically 4) specified for each vertex. Paired + * with AttributeUsage::Vector. Other common names include bone, bone_index, handle. + */ + static constexpr std::string_view indexed_joint = "indexed_joint"; +}; + +} // namespace lagrange diff --git a/modules/core/include/lagrange/attributes/condense_indexed_attribute.h b/modules/core/include/lagrange/attributes/condense_indexed_attribute.h index 0c69d518..677bd153 100644 --- a/modules/core/include/lagrange/attributes/condense_indexed_attribute.h +++ b/modules/core/include/lagrange/attributes/condense_indexed_attribute.h @@ -38,7 +38,7 @@ void condense_indexed_attribute( const std::string& new_attr_name = "") { static_assert(MeshTrait::is_mesh(), "MeshType is not a mesh"); - la_runtime_assert(mesh.has_indexed_attribute(attr_name)); + la_runtime_assert(mesh.has_indexed_attribute(attr_name), fmt::format("Missing attribute '{}'", attr_name)); using Index = typename MeshType::Index; using AttributeArray = typename MeshType::AttributeArray; diff --git a/modules/core/include/lagrange/chain_edges.h b/modules/core/include/lagrange/chain_edges.h index ef685c50..1a934993 100644 --- a/modules/core/include/lagrange/chain_edges.h +++ b/modules/core/include/lagrange/chain_edges.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -144,17 +145,55 @@ std::vector> chain_edges(const Allocator& edges, bool close_loo return chains; } +namespace detail { + +// Eigen integer matrix with 2 columns, one edge per row +template +size_t get_num_edges(const EdgeArray& edges) +{ + return static_cast(edges.rows()); +} + +// std::vector of edges +template +size_t get_num_edges(const std::vector& edges) +{ + return edges.size(); +} + +// Eigen integer matrix with 2 columns, one edge per row +template +std::pair get_edge_endpoints(EdgeArray& edges, EdgeIndex edge_index) +{ + return std::make_pair( + static_cast(edges(static_cast(edge_index), 0)), + static_cast(edges(static_cast(edge_index), 1))); +} + +// std::vector of edges +template +std::pair get_edge_endpoints( + const std::vector& edges, + EdgeIndex edge_index) +{ + const auto& e = edges[static_cast(edge_index)]; + return std::make_pair(static_cast(e[0]), static_cast(e[1])); +} + +} // namespace detail + /** * Chain undirected edges into chains and loops. * - * @param[in] edges The set of input undirected edges. + * @param[in] edges The set of input undirected edges. Can be a `std::vector` of 2D integer + * vectors, or an Eigen integer array with 2 columns (one edge per row). * @param[in] close_loop Whether to mark closed loops by setting the first and * last vertex to be the same. * * @returns The set of edge chains/loops. * - * @note Any vertices with more than 2 connected edges will serve as stopping - * vertices for the chain growing algorithm. + * @note Any vertices with more than 2 connected edges will serve as stopping vertices for the chain + * growing algorithm. */ template >> std::vector> chain_undirected_edges( @@ -166,27 +205,25 @@ std::vector> chain_undirected_edges( using Chain = std::vector; std::vector chains; VertexEdgeAdjList adj_list; - size_t num_edges = std::distance(edges.begin(), edges.end()); + size_t num_edges = detail::get_num_edges(edges); adj_list.reserve(num_edges); std::vector visited(num_edges, false); auto add_adj_connection = [&](Index ei) { - const auto& e = edges[ei]; - Index v0 = e[0]; - Index v1 = e[1]; + auto v = detail::get_edge_endpoints(edges, ei); - auto itr = adj_list.find(v0); + auto itr = adj_list.find(v.first); if (itr != adj_list.end()) { itr->second.push_back(ei); } else { - adj_list.insert({v0, {ei}}); + adj_list.insert({v.first, {ei}}); } - itr = adj_list.find(v1); + itr = adj_list.find(v.second); if (itr != adj_list.end()) { itr->second.push_back(ei); } else { - adj_list.insert({v1, {ei}}); + adj_list.insert({v.second, {ei}}); } }; @@ -208,12 +245,12 @@ std::vector> chain_undirected_edges( for (auto ei : adj_edges) { if (visited[ei]) continue; - const auto& e = edges[ei]; - if (e[0] == curr_v) { - chain.push_back(e[1]); + auto v = detail::get_edge_endpoints(edges, ei); + if (v.first == curr_v) { + chain.push_back(v.second); } else { - la_debug_assert(e[1] == curr_v); - chain.push_back(e[0]); + la_debug_assert(v.second == curr_v); + chain.push_back(v.first); } prev_v = curr_v; curr_v = chain.back(); @@ -233,12 +270,12 @@ std::vector> chain_undirected_edges( for (auto ei : range(num_edges)) { if (visited[ei]) continue; - const auto& e = edges[ei]; + auto v = detail::get_edge_endpoints(edges, ei); visited[ei] = true; Chain chain; chain.reserve(num_edges); - chain.push_back(e[0]); - chain.push_back(e[1]); + chain.push_back(v.first); + chain.push_back(v.second); grow_chain_forward(chain); grow_chain_backward(chain); diff --git a/modules/core/include/lagrange/combine_mesh_list.h b/modules/core/include/lagrange/combine_mesh_list.h index ff5dcac4..3b80e745 100644 --- a/modules/core/include/lagrange/combine_mesh_list.h +++ b/modules/core/include/lagrange/combine_mesh_list.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/compute_area.h b/modules/core/include/lagrange/compute_area.h new file mode 100644 index 00000000..ad88e28f --- /dev/null +++ b/modules/core/include/lagrange/compute_area.h @@ -0,0 +1,128 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace lagrange { +/// +/// @defgroup group-surfacemesh-utils Mesh utilities +/// @ingroup group-surfacemesh +/// +/// Various mesh processing utilities. +/// +/// @{ + +/// +/// Option struct for computing per-facet area. +/// +struct FacetAreaOptions +{ + /// Output attribute name for facet area. + std::string_view output_attribute_name = "@facet_area"; + + /// For 2D mesh only: whether the computed area should be signed. + bool use_signed_area = true; +}; + +/// +/// Compute per-facet area. +/// +/// @param[in,out] mesh The input mesh. +/// @param[in] options The options controlling the computation. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// +/// @return The attribute id of the facet area attribute. +/// @see `FacetAreaOptions` +/// +template +AttributeId compute_facet_area( + SurfaceMesh& mesh, + FacetAreaOptions options = {}); + +/// +/// Compute per-facet area. +/// +/// @param[in,out] mesh The input mesh. +/// @param[in] transformation Affine transformation to apply on mesh geometry. +/// @param[in] options The options controlling the computation. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// @tparam Dimension Mesh dimension. +/// +/// @return The attribute id of the facet area attribute. +/// @see `FacetAreaOptions` +/// +template +AttributeId compute_facet_area( + SurfaceMesh& mesh, + const Eigen::Transform& transformation, + FacetAreaOptions options = {}); + +/// +/// Option struct for computing mesh area. +/// +struct MeshAreaOptions +{ + /// Precomputed facet area attribute name. + /// If the attribute does not exist, a temp attribute will be computed and stored in this name. + std::string_view input_attribute_name = "@facet_area"; + + /// For 2D mesh only: whether the computed facet area (if any) should be signed. + bool use_signed_area = true; +}; + +/// +/// Compute mesh area. +/// +/// @param[in] mesh The input mesh. +/// @param[in] options The options controlling the computation. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// +/// @return The computed mesh area. +/// @see `MeshAreaOptions` +/// +template +Scalar compute_mesh_area( + const SurfaceMesh& mesh, + MeshAreaOptions options = {}); + +/// +/// Compute mesh area. +/// +/// @param[in] mesh The input mesh. +/// @param[in] transformation Affine transformation to apply on mesh geometry. +/// @param[in] options The options controlling the computation. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// @tparam Dimension Mesh dimension. +/// +/// @return The computed mesh area. +/// @see `MeshAreaOptions` +/// +template +Scalar compute_mesh_area( + const SurfaceMesh& mesh, + const Eigen::Transform& transformation, + MeshAreaOptions options = {}); + +/// @} +} // namespace lagrange diff --git a/modules/core/include/lagrange/compute_centroid.h b/modules/core/include/lagrange/compute_centroid.h new file mode 100644 index 00000000..2147df91 --- /dev/null +++ b/modules/core/include/lagrange/compute_centroid.h @@ -0,0 +1,92 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +namespace lagrange { + +/// +/// @defgroup group-surfacemesh-utils Mesh utilities +/// @ingroup group-surfacemesh +/// +/// Various mesh processing utilities. +/// +/// @{ + +/// +/// Option struct for computing per-facet centroid. +/// +struct FacetCentroidOptions +{ + /// Ouptut facet centroid attribute name. + std::string_view output_attribute_name = "@facet_centroid"; +}; + +/// +/// Compute per-facet centroid. +/// +/// @param[in,out] mesh The input mesh. +/// @param[in] options Option settings to control the computation. +/// +/// @tparam Scalar Mesh Scalar type. +/// @tparam Index Mesh Index type. +/// +/// @return The id of the facet centroid attribute. +/// @see `FacetCentroidOptions` +/// +template +AttributeId compute_facet_centroid( + SurfaceMesh& mesh, + FacetCentroidOptions options = {}); + +/// +/// Option struct for computing mesh centroid. +/// +struct MeshCentroidOptions +{ + /// Weighting type for mesh centroid computation. + enum WeightingType { + Uniform, ///< Per-facet centroid are weighted uniformly. + Area ///< Per-facet centroid are weighted by facet area. + } weighting_type = Area; + + /// Precomputed facet centroid attribute name. + /// If the attribute does not exist, a temp attribute will be computed and stored in this name. + std::string_view facet_centroid_attribute_name = "@facet_centroid"; + + /// Precomputed facet area attribute name. + /// If the attribute does not exist, a temp attribute will be computed and stored in this name. + std::string_view facet_area_attribute_name = "@facet_area"; +}; + +/// +/// Compute mesh centroid, where mesh centroid is defined as the weighted sum of facet centroids. +/// +/// @param[in] mesh The input mesh. +/// @param[out] centroid The buffer to store centroid coordinates. +/// @param[in] options Option settings to control the computation. +/// +/// @tparam Scalar Mesh Scalar type. +/// @tparam Index Mesh Index type. +/// +/// @see `MeshCentroidOptions` +/// +template +void compute_mesh_centroid( + const SurfaceMesh& mesh, + span centroid, + MeshCentroidOptions options = {}); + +/// @} +} // namespace lagrange diff --git a/modules/core/include/lagrange/compute_facet_area.h b/modules/core/include/lagrange/compute_facet_area.h index b37d3819..f85bd5ad 100644 --- a/modules/core/include/lagrange/compute_facet_area.h +++ b/modules/core/include/lagrange/compute_facet_area.h @@ -9,214 +9,10 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#pragma once - -#include -#include - -#include -#include -#include -#include - -namespace lagrange { -namespace internal { - -/// -/// Calculates the triangle areas. -/// -/// @param[in] mesh Input triangle mesh. -/// -/// @tparam MeshType Mesh type. -/// -/// @return #F x 1 array of triangle areas. -/// -template -auto compute_triangle_areas(const MeshType& mesh) -> AttributeArrayOf -{ - using Scalar = typename MeshType::Scalar; - using Index = typename MeshType::Index; - using Vector = Eigen::Matrix; - - const Index dim = mesh.get_dim(); - const Index num_facets = mesh.get_num_facets(); - const auto& vertices = mesh.get_vertices(); - const auto& facets = mesh.get_facets(); - AttributeArrayOf areas(num_facets, 1); - - if (dim == 2) { - for (Index i = 0; i < num_facets; i++) { - const Index i0 = facets(i, 0); - const Index i1 = facets(i, 1); - const Index i2 = facets(i, 2); - Vector v0, v1, v2; - v0 << vertices(i0, 0), vertices(i0, 1), 0.0; - v1 << vertices(i1, 0), vertices(i1, 1), 0.0; - v2 << vertices(i2, 0), vertices(i2, 1), 0.0; - Vector e1 = v1 - v0; - Vector e2 = v2 - v0; - areas(i, 0) = Scalar(0.5) * (e1.cross(e2)).norm(); - } - } else if (dim == 3) { - for (Index i = 0; i < num_facets; i++) { - const Index i0 = facets(i, 0); - const Index i1 = facets(i, 1); - const Index i2 = facets(i, 2); - Vector v0, v1, v2; - v0 << vertices(i0, 0), vertices(i0, 1), vertices(i0, 2); - v1 << vertices(i1, 0), vertices(i1, 1), vertices(i1, 2); - v2 << vertices(i2, 0), vertices(i2, 1), vertices(i2, 2); - Vector e1 = v1 - v0; - Vector e2 = v2 - v0; - areas(i, 0) = Scalar(0.5) * (e1.cross(e2)).norm(); - } - } else { - throw std::runtime_error("Unsupported dimention."); - } - - return areas; -} - -/// -/// Calculates the quad areas. -/// -/// @param[in] mesh Input quad mesh. -/// -/// @tparam MeshType Mesh type. -/// -/// @return #F x 1 array of quad areas. -/// -template -auto compute_quad_areas(const MeshType& mesh) -> AttributeArrayOf -{ - using Scalar = typename MeshType::Scalar; - using Index = typename MeshType::Index; - using Vector = Eigen::Matrix; - const Index dim = mesh.get_dim(); - const Index num_facets = mesh.get_num_facets(); - const auto& vertices = mesh.get_vertices(); - const auto& facets = mesh.get_facets(); - AttributeArrayOf areas(num_facets, 1); - - if (dim == 2) { - for (Index i = 0; i < num_facets; i++) { - const Index i0 = facets(i, 0); - const Index i1 = facets(i, 1); - const Index i2 = facets(i, 2); - const Index i3 = facets(i, 3); - Vector v0, v1, v2, v3; - v0 << vertices(i0, 0), vertices(i0, 1), 0.0f; - v1 << vertices(i1, 0), vertices(i1, 1), 0.0f; - v2 << vertices(i2, 0), vertices(i2, 1), 0.0f; - v3 << vertices(i3, 0), vertices(i3, 1), 0.0f; - Vector e10 = v1 - v0; - Vector e30 = v3 - v0; - Vector e12 = v1 - v2; - Vector e32 = v3 - v2; - areas(i, 0) = 0.5f * (e10.cross(e30)).norm() + 0.5f * (e12.cross(e32)).norm(); - } - } else if (dim == 3) { - for (Index i = 0; i < num_facets; i++) { - const Index i0 = facets(i, 0); - const Index i1 = facets(i, 1); - const Index i2 = facets(i, 2); - const Index i3 = facets(i, 3); - Vector v0, v1, v2, v3; - v0 << vertices(i0, 0), vertices(i0, 1), vertices(i0, 2); - v1 << vertices(i1, 0), vertices(i1, 1), vertices(i1, 2); - v2 << vertices(i2, 0), vertices(i2, 1), vertices(i2, 2); - v3 << vertices(i3, 0), vertices(i3, 1), vertices(i3, 2); - Vector e10 = v1 - v0; - Vector e30 = v3 - v0; - Vector e12 = v1 - v2; - Vector e32 = v3 - v2; - areas(i, 0) = 0.5f * (e10.cross(e30)).norm() + 0.5f * (e12.cross(e32)).norm(); - } - } else { - throw std::runtime_error("Unsupported dimention."); - } - - return areas; -} -} // namespace internal - -/// -/// Calculates the facet areas. -/// -/// @param[in] mesh Input mesh. -/// -/// @tparam MeshType Mesh type. -/// -/// @return #F x 1 array of facet areas. -/// -template -auto compute_facet_area_raw(const MeshType& mesh) -> AttributeArrayOf -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - using Index = typename MeshType::Index; - const Index vertex_per_facet = mesh.get_vertex_per_facet(); - if (vertex_per_facet == 3) { - return internal::compute_triangle_areas(mesh); - } else if (vertex_per_facet == 4) { - return internal::compute_quad_areas(mesh); - } else { - throw std::runtime_error("Unsupported facet type."); - } -} - -/// -/// Calculates the facet areas. The result is stored as a new facet attribute `area`. -/// -/// @param[in,out] mesh Input mesh. -/// -/// @tparam MeshType Mesh type. -/// -template -void compute_facet_area(MeshType& mesh) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - auto areas = compute_facet_area_raw(mesh); - mesh.add_facet_attribute("area"); - mesh.import_facet_attribute("area", areas); -} - -/// -/// Calculates the uv areas. -/// -/// @param[in] uv UV vertex positions. -/// @param[in] triangles UV triangle indices. -/// -/// @tparam DerivedUV Matrix type of UV vertex positions. -/// @tparam DerivedF Matrix type of UV triangle indices. -/// -/// @return #F x 1 array or triangle areas. -/// -template -Eigen::Matrix compute_uv_area_raw( - const Eigen::MatrixBase& uv, - const Eigen::MatrixBase& triangles) -{ - using Scalar = typename DerivedUV::Scalar; - using Index = typename DerivedF::Scalar; - - la_runtime_assert(uv.cols() == 2); - la_runtime_assert(triangles.cols() == 3); - const auto num_triangles = triangles.rows(); - - auto compute_single_triangle_area = [&uv, &triangles](Index i) { - const auto& f = triangles.row(i); - const auto& v0 = uv.row(f[0]); - const auto& v1 = uv.row(f[1]); - const auto& v2 = uv.row(f[2]); - return 0.5f * (v0[0] * v1[1] + v1[0] * v2[1] + v2[0] * v0[1] - v0[0] * v2[1] - - v1[0] * v0[1] - v2[0] * v1[1]); - }; +#pragma once - Eigen::Matrix areas(num_triangles); - for (auto i : range(safe_cast(num_triangles))) { - areas[i] = compute_single_triangle_area(i); - } - return areas; -} -} // namespace lagrange +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + // Replaced by compute_area.h. + #include +#endif diff --git a/modules/core/include/lagrange/compute_mesh_centroid.h b/modules/core/include/lagrange/compute_mesh_centroid.h index bfac4e82..1bc75b4b 100644 --- a/modules/core/include/lagrange/compute_mesh_centroid.h +++ b/modules/core/include/lagrange/compute_mesh_centroid.h @@ -9,64 +9,10 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#pragma once - -#include - -#include -#include - -namespace lagrange { - -template -struct ComputeMeshCentroidOutput -{ - // We get this for free when computing the centroid. - Scalar area; - // The centroid itself. Will be row vector to be consistent with lagrange. - Eigen::Matrix centroid; -}; - -// Compute the centroid of certain facets in a mesh -// mesh_ref, reference to the mesh -// active_facets, the facets that are included in the centroid computation. -// Empty would imply all facets. -template -ComputeMeshCentroidOutput compute_mesh_centroid( // - const MeshType& mesh_ref, - const typename MeshType::IndexList& active_facets = typename MeshType::IndexList()) -{ // - - using Scalar = typename MeshType::Scalar; - using Vertex3 = Eigen::Matrix; - using Output = ComputeMeshCentroidOutput; - - const auto& vertices = mesh_ref.get_vertices(); - const auto& facets = mesh_ref.get_facets(); - - la_runtime_assert(vertices.cols() == 3, "Currently, only 3 dimensions are supported"); - la_runtime_assert(facets.cols() == 3, "Currently, only triangles are supported"); - - Vertex3 centroid = Vertex3::Zero(); - Scalar area = 0; - - for (auto facet_id : range_sparse(mesh_ref.get_num_facets(), active_facets)) { - const Vertex3 v0 = vertices.row(facets(facet_id, 0)); - const Vertex3 v1 = vertices.row(facets(facet_id, 1)); - const Vertex3 v2 = vertices.row(facets(facet_id, 2)); - const Vertex3 tri_centroid = (v0 + v1 + v2) / 3.f; - const Scalar tri_area = (v1 - v0).cross(v2 - v0).norm() / 2.f; - centroid += tri_area * tri_centroid; - area += tri_area; - } // over triangles - centroid /= area; - - // Return - Output output; - output.area = area; - output.centroid = centroid; - return output; -} +#pragma once -} // namespace lagrange +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + // Replaced by compute_centroid.h. + #include +#endif diff --git a/modules/core/include/lagrange/compute_tangent_bitangent.h b/modules/core/include/lagrange/compute_tangent_bitangent.h index 91da256d..9259dd44 100644 --- a/modules/core/include/lagrange/compute_tangent_bitangent.h +++ b/modules/core/include/lagrange/compute_tangent_bitangent.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/compute_vertex_valence.h b/modules/core/include/lagrange/compute_vertex_valence.h index 121f20b7..8dac3c2c 100644 --- a/modules/core/include/lagrange/compute_vertex_valence.h +++ b/modules/core/include/lagrange/compute_vertex_valence.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2017 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/get_opposite_edge.h b/modules/core/include/lagrange/get_opposite_edge.h index c63f7115..704ccc1d 100644 --- a/modules/core/include/lagrange/get_opposite_edge.h +++ b/modules/core/include/lagrange/get_opposite_edge.h @@ -19,6 +19,7 @@ namespace lagrange { template +[[deprecated]] EdgeType get_opposite_edge( const Eigen::PlainObjectBase& facets, typename DerivedF::Scalar fid, diff --git a/modules/core/include/lagrange/internal/attribute_string_utils.h b/modules/core/include/lagrange/internal/attribute_string_utils.h index b53a4133..f6a2bf38 100644 --- a/modules/core/include/lagrange/internal/attribute_string_utils.h +++ b/modules/core/include/lagrange/internal/attribute_string_utils.h @@ -12,7 +12,9 @@ #pragma once #include +#include +#include #include namespace lagrange::internal { @@ -26,6 +28,15 @@ namespace lagrange::internal { /// std::string_view to_string(AttributeElement element); +/// +/// Returns a string representation of an attribute element type. +/// +/// @param[in] element Attribute element type. +/// +/// @return String representation. +/// +std::string to_string(BitField element); + /// /// Returns a string representation of an attribute usage. /// diff --git a/modules/core/include/lagrange/internal/find_attribute_utils.h b/modules/core/include/lagrange/internal/find_attribute_utils.h index 7212f6a9..9e34c032 100644 --- a/modules/core/include/lagrange/internal/find_attribute_utils.h +++ b/modules/core/include/lagrange/internal/find_attribute_utils.h @@ -12,8 +12,10 @@ #pragma once #include +#include #include +#include namespace lagrange::internal { @@ -41,7 +43,32 @@ template AttributeId find_matching_attribute( const SurfaceMesh& mesh, std::string_view name, - AttributeElement expected_element, + BitField expected_element, + AttributeUsage expected_usage, + size_t expected_channels); + +/// +/// Find an attribute from a selected set of ids, ensuring the usage and element type match an +/// expected target. If the provided name is empty, the first attribute with matching properties is +/// returned. If no such attribute is found, invalid_attribute_id() is returned instead. +/// +/// @param mesh Mesh where to look for attributes. +/// @param[in] selected_ids Selected attribute ids. +/// @param[in] expected_element Expected element type. +/// @param[in] expected_usage Expected attribute usage. +/// @param[in] expected_channels Expected number of channels. If 0, then the check is skipped. +/// +/// @tparam ExpectedValueType Expected attribute value type. +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// +/// @return Attribute id of the first matching attribute. +/// +template +AttributeId find_matching_attribute( + const SurfaceMesh& mesh, + const std::unordered_set &selected_ids, + BitField expected_element, AttributeUsage expected_usage, size_t expected_channels); @@ -50,8 +77,7 @@ AttributeId find_matching_attribute( /// target. This function does not allow empty names to be provided. /// /// @param mesh Mesh where to look for attributes. -/// @param[in] name Optional name of the attribute to find. If empty, the first -/// matching attribute id will be returned. +/// @param[in] name Name of the attribute to find. /// @param[in] expected_element Expected element type. /// @param[in] expected_usage Expected attribute usage. /// @param[in] expected_channels Expected number of channels. If 0, then the check is skipped. @@ -66,7 +92,7 @@ template AttributeId find_attribute( const SurfaceMesh& mesh, std::string_view name, - AttributeElement expected_element, + BitField expected_element, AttributeUsage expected_usage, size_t expected_channels); diff --git a/modules/core/include/lagrange/internal/skinning.h b/modules/core/include/lagrange/internal/skinning.h new file mode 100644 index 00000000..0db6db1c --- /dev/null +++ b/modules/core/include/lagrange/internal/skinning.h @@ -0,0 +1,280 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +/** + * This file contains commonly used functions related to skinning deformation on a mesh. + * + * `skinning_deform` deforms a mesh with weights. + * + * `skinning_extract_n` takes a weight matrix |V| x |H| and outputs indexed weights, up to n per + * vertex. + * + * `weights_to_mesh_attribute` and `weights_to_indexed_mesh_attribute` save a weights matrix as + * attributes. + */ + +namespace lagrange::internal { + +/** + * Performs linear blend skinning deformation on a mesh.. + * + * @param[in,out] mesh vertices of this mesh will be modified + * @param[in] original_vertices original positions of vertices + * @param[in] transforms vector of eigen affine transforms, describe the global movement of each handle/joint + * @param[in] weights |V| x |handle| weight matrix + * @param[in] weight_complement optional, acts as weights for an extra handle that does not move. + * + */ +template +void skinning_deform( + SurfaceMesh& mesh, + const lagrange::Attribute& original_vertices, + const std::vector>& transforms, + const Eigen::Matrix& weights, + const Eigen::Matrix& weight_complement = {}) +{ + la_runtime_assert((size_t)weights.cols() == transforms.size()); + la_runtime_assert(weights.rows() == mesh.get_num_vertices()); + la_runtime_assert( + (weight_complement.rows() == 0) || (weight_complement.rows() == mesh.get_num_vertices())); + + const Index num_vertices = mesh.get_num_vertices(); + const size_t num_handles = transforms.size(); + + auto original_view = lagrange::matrix_view(original_vertices); + auto verts = lagrange::vertex_ref(mesh); + verts.setZero(); + + for (Index v = 0; v < num_vertices; ++v) { + const Eigen::Matrix orig = original_view.row(v); + Scalar weight_sum = 0; + for (size_t h = 0; h < num_handles; ++h) { + Scalar weight = weights(v, h); + if (weight > 0) { + verts.row(v) += weight * (transforms[h] * orig); + } + weight_sum += weight; + } + if (weight_complement.rows() > 0 && weight_complement(v) > 0) { + verts.row(v) += (weight_complement(v) * orig); + weight_sum += weight_complement(v); + } + if (weight_sum > 0) { + verts.row(v) /= weight_sum; + } + } +} + +/** + * Performs linear blend skinning on a mesh, using weights information from the mesh attributes.. + * + * @param[in,out] mesh vertices of this mesh will be modified + * @param[in] original_vertices original positions of vertices + * @param[in] transforms vector of eigen affine transforms, describe the global movement of each handle/joint + * + */ +template +void skinning_deform( + SurfaceMesh& mesh, + const Attribute& original_vertices, + const std::vector>& transforms) +{ + auto weight_id = mesh.get_attribute_id(AttributeName::weight); + la_runtime_assert(weight_id != invalid_attribute_id()); + + auto& weight_attr = mesh.template get_attribute(weight_id); + + Eigen::Matrix weights = matrix_view(weight_attr); + skinning_deform(mesh, original_vertices, transforms, weights); +} + + +template +struct SkinningExtractNResult +{ + /** + * |V| x n weights (scalar). For each vertex, this will include the n most important + * weights. + */ + Eigen::Matrix weights; + + /** + * |V| x n indices (index). For each vertex, this will be the index of the n most + * important. + */ + Eigen::Matrix indices; +}; + +/** + * From a weight matrix |V| x |H|, constructs a weight matrix |V| x n, + * where n is an arbitrary contraint (typically 4 or 8). + * + * @param[in] weights |V| x |handle| weight matrix + * @param[in] n max number of weights for each vertex + * @param[in] weight_complement optional, acts as weights for an extra handle that does not move. + * + * @return |V| x n weights and |V| x n indices (see struct above). + */ +template +SkinningExtractNResult skinning_extract_n( + const Eigen::Matrix& weights, + int n, + const Eigen::Matrix& weight_complement = + Eigen::Matrix()) +{ + const Index num_vertices = Index(weights.rows()); + const Index num_handles = Index(weights.cols()); + Index num_implicit_handle = 0; + + la_runtime_assert(num_handles > 0); + if (weight_complement.rows() > 0 && + weight_complement.maxCoeff() > std::numeric_limits::epsilon()) { + la_runtime_assert(weight_complement.rows() == num_vertices); + // the weights do not sum up to 1, so we assume there is an implicit fixed handle + num_implicit_handle = 1; + } + + // Max number of handles + const Index num_handles_max = Index(n) - num_implicit_handle; + const Index num_handles_used = std::min(num_handles, num_handles_max); + + + SkinningExtractNResult result; + result.weights.resize(num_vertices, n); + result.indices.resize(num_vertices, n); + if (num_handles < Index(n)) { + // only need to initialize in this case + result.weights.setZero(); + result.indices.setZero(); + } + + if (num_handles <= num_handles_max) { + // we have more handle slots than handles, so just copy them over + la_runtime_assert(num_handles == num_handles_used); + for (Index i = 0; i < num_vertices; ++i) { + for (Index j = 0; j < num_handles; ++j) { + result.indices(i, j) = j; + result.weights(i, j) = weights(i, j); + } + } + + } else { + // we only pick some of the handles, find and copy the n most important ones + + for (Index i = 0; i < num_vertices; ++i) { + std::vector tmp(num_handles); + std::iota(tmp.begin(), tmp.end(), 0); + std::partial_sort( + tmp.begin(), + tmp.begin() + num_handles_used, + tmp.end(), + [&weights, i](const Index a, const Index b) -> bool { + return weights(i, a) > weights(i, b); + }); + + for (Index j = 0; j < num_handles_used; ++j) { + result.indices(i, j) = tmp[j]; + result.weights(i, j) = weights(i, tmp[j]); + } + } + } + + if (num_implicit_handle > 0) { + // add the implicit handle + for (Index i = 0; i < num_vertices; ++i) { + result.indices(i, num_handles_used) = num_handles_used; + result.weights(i, num_handles_used) = weight_complement(i); + } + } + + // normalize if we changed the active handles + if (num_handles > num_handles_max) { + for (Index i = 0; i < num_vertices; ++i) { + Scalar s = result.weights.row(i).sum(); + la_runtime_assert(s > 0); + result.weights.row(i) /= s; + } + } + + return result; +} + +/** + * Imports the weights matrix as weight attributes of the mesh. + * + * @param[in,out] mesh Mesh to modify + * @param[in] weights weights matrix + * + * @return new weights attribute id + */ +template +lagrange::AttributeId weights_to_mesh_attribute( + SurfaceMesh& mesh, + const Eigen::Matrix& weights) +{ + return mesh.template create_attribute( + AttributeName::weight, + AttributeElement::Vertex, + AttributeUsage::Vector, + weights.cols(), + {weights.data(), size_t(weights.size())}); +} + +/** + * Imports the weights matrix as indexed weight attributes of the mesh. + * + * @param[in,out] mesh Mesh to modify + * @param[in] weights Weights matrix + * @param[in] n max weights per vertex + * + * @return pair of new attribute ids: index and weight + */ +template < + typename Scalar, + typename Index, + typename WeightScalar = Scalar, + typename WeightIndex = Index> +std::pair weights_to_indexed_mesh_attribute( + SurfaceMesh& mesh, + const Eigen::Matrix& weights, + int n) +{ + auto result = skinning_extract_n(weights, n); + auto bone_id = mesh.template create_attribute( + AttributeName::indexed_joint, + AttributeElement::Vertex, + AttributeUsage::Vector, + result.indices.cols(), + {result.indices.data(), size_t(result.indices.size())}); + + auto weight_id = mesh.template create_attribute( + AttributeName::indexed_weight, + AttributeElement::Vertex, + AttributeUsage::Vector, + result.weights.cols(), + {result.weights.data(), size_t(result.weights.size())}); + return {bone_id, weight_id}; +} + + +} // namespace lagrange::internal diff --git a/modules/core/include/lagrange/legacy/combine_mesh_list.h b/modules/core/include/lagrange/legacy/combine_mesh_list.h index 0c58e526..d2a070a3 100644 --- a/modules/core/include/lagrange/legacy/combine_mesh_list.h +++ b/modules/core/include/lagrange/legacy/combine_mesh_list.h @@ -1,5 +1,5 @@ /* - * Copyright 2020 Adobe. All rights reserved. + * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/legacy/compute_corner_normal.h b/modules/core/include/lagrange/legacy/compute_corner_normal.h index d67cee5b..bd7d462d 100644 --- a/modules/core/include/lagrange/legacy/compute_corner_normal.h +++ b/modules/core/include/lagrange/legacy/compute_corner_normal.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/legacy/compute_facet_area.h b/modules/core/include/lagrange/legacy/compute_facet_area.h new file mode 100644 index 00000000..6e3ad7e7 --- /dev/null +++ b/modules/core/include/lagrange/legacy/compute_facet_area.h @@ -0,0 +1,227 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace lagrange { +namespace internal { + +/// +/// Calculates the triangle areas. +/// +/// @param[in] mesh Input triangle mesh. +/// +/// @tparam MeshType Mesh type. +/// +/// @return #F x 1 array of triangle areas. +/// +template +auto compute_triangle_areas(const MeshType& mesh) -> AttributeArrayOf +{ + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + using Vector = Eigen::Matrix; + + const Index dim = mesh.get_dim(); + const Index num_facets = mesh.get_num_facets(); + const auto& vertices = mesh.get_vertices(); + const auto& facets = mesh.get_facets(); + AttributeArrayOf areas(num_facets, 1); + + if (dim == 2) { + for (Index i = 0; i < num_facets; i++) { + const Index i0 = facets(i, 0); + const Index i1 = facets(i, 1); + const Index i2 = facets(i, 2); + Vector v0, v1, v2; + v0 << vertices(i0, 0), vertices(i0, 1), 0.0; + v1 << vertices(i1, 0), vertices(i1, 1), 0.0; + v2 << vertices(i2, 0), vertices(i2, 1), 0.0; + Vector e1 = v1 - v0; + Vector e2 = v2 - v0; + areas(i, 0) = Scalar(0.5) * (e1.cross(e2)).norm(); + } + } else if (dim == 3) { + for (Index i = 0; i < num_facets; i++) { + const Index i0 = facets(i, 0); + const Index i1 = facets(i, 1); + const Index i2 = facets(i, 2); + Vector v0, v1, v2; + v0 << vertices(i0, 0), vertices(i0, 1), vertices(i0, 2); + v1 << vertices(i1, 0), vertices(i1, 1), vertices(i1, 2); + v2 << vertices(i2, 0), vertices(i2, 1), vertices(i2, 2); + Vector e1 = v1 - v0; + Vector e2 = v2 - v0; + areas(i, 0) = Scalar(0.5) * (e1.cross(e2)).norm(); + } + } else { + throw std::runtime_error("Unsupported dimention."); + } + + return areas; +} + +/// +/// Calculates the quad areas. +/// +/// @param[in] mesh Input quad mesh. +/// +/// @tparam MeshType Mesh type. +/// +/// @return #F x 1 array of quad areas. +/// +template +auto compute_quad_areas(const MeshType& mesh) -> AttributeArrayOf +{ + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + using Vector = Eigen::Matrix; + + const Index dim = mesh.get_dim(); + const Index num_facets = mesh.get_num_facets(); + const auto& vertices = mesh.get_vertices(); + const auto& facets = mesh.get_facets(); + AttributeArrayOf areas(num_facets, 1); + + if (dim == 2) { + for (Index i = 0; i < num_facets; i++) { + const Index i0 = facets(i, 0); + const Index i1 = facets(i, 1); + const Index i2 = facets(i, 2); + const Index i3 = facets(i, 3); + Vector v0, v1, v2, v3; + v0 << vertices(i0, 0), vertices(i0, 1), 0.0f; + v1 << vertices(i1, 0), vertices(i1, 1), 0.0f; + v2 << vertices(i2, 0), vertices(i2, 1), 0.0f; + v3 << vertices(i3, 0), vertices(i3, 1), 0.0f; + Vector e10 = v1 - v0; + Vector e30 = v3 - v0; + Vector e12 = v1 - v2; + Vector e32 = v3 - v2; + areas(i, 0) = 0.5f * (e10.cross(e30)).norm() + 0.5f * (e12.cross(e32)).norm(); + } + } else if (dim == 3) { + for (Index i = 0; i < num_facets; i++) { + const Index i0 = facets(i, 0); + const Index i1 = facets(i, 1); + const Index i2 = facets(i, 2); + const Index i3 = facets(i, 3); + Vector v0, v1, v2, v3; + v0 << vertices(i0, 0), vertices(i0, 1), vertices(i0, 2); + v1 << vertices(i1, 0), vertices(i1, 1), vertices(i1, 2); + v2 << vertices(i2, 0), vertices(i2, 1), vertices(i2, 2); + v3 << vertices(i3, 0), vertices(i3, 1), vertices(i3, 2); + Vector e10 = v1 - v0; + Vector e30 = v3 - v0; + Vector e12 = v1 - v2; + Vector e32 = v3 - v2; + areas(i, 0) = 0.5f * (e10.cross(e30)).norm() + 0.5f * (e12.cross(e32)).norm(); + } + } else { + throw std::runtime_error("Unsupported dimention."); + } + + return areas; +} +} // namespace internal + +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/// +/// Calculates the facet areas. +/// +/// @param[in] mesh Input mesh. +/// +/// @tparam MeshType Mesh type. +/// +/// @return #F x 1 array of facet areas. +/// +template +auto compute_facet_area_raw(const MeshType& mesh) -> AttributeArrayOf +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + using Index = typename MeshType::Index; + const Index vertex_per_facet = mesh.get_vertex_per_facet(); + if (vertex_per_facet == 3) { + return internal::compute_triangle_areas(mesh); + } else if (vertex_per_facet == 4) { + return internal::compute_quad_areas(mesh); + } else { + throw std::runtime_error("Unsupported facet type."); + } +} + +/// +/// Calculates the facet areas. The result is stored as a new facet attribute `area`. +/// +/// @param[in,out] mesh Input mesh. +/// +/// @tparam MeshType Mesh type. +/// +template +void compute_facet_area(MeshType& mesh) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + auto areas = compute_facet_area_raw(mesh); + mesh.add_facet_attribute("area"); + mesh.import_facet_attribute("area", areas); +} + +/// +/// Calculates the uv areas. +/// +/// @param[in] uv UV vertex positions. +/// @param[in] triangles UV triangle indices. +/// +/// @tparam DerivedUV Matrix type of UV vertex positions. +/// @tparam DerivedF Matrix type of UV triangle indices. +/// +/// @return #F x 1 array or triangle areas. +/// +template +Eigen::Matrix compute_uv_area_raw( + const Eigen::MatrixBase& uv, + const Eigen::MatrixBase& triangles) +{ + using Scalar = typename DerivedUV::Scalar; + using Index = typename DerivedF::Scalar; + + la_runtime_assert(uv.cols() == 2); + la_runtime_assert(triangles.cols() == 3); + const auto num_triangles = triangles.rows(); + + auto compute_single_triangle_area = [&uv, &triangles](Index i) { + const auto& f = triangles.row(i); + const auto& v0 = uv.row(f[0]); + const auto& v1 = uv.row(f[1]); + const auto& v2 = uv.row(f[2]); + return 0.5f * (v0[0] * v1[1] + v1[0] * v2[1] + v2[0] * v0[1] - v0[0] * v2[1] - + v1[0] * v0[1] - v2[0] * v1[1]); + }; + + Eigen::Matrix areas(num_triangles); + for (auto i : range(safe_cast(num_triangles))) { + areas[i] = compute_single_triangle_area(i); + } + return areas; +} +} // namespace legacy +} // namespace lagrange diff --git a/modules/core/include/lagrange/legacy/compute_mesh_centroid.h b/modules/core/include/lagrange/legacy/compute_mesh_centroid.h new file mode 100644 index 00000000..877eceff --- /dev/null +++ b/modules/core/include/lagrange/legacy/compute_mesh_centroid.h @@ -0,0 +1,76 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +#include +#include +#include + +namespace lagrange { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +template +struct ComputeMeshCentroidOutput +{ + // We get this for free when computing the centroid. + Scalar area; + // The centroid itself. Will be row vector to be consistent with lagrange. + Eigen::Matrix centroid; +}; + +// Compute the centroid of certain facets in a mesh +// mesh_ref, reference to the mesh +// active_facets, the facets that are included in the centroid computation. +// Empty would imply all facets. +template +ComputeMeshCentroidOutput compute_mesh_centroid( // + const MeshType& mesh_ref, + const typename MeshType::IndexList& active_facets = typename MeshType::IndexList()) +{ // + + using Scalar = typename MeshType::Scalar; + using Vertex3 = Eigen::Matrix; + using Output = ComputeMeshCentroidOutput; + + const auto& vertices = mesh_ref.get_vertices(); + const auto& facets = mesh_ref.get_facets(); + + la_runtime_assert(vertices.cols() == 3, "Currently, only 3 dimensions are supported"); + la_runtime_assert(facets.cols() == 3, "Currently, only triangles are supported"); + + Vertex3 centroid = Vertex3::Zero(); + Scalar area = 0; + + for (auto facet_id : range_sparse(mesh_ref.get_num_facets(), active_facets)) { + const Vertex3 v0 = vertices.row(facets(facet_id, 0)); + const Vertex3 v1 = vertices.row(facets(facet_id, 1)); + const Vertex3 v2 = vertices.row(facets(facet_id, 2)); + const Vertex3 tri_centroid = (v0 + v1 + v2) / 3.f; + const Scalar tri_area = (v1 - v0).cross(v2 - v0).norm() / 2.f; + centroid += tri_area * tri_centroid; + area += tri_area; + } // over triangles + centroid /= area; + + // Return + Output output; + output.area = area; + output.centroid = centroid; + return output; +} + + +} // namespace legacy +} // namespace lagrange diff --git a/modules/core/include/lagrange/legacy/compute_normal.h b/modules/core/include/lagrange/legacy/compute_normal.h index 4ab74fda..5b871d1e 100644 --- a/modules/core/include/lagrange/legacy/compute_normal.h +++ b/modules/core/include/lagrange/legacy/compute_normal.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/legacy/compute_triangle_normal.h b/modules/core/include/lagrange/legacy/compute_triangle_normal.h index 063541a8..6aae8f44 100644 --- a/modules/core/include/lagrange/legacy/compute_triangle_normal.h +++ b/modules/core/include/lagrange/legacy/compute_triangle_normal.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2017 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/legacy/compute_vertex_normal.h b/modules/core/include/lagrange/legacy/compute_vertex_normal.h index e00f26f7..5f1eb15e 100644 --- a/modules/core/include/lagrange/legacy/compute_vertex_normal.h +++ b/modules/core/include/lagrange/legacy/compute_vertex_normal.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2017 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/legacy/normalize_meshes.h b/modules/core/include/lagrange/legacy/normalize_meshes.h index f57d6386..1b473f75 100644 --- a/modules/core/include/lagrange/legacy/normalize_meshes.h +++ b/modules/core/include/lagrange/legacy/normalize_meshes.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/legacy/reorder_mesh_vertices.h b/modules/core/include/lagrange/legacy/reorder_mesh_vertices.h new file mode 100644 index 00000000..3916248c --- /dev/null +++ b/modules/core/include/lagrange/legacy/reorder_mesh_vertices.h @@ -0,0 +1,117 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace lagrange { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/** + * Reorders (possibly with shrinking) the vertices in the mesh. + * + * Arguments: + * input_mesh + * forward_mapping: old2new mapping for the vertices in the mesh. + * forward_mapping[i] == INVALID or i -> vertex i will remain as is + * forward_mapping[i] = j, vertex i will be remapped to j + * if two vertices map to the same new index, they will be merged. + * ''Forward_mapping must be surjective'' + * + * Returns: + * output_mesh with the vertices merged. + * + * NOTES: + * All vertex and facet attributes are mapped from input to the output. + * The output_mesh's facets are the same as input_mesh's. + * The output_mesh's vertices are the same or a subset of the + * input_mesh's vertices. + * This is not a clean-up routine as-is. DEGENRATE facets can be present + * in the result. + */ +template +std::unique_ptr reorder_mesh_vertices( + const MeshType& mesh, + const typename MeshType::IndexList& forward_mapping) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + la_runtime_assert( + mesh.get_vertex_per_facet() == 3, + std::string("vertex per facet is ") + std::to_string(mesh.get_vertex_per_facet())); + + using Index = typename MeshType::Index; + using VertexArray = typename MeshType::VertexArray; + using FacetArray = typename MeshType::FacetArray; + using IndexList = typename MeshType::IndexList; + + const auto num_old_vertices = mesh.get_num_vertices(); + const auto& vertices = mesh.get_vertices(); + const auto& facets = mesh.get_facets(); + la_runtime_assert(num_old_vertices == safe_cast(forward_mapping.size())); + + auto forward_mapping_no_invalid = [&forward_mapping](const Index io) { + return forward_mapping[io] == invalid() ? io : forward_mapping[io]; + }; + + // Cound the number of new vertices + Index num_new_vertices = 0; + for (const auto io : range(num_old_vertices)) { + num_new_vertices = std::max(num_new_vertices, forward_mapping_no_invalid(io)); + } + num_new_vertices += 1; // num_of_*** = biggest index + 1 + la_runtime_assert( + num_new_vertices <= num_old_vertices, + "Number of vertices should not increase"); + + + // Create the backward mapping and the new vertices + IndexList backward_mapping(num_new_vertices, invalid()); + VertexArray vertices_new(num_new_vertices, mesh.get_dim()); + for (const auto io : range(num_old_vertices)) { + const auto in = forward_mapping_no_invalid(io); + backward_mapping[in] = io; + vertices_new.row(in) = vertices.row(io); + } + // The mapping should be surjective. + la_runtime_assert( + std::find(backward_mapping.begin(), backward_mapping.end(), invalid()) == + backward_mapping.end(), + "Forward mapping is not surjective"); + + // Create new faces + FacetArray facets_new(facets.rows(), facets.cols()); + for (auto i : range(facets.rows())) { + facets_new(i, 0) = forward_mapping_no_invalid(facets(i, 0)); + facets_new(i, 1) = forward_mapping_no_invalid(facets(i, 1)); + facets_new(i, 2) = forward_mapping_no_invalid(facets(i, 2)); + } + + // Create new mesh + auto mesh2 = create_mesh(std::move(vertices_new), std::move(facets_new)); + + // Port attributes + map_attributes(mesh, *mesh2, backward_mapping); + + return mesh2; +} + +} // namespace legacy +} // namespace lagrange diff --git a/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h b/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h index 8e9c2221..c5a13d8b 100644 --- a/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h +++ b/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h @@ -19,7 +19,6 @@ #include #include #include -#include #include namespace lagrange { diff --git a/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h b/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h index 6a1280ac..2afac473 100644 --- a/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h +++ b/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h @@ -23,7 +23,7 @@ #include #include #include -#include +#include // clang-format off #include @@ -62,6 +62,20 @@ std::unique_ptr resolve_vertex_nonmanifoldness(MeshType& mesh) const auto& vertices = mesh.get_vertices(); const auto& facets = mesh.get_facets(); + auto get_opposite_edge = [&](Index fid, Index vid) { + const auto& f = facets.row(fid); + if (f[0] == vid) { + return Edge(f[1], f[2]); + } else if (f[1] == vid) { + return Edge(f[2], f[0]); + } else if (f[2] == vid) { + return Edge(f[0], f[1]); + } else { + throw Error( + "Facet " + std::to_string(fid) + " does not contain vertex " + std::to_string(vid)); + } + }; + FacetArray out_facets(facets); Index vertex_count = mesh.get_num_vertices(); @@ -77,7 +91,7 @@ std::unique_ptr resolve_vertex_nonmanifoldness(MeshType& mesh) const auto& adj_facets = mesh.get_facets_adjacent_to_vertex(i); std::list rim_edges; for (Index fid : adj_facets) { - rim_edges.push_back(get_opposite_edge(facets, fid, i)); + rim_edges.push_back(get_opposite_edge(fid, i)); } auto chains = chain_edges(rim_edges); if (chains.size() > 1) { diff --git a/modules/core/include/lagrange/mesh_convert.impl.h b/modules/core/include/lagrange/mesh_convert.impl.h index 95926465..3a0981b9 100644 --- a/modules/core/include/lagrange/mesh_convert.impl.h +++ b/modules/core/include/lagrange/mesh_convert.impl.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -126,13 +127,14 @@ SurfaceMesh to_surface_mesh_internal(InputMeshType&& mesh) // 3rd - Transfer attributes auto usage_from_name = [](std::string_view name) { - if (starts_with(name, "normal")) { + if (starts_with(name, AttributeName::normal)) { return AttributeUsage::Normal; } - if (starts_with(name, "uv")) { + // "uv" is for legacy code with hardcoded "uv" name. + if (starts_with(name, AttributeName::texcoord) || starts_with(name, "uv")) { return AttributeUsage::UV; } - if (starts_with(name, "color")) { + if (starts_with(name, AttributeName::color)) { return AttributeUsage::Color; } return AttributeUsage::Vector; diff --git a/modules/core/include/lagrange/permute_vertices.h b/modules/core/include/lagrange/permute_vertices.h new file mode 100644 index 00000000..e5a7e369 --- /dev/null +++ b/modules/core/include/lagrange/permute_vertices.h @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#pragma once + +#include +#include + +namespace lagrange { + +/// +/// @defgroup group-surfacemesh-utils Mesh utilities +/// @ingroup group-surfacemesh +/// +/// Various mesh processing utilities. +/// +/// @{ + +/** + * Reorder vertices of a mesh based on a given permutation. i.e. rearrangement vertices so that they + * are ordered as specified by the `new_to_old` index array. The total number of vertices is + * unchnaged. + * + * @param[in,out] mesh The target mesh whose vertices will be reordered in place. + * @param[in] new_to_old The permutation index array specifying the new vertex order. + * This array can often be obteined via index-based sorting of the + * vertices with customized comparison. + * + * @see `remap_vertices` if two or more vertices may be combined. + */ +template +void permute_vertices(SurfaceMesh& mesh, span new_to_old); + +/// @} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/point_on_segment.h b/modules/core/include/lagrange/point_on_segment.h index 4cc559cb..d2f246eb 100644 --- a/modules/core/include/lagrange/point_on_segment.h +++ b/modules/core/include/lagrange/point_on_segment.h @@ -11,61 +11,5 @@ */ #pragma once -#include - -namespace lagrange { - -namespace internal { - -/// @internal -bool point_on_segment_2d(Eigen::Vector2d p, Eigen::Vector2d a, Eigen::Vector2d b); - -/// @internal -bool point_on_segment_3d(Eigen::Vector3d p, Eigen::Vector3d a, Eigen::Vector3d b); - -} - -/// -/// Test if a point lies exactly on a segment [a,b] using exact predicates. If the points are -/// collinear, each individual coordinate is examined to determine if the query point lies inside -/// the segment or outside of it. -/// -/// @param[in] p Query point. -/// @param[in] a First segment endpoint. -/// @param[in] b Second segment endpoint. -/// -/// @tparam PointType Point type. -/// -/// @return True if the query point lies exactly on the segment, False otherwise. -/// -template -bool point_on_segment( - const Eigen::MatrixBase& p, - const Eigen::MatrixBase& a, - const Eigen::MatrixBase& b) -{ - if (p.size() == 2 && a.size() == 2 && b.size() == 2) { - Eigen::Vector2d p2d(static_cast(p.x()), static_cast(p.y())); - Eigen::Vector2d a2d(static_cast(a.x()), static_cast(a.y())); - Eigen::Vector2d b2d(static_cast(b.x()), static_cast(b.y())); - return internal::point_on_segment_2d(p2d, a2d, b2d); - } else if (p.size() == 3 && a.size() == 3 && b.size() == 3) { - Eigen::Vector3d p3d( - static_cast(p[0]), - static_cast(p[1]), - static_cast(p[2])); - Eigen::Vector3d a3d( - static_cast(a[0]), - static_cast(a[1]), - static_cast(a[2])); - Eigen::Vector3d b3d( - static_cast(b[0]), - static_cast(b[1]), - static_cast(b[2])); - return internal::point_on_segment_3d(p3d, a3d, b3d); - } else { - throw std::runtime_error("Incompatible types"); - } -} - -} // namespace lagrange +#pragma message(" has been moved to ") +#include diff --git a/modules/core/include/lagrange/point_segment_squared_distance.h b/modules/core/include/lagrange/point_segment_squared_distance.h index 38180b34..f21ed747 100644 --- a/modules/core/include/lagrange/point_segment_squared_distance.h +++ b/modules/core/include/lagrange/point_segment_squared_distance.h @@ -1,125 +1,16 @@ -// Source: https://github.com/alicevision/geogram/blob/master/src/lib/geogram/basic/geometry_nd.h -// SPDX-License-Identifier: BSD-3-Clause -// -// Copyright (c) 2012-2014, Bruno Levy -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the ALICE Project-Team nor the names of its -// contributors may be used to endorse or promote products derived from this -// software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// If you modify this software, you should include a notice giving the -// name of the person performing the modification, the date of modification, -// and the reason for such modification. -// -// Contact: Bruno Levy -// -// Bruno.Levy@inria.fr -// http://www.loria.fr/~levy -// -// ALICE Project -// LORIA, INRIA Lorraine, -// Campus Scientifique, BP 239 -// 54506 VANDOEUVRE LES NANCY CEDEX -// FRANCE -// -// This file has been modified by Adobe. -// -// All modifications are Copyright 2020 Adobe. -// +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ #pragma once -#include - -#include - -namespace lagrange { - -/// -/// Computes the point closest to a given point in a nd segment -/// -/// @param[in] point the query point -/// @param[in] V0 first extremity of the segment -/// @param[in] V1 second extremity of the segment -/// @param[out] closest_point the point closest to @p point in the segment [\p V0, @p V1] -/// @param[out] lambda0 barycentric coordinate of the closest point relative to @p V0 -/// @param[out] lambda1 barycentric coordinate of the closest point relative to @p V1 -/// -/// @tparam PointType the class that represents the points. -/// -/// @return the squared distance between the point and the segment [\p V0, @p V1] -/// -template -auto point_segment_squared_distance( - const Eigen::MatrixBase& point, - const Eigen::MatrixBase& V0, - const Eigen::MatrixBase& V1, - Eigen::PlainObjectBase& closest_point, - ScalarOf& lambda0, - ScalarOf& lambda1) -> ScalarOf -{ - using Scalar = ScalarOf; - - auto l2 = (V0 - V1).squaredNorm(); - auto t = (point - V0).dot(V1 - V0); - if (t <= Scalar(0) || l2 == Scalar(0)) { - closest_point = V0; - lambda0 = Scalar(1); - lambda1 = Scalar(0); - return (point - V0).squaredNorm(); - } else if (t > l2) { - closest_point = V1; - lambda0 = Scalar(0); - lambda1 = Scalar(1); - return (point - V1).squaredNorm(); - } - lambda1 = t / l2; - lambda0 = Scalar(1) - lambda1; - closest_point = lambda0 * V0 + lambda1 * V1; - return (point - closest_point).squaredNorm(); -} - -/// -/// Computes the point closest to a given point in a nd segment -/// -/// @param[in] point the query point -/// @param[in] V0 first extremity of the segment -/// @param[in] V1 second extremity of the segment -/// -/// @tparam PointType the class that represents the points. -/// -/// @return the squared distance between the point and the segment [\p V0, @p V1] -/// -template -auto point_segment_squared_distance( - const Eigen::MatrixBase& point, - const Eigen::MatrixBase& V0, - const Eigen::MatrixBase& V1) -> ScalarOf -{ - PointType closest_point; - ScalarOf lambda0; - ScalarOf lambda1; - return point_segment_squared_distance(point, V0, V1, closest_point, lambda0, lambda1); -} - -} // namespace lagrange +#pragma message(" has been moved to " \ + "") +#include diff --git a/modules/core/include/lagrange/point_triangle_squared_distance.h b/modules/core/include/lagrange/point_triangle_squared_distance.h index 7247e675..5a71d59e 100644 --- a/modules/core/include/lagrange/point_triangle_squared_distance.h +++ b/modules/core/include/lagrange/point_triangle_squared_distance.h @@ -1,319 +1,16 @@ -// Source: https://github.com/alicevision/geogram/blob/master/src/lib/geogram/basic/geometry_nd.h -// SPDX-License-Identifier: BSD-3-Clause -// -// Copyright (c) 2012-2014, Bruno Levy -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the ALICE Project-Team nor the names of its -// contributors may be used to endorse or promote products derived from this -// software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// If you modify this software, you should include a notice giving the -// name of the person performing the modification, the date of modification, -// and the reason for such modification. -// -// Contact: Bruno Levy -// -// Bruno.Levy@inria.fr -// http://www.loria.fr/~levy -// -// ALICE Project -// LORIA, INRIA Lorraine, -// Campus Scientifique, BP 239 -// 54506 VANDOEUVRE LES NANCY CEDEX -// FRANCE -// -// This file has been modified by Adobe. -// -// All modifications are Copyright 2020 Adobe. -// +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ #pragma once -#include -#include - -#include - -namespace lagrange { - -/// -/// Computes the point closest to a given point in a nd triangle. See -/// http://www.geometrictools.com/LibMathematics/Distance/Distance.html -/// -/// @param[in] point the query point -/// @param[in] V0 first vertex of the triangle -/// @param[in] V1 second vertex of the triangle -/// @param[in] V2 third vertex of the triangle -/// @param[out] closest_point the point closest to @p point in the triangle (\p V0, @p V1, @p V2) -/// @param[out] lambda0 barycentric coordinate of the closest point relative to @p V0 -/// @param[out] lambda1 barycentric coordinate of the closest point relative to @p V1 -/// @param[out] lambda2 barycentric coordinate of the closest point relative to @p V2 -/// -/// @tparam PointType the class that represents the points. -/// -/// @return the squared distance between the point and the triangle (\p V0, @p V1, @p V2) -/// -template -auto point_triangle_squared_distance( - const Eigen::MatrixBase& point, - const Eigen::MatrixBase& V0, - const Eigen::MatrixBase& V1, - const Eigen::MatrixBase& V2, - Eigen::PlainObjectBase& closest_point, - ScalarOf& lambda0, - ScalarOf& lambda1, - ScalarOf& lambda2) -> ScalarOf -{ - using Scalar = ScalarOf; - - Eigen::Vector3d diff = V0.template cast() - point.template cast(); - Eigen::Vector3d edge0 = V1.template cast() - V0.template cast(); - Eigen::Vector3d edge1 = V2.template cast() - V0.template cast(); - double a00 = edge0.squaredNorm(); - double a01 = edge0.dot(edge1); - double a11 = edge1.squaredNorm(); - double b0 = diff.dot(edge0); - double b1 = diff.dot(edge1); - double c = diff.squaredNorm(); - double det = std::fabs(a00 * a11 - a01 * a01); - double s = a01 * b1 - a11 * b0; - double t = a01 * b0 - a00 * b1; - double sqr_distance; - - // If the triangle is degenerate - if (det < 1e-30) { - Scalar cur_l1, cur_l2; - PointType cur_closest; - Scalar result; - Scalar cur_dist = - point_segment_squared_distance(point, V0, V1, cur_closest, cur_l1, cur_l2); - result = cur_dist; - closest_point = cur_closest; - lambda0 = cur_l1; - lambda1 = cur_l2; - lambda2 = 0; - cur_dist = point_segment_squared_distance(point, V0, V2, cur_closest, cur_l1, cur_l2); - if (cur_dist < result) { - result = cur_dist; - closest_point = cur_closest; - lambda0 = cur_l1; - lambda2 = cur_l2; - lambda1 = Scalar(0); - } - cur_dist = point_segment_squared_distance(point, V1, V2, cur_closest, cur_l1, cur_l2); - if (cur_dist < result) { - result = cur_dist; - closest_point = cur_closest; - lambda1 = cur_l1; - lambda2 = cur_l2; - lambda0 = Scalar(0); - } - return result; - } - - if (s + t <= det) { - if (s < 0) { - if (t < 0) { // region 4 - if (b0 < 0) { - t = 0; - if (-b0 >= a00) { - s = 1; - sqr_distance = a00 + 2 * b0 + c; - } else { - s = -b0 / a00; - sqr_distance = b0 * s + c; - } - } else { - s = 0; - if (b1 >= 0) { - t = 0; - sqr_distance = c; - } else if (-b1 >= a11) { - t = 1; - sqr_distance = a11 + 2 * b1 + c; - } else { - t = -b1 / a11; - sqr_distance = b1 * t + c; - } - } - } else { // region 3 - s = 0; - if (b1 >= 0) { - t = 0; - sqr_distance = c; - } else if (-b1 >= a11) { - t = 1; - sqr_distance = a11 + 2 * b1 + c; - } else { - t = -b1 / a11; - sqr_distance = b1 * t + c; - } - } - } else if (t < 0) { // region 5 - t = 0; - if (b0 >= 0) { - s = 0; - sqr_distance = c; - } else if (-b0 >= a00) { - s = 1; - sqr_distance = a00 + 2 * b0 + c; - } else { - s = -b0 / a00; - sqr_distance = b0 * s + c; - } - } else { // region 0 - // minimum at interior point - double inv_det = double(1) / det; - s *= inv_det; - t *= inv_det; - sqr_distance = s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; - } - } else { - double tmp0, tmp1, numer, denom; - - if (s < 0) { // region 2 - tmp0 = a01 + b0; - tmp1 = a11 + b1; - if (tmp1 > tmp0) { - numer = tmp1 - tmp0; - denom = a00 - 2 * a01 + a11; - if (numer >= denom) { - s = 1; - t = 0; - sqr_distance = a00 + 2 * b0 + c; - } else { - s = numer / denom; - t = 1 - s; - sqr_distance = - s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; - } - } else { - s = 0; - if (tmp1 <= 0) { - t = 1; - sqr_distance = a11 + 2 * b1 + c; - } else if (b1 >= 0) { - t = 0; - sqr_distance = c; - } else { - t = -b1 / a11; - sqr_distance = b1 * t + c; - } - } - } else if (t < 0) { // region 6 - tmp0 = a01 + b1; - tmp1 = a00 + b0; - if (tmp1 > tmp0) { - numer = tmp1 - tmp0; - denom = a00 - 2 * a01 + a11; - if (numer >= denom) { - t = 1; - s = 0; - sqr_distance = a11 + 2 * b1 + c; - } else { - t = numer / denom; - s = 1 - t; - sqr_distance = - s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; - } - } else { - t = 0; - if (tmp1 <= 0) { - s = 1; - sqr_distance = a00 + 2 * b0 + c; - } else if (b0 >= 0) { - s = 0; - sqr_distance = c; - } else { - s = -b0 / a00; - sqr_distance = b0 * s + c; - } - } - } else { // region 1 - numer = a11 + b1 - a01 - b0; - if (numer <= 0) { - s = 0; - t = 1; - sqr_distance = a11 + 2 * b1 + c; - } else { - denom = a00 - 2 * a01 + a11; - if (numer >= denom) { - s = 1; - t = 0; - sqr_distance = a00 + 2 * b0 + c; - } else { - s = numer / denom; - t = 1 - s; - sqr_distance = - s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; - } - } - } - } - - // Account for numerical round-off error. - if (sqr_distance < 0) { - sqr_distance = 0; - } - - closest_point = (V0.template cast() + s * edge0 + t * edge1).template cast(); - lambda0 = Scalar(1 - s - t); - lambda1 = Scalar(s); - lambda2 = Scalar(t); - return Scalar(sqr_distance); -} - -/// -/// @brief Computes the squared distance between a point and a nd triangle. -/// @details See http://www.geometrictools.com/LibMathematics/Distance/Distance.html -/// -/// @param[in] point the query point -/// @param[in] V0 first vertex of the triangle -/// @param[in] V1 second vertex of the triangle -/// @param[in] V2 third vertex of the triangle -/// -/// @tparam PointType the class that represents the points. -/// -/// @return the squared distance between the point and the triangle (\p V0, @p V1, @p V2) -/// -template -auto point_triangle_squared_distance( - const Eigen::MatrixBase& point, - const Eigen::MatrixBase& V0, - const Eigen::MatrixBase& V1, - const Eigen::MatrixBase& V2) -> ScalarOf -{ - PointType closest_point; - ScalarOf lambda1, lambda2, lambda3; - return point_triangle_squared_distance( - point, - V0, - V1, - V2, - closest_point, - lambda1, - lambda2, - lambda3); -} - -} // namespace lagrange +#pragma message(" has been moved to " \ + "") +#include diff --git a/modules/core/include/lagrange/quad_to_tri.h b/modules/core/include/lagrange/quad_to_tri.h index 3927a75f..4a7c9fed 100644 --- a/modules/core/include/lagrange/quad_to_tri.h +++ b/modules/core/include/lagrange/quad_to_tri.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/remap_vertices.h b/modules/core/include/lagrange/remap_vertices.h new file mode 100644 index 00000000..9c00d20b --- /dev/null +++ b/modules/core/include/lagrange/remap_vertices.h @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#pragma once + +#include +#include + +namespace lagrange { +/// +/// @defgroup group-surfacemesh-utils Mesh utilities +/// @ingroup group-surfacemesh +/// +/// Various mesh processing utilities. +/// +/// @{ + +/** + * Remap vertices options. + */ +struct RemapVerticesOptions +{ + /// Collision policy control the behavior when two or more vertices are mapped into the same + /// output vertex. + enum class CollisionPolicy { + /// Take the average of all involved vertices. + Average, + + /// Keep the value of the first vertex. + KeepFirst, + + /// Throw error if collision is detected. + Error, + }; + + /// Collision policy for float or double valued attributes + CollisionPolicy collision_policy_float = CollisionPolicy::Average; + + /// Collision policy for integral valued attributes + CollisionPolicy collision_policy_integral = CollisionPolicy::KeepFirst; +}; + +/** + * Remap vertices of a mesh based on provided forward mapping. + * + * @param[in,out] mesh The target mesh. + * @param[in] forward_mapping Vertex mapping where vertex `i` will be remapped to + * vertex `forward_mapping[i]`. + * + * @note + * * All vertex attributes will be updated. + * * The order of facets are unchanged. + * * If two vertices are mapped to the same index, they will be merged. + * * `forward_mapping` must be surjective. + * * Edge information will be recomputed if present. + * + * @see permute_vertices for simply permuting the vertex order. + */ +template +void remap_vertices( + SurfaceMesh& mesh, + span forward_mapping, + RemapVerticesOptions options = {}); + +/// @} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/reorder_mesh_vertices.h b/modules/core/include/lagrange/reorder_mesh_vertices.h index 7bb2d761..de170d65 100644 --- a/modules/core/include/lagrange/reorder_mesh_vertices.h +++ b/modules/core/include/lagrange/reorder_mesh_vertices.h @@ -9,103 +9,10 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -namespace lagrange { - -/** - * Reorders (possibly with shrinking) the vertices in the mesh. - * - * Arguments: - * input_mesh - * forward_mapping: old2new mapping for the vertices in the mesh. - * forward_mapping[i] == INVALID or i -> vertex i will remain as is - * forward_mapping[i] = j, vertex i will be remapped to j - * if two vertices map to the same new index, they will be merged. - * ''Forward_mapping must be surjective'' - * - * Returns: - * output_mesh with the vertices merged. - * - * NOTES: - * All vertex and facet attributes are mapped from input to the output. - * The output_mesh's facets are the same as input_mesh's. - * The output_mesh's vertices are the same or a subset of the - * input_mesh's vertices. - * This is not a clean-up routine as-is. DEGENRATE facets can be present - * in the result. - */ -template -std::unique_ptr reorder_mesh_vertices( - const MeshType& mesh, - const typename MeshType::IndexList& forward_mapping) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - la_runtime_assert( - mesh.get_vertex_per_facet() == 3, - std::string("vertex per facet is ") + std::to_string(mesh.get_vertex_per_facet())); - - using Index = typename MeshType::Index; - using VertexArray = typename MeshType::VertexArray; - using FacetArray = typename MeshType::FacetArray; - using IndexList = typename MeshType::IndexList; - const auto num_old_vertices = mesh.get_num_vertices(); - const auto& vertices = mesh.get_vertices(); - const auto& facets = mesh.get_facets(); - la_runtime_assert(num_old_vertices == safe_cast(forward_mapping.size())); - - auto forward_mapping_no_invalid = [&forward_mapping](const Index io) { - return forward_mapping[io] == invalid() ? io : forward_mapping[io]; - }; - - // Cound the number of new vertices - Index num_new_vertices = 0; - for (const auto io : range(num_old_vertices)) { - num_new_vertices = std::max(num_new_vertices, forward_mapping_no_invalid(io)); - } - num_new_vertices += 1; // num_of_*** = biggest index + 1 - la_runtime_assert(num_new_vertices <= num_old_vertices, "Number of vertices should not increase"); - - - // Create the backward mapping and the new vertices - IndexList backward_mapping(num_new_vertices, invalid()); - VertexArray vertices_new(num_new_vertices, mesh.get_dim()); - for (const auto io : range(num_old_vertices)) { - const auto in = forward_mapping_no_invalid(io); - backward_mapping[in] = io; - vertices_new.row(in) = vertices.row(io); - } - // The mapping should be surjective. - la_runtime_assert( - std::find(backward_mapping.begin(), backward_mapping.end(), invalid()) == - backward_mapping.end(), - "Forward mapping is not surjective"); - - // Create new faces - FacetArray facets_new(facets.rows(), facets.cols()); - for (auto i : range(facets.rows())) { - facets_new(i, 0) = forward_mapping_no_invalid(facets(i, 0)); - facets_new(i, 1) = forward_mapping_no_invalid(facets(i, 1)); - facets_new(i, 2) = forward_mapping_no_invalid(facets(i, 2)); - } - - // Create new mesh - auto mesh2 = create_mesh(std::move(vertices_new), std::move(facets_new)); - - // Port attributes - map_attributes(mesh, *mesh2, backward_mapping); - - return mesh2; -} +#pragma once -} // namespace lagrange +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + // Please use remap_vertices.h instead. + #include +#endif diff --git a/modules/core/include/lagrange/segment_segment_squared_distance.h b/modules/core/include/lagrange/segment_segment_squared_distance.h new file mode 100644 index 00000000..f81ba7d3 --- /dev/null +++ b/modules/core/include/lagrange/segment_segment_squared_distance.h @@ -0,0 +1,139 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +namespace lagrange { + +/// +/// Computes the squared distance between two N-d line segments, and the closest pair of points +/// whose separation is this distance. +/// +/// @param[in] U0 first extremity of the first segment +/// @param[in] U1 second extremity of the first segment +/// @param[in] V0 first extremity of the second segment +/// @param[in] V1 second extremity of the second segment +/// @param[out] closest_pointU the closest point on segment [@p U0, @p U1] +/// @param[out] closest_pointV the closest point on segment [@p V0, @p V1] +/// @param[out] lambdaU barycentric coordinate of the closest point relative to [@p U0, U1] +/// @param[out] lambdaV barycentric coordinate of the closest point relative to [@p V0, V1] +/// +/// @tparam PointType the class that represents the points. +/// +/// @return the squared distance between the segments [@p U0, @p U1] and [@p V0, @p V1] +/// +/// @note Adapted from Real-Time Collision Detection by Christer Ericson, published by Morgan +/// Kaufmann Publishers, (c) 2005 Elsevier Inc. This function was modified for use by Siddhartha +/// Chaudhuri in the Thea library, from where this version was taken on 28 Sep 2022. All +/// modifications beyond Thea are Copyright 2022 Adobe. +/// +template +auto segment_segment_squared_distance( + const Eigen::MatrixBase& U0, + const Eigen::MatrixBase& U1, + const Eigen::MatrixBase& V0, + const Eigen::MatrixBase& V1, + Eigen::PlainObjectBase& closest_pointU, + Eigen::PlainObjectBase& closest_pointV, + ScalarOf& lambdaU, + ScalarOf& lambdaV) -> ScalarOf +{ + using Scalar = ScalarOf; + using Vector = typename PointType::PlainObject; + + constexpr Scalar ZERO = static_cast(0); + constexpr Scalar ONE = static_cast(1); + constexpr Scalar EPS = std::numeric_limits::epsilon(); // is this the best choice? + + // Expose these in the function signature if we need to support unbounded lines as well + constexpr bool is_lineU = false; + constexpr bool is_lineV = false; + + Vector d1 = U1 - U0; // Direction vector of segment U + Vector d2 = V1 - V0; // Direction vector of segment V + Vector r = U0 - V0; + Scalar a = d1.squaredNorm(); // Squared length of segment U, always nonnegative + Scalar e = d2.squaredNorm(); // Squared length of segment V, always nonnegative + Scalar f = d2.dot(r); + + // Check if either or both segments degenerate into points + if (std::abs(a) < EPS) { + if (std::abs(e) < EPS) { + // Both segments degenerate into points + lambdaU = lambdaV = 0; + closest_pointU = U0; + closest_pointV = V0; + return (closest_pointU - closest_pointV).dot(closest_pointU - closest_pointV); + } else { + // First segment degenerates into a point + lambdaU = 0; + lambdaV = f / e; // lambdaU = 0 => lambdaV = (b*lambdaU + f) / e = f / e + + if (!is_lineV) lambdaV = std::clamp(lambdaV, ZERO, ONE); + } + } else { + Scalar c = d1.dot(r); + if (std::abs(e) < EPS) { + // Second segment degenerates into a point + lambdaV = 0; + lambdaU = -c / a; + + if (!is_lineU) // lambdaV = 0 => lambdaU = (b*lambdaV - c) / a = -c / a + lambdaU = std::clamp(lambdaU, ZERO, ONE); + } else { + // The general nondegenerate case starts here + Scalar b = d1.dot(d2); + Scalar denom = a * e - b * b; // Always nonnegative + + // If segments not parallel, compute closest point on L1 to L2, and clamp to segment U. + // Else pick arbitrary lambdaU (here 0) + if (std::abs(denom) >= EPS) { + lambdaU = (b * f - c * e) / denom; + + if (!is_lineU) lambdaU = std::clamp(lambdaU, ZERO, ONE); + } else + lambdaU = 0; + + // Compute point on L2 closest to U(lambdaU) using: + // lambdaV = Dot((P1+D1*lambdaU)-P2,D2) / Dot(D2,D2) = (b*lambdaU + f) / e + lambdaV = (b * lambdaU + f) / e; + + if (!is_lineV) { + // If lambdaV in [0,1] done. Else clamp lambdaV, recompute lambdaU for the new value + // of lambdaV using: + // lambdaU = Dot((P2+D2*lambdaV)-P1,D1) / Dot(D1,D1)= (lambdaV*b - c) / a + // and clamp lambdaU to [0, 1] + if (lambdaV < 0) { + lambdaV = 0; + lambdaU = -c / a; + + if (!is_lineU) lambdaU = std::clamp(lambdaU, ZERO, ONE); + } else if (lambdaV > 1) { + lambdaV = 1; + lambdaU = (b - c) / a; + + if (!is_lineU) lambdaU = std::clamp(lambdaU, ZERO, ONE); + } + } + } + } + + closest_pointU = U0 + lambdaU * d1; + closest_pointV = V0 + lambdaV * d2; + return (closest_pointU - closest_pointV).squaredNorm(); +} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/utils/DisjointSets.h b/modules/core/include/lagrange/utils/DisjointSets.h index b5b600b2..4a68758d 100644 --- a/modules/core/include/lagrange/utils/DisjointSets.h +++ b/modules/core/include/lagrange/utils/DisjointSets.h @@ -1,5 +1,5 @@ /* - * Copyright 2020 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/utils/StackSet.h b/modules/core/include/lagrange/utils/StackSet.h index d87701ec..64e9a4ff 100644 --- a/modules/core/include/lagrange/utils/StackSet.h +++ b/modules/core/include/lagrange/utils/StackSet.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/include/lagrange/utils/StackVector.h b/modules/core/include/lagrange/utils/StackVector.h index 4d8d287c..b0d0b788 100644 --- a/modules/core/include/lagrange/utils/StackVector.h +++ b/modules/core/include/lagrange/utils/StackVector.h @@ -144,7 +144,7 @@ struct StackVector template auto to_tuple() { - assert(D == m_size); + la_debug_assert(D == m_size); static_assert(D <= N, "Invalid size"); return to_tuple_helper(std::make_index_sequence()); } diff --git a/modules/core/include/lagrange/utils/hash.h b/modules/core/include/lagrange/utils/hash.h new file mode 100644 index 00000000..3217c6ce --- /dev/null +++ b/modules/core/include/lagrange/utils/hash.h @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +namespace lagrange { + +/// @addtogroup group-utils-misc +/// @{ + +/// Hash an object @a v and combine it with an existing hash value @a seed. NOT commutative. +/// +/// Copied from https://www.boost.org/doc/libs/1_64_0/boost/functional/hash/hash.hpp. +/// SPDX-License-Identifier: BSL-1.0 +template +void hash_combine(size_t& seed, const T& v) +{ + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + + (seed >> 2); // magic random number ensures spreading of hashes +} + +namespace detail { + +template +size_t ordered_pair_hash_value(const U& u, const V& v) +{ + size_t h = std::hash{}(u); + hash_combine(h, v); + return h; +} + +} // namespace detail + +/// Compute an order-dependent hash of a pair of values. The default template handles `std::array`, +/// `std::vector`, Eigen vectors, raw pointers/C-arrays, and other classes with an array indexing +/// `operator[]`. (Note that only the first two elements of the array will be hashed.) Other +/// specializations handle `std::pair` etc. +template +struct OrderedPairHash +{ + size_t operator()(const T& k) const { return detail::ordered_pair_hash_value(k[0], k[1]); } +}; + +template +struct OrderedPairHash> +{ + size_t operator()(const std::pair& k) const + { + return detail::ordered_pair_hash_value(k.first, k.second); + } +}; + +/// @} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/utils/point_on_segment.h b/modules/core/include/lagrange/utils/point_on_segment.h new file mode 100644 index 00000000..4cc559cb --- /dev/null +++ b/modules/core/include/lagrange/utils/point_on_segment.h @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +namespace lagrange { + +namespace internal { + +/// @internal +bool point_on_segment_2d(Eigen::Vector2d p, Eigen::Vector2d a, Eigen::Vector2d b); + +/// @internal +bool point_on_segment_3d(Eigen::Vector3d p, Eigen::Vector3d a, Eigen::Vector3d b); + +} + +/// +/// Test if a point lies exactly on a segment [a,b] using exact predicates. If the points are +/// collinear, each individual coordinate is examined to determine if the query point lies inside +/// the segment or outside of it. +/// +/// @param[in] p Query point. +/// @param[in] a First segment endpoint. +/// @param[in] b Second segment endpoint. +/// +/// @tparam PointType Point type. +/// +/// @return True if the query point lies exactly on the segment, False otherwise. +/// +template +bool point_on_segment( + const Eigen::MatrixBase& p, + const Eigen::MatrixBase& a, + const Eigen::MatrixBase& b) +{ + if (p.size() == 2 && a.size() == 2 && b.size() == 2) { + Eigen::Vector2d p2d(static_cast(p.x()), static_cast(p.y())); + Eigen::Vector2d a2d(static_cast(a.x()), static_cast(a.y())); + Eigen::Vector2d b2d(static_cast(b.x()), static_cast(b.y())); + return internal::point_on_segment_2d(p2d, a2d, b2d); + } else if (p.size() == 3 && a.size() == 3 && b.size() == 3) { + Eigen::Vector3d p3d( + static_cast(p[0]), + static_cast(p[1]), + static_cast(p[2])); + Eigen::Vector3d a3d( + static_cast(a[0]), + static_cast(a[1]), + static_cast(a[2])); + Eigen::Vector3d b3d( + static_cast(b[0]), + static_cast(b[1]), + static_cast(b[2])); + return internal::point_on_segment_3d(p3d, a3d, b3d); + } else { + throw std::runtime_error("Incompatible types"); + } +} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/utils/point_segment_squared_distance.h b/modules/core/include/lagrange/utils/point_segment_squared_distance.h new file mode 100644 index 00000000..38180b34 --- /dev/null +++ b/modules/core/include/lagrange/utils/point_segment_squared_distance.h @@ -0,0 +1,125 @@ +// Source: https://github.com/alicevision/geogram/blob/master/src/lib/geogram/basic/geometry_nd.h +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright (c) 2012-2014, Bruno Levy +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the ALICE Project-Team nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// If you modify this software, you should include a notice giving the +// name of the person performing the modification, the date of modification, +// and the reason for such modification. +// +// Contact: Bruno Levy +// +// Bruno.Levy@inria.fr +// http://www.loria.fr/~levy +// +// ALICE Project +// LORIA, INRIA Lorraine, +// Campus Scientifique, BP 239 +// 54506 VANDOEUVRE LES NANCY CEDEX +// FRANCE +// +// This file has been modified by Adobe. +// +// All modifications are Copyright 2020 Adobe. +// +#pragma once + +#include + +#include + +namespace lagrange { + +/// +/// Computes the point closest to a given point in a nd segment +/// +/// @param[in] point the query point +/// @param[in] V0 first extremity of the segment +/// @param[in] V1 second extremity of the segment +/// @param[out] closest_point the point closest to @p point in the segment [\p V0, @p V1] +/// @param[out] lambda0 barycentric coordinate of the closest point relative to @p V0 +/// @param[out] lambda1 barycentric coordinate of the closest point relative to @p V1 +/// +/// @tparam PointType the class that represents the points. +/// +/// @return the squared distance between the point and the segment [\p V0, @p V1] +/// +template +auto point_segment_squared_distance( + const Eigen::MatrixBase& point, + const Eigen::MatrixBase& V0, + const Eigen::MatrixBase& V1, + Eigen::PlainObjectBase& closest_point, + ScalarOf& lambda0, + ScalarOf& lambda1) -> ScalarOf +{ + using Scalar = ScalarOf; + + auto l2 = (V0 - V1).squaredNorm(); + auto t = (point - V0).dot(V1 - V0); + if (t <= Scalar(0) || l2 == Scalar(0)) { + closest_point = V0; + lambda0 = Scalar(1); + lambda1 = Scalar(0); + return (point - V0).squaredNorm(); + } else if (t > l2) { + closest_point = V1; + lambda0 = Scalar(0); + lambda1 = Scalar(1); + return (point - V1).squaredNorm(); + } + lambda1 = t / l2; + lambda0 = Scalar(1) - lambda1; + closest_point = lambda0 * V0 + lambda1 * V1; + return (point - closest_point).squaredNorm(); +} + +/// +/// Computes the point closest to a given point in a nd segment +/// +/// @param[in] point the query point +/// @param[in] V0 first extremity of the segment +/// @param[in] V1 second extremity of the segment +/// +/// @tparam PointType the class that represents the points. +/// +/// @return the squared distance between the point and the segment [\p V0, @p V1] +/// +template +auto point_segment_squared_distance( + const Eigen::MatrixBase& point, + const Eigen::MatrixBase& V0, + const Eigen::MatrixBase& V1) -> ScalarOf +{ + PointType closest_point; + ScalarOf lambda0; + ScalarOf lambda1; + return point_segment_squared_distance(point, V0, V1, closest_point, lambda0, lambda1); +} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/utils/point_triangle_squared_distance.h b/modules/core/include/lagrange/utils/point_triangle_squared_distance.h new file mode 100644 index 00000000..7247e675 --- /dev/null +++ b/modules/core/include/lagrange/utils/point_triangle_squared_distance.h @@ -0,0 +1,319 @@ +// Source: https://github.com/alicevision/geogram/blob/master/src/lib/geogram/basic/geometry_nd.h +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright (c) 2012-2014, Bruno Levy +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the ALICE Project-Team nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// If you modify this software, you should include a notice giving the +// name of the person performing the modification, the date of modification, +// and the reason for such modification. +// +// Contact: Bruno Levy +// +// Bruno.Levy@inria.fr +// http://www.loria.fr/~levy +// +// ALICE Project +// LORIA, INRIA Lorraine, +// Campus Scientifique, BP 239 +// 54506 VANDOEUVRE LES NANCY CEDEX +// FRANCE +// +// This file has been modified by Adobe. +// +// All modifications are Copyright 2020 Adobe. +// +#pragma once + +#include +#include + +#include + +namespace lagrange { + +/// +/// Computes the point closest to a given point in a nd triangle. See +/// http://www.geometrictools.com/LibMathematics/Distance/Distance.html +/// +/// @param[in] point the query point +/// @param[in] V0 first vertex of the triangle +/// @param[in] V1 second vertex of the triangle +/// @param[in] V2 third vertex of the triangle +/// @param[out] closest_point the point closest to @p point in the triangle (\p V0, @p V1, @p V2) +/// @param[out] lambda0 barycentric coordinate of the closest point relative to @p V0 +/// @param[out] lambda1 barycentric coordinate of the closest point relative to @p V1 +/// @param[out] lambda2 barycentric coordinate of the closest point relative to @p V2 +/// +/// @tparam PointType the class that represents the points. +/// +/// @return the squared distance between the point and the triangle (\p V0, @p V1, @p V2) +/// +template +auto point_triangle_squared_distance( + const Eigen::MatrixBase& point, + const Eigen::MatrixBase& V0, + const Eigen::MatrixBase& V1, + const Eigen::MatrixBase& V2, + Eigen::PlainObjectBase& closest_point, + ScalarOf& lambda0, + ScalarOf& lambda1, + ScalarOf& lambda2) -> ScalarOf +{ + using Scalar = ScalarOf; + + Eigen::Vector3d diff = V0.template cast() - point.template cast(); + Eigen::Vector3d edge0 = V1.template cast() - V0.template cast(); + Eigen::Vector3d edge1 = V2.template cast() - V0.template cast(); + double a00 = edge0.squaredNorm(); + double a01 = edge0.dot(edge1); + double a11 = edge1.squaredNorm(); + double b0 = diff.dot(edge0); + double b1 = diff.dot(edge1); + double c = diff.squaredNorm(); + double det = std::fabs(a00 * a11 - a01 * a01); + double s = a01 * b1 - a11 * b0; + double t = a01 * b0 - a00 * b1; + double sqr_distance; + + // If the triangle is degenerate + if (det < 1e-30) { + Scalar cur_l1, cur_l2; + PointType cur_closest; + Scalar result; + Scalar cur_dist = + point_segment_squared_distance(point, V0, V1, cur_closest, cur_l1, cur_l2); + result = cur_dist; + closest_point = cur_closest; + lambda0 = cur_l1; + lambda1 = cur_l2; + lambda2 = 0; + cur_dist = point_segment_squared_distance(point, V0, V2, cur_closest, cur_l1, cur_l2); + if (cur_dist < result) { + result = cur_dist; + closest_point = cur_closest; + lambda0 = cur_l1; + lambda2 = cur_l2; + lambda1 = Scalar(0); + } + cur_dist = point_segment_squared_distance(point, V1, V2, cur_closest, cur_l1, cur_l2); + if (cur_dist < result) { + result = cur_dist; + closest_point = cur_closest; + lambda1 = cur_l1; + lambda2 = cur_l2; + lambda0 = Scalar(0); + } + return result; + } + + if (s + t <= det) { + if (s < 0) { + if (t < 0) { // region 4 + if (b0 < 0) { + t = 0; + if (-b0 >= a00) { + s = 1; + sqr_distance = a00 + 2 * b0 + c; + } else { + s = -b0 / a00; + sqr_distance = b0 * s + c; + } + } else { + s = 0; + if (b1 >= 0) { + t = 0; + sqr_distance = c; + } else if (-b1 >= a11) { + t = 1; + sqr_distance = a11 + 2 * b1 + c; + } else { + t = -b1 / a11; + sqr_distance = b1 * t + c; + } + } + } else { // region 3 + s = 0; + if (b1 >= 0) { + t = 0; + sqr_distance = c; + } else if (-b1 >= a11) { + t = 1; + sqr_distance = a11 + 2 * b1 + c; + } else { + t = -b1 / a11; + sqr_distance = b1 * t + c; + } + } + } else if (t < 0) { // region 5 + t = 0; + if (b0 >= 0) { + s = 0; + sqr_distance = c; + } else if (-b0 >= a00) { + s = 1; + sqr_distance = a00 + 2 * b0 + c; + } else { + s = -b0 / a00; + sqr_distance = b0 * s + c; + } + } else { // region 0 + // minimum at interior point + double inv_det = double(1) / det; + s *= inv_det; + t *= inv_det; + sqr_distance = s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; + } + } else { + double tmp0, tmp1, numer, denom; + + if (s < 0) { // region 2 + tmp0 = a01 + b0; + tmp1 = a11 + b1; + if (tmp1 > tmp0) { + numer = tmp1 - tmp0; + denom = a00 - 2 * a01 + a11; + if (numer >= denom) { + s = 1; + t = 0; + sqr_distance = a00 + 2 * b0 + c; + } else { + s = numer / denom; + t = 1 - s; + sqr_distance = + s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; + } + } else { + s = 0; + if (tmp1 <= 0) { + t = 1; + sqr_distance = a11 + 2 * b1 + c; + } else if (b1 >= 0) { + t = 0; + sqr_distance = c; + } else { + t = -b1 / a11; + sqr_distance = b1 * t + c; + } + } + } else if (t < 0) { // region 6 + tmp0 = a01 + b1; + tmp1 = a00 + b0; + if (tmp1 > tmp0) { + numer = tmp1 - tmp0; + denom = a00 - 2 * a01 + a11; + if (numer >= denom) { + t = 1; + s = 0; + sqr_distance = a11 + 2 * b1 + c; + } else { + t = numer / denom; + s = 1 - t; + sqr_distance = + s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; + } + } else { + t = 0; + if (tmp1 <= 0) { + s = 1; + sqr_distance = a00 + 2 * b0 + c; + } else if (b0 >= 0) { + s = 0; + sqr_distance = c; + } else { + s = -b0 / a00; + sqr_distance = b0 * s + c; + } + } + } else { // region 1 + numer = a11 + b1 - a01 - b0; + if (numer <= 0) { + s = 0; + t = 1; + sqr_distance = a11 + 2 * b1 + c; + } else { + denom = a00 - 2 * a01 + a11; + if (numer >= denom) { + s = 1; + t = 0; + sqr_distance = a00 + 2 * b0 + c; + } else { + s = numer / denom; + t = 1 - s; + sqr_distance = + s * (a00 * s + a01 * t + 2 * b0) + t * (a01 * s + a11 * t + 2 * b1) + c; + } + } + } + } + + // Account for numerical round-off error. + if (sqr_distance < 0) { + sqr_distance = 0; + } + + closest_point = (V0.template cast() + s * edge0 + t * edge1).template cast(); + lambda0 = Scalar(1 - s - t); + lambda1 = Scalar(s); + lambda2 = Scalar(t); + return Scalar(sqr_distance); +} + +/// +/// @brief Computes the squared distance between a point and a nd triangle. +/// @details See http://www.geometrictools.com/LibMathematics/Distance/Distance.html +/// +/// @param[in] point the query point +/// @param[in] V0 first vertex of the triangle +/// @param[in] V1 second vertex of the triangle +/// @param[in] V2 third vertex of the triangle +/// +/// @tparam PointType the class that represents the points. +/// +/// @return the squared distance between the point and the triangle (\p V0, @p V1, @p V2) +/// +template +auto point_triangle_squared_distance( + const Eigen::MatrixBase& point, + const Eigen::MatrixBase& V0, + const Eigen::MatrixBase& V1, + const Eigen::MatrixBase& V2) -> ScalarOf +{ + PointType closest_point; + ScalarOf lambda1, lambda2, lambda3; + return point_triangle_squared_distance( + point, + V0, + V1, + V2, + closest_point, + lambda1, + lambda2, + lambda3); +} + +} // namespace lagrange diff --git a/modules/core/include/lagrange/utils/warnoff.h b/modules/core/include/lagrange/utils/warnoff.h index 931909bc..a1866a62 100644 --- a/modules/core/include/lagrange/utils/warnoff.h +++ b/modules/core/include/lagrange/utils/warnoff.h @@ -38,6 +38,7 @@ #pragma clang diagnostic ignored "-Wextra-semi" #pragma clang diagnostic ignored "-Wunused-lambda-capture" #pragma clang diagnostic ignored "-Wunused-variable" +#pragma clang diagnostic ignored "-Wunused-but-set-variable" #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-local-typedefs" diff --git a/modules/core/include/lagrange/views.h b/modules/core/include/lagrange/views.h index c9b8889e..ef3d0f98 100644 --- a/modules/core/include/lagrange/views.h +++ b/modules/core/include/lagrange/views.h @@ -72,7 +72,7 @@ using RowMatrixView = Eigen::Map, Eigen::Unaligned>; /// Type alias for row-major const matrix view. template -using ConstRowMatrixView = Eigen::Map, Eigen::Unaligned>; +using ConstRowMatrixView = const Eigen::Map, Eigen::Unaligned>; /// Type alias for one-dimensional column Eigen vectors. template @@ -84,7 +84,7 @@ using VectorView = Eigen::Map, Eigen::Unaligned>; /// Type alias for row-major const vector view. template -using ConstVectorView = Eigen::Map, Eigen::Unaligned>; +using ConstVectorView = const Eigen::Map, Eigen::Unaligned>; //////////////////////////////////////////////////////////////////////////////// /// @} diff --git a/modules/core/performance/marquee.cpp b/modules/core/performance/marquee.cpp index 24edc5d0..62d78634 100644 --- a/modules/core/performance/marquee.cpp +++ b/modules/core/performance/marquee.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include diff --git a/modules/core/python/include/lagrange/python/utils/StackVector.h b/modules/core/python/include/lagrange/python/utils/StackVector.h new file mode 100644 index 00000000..0efb95b3 --- /dev/null +++ b/modules/core/python/include/lagrange/python/utils/StackVector.h @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +// clang-format off +#include +#include +#include +#include +// clang-format on + +NAMESPACE_BEGIN(NB_NAMESPACE) +NAMESPACE_BEGIN(detail) + +/// +/// Type caster to map `lagrange::StackVector` to python list data structure. +/// +/// @note Data are __copied__ between the two data structures. +/// +template struct type_caster> + : list_caster, T> { }; + +NAMESPACE_END(detail) +NAMESPACE_END(NB_NAMESPACE) diff --git a/modules/core/python/src/bind_surface_mesh.h b/modules/core/python/src/bind_surface_mesh.h index 3c63de4b..653754cc 100644 --- a/modules/core/python/src/bind_surface_mesh.h +++ b/modules/core/python/src/bind_surface_mesh.h @@ -61,7 +61,7 @@ void bind_surface_mesh(nanobind::module_& m) auto [data, shape, stride] = tensor_to_span(b); la_runtime_assert(is_dense(shape, stride)); la_runtime_assert(check_shape(shape, invalid(), self.get_dimension())); - self.add_vertices(shape[0], data); + self.add_vertices(static_cast(shape[0]), data); }); // TODO: combine all add facet functions into `add_facets`. surface_mesh_class.def("add_triangle", &MeshType::add_triangle); @@ -69,14 +69,14 @@ void bind_surface_mesh(nanobind::module_& m) auto [data, shape, stride] = tensor_to_span(b); la_runtime_assert(is_dense(shape, stride)); la_runtime_assert(check_shape(shape, invalid(), 3)); - self.add_triangles(shape[0], data); + self.add_triangles(static_cast(shape[0]), data); }); surface_mesh_class.def("add_quad", &MeshType::add_quad); surface_mesh_class.def("add_quads", [](MeshType& self, Tensor b) { auto [data, shape, stride] = tensor_to_span(b); la_runtime_assert(is_dense(shape, stride)); la_runtime_assert(check_shape(shape, invalid(), 4)); - self.add_quads(shape[0], data); + self.add_quads(static_cast(shape[0]), data); }); surface_mesh_class.def("add_polygon", [](MeshType& self, Tensor b) { auto [data, shape, stride] = tensor_to_span(b); @@ -87,7 +87,7 @@ void bind_surface_mesh(nanobind::module_& m) surface_mesh_class.def("add_polygons", [](MeshType& self, Tensor b) { auto [data, shape, stride] = tensor_to_span(b); la_runtime_assert(is_dense(shape, stride)); - self.add_polygons(shape[0], shape[1], data); + self.add_polygons(static_cast(shape[0]), static_cast(shape[1]), data); }); surface_mesh_class.def( "add_hybrid", @@ -162,7 +162,8 @@ void bind_surface_mesh(nanobind::module_& m) auto [index_data, index_shape, index_stride] = tensor_to_span(initial_indices); la_runtime_assert(is_dense(value_shape, value_stride)); la_runtime_assert(is_dense(index_shape, index_stride)); - Index num_channels = value_shape.size() == 1 ? 1 : value_shape[1]; + Index num_channels = + value_shape.size() == 1 ? 1 : static_cast(value_shape[1]); return self.template create_attribute( name, element, @@ -198,7 +199,7 @@ void bind_surface_mesh(nanobind::module_& m) using ValueType = typename std::decay_t::Scalar; auto [data, shape, stride] = tensor_to_span(tensor); la_runtime_assert(is_dense(shape, stride)); - Index num_channels = shape.size() == 1 ? 1 : shape[1]; + Index num_channels = shape.size() == 1 ? 1 : static_cast(shape[1]); AttributeId id; auto owner = std::make_shared(nb::cast(values)); if constexpr (std::is_const_v) { @@ -247,8 +248,9 @@ void bind_surface_mesh(nanobind::module_& m) auto [index_data, index_shape, index_stride] = tensor_to_span(index_tensor); la_runtime_assert(is_dense(value_shape, value_stride)); la_runtime_assert(is_dense(index_shape, index_stride)); - Index num_values = value_shape[0]; - Index num_channels = value_shape.size() == 1 ? 1 : value_shape[1]; + Index num_values = static_cast(value_shape[0]); + Index num_channels = + value_shape.size() == 1 ? 1 : static_cast(value_shape[1]); AttributeId id; auto value_owner = std::make_shared(nb::cast(values)); @@ -359,7 +361,7 @@ void bind_surface_mesh(nanobind::module_& m) auto owner = std::make_shared(nb::cast(tensor)); auto id = self.wrap_as_vertices( make_shared_span(owner, values.data(), values.size()), - num_vertices); + static_cast(num_vertices)); auto& attr = self.template ref_attribute(id); attr.set_growth_policy(AttributeGrowthPolicy::WarnAndCopy); }); @@ -387,8 +389,8 @@ void bind_surface_mesh(nanobind::module_& m) auto owner = std::make_shared(nb::cast(tensor)); auto id = self.wrap_as_facets( make_shared_span(owner, values.data(), values.size()), - num_facets, - vertex_per_facet); + static_cast(num_facets), + static_cast(vertex_per_facet)); auto& attr = self.template ref_attribute(id); attr.set_growth_policy(AttributeGrowthPolicy::WarnAndCopy); }); diff --git a/modules/core/python/src/bind_utilities.h b/modules/core/python/src/bind_utilities.h index 831ef827..675316b2 100644 --- a/modules/core/python/src/bind_utilities.h +++ b/modules/core/python/src/bind_utilities.h @@ -13,7 +13,9 @@ #include #include +#include #include +#include #include #include #include @@ -21,9 +23,13 @@ #include #include #include +#include #include +#include +#include #include #include +#include // clang-format off #include @@ -229,6 +235,80 @@ void bind_utilities(nanobind::module_& m) "mesh"_a, "name"_a, "new_element"_a); + nb::class_(m, "FacetAreaOptions") + .def(nb::init<>()) + .def_readwrite("output_attribute_name", &FacetAreaOptions::output_attribute_name); + m.def( + "compute_facet_area", + &lagrange::compute_facet_area, + "mesh"_a, + "options"_a = FacetAreaOptions()); + nb::class_(m, "MeshAreaOptions") + .def(nb::init<>()) + .def_readwrite("input_attribute_name", &MeshAreaOptions::input_attribute_name); + m.def( + "compute_mesh_area", + &lagrange::compute_mesh_area, + "mesh"_a, + "options"_a = MeshAreaOptions()); + nb::class_(m, "FacetCentroidOptions") + .def(nb::init<>()) + .def_readwrite("output_attribute_name", &FacetCentroidOptions::output_attribute_name); + m.def( + "compute_facet_centroid", + &lagrange::compute_facet_centroid, + "mesh"_a, + "options"_a = FacetCentroidOptions()); + nb::enum_(m, "CentroidWeightingType") + .value("Uniform", MeshCentroidOptions::Uniform) + .value("Area", MeshCentroidOptions::Area); + nb::class_(m, "MeshCentroidOptions") + .def(nb::init<>()) + .def_readwrite("weighting_type", &MeshCentroidOptions::weighting_type) + .def_readwrite( + "facet_centroid_attribute_name", + &MeshCentroidOptions::facet_centroid_attribute_name) + .def_readwrite( + "facet_area_attribute_name", + &MeshCentroidOptions::facet_area_attribute_name); + m.def( + "compute_mesh_centroid", + [](const SurfaceMesh& mesh, MeshCentroidOptions opt) { + const Index dim = mesh.get_dimension(); + std::vector centroid(dim, invalid()); + compute_mesh_centroid(mesh, centroid, opt); + return centroid; + }, + "mesh"_a, + "options"_a = MeshCentroidOptions()); + m.def( + "permute_vertices", + [](MeshType& mesh, Tensor new_to_old) { + auto [data, shape, stride] = tensor_to_span(new_to_old); + la_runtime_assert(is_dense(shape, stride)); + permute_vertices(mesh, data); + }, + "mesh"_a, + "new_to_old"_a); + + nb::enum_(m, "CollisionPolicy") + .value("Average", RemapVerticesOptions::CollisionPolicy::Average) + .value("KeepFirst", RemapVerticesOptions::CollisionPolicy::KeepFirst) + .value("Error", RemapVerticesOptions::CollisionPolicy::Error); + nb::class_(m, "RemapVerticesOptions") + .def(nb::init<>()) + .def_readwrite("collision_policy_float", &RemapVerticesOptions::collision_policy_float) + .def_readwrite("collision_policy_integral", &RemapVerticesOptions::collision_policy_integral); + m.def( + "remap_vertices", + [](MeshType& mesh, Tensor old_to_new, RemapVerticesOptions opt) { + auto [data, shape, stride] = tensor_to_span(old_to_new); + la_runtime_assert(is_dense(shape, stride)); + remap_vertices(mesh, data, opt); + }, + "mesh"_a, + "old_to_new"_a, + "options"_a = RemapVerticesOptions()); } } // namespace lagrange::python diff --git a/modules/core/python/src/core.cpp b/modules/core/python/src/core.cpp index 38c0b330..4b8b8c5f 100644 --- a/modules/core/python/src/core.cpp +++ b/modules/core/python/src/core.cpp @@ -33,6 +33,9 @@ void populate_core_module(nb::module_& m) using Scalar = double; using Index = uint32_t; + m.attr("invalid_scalar") = lagrange::invalid(); + m.attr("invalid_index") = nb::int_(lagrange::invalid()); + lagrange::python::bind_enum(m); lagrange::python::bind_surface_mesh(m); lagrange::python::bind_attribute(m); diff --git a/modules/core/python/tests/assets.py b/modules/core/python/tests/assets.py index 49a65c21..4af0cd68 100644 --- a/modules/core/python/tests/assets.py +++ b/modules/core/python/tests/assets.py @@ -126,9 +126,4 @@ def cube_with_uv(cube): ] ), ) - uv_attr = mesh.indexed_attribute(id) - #uv_attr.values.create_internal_copy() - #uv_attr.indices.create_internal_copy() - #uv_attr.values.growth_policy = lagrange.AttributeGrowthPolicy.WarnAndCopy - #uv_attr.indices.growth_policy = lagrange.AttributeGrowthPolicy.WarnAndCopy return mesh diff --git a/modules/core/python/tests/test_compute_centroid.py b/modules/core/python/tests/test_compute_centroid.py new file mode 100644 index 00000000..af2b0e0d --- /dev/null +++ b/modules/core/python/tests/test_compute_centroid.py @@ -0,0 +1,35 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import numpy as np +from numpy.linalg import norm +import math +import pytest + +from .assets import single_triangle, cube + +class TestComputeCentroid: + def test_cube(self, cube): + mesh = cube + c = lagrange.compute_mesh_centroid(mesh) + assert np.all(c == [0.5, 0.5, 0.5]) + + def test_triangle(self, single_triangle): + mesh = single_triangle + c = lagrange.compute_mesh_centroid(mesh) + assert np.all(c == [1/3, 1/3, 1/3]) + + def test_empty_mesh(self): + mesh = lagrange.SurfaceMesh() + with pytest.raises(Exception): + c = lagrange.compute_mesh_centroid(mesh) diff --git a/modules/core/python/tests/test_compute_facet_area.py b/modules/core/python/tests/test_compute_facet_area.py new file mode 100644 index 00000000..8ce164bc --- /dev/null +++ b/modules/core/python/tests/test_compute_facet_area.py @@ -0,0 +1,71 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import numpy as np +from numpy.linalg import norm +import math +import pytest + +from .assets import single_triangle, cube + + +class TestComputeFacetArea: + def test_cube(self, cube): + mesh = cube + opt = lagrange.FacetAreaOptions() + attr_id = lagrange.compute_facet_area(mesh, options=opt) + assert mesh.get_attribute_name(attr_id) == opt.output_attribute_name + + area_attr = mesh.attribute(attr_id) + assert area_attr.num_elements == 6 + assert area_attr.num_channels == 1 + assert np.all(area_attr.data == pytest.approx(1)) + + def test_triangle(self, single_triangle): + mesh = single_triangle + opt = lagrange.FacetAreaOptions() + attr_id = lagrange.compute_facet_area(mesh, options=opt) + assert mesh.get_attribute_name(attr_id) == opt.output_attribute_name + + area_attr = mesh.attribute(attr_id) + assert area_attr.num_elements == 1 + assert area_attr.num_channels == 1 + assert area_attr.data == pytest.approx(math.sqrt(3) / 2) + + def test_degenerate_triangle(self): + mesh = lagrange.SurfaceMesh() + mesh.add_vertex([0, 0, 0]) + mesh.add_vertex([1, 1, 1]) + mesh.add_vertex([2, 2, 2]) + mesh.add_triangle(0, 1, 2) + + attr_id = lagrange.compute_facet_area(mesh) + area_attr = mesh.attribute(attr_id) + assert area_attr.num_elements == 1 + assert area_attr.num_channels == 1 + assert area_attr.data == pytest.approx(0) + + def test_2D_triangle(self): + mesh = lagrange.SurfaceMesh(2) + mesh.add_vertex([0, 0]) + mesh.add_vertex([0, 1]) + mesh.add_vertex([1, 0]) + mesh.add_triangle(0, 1, 2) + mesh.add_triangle(0, 2, 1) + + attr_id = lagrange.compute_facet_area(mesh) + area_attr = mesh.attribute(attr_id) + assert area_attr.num_elements == 2 + assert area_attr.num_channels == 1 + assert area_attr.data[0] == pytest.approx(-0.5) + assert area_attr.data[1] == pytest.approx(0.5) diff --git a/modules/core/python/tests/test_permute_vertices.py b/modules/core/python/tests/test_permute_vertices.py new file mode 100644 index 00000000..33daec90 --- /dev/null +++ b/modules/core/python/tests/test_permute_vertices.py @@ -0,0 +1,74 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import numpy as np +import pytest + +from .assets import cube, cube_with_uv + + +class TestPermuteVertices: + def test_cube(self, cube): + mesh = cube + old_vertices = np.copy(mesh.vertices) + + order = np.arange(0, mesh.num_vertices) + lagrange.permute_vertices(mesh, order) + assert np.all(mesh.vertices == old_vertices) + + order = np.flip(order) + lagrange.permute_vertices(mesh, order) + assert np.all(mesh.vertices == np.flip(old_vertices, axis=0)) + + def test_invalid_permutation(self, cube): + mesh = cube + order = np.arange(0, mesh.num_vertices) + order[-1] = 0 + with pytest.raises(Exception): + lagrange.permute_vertices(mesh, order) + + def test_with_uv(self, cube_with_uv): + mesh = cube_with_uv + mesh.initialize_edges() + mesh.create_attribute( + name="corner_index", + element=lagrange.AttributeElement.Corner, + usage=lagrange.AttributeUsage.Scalar, + initial_values=np.arange(0, mesh.num_corners, dtype=np.intc), + initial_indices=np.array([], dtype=np.uint32), + ) + assert mesh.has_attribute("uv") + assert mesh.has_attribute("corner_index") + uv_attr = mesh.indexed_attribute("uv") + uv_coords = np.copy(uv_attr.values.data) + uv_indices = np.copy(uv_attr.indices.data) + + order = np.arange(0, mesh.num_vertices) + order = np.flip(order) + lagrange.permute_vertices(mesh, order) + + # UV should be unchanged + assert mesh.has_edges + uv_attr = mesh.indexed_attribute("uv") + assert np.all(uv_attr.values.data == uv_coords) + assert np.all(uv_attr.indices.data == uv_indices) + + # Corner index should be unchnaged. + corner_index_attr = mesh.attribute("corner_index") + assert np.all(corner_index_attr.data == np.arange(mesh.num_corners, dtype=np.intc)) + + for i in range(mesh.num_vertices): + ci = mesh.get_first_corner_around_vertex(i) + while ci < mesh.num_corners: + assert mesh.get_corner_vertex(ci) == i + ci = mesh.get_next_corner_around_vertex(ci) diff --git a/modules/core/python/tests/test_remap_vertices.py b/modules/core/python/tests/test_remap_vertices.py new file mode 100644 index 00000000..5e34497a --- /dev/null +++ b/modules/core/python/tests/test_remap_vertices.py @@ -0,0 +1,92 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import numpy as np +import pytest + +from .assets import cube, cube_with_uv + + +class TestRemapVertices: + def test_cube(self, cube): + mesh = cube + mapping = np.zeros(mesh.num_vertices, dtype=np.intc) + lagrange.remap_vertices(mesh, mapping) + assert mesh.num_vertices == 1 + assert mesh.num_facets == 6 + + def test_identity(self, cube): + mesh = cube + old_vertices = np.copy(mesh.vertices) + mapping = np.arange(mesh.num_vertices, dtype=np.intc) + lagrange.remap_vertices(mesh, mapping) + assert mesh.num_vertices == 8 + assert mesh.num_facets == 6 + assert np.all(old_vertices == mesh.vertices) + + def test_reverse(self, cube): + mesh = cube + old_vertices = np.copy(mesh.vertices) + mapping = np.flip(np.arange(mesh.num_vertices, dtype=np.intc)) + lagrange.remap_vertices(mesh, mapping) + assert mesh.num_vertices == 8 + assert mesh.num_facets == 6 + assert np.all(old_vertices == np.flip(mesh.vertices, axis=0)) + + def test_invalid_mapping(self, cube): + mesh = cube + mapping = np.array([0, 0, 0, 0, 1, 1, 1, 3], dtype=np.intc) + with pytest.raises(Exception): + lagrange.remap_vertices(mesh, mapping) + + def test_with_uv(self, cube_with_uv): + mesh = cube_with_uv + assert mesh.has_attribute("uv") + uv_attr = mesh.indexed_attribute("uv") + uv_values = np.copy(uv_attr.values.data) + uv_indices = np.copy(uv_attr.indices.data) + + mapping = np.array([0, 0, 0, 0, 1, 2, 3, 4], dtype=np.intc) + lagrange.remap_vertices(mesh, mapping) + assert mesh.num_vertices == 5 + assert mesh.num_facets == 6 + + assert mesh.has_attribute("uv") + uv_attr = mesh.indexed_attribute("uv") + assert np.all(uv_attr.values.data == uv_values) + assert np.all(uv_attr.indices.data == uv_indices) + + def test_policy(self, cube): + mesh = cube + id = mesh.create_attribute( + "vertex_index", + lagrange.AttributeElement.Vertex, + lagrange.AttributeUsage.Scalar, + np.arange(8, dtype=np.uint32), + np.array([]) + ) + mapping = np.array([0, 0, 0, 0, 1, 2, 3, 4], dtype=np.intc) + options = lagrange.RemapVerticesOptions() + options.collision_policy_float = lagrange.CollisionPolicy.Average + options.collision_policy_integral = lagrange.CollisionPolicy.KeepFirst + + lagrange.remap_vertices(mesh, mapping) + id_attr = mesh.attribute(id) + assert np.all(mesh.vertices[0] == [0.5, 0.5, 0]) + assert id_attr.data[0] == 0 + + options.collision_policy_float = lagrange.CollisionPolicy.KeepFirst + + options.collision_policy_float = lagrange.CollisionPolicy.Error + with pytest.raises(Exception): + lagrange.remap_vertices(mesh, mapping) diff --git a/modules/core/src/SurfaceMesh.cpp b/modules/core/src/SurfaceMesh.cpp index 133dc511..81f35a4a 100644 --- a/modules/core/src/SurfaceMesh.cpp +++ b/modules/core/src/SurfaceMesh.cpp @@ -1183,6 +1183,7 @@ const AttributeBase& SurfaceMesh::get_attribute_base(std::string_ template const AttributeBase& SurfaceMesh::get_attribute_base(AttributeId id) const { + la_debug_assert(id != invalid_attribute_id()); return m_attributes->read_base(id); } @@ -1198,6 +1199,7 @@ template const Attribute& SurfaceMesh::get_attribute(AttributeId id) const { la_debug_assert(id != invalid_attribute_id()); + la_debug_assert(!is_attribute_indexed(id)); return m_attributes->template read(id); } @@ -1213,6 +1215,7 @@ internal::weak_ptr SurfaceMesh::_get_attribu AttributeId id) const { la_debug_assert(id != invalid_attribute_id()); + la_debug_assert(!is_attribute_indexed(id)); return m_attributes->_get_weak_ptr(id); } @@ -1229,6 +1232,8 @@ template auto SurfaceMesh::get_indexed_attribute(AttributeId id) const -> const IndexedAttribute& { + la_debug_assert(id != invalid_attribute_id()); + la_debug_assert(is_attribute_indexed(id)); return m_attributes->template read_indexed(id); } @@ -1244,6 +1249,7 @@ template Attribute& SurfaceMesh::ref_attribute(AttributeId id) { la_debug_assert(id != invalid_attribute_id()); + la_debug_assert(!is_attribute_indexed(id)); return m_attributes->template write(id); } @@ -1257,6 +1263,7 @@ template internal::weak_ptr SurfaceMesh::_ref_attribute_ptr(AttributeId id) { la_debug_assert(id != invalid_attribute_id()); + la_debug_assert(!is_attribute_indexed(id)); return m_attributes->_ref_weak_ptr(id); } @@ -1273,6 +1280,8 @@ template auto SurfaceMesh::ref_indexed_attribute(AttributeId id) -> IndexedAttribute& { + la_debug_assert(id != invalid_attribute_id()); + la_debug_assert(is_attribute_indexed(id)); return m_attributes->template write_indexed(id); } @@ -1817,9 +1826,30 @@ void SurfaceMesh::shrink_to_fit() template void SurfaceMesh::compress_if_regular() { - // TODO - // - If all facets have same size, and mesh has hybrid storage, delete facet_to_first_corner + - // corner_facet attributes. + if (is_regular()) { + // Nothing to do + return; + } + // Check whether all facets have the same size + Index nvpf = 0; + bool same_size = true; + for (Index f = 0; f < get_num_facets(); ++f) { + const Index nv = get_facet_size(f); + if (nvpf == 0) { + nvpf = nv; + } + if (nvpf != nv) { + same_size = false; + } + } + // If so, delete hybrid storage attributes + if (same_size) { + la_debug_assert(m_corner_to_facet_id != invalid_attribute_id()); + delete_attribute(s_facet_to_first_corner, AttributeDeletePolicy::Force); + delete_attribute(s_corner_to_facet, AttributeDeletePolicy::Force); + m_vertex_per_facet = nvpf; + } + la_debug_assert(is_regular()); } //////////////////////////////////////////////////////////////////////////////// diff --git a/modules/core/src/combine_meshes.cpp b/modules/core/src/combine_meshes.cpp index fc6bcb17..9b2a768f 100644 --- a/modules/core/src/combine_meshes.cpp +++ b/modules/core/src/combine_meshes.cpp @@ -42,7 +42,7 @@ bool validate_attribute_metadata( logger().warn("combine_meshes: attribute {} has inconsistent ValueType.", name); return false; } - const auto& local_attr = mesh.template get_attribute(name); + const auto& local_attr = mesh.get_attribute_base(name); if (local_attr.get_usage() != target_usage) { logger().warn("combine_meshes: attribute {} has inconsistent usage!", name); return false; @@ -81,14 +81,10 @@ void combine_attributes( { la_debug_assert(num_meshes > 0); - auto resize_value_attribute = [&](std::string_view name, auto&& attr) { - using AttributeType = std::decay_t; - using ValueType = typename AttributeType::ValueType; - + auto resize_value_attribute = [&](auto&& attr, auto mesh_to_attr) { size_t num_elements = 0; for (size_t i = 0; i < num_meshes; ++i) { - const auto& mesh = get_mesh(i); - const auto& local_attr = mesh.template get_attribute(name); + const auto& local_attr = mesh_to_attr(i); num_elements += local_attr.get_num_elements(); } attr.resize_elements(num_elements); @@ -101,7 +97,9 @@ void combine_attributes( AttributeUsage usage = attr.get_usage(); if (attr.get_element_type() == Value) { - resize_value_attribute(name, attr); + resize_value_attribute(attr, [&](auto i) -> decltype(auto) { + return get_mesh(i).template get_attribute(name); + }); } auto attr_view = matrix_ref(attr); @@ -128,7 +126,9 @@ void combine_attributes( auto& index_attr = attr.indices(); la_debug_assert(index_attr.get_num_elements() == out_mesh.get_num_corners()); - resize_value_attribute(name, value_attr); + resize_value_attribute(value_attr, [&](auto i) -> decltype(auto) { + return get_mesh(i).template get_indexed_attribute(name).values(); + }); auto value_view = matrix_ref(value_attr); auto index_view = matrix_ref(index_attr); @@ -143,7 +143,7 @@ void combine_attributes( value_view.middleRows(value_count, local_value_view.rows()) = local_value_view.array() + static_cast(offset); index_view.middleRows(index_count, local_index_view.rows()) = - local_index_view.array() + value_count; + local_index_view.array() + static_cast(value_count); value_count += local_value_view.rows(); index_count += local_index_view.rows(); offset += offset_from_usage(mesh, usage); diff --git a/modules/core/src/compute_area.cpp b/modules/core/src/compute_area.cpp new file mode 100644 index 00000000..b492c45d --- /dev/null +++ b/modules/core/src/compute_area.cpp @@ -0,0 +1,399 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +#include +// clang-format on + +#include + +namespace lagrange { + +namespace { + +template +Scalar triangle_area_3D(span a, span b, span c) +{ + const Scalar n0 = (a[1] - b[1]) * (a[2] - c[2]) - (a[1] - c[1]) * (a[2] - b[2]); + const Scalar n1 = -(a[0] - b[0]) * (a[2] - c[2]) + (a[0] - c[0]) * (a[2] - b[2]); + const Scalar n2 = (a[0] - b[0]) * (a[1] - c[1]) - (a[0] - c[0]) * (a[1] - b[1]); + + return std::sqrt(n0 * n0 + n1 * n1 + n2 * n2) / 2; +} + +template +Scalar triangle_area_2D(span a, span b, span c) +{ + return ((a[0] - b[0]) * (a[1] - c[1]) - (a[0] - c[0]) * (a[1] - b[1])) / 2; +} + +template +Scalar +quad_area_2D(span a, span b, span c, span d) +{ + Scalar _center[2]{(a[0] + b[0] + c[0] + d[0]) / 4, (a[1] + b[1] + c[1] + d[1]) / 4}; + span center(_center, 2); + return triangle_area_2D(a, b, center) + triangle_area_2D(b, c, center) + + triangle_area_2D(c, d, center) + triangle_area_2D(d, a, center); +} + +template +Scalar +quad_area_3D(span a, span b, span c, span d) +{ + Scalar _center[3]{ + (a[0] + b[0] + c[0] + d[0]) / 4, + (a[1] + b[1] + c[1] + d[1]) / 4, + (a[2] + b[2] + c[2] + d[2]) / 4}; + span center(_center, 3); + return triangle_area_3D(a, b, center) + triangle_area_3D(b, c, center) + + triangle_area_3D(c, d, center) + triangle_area_3D(d, a, center); +} + +template +class FacetPositionsView +{ +public: + FacetPositionsView( + const SurfaceMesh& mesh, + const Index fid, + const ConstRowMatrixView& vertex_positions) + : m_vertex_positions(vertex_positions) + , m_vertices_indices(mesh.get_facet_vertices(fid)) + , m_mesh_dim(mesh.get_dimension()) + {} + + span operator()(const Index local_vertex_index) const + { + return { + m_vertex_positions.data() + m_vertices_indices[local_vertex_index] * m_mesh_dim, + static_cast(m_mesh_dim)}; + } + +private: + const ConstRowMatrixView& m_vertex_positions; + const span m_vertices_indices; + const Index m_mesh_dim; +}; + +template +class FacetPositionsTransformed +{ +private: + using Position = Eigen::Vector; + +public: + FacetPositionsTransformed( + const SurfaceMesh& mesh, + const Index fid, + const ConstRowMatrixView& vertex_positions, + const Eigen::Transform& transformation) + : m_transformed_positions(mesh.get_facet_size(fid)) + { + FacetPositionsView positions_view(mesh, fid, vertex_positions); + for (const auto local_vertex_idx : range(m_transformed_positions.size())) { + const auto vertex_position = positions_view(static_cast(local_vertex_idx)); + la_debug_assert(vertex_position.size() == Dimension); + m_transformed_positions[local_vertex_idx] = + transformation * Eigen::Map(vertex_position.data()); + } + } + + const span operator()(const Index local_vertex_index) + { + const auto& position = m_transformed_positions[local_vertex_index]; + return {position.data(), static_cast(position.size())}; + } + +private: + SmallVector m_transformed_positions; +}; + +template +void compute_triangle_area( + SurfaceMesh& mesh, + AttributeId id, + bool use_signed_area, + FacetPositionsArgs... args) +{ + const auto num_facets = mesh.get_num_facets(); + const auto dim = mesh.get_dimension(); + + auto& attr = mesh.template ref_attribute(id); + la_debug_assert(static_cast(attr.get_num_elements()) == num_facets); + la_debug_assert(static_cast(attr.get_num_channels()) == 1); + auto attr_ref = matrix_ref(attr); + + if (dim == 3) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + FacetPositions p(mesh, fid, std::forward(args)...); + attr_ref(fid) = triangle_area_3D(p(0), p(1), p(2)); + }); + } else if (dim == 2) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + FacetPositions p(mesh, fid, std::forward(args)...); + attr_ref(fid) = triangle_area_2D(p(0), p(1), p(2)); + }); + if (!use_signed_area) attr_ref = attr_ref.array().abs(); + } else { + // High dimensional triangle area can be computed from the edge lengths. + // This is not implemented due to limited use cases. + throw Error("High dimensional triangle area computation is not implemented!"); + } +} + +template +void compute_quad_area( + SurfaceMesh& mesh, + AttributeId id, + bool use_signed_area, + FacetPositionsArgs... args) +{ + const auto num_facets = mesh.get_num_facets(); + const auto dim = mesh.get_dimension(); + + auto& attr = mesh.template ref_attribute(id); + la_debug_assert(static_cast(attr.get_num_elements()) == num_facets); + la_debug_assert(static_cast(attr.get_num_channels()) == 1); + auto attr_ref = matrix_ref(attr); + + if (dim == 3) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + FacetPositions p(mesh, fid, std::forward(args)...); + attr_ref(fid) = quad_area_3D(p(0), p(1), p(2), p(3)); + }); + } else if (dim == 2) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + FacetPositions p(mesh, fid, std::forward(args)...); + attr_ref(fid) = quad_area_2D(p(0), p(1), p(2), p(3)); + }); + if (!use_signed_area) attr_ref = attr_ref.array().abs(); + } else { + // This is not implemented due to limited use cases. + throw Error("High dimensional quad area computation is not implemented!"); + } +} + +template +void compute_polygon_area( + SurfaceMesh& mesh, + AttributeId id, + bool use_signed_area, + FacetPositionsArgs... args) +{ + const auto num_facets = mesh.get_num_facets(); + const auto dim = mesh.get_dimension(); + + auto& attr = mesh.template ref_attribute(id); + la_debug_assert(static_cast(attr.get_num_elements()) == num_facets); + la_debug_assert(static_cast(attr.get_num_channels()) == 1); + auto attr_ref = matrix_ref(attr); + + if (dim == 3) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + FacetPositions p(mesh, fid, std::forward(args)...); + const auto n = static_cast(mesh.get_facet_size(fid)); + Scalar _center[3]{0, 0, 0}; + for (Index i = 0; i < n; i++) { + const auto position = p(i); + _center[0] += position[0]; + _center[1] += position[1]; + _center[2] += position[2]; + } + _center[0] /= n; + _center[1] /= n; + _center[2] /= n; + span center(_center, 3); + attr_ref(fid) = 0; + for (Index i = 0; i < n; i++) { + Index prev = (i + n - 1) % n; + Index curr = i; + attr_ref(fid) += triangle_area_3D(p(prev), p(curr), center); + } + }); + } else if (dim == 2) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + FacetPositions p(mesh, fid, std::forward(args)...); + const auto n = static_cast(mesh.get_facet_size(fid)); + // 2D triangle area is signed, so no need of computing polygon center. + Scalar _O[2]{0, 0}; + span O(_O, 2); + attr_ref(fid) = 0; + for (Index i = 0; i < n; i++) { + Index prev = (i + n - 1) % n; + Index curr = i; + attr_ref(fid) += triangle_area_2D(p(prev), p(curr), O); + } + }); + if (!use_signed_area) attr_ref = attr_ref.array().abs(); + } else { + // This is not implemented due to limited use cases. + throw Error("High dimensional area computation is not implemented!"); + } +} + +template +AttributeId compute_facet_area_impl( + SurfaceMesh& mesh, + FacetAreaOptions options, + FacetPositionsArgs... args) +{ + AttributeId id = internal::find_or_create_attribute( + mesh, + options.output_attribute_name, + Facet, + AttributeUsage::Scalar, + 1, + internal::ResetToDefault::No); + + const auto& vertex_positions = vertex_view(mesh); + if (mesh.is_triangle_mesh()) { + compute_triangle_area( + mesh, + id, + options.use_signed_area, + vertex_positions, + std::forward(args)...); + } else if (mesh.is_quad_mesh()) { + compute_quad_area( + mesh, + id, + options.use_signed_area, + vertex_positions, + std::forward(args)...); + } else { + compute_polygon_area( + mesh, + id, + options.use_signed_area, + vertex_positions, + std::forward(args)...); + } + return id; +} + + +template +Scalar compute_mesh_area_impl( + SurfaceMesh& mesh, + MeshAreaOptions options, + FacetPositionsArgs... args) +{ + AttributeId area_id = invalid_attribute_id(); + if (!mesh.has_attribute(options.input_attribute_name)) { + const FacetAreaOptions fa_options = {options.input_attribute_name, options.use_signed_area}; + area_id = compute_facet_area_impl( + mesh, + fa_options, + std::forward(args)...); + } else { + area_id = mesh.get_attribute_id(options.input_attribute_name); + } + la_debug_assert(area_id != invalid_attribute_id()); + + const auto& attr = mesh.template get_attribute(area_id); + auto attr_ref = matrix_view(attr); + + const Scalar mesh_area = tbb::parallel_reduce( + tbb::blocked_range(static_cast(0), static_cast(mesh.get_num_facets())), + static_cast(0), + [&](const tbb::blocked_range& tbb_range, Scalar area_sum) { + for (auto fid = tbb_range.begin(); fid != tbb_range.end(); ++fid) { + area_sum += attr_ref(fid); + } + return area_sum; + }, + std::plus()); + return mesh_area; +} + +} // namespace + +template +AttributeId compute_facet_area(SurfaceMesh& mesh, FacetAreaOptions options) +{ + return compute_facet_area_impl>(mesh, options); +} + +template +AttributeId compute_facet_area( + SurfaceMesh& mesh, + const Eigen::Transform& transformation, + FacetAreaOptions options) +{ + return compute_facet_area_impl< + Scalar, + Index, + FacetPositionsTransformed>(mesh, options, transformation); +} + +template +Scalar compute_mesh_area(const SurfaceMesh& mesh, MeshAreaOptions options) +{ + SurfaceMesh shallow_copy = mesh; + return compute_mesh_area_impl>( + shallow_copy, + options); +} + +template +Scalar compute_mesh_area( + const SurfaceMesh& mesh, + const Eigen::Transform& transformation, + MeshAreaOptions options) +{ + SurfaceMesh shallow_copy = mesh; + return compute_mesh_area_impl< + Scalar, + Index, + FacetPositionsTransformed>(shallow_copy, options, transformation); +} + +#define LA_X_compute_facet_area(_, Scalar, Index) \ + template AttributeId compute_facet_area( \ + SurfaceMesh&, \ + FacetAreaOptions); \ + template Scalar compute_mesh_area( \ + const SurfaceMesh&, \ + MeshAreaOptions); + +#define LA_X_compute_facet_area_dim(_, Scalar, Index, Dimension) \ + template AttributeId compute_facet_area( \ + SurfaceMesh&, \ + const Eigen::Transform&, \ + FacetAreaOptions); \ + template Scalar compute_mesh_area( \ + const SurfaceMesh&, \ + const Eigen::Transform&, \ + MeshAreaOptions); + +#define LA_X_dimension(Data, Scalar, Index) \ + LA_X_compute_facet_area_dim(Data, Scalar, Index, 2) \ + LA_X_compute_facet_area_dim(Data, Scalar, Index, 3) + +LA_SURFACE_MESH_X(compute_facet_area, 0) +LA_SURFACE_MESH_X(dimension, 0) + +} // namespace lagrange diff --git a/modules/core/src/compute_centroid.cpp b/modules/core/src/compute_centroid.cpp new file mode 100644 index 00000000..ce8b30eb --- /dev/null +++ b/modules/core/src/compute_centroid.cpp @@ -0,0 +1,141 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +namespace lagrange { + +template +AttributeId compute_facet_centroid(SurfaceMesh& mesh, FacetCentroidOptions options) +{ + const auto num_facets = mesh.get_num_facets(); + const auto dim = mesh.get_dimension(); + AttributeId id = internal::find_or_create_attribute( + mesh, + options.output_attribute_name, + Facet, + AttributeUsage::Vector, + dim, + internal::ResetToDefault::No); + + auto& attr = mesh.template ref_attribute(id); + la_debug_assert(static_cast(attr.get_num_elements()) == num_facets); + la_debug_assert(static_cast(attr.get_num_channels()) == dim); + auto attr_ref = matrix_ref(attr); + + const auto& vertex_positions = vertex_view(mesh); + if (mesh.is_triangle_mesh()) { + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + const auto facet_vertices = mesh.get_facet_vertices(fid); + attr_ref.row(fid) = + (vertex_positions.row(facet_vertices[0]) + vertex_positions.row(facet_vertices[1]) + + vertex_positions.row(facet_vertices[2])) / + 3; + }); + } else { + attr_ref.setZero(); + tbb::parallel_for(Index(0), num_facets, [&](Index fid) { + const auto facet_vertices = mesh.get_facet_vertices(fid); + const size_t n = facet_vertices.size(); + + for (size_t i = 0; i < n; ++i) { + attr_ref.row(fid) += vertex_positions.row(facet_vertices[i]); + } + attr_ref.row(fid) /= static_cast(n); + }); + } + + return id; +} + +template +void compute_mesh_centroid( + const SurfaceMesh& mesh, + span centroid, + MeshCentroidOptions options) +{ + SurfaceMesh working_mesh = mesh; + const auto dim = working_mesh.get_dimension(); + la_runtime_assert(dim == 2 || dim == 3); + la_runtime_assert(centroid.size() == dim); + la_runtime_assert(mesh.get_num_vertices() > 0); + switch (options.weighting_type) { + case MeshCentroidOptions::Uniform: { + const auto& vertices = vertex_view(working_mesh); + const auto c = vertices.colwise().mean().eval(); + centroid[0] = c[0]; + centroid[1] = c[1]; + if (dim == 3) centroid[2] = c[2]; + } break; + case MeshCentroidOptions::Area: { + la_runtime_assert(mesh.get_num_facets() > 0); + AttributeId centroid_id = invalid_attribute_id(); + if (!working_mesh.has_attribute(options.facet_centroid_attribute_name)) { + FacetCentroidOptions fc_options; + fc_options.output_attribute_name = options.facet_centroid_attribute_name; + centroid_id = compute_facet_centroid(working_mesh, fc_options); + } else { + centroid_id = working_mesh.get_attribute_id(options.facet_centroid_attribute_name); + } + la_debug_assert(centroid_id != invalid_attribute_id()); + const auto& centroid_attr = working_mesh.template get_attribute(centroid_id); + const auto centroids = matrix_view(centroid_attr); + + AttributeId area_id = invalid_attribute_id(); + if (!working_mesh.has_attribute(options.facet_area_attribute_name)) { + FacetAreaOptions fa_options; + fa_options.use_signed_area = false; + fa_options.output_attribute_name = options.facet_area_attribute_name; + area_id = compute_facet_area(working_mesh, fa_options); + } else { + area_id = working_mesh.get_attribute_id(options.facet_area_attribute_name); + } + la_debug_assert(area_id != invalid_attribute_id()); + const auto& area_attr = working_mesh.template get_attribute(area_id); + const auto areas = vector_view(area_attr); + const Scalar total_area = areas.array().sum(); + la_runtime_assert(total_area > 0, "Mesh must not have 0 total area."); + + auto c = ((areas.transpose() * centroids) / total_area).eval(); + la_runtime_assert(c.array().isFinite().all(), "Numerical error in centorid computation."); + centroid[0] = c[0]; + centroid[1] = c[1]; + if (dim == 3) centroid[2] = c[2]; + } break; + default: throw Error("Unsupported centroid weighting type."); + } +} + +#define LA_X_compute_centroid(_, Scalar, Index) \ + template AttributeId compute_facet_centroid( \ + SurfaceMesh&, \ + FacetCentroidOptions); \ + template void compute_mesh_centroid( \ + const SurfaceMesh&, \ + span, \ + MeshCentroidOptions); +LA_SURFACE_MESH_X(compute_centroid, 0) + +} // namespace lagrange diff --git a/modules/core/src/compute_components.cpp b/modules/core/src/compute_components.cpp index c5e06282..f420e0d6 100644 --- a/modules/core/src/compute_components.cpp +++ b/modules/core/src/compute_components.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -100,6 +101,7 @@ size_t compute_components(SurfaceMesh& mesh, ComponentOptions opt case ComponentOptions::ConnectivityType::Vertex: return compute_vertex_based_components(mesh, id); case ComponentOptions::ConnectivityType::Edge: return compute_edge_based_components(mesh, id); + default: throw Error("Unsupported connectivity type"); } } diff --git a/modules/core/src/compute_normal.cpp b/modules/core/src/compute_normal.cpp index dd2db604..9bcd4788 100644 --- a/modules/core/src/compute_normal.cpp +++ b/modules/core/src/compute_normal.cpp @@ -182,7 +182,7 @@ AttributeId compute_normal_internal( auto compute_weighted_corner_normal = [&, facet_normal = facet_normal](Index ci) -> Eigen::Matrix { auto n = internal::compute_weighted_corner_normal(mesh, ci, options.weight_type); - Scalar sign = std::copysign(1, n.dot(facet_normal.row(mesh.get_corner_facet(ci)))); + Scalar sign = std::copysign(1.f, n.dot(facet_normal.row(mesh.get_corner_facet(ci)))); n *= sign; return n; }; diff --git a/modules/core/src/compute_vertex_valence.cpp b/modules/core/src/compute_vertex_valence.cpp index 5a6cb697..aa9491c5 100644 --- a/modules/core/src/compute_vertex_valence.cpp +++ b/modules/core/src/compute_vertex_valence.cpp @@ -37,7 +37,7 @@ AttributeId compute_vertex_valence(SurfaceMesh& mesh, VertexValen la_debug_assert( static_cast(adjacency_list.get_num_entries()) == mesh.get_num_vertices()); for (auto i : range(mesh.get_num_vertices())) { - valence[i] = adjacency_list.get_num_neighbors(i); + valence[i] = static_cast(adjacency_list.get_num_neighbors(static_cast(i))); } return id; diff --git a/modules/core/src/compute_vertex_vertex_adjacency.cpp b/modules/core/src/compute_vertex_vertex_adjacency.cpp index 8e10d478..e53b052a 100644 --- a/modules/core/src/compute_vertex_vertex_adjacency.cpp +++ b/modules/core/src/compute_vertex_vertex_adjacency.cpp @@ -55,7 +55,7 @@ AdjacencyList compute_vertex_vertex_adjacency(SurfaceMesh& std::rotate(adjacency_index.rbegin(), adjacency_index.rbegin() + 1, adjacency_index.rend()); std::partial_sum(adjacency_index.begin(), adjacency_index.end(), adjacency_index.begin()); - const Index total_size = adjacency_index.back(); + const Index total_size = static_cast(adjacency_index.back()); adjacency_data.resize(total_size); // Gather adjacency data with duplicates. Note: We could do this loop in parallel by using a diff --git a/modules/core/src/internal/attribute_string_utils.cpp b/modules/core/src/internal/attribute_string_utils.cpp index ed5939f9..a65e55e4 100644 --- a/modules/core/src/internal/attribute_string_utils.cpp +++ b/modules/core/src/internal/attribute_string_utils.cpp @@ -26,10 +26,22 @@ std::string_view to_string(AttributeElement element) LA_ENUM_CASE(AttributeElement, Corner); LA_ENUM_CASE(AttributeElement, Value); LA_ENUM_CASE(AttributeElement, Indexed); - default: la_debug_assert(false, "Unsupported enum type"); return ""; + default: + la_debug_assert(false, "Unsupported enum type"); return ""; } } +std::string to_string(BitField element) { + std::string ret; + if (element.test_any(AttributeElement::Vertex)) ret += "Vertex;"; + if (element.test_any(AttributeElement::Facet)) ret += "Facet;"; + if (element.test_any(AttributeElement::Edge)) ret += "Edge;"; + if (element.test_any(AttributeElement::Corner)) ret += "Corner;"; + if (element.test_any(AttributeElement::Value)) ret += "Value;"; + if (element.test_any(AttributeElement::Indexed)) ret += "Indexed;"; + return ret; +} + std::string_view to_string(AttributeUsage usage) { switch (usage) { diff --git a/modules/core/src/internal/find_attribute_utils.cpp b/modules/core/src/internal/find_attribute_utils.cpp index 5cca9b9a..105d5fa6 100644 --- a/modules/core/src/internal/find_attribute_utils.cpp +++ b/modules/core/src/internal/find_attribute_utils.cpp @@ -31,7 +31,7 @@ template void check_attribute( const SurfaceMesh& mesh, AttributeId id, - AttributeElement expected_element, + BitField expected_element, AttributeUsage expected_usage, size_t expected_channels, ShouldBeWritable expected_writable) @@ -49,7 +49,7 @@ void check_attribute( to_string(expected_usage), to_string(attr.get_usage()))); la_runtime_assert( - attr.get_element_type() == expected_element, + expected_element.test(attr.get_element_type()), fmt::format( "Attribute element type should be {}, not {}", to_string(expected_element), @@ -65,46 +65,70 @@ void check_attribute( } if (expected_writable == ShouldBeWritable::Yes) { - if (expected_element != Indexed) { - const auto& attr = mesh.template get_attribute(id); - la_runtime_assert(!attr.is_read_only(), "Attribute is read only"); - } else { + if (mesh.is_attribute_indexed(id)) { const auto& attr = mesh.template get_indexed_attribute(id); la_runtime_assert(!attr.values().is_read_only(), "Attribute is read only"); la_runtime_assert(!attr.indices().is_read_only(), "Attribute is read only"); + } else { + const auto& attr = mesh.template get_attribute(id); + la_runtime_assert(!attr.is_read_only(), "Attribute is read only"); } } } +template +AttributeId find_matching_attribute_internal( + const SurfaceMesh& mesh, + const std::unordered_set* selected_ids, + BitField expected_element, + AttributeUsage expected_usage, + size_t expected_channels) +{ + AttributeId id = invalid_attribute_id(); + // No attribute name provided. Iterate until we find the first matching attribute. + seq_foreach_named_attribute_read(mesh, [&](auto attr_name, auto&& attr) { + auto current_id = mesh.get_attribute_id(attr_name); + if (selected_ids && !selected_ids->count(current_id)) { + return; + } + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + if (id == invalid_attribute_id() && attr.get_usage() == expected_usage && + expected_element.test_any(attr.get_element_type()) && + (expected_channels == 0 || attr.get_num_channels() == expected_channels) && + std::is_same_v) { + logger().trace( + "Found attribute '{}' with element type {}, usage {}, value type {}.", + attr_name, + to_string(expected_element), + to_string(expected_usage), + string_from_scalar()); + id = mesh.get_attribute_id(attr_name); + } + }); + + return id; +} + } // namespace template AttributeId find_matching_attribute( const SurfaceMesh& mesh, std::string_view name, - AttributeElement expected_element, + BitField expected_element, AttributeUsage expected_usage, size_t expected_channels) { AttributeId id = invalid_attribute_id(); if (name.empty()) { // No attribute name provided. Iterate until we find the first matching attribute. - seq_foreach_named_attribute_read(mesh, [&](auto name, auto&& attr) { - using AttributeType = std::decay_t; - using ValueType = typename AttributeType::ValueType; - if (id == invalid_attribute_id() && attr.get_usage() == expected_usage && - attr.get_element_type() == expected_element && - (expected_channels == 0 || attr.get_num_channels() == expected_channels) && - std::is_same_v) { - logger().trace( - "Found attribute '{}' with element type {}, usage {}, value type {}.", - name, - to_string(expected_element), - to_string(expected_usage), - string_from_scalar()); - id = mesh.get_attribute_id(name); - } - }); + return find_matching_attribute_internal( + mesh, + nullptr, + expected_element, + expected_usage, + expected_channels); } else { // Check user-provided attribute compatibility id = mesh.get_attribute_id(name); @@ -120,11 +144,27 @@ AttributeId find_matching_attribute( return id; } +template +AttributeId find_matching_attribute( + const SurfaceMesh& mesh, + const std::unordered_set& selected_ids, + BitField expected_element, + AttributeUsage expected_usage, + size_t expected_channels) +{ + return find_matching_attribute_internal( + mesh, + &selected_ids, + expected_element, + expected_usage, + expected_channels); +} + template AttributeId find_attribute( const SurfaceMesh& mesh, std::string_view name, - AttributeElement expected_element, + BitField expected_element, AttributeUsage expected_usage, size_t expected_channels) { @@ -192,13 +232,19 @@ AttributeId find_or_create_attribute( template AttributeId find_matching_attribute( \ const SurfaceMesh& mesh, \ std::string_view name, \ - AttributeElement expected_element, \ + BitField expected_element, \ + AttributeUsage expected_usage, \ + size_t expected_channels); \ + template AttributeId find_matching_attribute( \ + const SurfaceMesh& mesh, \ + const std::unordered_set& selected_ids, \ + BitField expected_element, \ AttributeUsage expected_usage, \ size_t expected_channels); \ template AttributeId find_attribute( \ const SurfaceMesh& mesh, \ std::string_view name, \ - AttributeElement expected_element, \ + BitField expected_element, \ AttributeUsage expected_usage, \ size_t expected_channels); \ template AttributeId find_or_create_attribute( \ diff --git a/modules/core/src/map_attribute.cpp b/modules/core/src/map_attribute.cpp index 03241b1a..239c536b 100644 --- a/modules/core/src/map_attribute.cpp +++ b/modules/core/src/map_attribute.cpp @@ -126,13 +126,19 @@ AttributeId map_attribute_internal( num_elements = mesh.get_num_corners(); switch (old_element) { case AttributeElement::Vertex: - src_element = [&](size_t c) { return mesh.get_corner_vertex(c); }; + src_element = [&](size_t c) { + return static_cast(mesh.get_corner_vertex(static_cast(c))); + }; break; case AttributeElement::Facet: - src_element = [&](size_t c) { return mesh.get_corner_facet(c); }; + src_element = [&](size_t c) { + return static_cast(mesh.get_corner_facet(static_cast(c))); + }; break; case AttributeElement::Edge: - src_element = [&](size_t c) { return mesh.get_corner_edge(c); }; + src_element = [&](size_t c) { + return static_cast(mesh.get_corner_edge(static_cast(c))); + }; break; case AttributeElement::Corner: src_element = [](size_t c) { return c; }; break; case AttributeElement::Value: @@ -191,13 +197,19 @@ AttributeId map_attribute_internal( } else { switch (new_element) { case AttributeElement::Vertex: - dst_element = [&](size_t c) { return mesh.get_corner_vertex(c); }; + dst_element = [&](size_t c) { + return static_cast(mesh.get_corner_vertex(static_cast(c))); + }; break; case AttributeElement::Facet: - dst_element = [&](size_t c) { return mesh.get_corner_facet(c); }; + dst_element = [&](size_t c) { + return static_cast(mesh.get_corner_facet(static_cast(c))); + }; break; case AttributeElement::Edge: - dst_element = [&](size_t c) { return mesh.get_corner_edge(c); }; + dst_element = [&](size_t c) { + return static_cast(mesh.get_corner_edge(static_cast(c))); + }; break; case AttributeElement::Corner: break; case AttributeElement::Value: new_attr.resize_elements(num_elements); break; diff --git a/modules/core/src/mapbox/earcut.h b/modules/core/src/mapbox/earcut.h index 9de2fe2d..02ac1c45 100644 --- a/modules/core/src/mapbox/earcut.h +++ b/modules/core/src/mapbox/earcut.h @@ -296,11 +296,9 @@ void Earcut::earcutLinked(Node* ear, int pass) Node* prev; Node* next; - int iterations = 0; // iterate through ears, slicing them one by one while (ear->prev != ear->next) { - iterations++; prev = ear->prev; next = ear->next; diff --git a/modules/core/src/permute_vertices.cpp b/modules/core/src/permute_vertices.cpp new file mode 100644 index 00000000..24d904d3 --- /dev/null +++ b/modules/core/src/permute_vertices.cpp @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace lagrange { + +template +void permute_vertices(SurfaceMesh& mesh, span new_to_old) +{ + la_runtime_assert(mesh.get_num_vertices() == (Index)(new_to_old.size())); + const Index num_vertices = mesh.get_num_vertices(); + constexpr int invalid_index = -1; // Eigen::PermutationMatrix::Scalar is int. + + // Initialize permuation matrix + Eigen::PermutationMatrix P(num_vertices); + auto& old_to_new = P.indices(); + old_to_new.setConstant(invalid_index); + for (Index i = 0; i < num_vertices; i++) { + la_runtime_assert(new_to_old[i] < num_vertices, "`new_to_old` index out of bound!"); + old_to_new[new_to_old[i]] = static_cast(i); + } + la_runtime_assert( + std::find(old_to_new.begin(), old_to_new.end(), invalid_index) == old_to_new.end(), + "`new_to_old` is not a valid permutation of [0, ..., num_vertices-1]!"); + par_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + if (attr.get_usage() == AttributeUsage::VertexIndex) { + auto& attr_ref = mesh.template ref_attribute(name); + auto data = attr_ref.ref_all(); + std::transform(data.begin(), data.end(), data.begin(), [&](ValueType i) { + return static_cast(old_to_new[static_cast(i)]); + }); + } + }); + + // Permute runs in O(n) time. + auto permute = [&](auto&& attr) { + auto data = matrix_ref(attr); + data = P * data; + }; + + // Permute vertex attributes (positions included). + par_foreach_attribute_write(mesh, permute); + + // Facet, corner, edge and indexed attributes are all unchanged. +} + +#define LA_X_permute_vertices(_, Scalar, Index) \ + template void permute_vertices(SurfaceMesh&, span); +LA_SURFACE_MESH_X(permute_vertices, 0) + +} // namespace lagrange diff --git a/modules/core/src/remap_vertices.cpp b/modules/core/src/remap_vertices.cpp new file mode 100644 index 00000000..d7135eef --- /dev/null +++ b/modules/core/src/remap_vertices.cpp @@ -0,0 +1,214 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace lagrange { + +template +void remap_vertices( + SurfaceMesh& mesh, + span old_to_new, + RemapVerticesOptions options) +{ + const Index num_vertices = mesh.get_num_vertices(); + la_runtime_assert((Index)old_to_new.size() == num_vertices); + + // The internal data structure for edges (e.g. $vertex_to_first_corner and + // $next_corner_around_vertex) cannot be easily updated. + if (mesh.has_edges()) { + throw Error( + "Remap vertices will invalidate edge data in mesh. Please clear_edges() first."); + } + + // Compute the backward order. + std::vector new_to_old_indices(num_vertices + 1, 0); + std::vector new_to_old(num_vertices); + for (Index i = 0; i < num_vertices; ++i) { + Index j = old_to_new[i]; + la_runtime_assert( + j < num_vertices, + "New vertex index cannot exceeds existing number of vertices!"); + ++new_to_old_indices[j + 1]; + } + size_t num_out_vertices = num_vertices; + for (; num_out_vertices != 0 && new_to_old_indices[num_out_vertices] == 0; --num_out_vertices) { + } + new_to_old_indices.resize(num_out_vertices + 1); + std::partial_sum( + new_to_old_indices.begin(), + new_to_old_indices.end(), + new_to_old_indices.begin()); + la_debug_assert(new_to_old_indices.back() == num_vertices); + + for (Index i = 0; i < num_vertices; i++) { + Index j = old_to_new[i]; + new_to_old[new_to_old_indices[j]++] = i; + } + std::rotate( + new_to_old_indices.begin(), + std::prev(new_to_old_indices.end()), + new_to_old_indices.end()); + new_to_old_indices[0] = 0; + + // Surjective check! + for (Index i = 0; i < num_out_vertices; i++) { + la_runtime_assert( + new_to_old_indices[i] < new_to_old_indices[i + 1], + "old_to_new mapping is not surjective!"); + } + + // Remap an attribute. If multiple entries are mapped to the same slot, they will be averaged. + auto remap_average = [&](auto&& attr) { + auto usage = attr.get_usage(); + if (usage == AttributeUsage::VertexIndex || usage == AttributeUsage::FacetIndex || + usage == AttributeUsage::CornerIndex) { + throw Error("remap_vertices cannot average indices!"); + } + + auto data = matrix_ref(attr); + using ValueType = typename std::decay_t::Scalar; + // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. + Eigen::Matrix data_copy = data; + + for (Index i = 0; i < num_out_vertices; i++) { + data.row(i).setZero(); + for (Index j = new_to_old_indices[i]; j < new_to_old_indices[i + 1]; j++) { + data.row(i) += data_copy.row(new_to_old[j]); + } + data.row(i) /= + static_cast(new_to_old_indices[i + 1] - new_to_old_indices[i]); + } + attr.resize_elements(num_out_vertices); + }; + + // Remap an attribute. If multiple entries are mapped to the same slot, keep the first. + auto remap_keep_first = [&](auto&& attr) { + auto data = matrix_ref(attr); + using ValueType = typename std::decay_t::Scalar; + // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. + Eigen::Matrix data_copy = data; + + for (Index i = 0; i < num_out_vertices; i++) { + const Index j = new_to_old_indices[i]; + data.row(i) = data_copy.row(new_to_old[j]); + } + attr.resize_elements(num_out_vertices); + }; + + // Remap an attribute. If multiple entries are mapped to the same slot, throw error. + auto remap_injective = [&](auto&& attr) { + auto data = matrix_ref(attr); + using ValueType = typename std::decay_t::Scalar; + // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. + Eigen::Matrix data_copy = data; + + for (Index i = 0; i < num_out_vertices; i++) { + const Index j = new_to_old_indices[i]; + la_runtime_assert( + new_to_old_indices[i + 1] == j + 1, + "Vertex mapping policy does not allow collision."); + data.row(i) = data_copy.row(new_to_old[j]); + } + attr.resize_elements(num_out_vertices); + }; + + // Remap vertex attributes. + par_foreach_named_attribute_write( + mesh, + [&](std::string_view name, auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + if (name == mesh.attr_name_vertex_to_first_corner() || + name == mesh.attr_name_next_corner_around_vertex()) + return; + + if constexpr (std::is_integral_v) { + switch (options.collision_policy_integral) { + case RemapVerticesOptions::CollisionPolicy::Average: + remap_average(std::forward(attr)); + break; + case RemapVerticesOptions::CollisionPolicy::KeepFirst: + remap_keep_first(std::forward(attr)); + break; + case RemapVerticesOptions::CollisionPolicy::Error: + remap_injective(std::forward(attr)); + break; + default: + throw Error(fmt::format( + "Unsupported collision policy {}", + int(options.collision_policy_integral))); + } + } else { + switch (options.collision_policy_float) { + case RemapVerticesOptions::CollisionPolicy::Average: + remap_average(std::forward(attr)); + break; + case RemapVerticesOptions::CollisionPolicy::KeepFirst: + remap_keep_first(std::forward(attr)); + break; + case RemapVerticesOptions::CollisionPolicy::Error: + remap_injective(std::forward(attr)); + break; + default: + throw Error(fmt::format( + "Unsupported collision policy {}", + int(options.collision_policy_float))); + } + } + }); + + // Update vertex indices. + par_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + // Only remap vertex indices that are not associated with vertex element because vertex + // attributes have already been updated in previous step. + if (attr.get_usage() == AttributeUsage::VertexIndex && + attr.get_element_type() != AttributeElement::Vertex) { + auto& attr_ref = mesh.template ref_attribute(name); + auto data = attr_ref.ref_all(); + std::transform(data.begin(), data.end(), data.begin(), [&](ValueType i) { + return static_cast(old_to_new[static_cast(i)]); + }); + } + }); + + // Remap vertices. This must be done last because `remove_vertices` also delete facets adjacent + // to the vertices. + // + // TODO: we has already resized the vertex-to-position attribute, but there is no way of + // changing `SurfaceMesh::m_num_vertices`. Using `SurfaceMesh::remove_vertices` works, but it + // feels like an overkill and a fragile solution... + mesh.remove_vertices([&](Index i) { return i >= num_out_vertices; }); +} + +#define LA_X_remap_vertices(_, Scalar, Index) \ + template void remap_vertices( \ + SurfaceMesh&, \ + span, \ + RemapVerticesOptions); +LA_SURFACE_MESH_X(remap_vertices, 0) + +} // namespace lagrange diff --git a/modules/core/src/triangulate_polygonal_facets.cpp b/modules/core/src/triangulate_polygonal_facets.cpp index f88d1c0f..ed6c8445 100644 --- a/modules/core/src/triangulate_polygonal_facets.cpp +++ b/modules/core/src/triangulate_polygonal_facets.cpp @@ -112,9 +112,9 @@ auto find_best_2d_axes(const SurfaceMesh& mesh, Index f) auto nrm = facet_normal(mesh, f); const Index max_axis = [&] { - Eigen::Index max_axis; - nrm.cwiseAbs().maxCoeff(&max_axis); - return static_cast(max_axis); + Eigen::Index max_axis_; + nrm.cwiseAbs().maxCoeff(&max_axis_); + return static_cast(max_axis_); }(); std::array axes; @@ -302,6 +302,8 @@ void triangulate_polygonal_facets(SurfaceMesh& mesh) "facets. Please remap them manually."); } } + + mesh.compress_if_regular(); } // Iterate over mesh (scalar, index) types diff --git a/modules/core/src/unify_index_buffer.cpp b/modules/core/src/unify_index_buffer.cpp index d3d00a4c..72dd5ea4 100644 --- a/modules/core/src/unify_index_buffer.cpp +++ b/modules/core/src/unify_index_buffer.cpp @@ -115,7 +115,7 @@ SurfaceMesh unify_index_buffer( std::vector corner_to_vertex(mesh.get_num_corners(), invalid()); size_t num_unique_corners = corner_group_indices.size() - 1; logger().debug("Unified index buffer: {} vertices", num_unique_corners); - output_mesh.add_vertices(num_unique_corners, [&](Index i, span p) { + output_mesh.add_vertices(static_cast(num_unique_corners), [&](Index i, span p) { for (size_t j = corner_group_indices[i]; j < corner_group_indices[i + 1]; j++) { const auto cid = corner_groups[j]; corner_to_vertex[cid] = i; @@ -157,9 +157,11 @@ SurfaceMesh unify_index_buffer( auto& out_attr = output_mesh.template ref_attribute(id); out_attr.resize_elements(num_unique_corners); for (auto vid : range(num_unique_corners)) { - Index cid = corner_groups[corner_group_indices[vid]]; - Index prev_vid = mesh.get_corner_vertex(cid); - out_attr.ref(vid) = attr.get(prev_vid); + auto cid = corner_groups[corner_group_indices[vid]]; + auto prev_vid = mesh.get_corner_vertex(cid); + auto source_value = attr.get_row(prev_vid); + auto target_value = out_attr.ref_row(vid); + std::copy(source_value.begin(), source_value.end(), target_value.begin()); } }); diff --git a/modules/core/src/utils/DisjointSets.cpp b/modules/core/src/utils/DisjointSets.cpp index 3bf1fbdc..6928e207 100644 --- a/modules/core/src/utils/DisjointSets.cpp +++ b/modules/core/src/utils/DisjointSets.cpp @@ -51,7 +51,7 @@ std::vector> DisjointSets::extract_disjoint_se std::vector> disjoint_sets(counter); for (auto i : range(num_entries)) { - disjoint_sets[index_map[i]].push_back(i); + disjoint_sets[index_map[i]].push_back(static_cast(i)); } return disjoint_sets; } @@ -59,7 +59,7 @@ std::vector> DisjointSets::extract_disjoint_se template size_t DisjointSets::extract_disjoint_set_indices(std::vector& index_map) { - const IndexType num_entries = size(); + const IndexType num_entries = static_cast(size()); index_map.resize(num_entries, invalid()); return extract_disjoint_set_indices({index_map.data(), index_map.size()}); } @@ -77,13 +77,13 @@ size_t DisjointSets::extract_disjoint_set_indices(span ind // Assign each roots a unique index. for (auto i : range(num_entries)) { - const auto root = find(i); + const auto root = find(static_cast(i)); if (static_cast(i) == root) index_map[i] = counter++; } // Assign all members the same index as their root. for (auto i : range(num_entries)) { - const auto root = find(i); + const auto root = find(static_cast(i)); la_debug_assert(index_map[root] != invalid()); index_map[i] = index_map[root]; } diff --git a/modules/core/src/point_on_segment.cpp b/modules/core/src/utils/point_on_segment.cpp similarity index 97% rename from modules/core/src/point_on_segment.cpp rename to modules/core/src/utils/point_on_segment.cpp index d40adc38..1cca2b4f 100644 --- a/modules/core/src/point_on_segment.cpp +++ b/modules/core/src/utils/point_on_segment.cpp @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include +#include #include diff --git a/modules/core/tests/compute_tangent_bitangent_mikktspace.h b/modules/core/tests/compute_tangent_bitangent_mikktspace.h index 5830a301..890c55d0 100644 --- a/modules/core/tests/compute_tangent_bitangent_mikktspace.h +++ b/modules/core/tests/compute_tangent_bitangent_mikktspace.h @@ -1,3 +1,14 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ #pragma once #ifdef LAGRANGE_WITH_MIKKTSPACE diff --git a/modules/core/tests/test_compute_centroid.cpp b/modules/core/tests/test_compute_centroid.cpp new file mode 100644 index 00000000..1295a2de --- /dev/null +++ b/modules/core/tests/test_compute_centroid.cpp @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include + +#include +#include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif + +#include + +#include +#include + +TEST_CASE("compute_mesh_centroid", "[core][surface]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + constexpr Scalar a = 0.5; + constexpr Scalar b = 2.0; + constexpr Scalar large_number = 1e3; + + SurfaceMesh mesh; + mesh.add_vertex({-a / 2, -b / 2, 0}); // 0 + mesh.add_vertex({a / 2, -b / 2, 0}); // 1 + mesh.add_vertex({a / 2, 0, 0}); // 2 + mesh.add_vertex({a / 2, b / 4, 0}); // 3 + mesh.add_vertex({a / 2, b / 2, 0}); // 4 + mesh.add_vertex({-a / 2, b / 2, 0}); // 5 + mesh.add_vertex({large_number, large_number, large_number}); // 6 + mesh.add_vertex({-large_number, -large_number, -large_number}); // 7 + mesh.add_vertex({2 * large_number, 2 * large_number, 2 * large_number}); // 8 + + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(6, 7, 8); // facet with 0 area. + mesh.add_triangle(0, 2, 3); + mesh.add_triangle(0, 3, 4); + mesh.add_triangle(0, 4, 5); + + constexpr Scalar eps = std::numeric_limits::epsilon(); + + MeshCentroidOptions options; + options.weighting_type = MeshCentroidOptions::Area; + std::array centroid; + + SECTION("Uniform") + { + options.weighting_type = MeshCentroidOptions::Uniform; + compute_mesh_centroid(mesh, centroid, options); + REQUIRE(centroid[0] > 100); + REQUIRE(centroid[1] > 100); + REQUIRE(centroid[2] > 100); + } + SECTION("No transformation") + { + compute_mesh_centroid(mesh, centroid, options); + REQUIRE_THAT(centroid[0], Catch::Matchers::WithinAbs(0, eps)); + REQUIRE_THAT(centroid[1], Catch::Matchers::WithinAbs(0, eps)); + REQUIRE_THAT(centroid[2], Catch::Matchers::WithinAbs(0, eps)); + } + + SECTION("With transformation") + { + using Matrix = Eigen::Matrix; + using Vector = Eigen::Matrix; + // Rigid transform the mesh. + Vector tr(-1, 3, 4); + Matrix rot = + Eigen::AngleAxis(1.2365, Vector(-1, 2, 5.1).normalized()).toRotationMatrix(); + + auto vertices = vertex_ref(mesh); + Eigen::Matrix new_vertices = + ((vertices * rot.transpose()).rowwise() + tr.transpose()).eval(); + vertices = new_vertices; + + compute_mesh_centroid(mesh, centroid, options); + REQUIRE_THAT(centroid[0], Catch::Matchers::WithinAbs(tr[0], eps)); + REQUIRE_THAT(centroid[1], Catch::Matchers::WithinAbs(tr[1], eps)); + REQUIRE_THAT(centroid[2], Catch::Matchers::WithinAbs(tr[2], eps)); + } +} + +TEST_CASE("compute_mesh_centroid benchmark", "[core][surface][centroid][!benchmark]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + MeshCentroidOptions options; + + auto mesh = lagrange::testing::load_surface_mesh("open/core/dragon.obj"); + + BENCHMARK_ADVANCED("compute_mesh_centroid")(Catch::Benchmark::Chronometer meter) + { + if (mesh.has_attribute(options.facet_area_attribute_name)) + mesh.delete_attribute(options.facet_area_attribute_name, AttributeDeletePolicy::Force); + if (mesh.has_attribute(options.facet_centroid_attribute_name)) + mesh.delete_attribute( + options.facet_centroid_attribute_name, + AttributeDeletePolicy::Force); + std::vector centroid(3); + meter.measure( + [&]() { return compute_mesh_centroid(mesh, centroid, options); }); + }; + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + using MeshType = TriangleMesh3D; + auto legacy_mesh = to_legacy_mesh(mesh); + + BENCHMARK_ADVANCED("legacy::compute_mesh_centroid")(Catch::Benchmark::Chronometer meter) + { + if (legacy_mesh->has_facet_attribute("area")) legacy_mesh->remove_facet_attribute("area"); + meter.measure([&]() { return compute_mesh_centroid(*legacy_mesh); }); + }; +#endif +} diff --git a/modules/core/tests/test_compute_components.cpp b/modules/core/tests/test_compute_components.cpp index 3ac69435..719224f8 100644 --- a/modules/core/tests/test_compute_components.cpp +++ b/modules/core/tests/test_compute_components.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2017 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/modules/core/tests/test_compute_facet_area.cpp b/modules/core/tests/test_compute_facet_area.cpp index d88846d4..b02db92e 100644 --- a/modules/core/tests/test_compute_facet_area.cpp +++ b/modules/core/tests/test_compute_facet_area.cpp @@ -10,14 +10,204 @@ * governing permissions and limitations under the License. */ #include +#include #include +#include #include #include +#include #include #include +#include +#include -TEST_CASE("2DTriangleFacetArea", "[mesh][triangle][area]") +TEST_CASE("compute_facet_area", "[core][area][surface]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + constexpr Scalar eps = std::numeric_limits::epsilon(); + FacetAreaOptions options; + + SECTION("2D triangle") + { + SurfaceMesh mesh(2); + mesh.add_vertex({0, 0}); + mesh.add_vertex({1, 0}); + mesh.add_vertex({0, 1}); + mesh.add_vertex({1, 1}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 3); + + auto id = compute_facet_area(mesh, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(0.5, eps)); + REQUIRE_THAT(areas[1], Catch::Matchers::WithinAbs(0.5, eps)); + } + + SECTION("2D quad") + { + SurfaceMesh mesh(2); + mesh.add_vertex({0, 0}); + mesh.add_vertex({1, 0}); + mesh.add_vertex({1, 1}); + mesh.add_vertex({0, 1}); + mesh.add_quad(0, 1, 2, 3); + + auto id = compute_facet_area(mesh, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(1.0, eps)); + } + + SECTION("2D polygon") + { + SurfaceMesh mesh(2); + mesh.add_vertex({0, 0}); + mesh.add_vertex({1, 0}); + mesh.add_vertex({2, 0}); + mesh.add_vertex({2, 1}); + mesh.add_vertex({2, 2}); + mesh.add_vertex({1, 2}); + mesh.add_vertex({0, 2}); + mesh.add_vertex({0, 1}); + mesh.add_polygon({0, 1, 2, 3, 4, 5, 6, 7}); + + auto id = compute_facet_area(mesh, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(4.0, eps)); + } + + SECTION("3D triangle") + { + SurfaceMesh mesh(3); + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({1, 1, 0}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 3); + + auto id = compute_facet_area(mesh, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(0.5, eps)); + REQUIRE_THAT(areas[1], Catch::Matchers::WithinAbs(0.5, eps)); + } + + SECTION("3D quad") + { + SurfaceMesh mesh(3); + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({1, 1, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_quad(0, 1, 2, 3); + + auto id = compute_facet_area(mesh, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(1.0, eps)); + } + + SECTION("3D polygon") + { + SurfaceMesh mesh(3); + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({2, 0, 0}); + mesh.add_vertex({2, 1, 0}); + mesh.add_vertex({2, 2, 0}); + mesh.add_vertex({1, 2, 0}); + mesh.add_vertex({0, 2, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_polygon({0, 1, 2, 3, 4, 5, 6, 7}); + + auto id = compute_facet_area(mesh, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(4.0, eps)); + } + + SECTION("3D quad transformed") + { + SurfaceMesh mesh(3); + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({1, 1, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_quad(0, 1, 2, 3); + + auto transformation = Eigen::Transform::Identity(); + transformation.linear().diagonal().fill(2.); + + auto id = compute_facet_area(mesh, transformation, options); + REQUIRE(mesh.template is_attribute_type(id)); + REQUIRE(!mesh.is_attribute_indexed(id)); + REQUIRE(mesh.get_attribute_name(id) == options.output_attribute_name); + + const auto& areas = vector_view(mesh.get_attribute(id)); + REQUIRE_THAT(areas[0], Catch::Matchers::WithinAbs(4.0, eps)); + } +} + +TEST_CASE("compute_facet_area benchmark", "[surface][attribute][area][!benchmark]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + FacetAreaOptions options; + options.output_attribute_name = "facet_area"; + + auto mesh = lagrange::testing::load_surface_mesh("open/core/dragon.obj"); + compute_facet_area(mesh, options); + + BENCHMARK_ADVANCED("compute_area")(Catch::Benchmark::Chronometer meter) + { + if (mesh.has_attribute(options.output_attribute_name)) + mesh.delete_attribute(options.output_attribute_name, AttributeDeletePolicy::Force); + meter.measure([&]() { return compute_facet_area(mesh, options); }); + }; + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + using MeshType = TriangleMesh3D; + auto legacy_mesh = to_legacy_mesh(mesh); + + BENCHMARK_ADVANCED("legacy::compute_facet_area")(Catch::Benchmark::Chronometer meter) + { + if (legacy_mesh->has_facet_attribute("area")) legacy_mesh->remove_facet_attribute("area"); + meter.measure([&]() { return compute_facet_area(*legacy_mesh); }); + }; +#endif +} + + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + +TEST_CASE("legacy::compute_facet_area 2DTriangleFacetArea", "[mesh][triangle][area]") { using namespace lagrange; @@ -36,7 +226,7 @@ TEST_CASE("2DTriangleFacetArea", "[mesh][triangle][area]") REQUIRE(areas(1, 0) == Catch::Approx(0.5)); } -TEST_CASE("3DTriangleFacetArea", "[mesh][triangle][area]") +TEST_CASE("legacy::compute_facet_area 3DTriangleFacetArea", "[mesh][triangle][area]") { using namespace lagrange; @@ -55,7 +245,7 @@ TEST_CASE("3DTriangleFacetArea", "[mesh][triangle][area]") REQUIRE(areas(1, 0) == Catch::Approx(0.5)); } -TEST_CASE("2DQuadFacetArea", "[mesh][quad][area]") +TEST_CASE("legacy::compute_facet_area 2DQuadFacetArea", "[mesh][quad][area]") { using namespace lagrange; @@ -73,7 +263,7 @@ TEST_CASE("2DQuadFacetArea", "[mesh][quad][area]") REQUIRE(areas(0, 0) == Catch::Approx(1.0)); } -TEST_CASE("3DQuadFacetArea", "[mesh][quad][area]") +TEST_CASE("legacy::compute_facet_area 3DQuadFacetArea", "[mesh][quad][area]") { using namespace lagrange; @@ -91,7 +281,7 @@ TEST_CASE("3DQuadFacetArea", "[mesh][quad][area]") REQUIRE(areas(0, 0) == Catch::Approx(1.0)); } -TEST_CASE("SingleUVArea", "[mesh][uv][area]") +TEST_CASE("legacy::compute_facet_area SingleUVArea", "[mesh][uv][area]") { Eigen::Matrix uv; uv << 0.0, 0.0, 1.0, 0.0, 0.0, 1.0; @@ -103,7 +293,7 @@ TEST_CASE("SingleUVArea", "[mesh][uv][area]") REQUIRE(0.5 == areas[0]); } -TEST_CASE("UVArea", "[mesh][uv][area]") +TEST_CASE("legacy::compute_facet_area UVArea", "[mesh][uv][area]") { Eigen::Matrix uv; uv << 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0; @@ -119,3 +309,4 @@ TEST_CASE("UVArea", "[mesh][uv][area]") REQUIRE(-0.5 == areas[1]); REQUIRE(0.0 == areas[2]); } +#endif diff --git a/modules/core/tests/test_compute_mesh_centroid.cpp b/modules/core/tests/test_compute_mesh_centroid.cpp index 12adc6df..20533350 100644 --- a/modules/core/tests/test_compute_mesh_centroid.cpp +++ b/modules/core/tests/test_compute_mesh_centroid.cpp @@ -10,7 +10,6 @@ * governing permissions and limitations under the License. */ #include -#include #include #include @@ -22,6 +21,8 @@ #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + TEST_CASE("ComputeMeshCentroid", "[mesh][centroid]") { using namespace lagrange; @@ -73,3 +74,4 @@ TEST_CASE("ComputeMeshCentroid", "[mesh][centroid]") } // end of TEST +#endif diff --git a/modules/core/tests/test_compute_tangent_bitangent.cpp b/modules/core/tests/test_compute_tangent_bitangent.cpp index a7260853..70ce5243 100644 --- a/modules/core/tests/test_compute_tangent_bitangent.cpp +++ b/modules/core/tests/test_compute_tangent_bitangent.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include @@ -304,8 +305,8 @@ auto weld_mesh(lagrange::SurfaceMesh mesh) // results from Mikktspace implementation, we must weld our input UV and normal attributes // as pre-processing. auto legacy_mesh = lagrange::to_legacy_mesh(mesh); - condense_indexed_attribute(*legacy_mesh, "uv"); - condense_indexed_attribute(*legacy_mesh, "normal"); + condense_indexed_attribute(*legacy_mesh, std::string(lagrange::AttributeName::texcoord)); + condense_indexed_attribute(*legacy_mesh, std::string(lagrange::AttributeName::normal)); return lagrange::to_surface_mesh_copy(*legacy_mesh); }; diff --git a/modules/core/tests/test_map_attribute.cpp b/modules/core/tests/test_map_attribute.cpp index 8a2d885f..a0d9d814 100644 --- a/modules/core/tests/test_map_attribute.cpp +++ b/modules/core/tests/test_map_attribute.cpp @@ -238,10 +238,7 @@ void test_map_attribute_all() "cube_soup.obj"}; for (fs::path filename : filenames) { - fs::path input_path = lagrange::testing::get_data_path("open/core" / filename); - REQUIRE(fs::exists(input_path)); - - auto mesh = lagrange::io::load_mesh_obj(input_path.string()).mesh; + auto mesh = lagrange::testing::load_surface_mesh("open/core" / filename); test_map_attribute_types(mesh, 1); test_map_attribute_types(mesh, 4); mesh.initialize_edges(); @@ -262,9 +259,7 @@ void test_map_attribute_invalid() "cube_soup.obj"}; for (fs::path filename : filenames) { - fs::path input_path = lagrange::testing::get_data_path("open/core" / filename); - REQUIRE(fs::exists(input_path)); - auto mesh = lagrange::io::load_mesh_obj(input_path.string()).mesh; + auto mesh = lagrange::testing::load_surface_mesh("open/core" / filename); mesh.initialize_edges(); test_map_attribute_value_invalid(mesh, 1); } diff --git a/modules/core/tests/test_permute_vertices.cpp b/modules/core/tests/test_permute_vertices.cpp new file mode 100644 index 00000000..8f91934a --- /dev/null +++ b/modules/core/tests/test_permute_vertices.cpp @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif + +#include +#include + +TEST_CASE("permute_vertices", "[surface][reorder_vertices]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + using RowI = Eigen::Matrix; + + Eigen::Matrix vertices; + vertices << 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0; + + SurfaceMesh mesh; + mesh.add_vertex({vertices.row(0).data(), 3}); + mesh.add_vertex({vertices.row(1).data(), 3}); + mesh.add_vertex({vertices.row(2).data(), 3}); + mesh.add_vertex({vertices.row(3).data(), 3}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(0, 2, 3); + + SECTION("identity") + { + std::vector order{0, 1, 2, 3}; + permute_vertices(mesh, order); + REQUIRE(mesh.get_num_vertices() == 4); + + auto new_vertices = vertex_view(mesh); + auto new_facets = facet_view(mesh); + REQUIRE((new_vertices.array() == vertices.array()).all()); + REQUIRE((new_facets.row(0).array() == RowI(0, 1, 2).array()).all()); + REQUIRE((new_facets.row(1).array() == RowI(0, 2, 3).array()).all()); + lagrange::testing::check_mesh(mesh); + } + + SECTION("reverse") + { + std::vector order{3, 2, 1, 0}; + permute_vertices(mesh, order); + REQUIRE(mesh.get_num_vertices() == 4); + + auto new_vertices = vertex_view(mesh); + auto new_facets = facet_view(mesh); + REQUIRE((new_vertices.row(0).array() == vertices.row(3).array()).all()); + REQUIRE((new_vertices.row(1).array() == vertices.row(2).array()).all()); + REQUIRE((new_vertices.row(2).array() == vertices.row(1).array()).all()); + REQUIRE((new_vertices.row(3).array() == vertices.row(0).array()).all()); + REQUIRE((new_facets.row(0).array() == RowI(3, 2, 1).array()).all()); + REQUIRE((new_facets.row(1).array() == RowI(3, 1, 0).array()).all()); + lagrange::testing::check_mesh(mesh); + } + + SECTION("with attributes") + { + auto id = mesh.template create_attribute( + "vertex_index", + AttributeElement::Vertex, + AttributeUsage::Scalar); + auto data = matrix_ref(mesh.ref_attribute(id)); + data << 1, 2, 3, 4; + + std::vector order{3, 2, 1, 0}; + permute_vertices(mesh, order); + REQUIRE(mesh.get_num_vertices() == 4); + + const auto& attr = mesh.get_attribute(id); + REQUIRE(attr.get(0) == 4); + REQUIRE(attr.get(1) == 3); + REQUIRE(attr.get(2) == 2); + REQUIRE(attr.get(3) == 1); + lagrange::testing::check_mesh(mesh); + } + + SECTION("with connectivity") + { + mesh.initialize_edges(); + std::vector order{3, 2, 1, 0}; + permute_vertices(mesh, order); + + for (Index i = 0; i < 4; i++) { + Index ci = mesh.get_first_corner_around_vertex(i); + REQUIRE(mesh.get_corner_vertex(ci) == i); + } + lagrange::testing::check_mesh(mesh); + } + + SECTION("invalid permuation") + { + std::vector order{3, 2, 1, 1000}; // Index out of bound. + LA_REQUIRE_THROWS(permute_vertices(mesh, order)); + + order = {3, 2, 1, 1}; // Invalid permuation. + LA_REQUIRE_THROWS(permute_vertices(mesh, order)); + } +} + +TEST_CASE("permute_vertices benchmark", "[surface][core][permute][!benchmark]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + auto mesh = lagrange::testing::load_surface_mesh("open/core/dragon.obj"); + Index num_vertices = mesh.get_num_vertices(); + std::vector order(num_vertices); + for (Index i = 0; i < num_vertices; i++) { + order[i] = num_vertices - 1 - i; + } + + BENCHMARK("permute_vertices") + { + return permute_vertices(mesh, order); + }; + BENCHMARK("remap_vertices") + { + return remap_vertices(mesh, order); + }; +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + using VertexArray = Eigen::Matrix; + using FacetArray = Eigen::Matrix; + using MeshType = Mesh; + auto legacy_mesh = to_legacy_mesh(mesh); + BENCHMARK("reorder_mesh_vertices") + { + return reorder_mesh_vertices(*legacy_mesh, order); + }; +#endif +} diff --git a/modules/core/tests/test_remap_vertices.cpp b/modules/core/tests/test_remap_vertices.cpp new file mode 100644 index 00000000..5d6d7a7f --- /dev/null +++ b/modules/core/tests/test_remap_vertices.cpp @@ -0,0 +1,204 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include + +#include + +TEST_CASE("remap_vertices", "[core][surface][utilities]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 0}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 3); + + const Eigen::Matrix input_vertices = + vertex_view(mesh); + const Eigen::Matrix input_facets = facet_view(mesh); + + SECTION("All collapse to one") + { + std::vector old_to_new{0, 0, 0, 0}; + + SECTION("mixed") + { + remap_vertices(mesh, old_to_new); + + auto vertices = vertex_view(mesh); + REQUIRE(vertices(0) == Catch::Approx(0.25)); + REQUIRE(vertices(1) == Catch::Approx(0.25)); + REQUIRE(vertices(2) == Catch::Approx(0)); + } + + SECTION("keep first") + { + RemapVerticesOptions options; + options.collision_policy_float = RemapVerticesOptions::CollisionPolicy::KeepFirst; + remap_vertices(mesh, old_to_new, options); + + auto vertices = vertex_view(mesh); + REQUIRE(vertices(0) == Catch::Approx(0)); + REQUIRE(vertices(1) == Catch::Approx(0)); + REQUIRE(vertices(2) == Catch::Approx(0)); + } + + SECTION("average") + { + RemapVerticesOptions options; + options.collision_policy_float = RemapVerticesOptions::CollisionPolicy::Average; + remap_vertices(mesh, old_to_new, options); + + auto vertices = vertex_view(mesh); + REQUIRE(vertices(0) == Catch::Approx(0.25)); + REQUIRE(vertices(1) == Catch::Approx(0.25)); + REQUIRE(vertices(2) == Catch::Approx(0)); + } + + REQUIRE(mesh.get_num_vertices() == 1); + REQUIRE(mesh.get_num_facets() == 2); + + auto facets = facet_view(mesh); + REQUIRE(facets.maxCoeff() == 0); + REQUIRE(facets.minCoeff() == 0); + } + + SECTION("Nothing should happen") + { + std::vector old_to_new{0, 1, 2, 3}; + remap_vertices(mesh, old_to_new); + REQUIRE(mesh.get_num_vertices() == 4); + REQUIRE(mesh.get_num_facets() == 2); + + auto vertices = vertex_view(mesh); + REQUIRE(vertices == input_vertices); + auto facets = facet_view(mesh); + REQUIRE(facets == input_facets); + } + + SECTION("Only two points should remain") + { + std::vector old_to_new = {1, 1, 0, 0}; + remap_vertices(mesh, old_to_new); + REQUIRE(mesh.get_num_vertices() == 2); + REQUIRE(mesh.get_num_facets() == 2); + + auto vertices = vertex_view(mesh); + REQUIRE(vertices(0, 0) == Catch::Approx(0)); + REQUIRE(vertices(0, 1) == Catch::Approx(0.5)); + REQUIRE(vertices(0, 2) == Catch::Approx(0)); + REQUIRE(vertices(1, 0) == Catch::Approx(0.5)); + REQUIRE(vertices(1, 1) == Catch::Approx(0)); + REQUIRE(vertices(1, 2) == Catch::Approx(0)); + + auto facets = facet_view(mesh); + REQUIRE(facets(0, 0) == 1); + REQUIRE(facets(0, 1) == 1); + REQUIRE(facets(0, 2) == 0); + REQUIRE(facets(1, 0) == 0); + REQUIRE(facets(1, 1) == 1); + REQUIRE(facets(1, 2) == 0); + } + + SECTION("with edges") + { + ScopedLogLevel guard(spdlog::level::err); + mesh.initialize_edges(); + + std::vector old_to_new{0, 2, 1, 0}; + LA_REQUIRE_THROWS(remap_vertices(mesh, old_to_new)); + } + + SECTION("Invalid ordering") + { + std::vector old_to_new = {3, 3, 2, 2}; // non surjective. + LA_REQUIRE_THROWS(remap_vertices(mesh, old_to_new)); + + old_to_new = {0, 1, 2, 7}; // Exceeding bound. + LA_REQUIRE_THROWS(remap_vertices(mesh, old_to_new)); + } + + SECTION("Vertex attribute") + { + std::array indices{0, 1, 2, 3}; + auto id = mesh.create_attribute( + "vertex_index", + AttributeElement::Vertex, + AttributeUsage::VertexIndex, + 1, + indices); + + std::vector old_to_new{0, 0, 0, 0}; + + SECTION("mixed") + { + RemapVerticesOptions options; + options.collision_policy_float = RemapVerticesOptions::CollisionPolicy::Average; + options.collision_policy_integral = RemapVerticesOptions::CollisionPolicy::KeepFirst; + remap_vertices(mesh, old_to_new, options); + + auto& attr = mesh.get_attribute(id); + REQUIRE(attr.get_num_elements() == 1); + REQUIRE(attr.get(0) == 0); + } + + SECTION("keep first") + { + RemapVerticesOptions options; + options.collision_policy_float = RemapVerticesOptions::CollisionPolicy::KeepFirst; + options.collision_policy_integral = RemapVerticesOptions::CollisionPolicy::KeepFirst; + remap_vertices(mesh, old_to_new, options); + + auto& attr = mesh.get_attribute(id); + REQUIRE(attr.get_num_elements() == 1); + REQUIRE(attr.get(0) == 0); + } + + SECTION("average") + { + RemapVerticesOptions options; + options.collision_policy_float = RemapVerticesOptions::CollisionPolicy::Average; + options.collision_policy_integral = RemapVerticesOptions::CollisionPolicy::Average; + LA_REQUIRE_THROWS(remap_vertices(mesh, old_to_new, options)); + } + + SECTION("error") + { + RemapVerticesOptions options; + options.collision_policy_float = RemapVerticesOptions::CollisionPolicy::Error; + options.collision_policy_integral = RemapVerticesOptions::CollisionPolicy::Error; + LA_REQUIRE_THROWS(remap_vertices(mesh, old_to_new, options)); + + old_to_new = {3, 2, 1, 0}; + remap_vertices(mesh, old_to_new, options); + + auto& attr = mesh.get_attribute(id); + REQUIRE(attr.get_num_elements() == 4); + REQUIRE(attr.get(0) == 3); + REQUIRE(attr.get(1) == 2); + REQUIRE(attr.get(2) == 1); + REQUIRE(attr.get(3) == 0); + } + } +} diff --git a/modules/core/tests/test_surface_mesh.cpp b/modules/core/tests/test_surface_mesh.cpp index 0e764890..5cb9a12e 100644 --- a/modules/core/tests/test_surface_mesh.cpp +++ b/modules/core/tests/test_surface_mesh.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -41,285 +42,6 @@ constexpr E invalid_enum() return static_cast(std::numeric_limits::max()); } -// Check whether the function f : X --> Y restricted to the elements that maps onto Y, is -// surjective. Index.e. each element of [first, last) has an antecedent through f. -template -bool is_restriction_surjective(lagrange::span func, Index first, Index last) -{ - using namespace lagrange; - std::vector inv(last - first, invalid()); - for (Index x = 0; x < Index(func.size()); ++x) { - const Index y = func[x]; - if (y >= first && y < last) { - inv[y - first] = x; - } - } - // No element in the range [first, last) has no predecessor through the function - return std::none_of(inv.begin(), inv.end(), [&](Index y) { return y == invalid(); }); -} - -// Check whether the function f : X --> Y restricted to the elements that maps onto Y, is injective. -// Index.e. any two elements that maps onto [first, last) map to different values in [first, last). -template -bool is_restriction_injective(lagrange::span func, Index first, Index last) -{ - using namespace lagrange; - std::vector inv(last - first, invalid()); - for (Index x = 0; x < Index(func.size()); ++x) { - const Index y = func[x]; - if (y >= first && y < last) { - // Just found another x' that maps to the same value y - if (inv[y - first] != invalid() && inv[y - first] != x) { - return false; - } - inv[y - first] = x; - } - } - return true; -} - -// Check whether elements map within [first, last) -template -bool is_in_range(lagrange::span func, Index first, Index last) -{ - return std::all_of(func.begin(), func.end(), [&](Index y) { return first <= y && y < last; }); -} - -template -bool is_in_range_or_invalid(lagrange::span func, Index first, Index last) -{ - return std::all_of(func.begin(), func.end(), [&](Index y) { - return (first <= y && y < last) || y == lagrange::invalid(); - }); -} - -template -bool is_surjective(lagrange::span func, Index first, Index last) -{ - return is_in_range(func, first, last) && is_restriction_surjective(func, first, last); -} - -template -bool is_injective(lagrange::span func, Index first, Index last) -{ - return is_in_range(func, first, last) && is_restriction_injective(func, first, last); -} - -template -void check_valid(const MeshType& mesh) -{ - using namespace lagrange; - using Index = typename MeshType::Index; - const Index nv = mesh.get_num_vertices(); - const Index nf = mesh.get_num_facets(); - const Index nc = mesh.get_num_corners(); - const Index ne = mesh.get_num_edges(); - - // Ensure that (V, F) is well-formed - for (Index f = 0; f < nf; ++f) { - const Index c0 = mesh.get_facet_corner_begin(f); - const Index c1 = mesh.get_facet_corner_end(f); - REQUIRE(c0 < c1); - for (Index c = c0; c < c1; ++c) { - const Index v = mesh.get_corner_vertex(c); - REQUIRE(mesh.get_corner_facet(c) == f); - REQUIRE((v >= 0 && v < nv)); - } - } - - // Ensure that each attribute has the correct number of elements - seq_foreach_attribute_read(mesh, [&](auto&& attr) { - REQUIRE(attr.get_num_elements() == nv); - }); - seq_foreach_attribute_read(mesh, [&](auto&& attr) { - REQUIRE(attr.get_num_elements() == nf); - }); - seq_foreach_attribute_read(mesh, [&](auto&& attr) { - REQUIRE(attr.get_num_elements() == nc); - }); - - // Ensure that each element index is in range (or an invalid index) - seq_foreach_attribute_read(mesh, [&](auto&& attr) { - using AttributeType = std::decay_t; - using ValueType = typename AttributeType::ValueType; - Index n = 0; - auto usage = attr.get_usage(); - if (usage == AttributeUsage::VertexIndex) { - n = nv; - } else if (usage == AttributeUsage::FacetIndex) { - n = nf; - } else if (usage == AttributeUsage::CornerIndex) { - n = nc; - } else if (usage == AttributeUsage::EdgeIndex) { - n = ne; - } else { - return; - } - REQUIRE(std::is_same_v); - if constexpr (std::is_same_v) { - if constexpr (AttributeType::IsIndexed) { - REQUIRE(is_in_range_or_invalid(attr.values().get_all(), 0, n)); - } else { - REQUIRE(is_in_range_or_invalid(attr.get_all(), 0, n)); - } - } else { - LA_IGNORE(n); - } - }); - - // Ensure that only hybrid meshes have c <--> f attributes - if (mesh.is_hybrid()) { - REQUIRE(mesh.attr_id_facet_to_first_corner() != invalid_attribute_id()); - REQUIRE(mesh.attr_id_corner_to_facet() != invalid_attribute_id()); - } else { - REQUIRE(mesh.is_regular()); - REQUIRE(mesh.attr_id_facet_to_first_corner() == invalid_attribute_id()); - REQUIRE(mesh.attr_id_corner_to_facet() == invalid_attribute_id()); - } - - // Ensure that edge and connectivity information is well-formed - if (mesh.has_edges()) { - // Check that all facet edges have a single corresponding edge in the global indexing - auto c2e = mesh.template get_attribute(mesh.attr_id_corner_to_edge()).get_all(); - auto e2c = - mesh.template get_attribute(mesh.attr_id_edge_to_first_corner()).get_all(); - auto v2c = - mesh.template get_attribute(mesh.attr_id_vertex_to_first_corner()).get_all(); - auto next_around_edge = - mesh.template get_attribute(mesh.attr_id_next_corner_around_edge()).get_all(); - auto next_around_vertex = - mesh.template get_attribute(mesh.attr_id_next_corner_around_vertex()).get_all(); - REQUIRE(is_surjective(c2e, 0, ne)); - REQUIRE(is_injective(e2c, 0, nc)); - REQUIRE(is_in_range_or_invalid(v2c, 0, nc)); - REQUIRE(is_restriction_injective( - v2c, - 0, - nc)); // may have isolated vertices that map to invalid<>() - - // Make sure that e2c contains the name number of edges as the mesh - std::set> edges; - for (Index f = 0; f < nf; ++f) { - for (Index lv0 = 0, s = mesh.get_facet_size(f); lv0 < s; ++lv0) { - const Index lv1 = (lv0 + 1) % s; - const Index v0 = mesh.get_facet_vertex(f, lv0); - const Index v1 = mesh.get_facet_vertex(f, lv1); - edges.emplace(std::min(v0, v1), std::max(v0, v1)); - } - } - std::vector> mesh_edges; - for (Index e = 0; e < ne; ++e) { - auto v = mesh.get_edge_vertices(e); - mesh_edges.push_back({v[0], v[1]}); - } - REQUIRE(edges.size() == e2c.size()); - // Make sure we don't have edges that are not in the mesh? - edges.clear(); - for (auto c : e2c) { - const Index f = mesh.get_corner_facet(c); - const Index first_corner = mesh.get_facet_corner_begin(f); - const Index s = mesh.get_facet_size(f); - const Index lv0 = c - first_corner; - const Index lv1 = (lv0 + 1) % s; - const Index v0 = mesh.get_facet_vertex(f, lv0); - const Index v1 = mesh.get_facet_vertex(f, lv1); - edges.emplace(std::min(v0, v1), std::max(v0, v1)); - } - REQUIRE(edges.size() == e2c.size()); - // Make sure that every corner points to an edge and back to the same vertex - for (Index f = 0; f < nf; ++f) { - const Index first_corner = mesh.get_facet_corner_begin(f); - for (Index lv0 = 0, s = mesh.get_facet_size(f); lv0 < s; ++lv0) { - const Index v0 = mesh.get_facet_vertex(f, lv0); - const Index c = first_corner + lv0; - const Index e = c2e[c]; - const Index c_other = e2c[e]; - const Index v_other = mesh.get_corner_vertex(c_other); - REQUIRE(v0 == v_other); - } - } - // Check that for every vertex / every edge, the "chain" of corners around it touches - // all the incident corners exactly once (no duplicate). - std::vector> corners_around_vertex(nv); - std::vector> corners_around_edge(ne); - std::vector> facets_around_vertex(nv); - std::vector> facets_around_edge(ne); - for (Index f = 0; f < nf; ++f) { - const Index first_corner = mesh.get_facet_corner_begin(f); - for (Index lv = 0, s = mesh.get_facet_size(f); lv < s; ++lv) { - const Index v = mesh.get_facet_vertex(f, lv); - const Index c = first_corner + lv; - const Index e = c2e[c]; - REQUIRE(mesh.get_edge(f, lv) == e); - REQUIRE(mesh.get_corner_edge(c) == e); - REQUIRE(corners_around_vertex[v].count(c) == 0); - REQUIRE(corners_around_edge[e].count(c) == 0); - corners_around_vertex[v].insert(c); - corners_around_edge[e].insert(c); - facets_around_vertex[v].insert(f); - facets_around_edge[e].insert(f); - } - } - std::unordered_set corners_around; - std::unordered_set facets_around; - for (Index v = 0; v < nv; ++v) { - corners_around.clear(); - facets_around.clear(); - const Index c0 = v2c[v]; - REQUIRE(mesh.get_first_corner_around_vertex(v) == c0); - REQUIRE(mesh.get_one_corner_around_vertex(v) == c0); - for (Index ci = c0; ci != invalid(); ci = next_around_vertex[ci]) { - REQUIRE(mesh.get_next_corner_around_vertex(ci) == next_around_vertex[ci]); - REQUIRE(corners_around_vertex[v].count(ci)); - REQUIRE(corners_around.count(ci) == 0); - corners_around.insert(ci); - } - mesh.foreach_corner_around_vertex(v, [&](Index c) { - REQUIRE(corners_around.count(c)); - }); - mesh.foreach_facet_around_vertex(v, [&](Index f) { - REQUIRE(facets_around_vertex[v].count(f)); - facets_around.insert(f); - }); - REQUIRE(corners_around.size() == corners_around_vertex[v].size()); - REQUIRE(facets_around.size() == facets_around_vertex[v].size()); - REQUIRE( - corners_around.size() == - safe_cast(mesh.count_num_corners_around_vertex(v))); - } - for (Index e = 0; e < ne; ++e) { - corners_around.clear(); - facets_around.clear(); - const Index c0 = e2c[e]; - REQUIRE(mesh.get_first_corner_around_edge(e) == c0); - REQUIRE(mesh.get_one_corner_around_edge(e) == c0); - for (Index ci = c0; ci != invalid(); ci = next_around_edge[ci]) { - REQUIRE(mesh.get_next_corner_around_edge(ci) == next_around_edge[ci]); - REQUIRE(corners_around_edge[e].count(ci)); - REQUIRE(corners_around.count(ci) == 0); - corners_around.insert(ci); - } - mesh.foreach_corner_around_edge(e, [&](Index c) { REQUIRE(corners_around.count(c)); }); - Index first_facet = invalid(); - mesh.foreach_facet_around_edge(e, [&](Index f) { - REQUIRE(facets_around_edge[e].count(f)); - facets_around.insert(f); - if (first_facet == invalid()) { - first_facet = f; - } - }); - REQUIRE(mesh.get_one_facet_around_edge(e) == first_facet); - REQUIRE(corners_around.size() == corners_around_edge[e].size()); - REQUIRE(facets_around.size() == facets_around_edge[e].size()); - REQUIRE( - corners_around.size() == safe_cast(mesh.count_num_corners_around_edge(e))); - if (mesh.is_boundary_edge(e)) { - REQUIRE(corners_around.size() == 1); - } - } - } -} - // get_edge_vertices for invalid c template @@ -349,7 +71,7 @@ void test_mesh_construction() mesh.add_vertex(p3d); mesh.add_vertex({Scalar(0.1), Scalar(0.2), Scalar(0.3)}); nv = mesh.get_num_vertices(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 0; i < nv; ++i) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -359,7 +81,7 @@ void test_mesh_construction() } mesh.add_vertices(5); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = nv; i < mesh.get_num_vertices(); ++i) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -372,7 +94,7 @@ void test_mesh_construction() std::vector buffer(4 * dim); std::iota(buffer.begin(), buffer.end(), 11); mesh.add_vertices(4, buffer); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = nv, j = 0; i < mesh.get_num_vertices(); ++i, ++j) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -389,7 +111,7 @@ void test_mesh_construction() p[1] = Scalar(1.2); p[2] = Scalar(1.3); }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = nv; i < mesh.get_num_vertices(); ++i) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -413,7 +135,7 @@ void test_mesh_construction() mesh.add_vertex(p2d); mesh.add_vertex({Scalar(9.1), Scalar(9.2)}); nv = mesh.get_num_vertices(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 0; i < nv; ++i) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -422,7 +144,7 @@ void test_mesh_construction() } mesh.add_vertices(5); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = nv; i < mesh.get_num_vertices(); ++i) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -437,7 +159,7 @@ void test_mesh_construction() p[0] = Scalar(1.1); p[1] = Scalar(1.2); }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = nv; i < mesh.get_num_vertices(); ++i) { auto p = mesh.get_position(i); REQUIRE(p.size() == dim); @@ -453,7 +175,7 @@ void test_mesh_construction() mesh.add_triangle(0, 1, 2); REQUIRE(mesh.is_triangle_mesh()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); { auto f = mesh.get_facet_vertices(0); REQUIRE(f.size() == 3); @@ -467,7 +189,7 @@ void test_mesh_construction() mesh.add_quad(0, 1, 2, 3); REQUIRE(mesh.is_hybrid()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); { auto f = mesh.get_facet_vertices(1); REQUIRE(f.size() == 4); @@ -478,7 +200,7 @@ void test_mesh_construction() } mesh.add_polygon(5); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_facets() == 3); { auto f = mesh.get_facet_vertices(2); @@ -490,7 +212,7 @@ void test_mesh_construction() const Index poly[5] = {1, 2, 3, 4, 5}; mesh.add_polygon(poly); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); { auto f = mesh.get_facet_vertices(3); REQUIRE(f.size() == 5); @@ -504,7 +226,7 @@ void test_mesh_construction() f[i] = i + 2; } }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); { auto f = mesh.get_facet_vertices(4); REQUIRE(f.size() == 5); @@ -520,7 +242,7 @@ void test_mesh_construction() mesh.add_vertices(10); mesh.add_triangles(3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 0; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 3); @@ -531,7 +253,7 @@ void test_mesh_construction() const Index tri[] = {0, 1, 2, 3, 1, 2, 4, 1, 2}; mesh.add_triangles(3, tri); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 3, j = 0; i < mesh.get_num_facets(); ++i, ++j) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 3); @@ -547,7 +269,7 @@ void test_mesh_construction() f[i] = i; } }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 6; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 3); @@ -562,7 +284,7 @@ void test_mesh_construction() mesh.add_vertices(10); mesh.add_quads(3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 0; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 4); @@ -573,7 +295,7 @@ void test_mesh_construction() const Index quad[] = {0, 1, 2, 3, 4, 1, 2, 3}; mesh.add_quads(2, quad); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 3, j = 0; i < mesh.get_num_facets(); ++i, ++j) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 4); @@ -589,7 +311,7 @@ void test_mesh_construction() f[i] = i; } }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 5; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 4); @@ -604,7 +326,7 @@ void test_mesh_construction() mesh.add_vertices(10); mesh.add_polygons(3, 5); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 0; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 5); @@ -615,7 +337,7 @@ void test_mesh_construction() const Index poly[] = {0, 1, 2, 3, 4, 5, 1, 2, 3, 4}; mesh.add_polygons(2, 5, poly); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 3, j = 0; i < mesh.get_num_facets(); ++i, ++j) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 5); @@ -631,7 +353,7 @@ void test_mesh_construction() f[i] = i; } }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 5; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == 5); @@ -648,7 +370,7 @@ void test_mesh_construction() const Index sizes[] = {3, 5}; const Index indices[] = {0, 1, 3, 0, 1, 2, 3, 4}; mesh.add_hybrid(sizes); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 0, o = 0; i < mesh.get_num_facets(); ++i) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == sizes[i]); @@ -659,7 +381,7 @@ void test_mesh_construction() } mesh.add_hybrid(sizes, indices); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 2, j = 0, o = 0; i < mesh.get_num_facets(); ++i, ++j) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == sizes[j]); @@ -684,7 +406,7 @@ void test_mesh_construction() t[i] = i; } }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); for (Index i = 4, j = 0; i < mesh.get_num_facets(); ++i, ++j) { auto f = mesh.get_facet_vertices(i); REQUIRE(f.size() == j + 3); @@ -705,28 +427,28 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(20); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 20); mesh.remove_vertices({0, 1, 2}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 17); { Index v[] = {0, 1}; mesh.remove_vertices(v); } - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 15); std::vector v2{0, 1}; mesh.remove_vertices(v2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 13); std::array v3{{0, 1, 4}}; mesh.remove_vertices(v3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 10); mesh.remove_vertices([](Index v) noexcept { return (v == 0) || (v == 2); }); REQUIRE(mesh.get_num_vertices() == 8); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } // Simple removal test with hybrid storage @@ -737,30 +459,30 @@ void test_element_removal(bool with_edges) REQUIRE(mesh.has_edges()); } mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.is_regular()); mesh.add_quad(0, 1, 2, 3); REQUIRE(mesh.is_hybrid()); REQUIRE(mesh.get_num_facets() == 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); Index f[] = {1}; mesh.remove_facets(f); REQUIRE(mesh.get_num_facets() == 1); REQUIRE(mesh.is_hybrid()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); Index f2[] = {0}; mesh.remove_facets(f2); REQUIRE(mesh.is_hybrid()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); REQUIRE(mesh.is_hybrid()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } // Facet removal simple @@ -771,17 +493,17 @@ void test_element_removal(bool with_edges) REQUIRE(mesh.has_edges()); } mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_facets({1}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_facets() == 3); } @@ -790,25 +512,25 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); const Index num_corners = 3 + 4 + 3 + 5; auto id = mesh.template create_attribute("color", lagrange::AttributeElement::Corner); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); auto& attr = mesh.template ref_attribute(id); std::iota(attr.ref_all().begin(), attr.ref_all().end(), 123); REQUIRE(mesh.get_num_facets() == 4); mesh.remove_facets([](Index f) noexcept { return f == 1; }); REQUIRE(mesh.get_num_facets() == 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(attr.get_num_elements() == num_corners - 4); // Ensure corner attributes are properly shifted for (Index c = 3, i = 7; c < attr.get_num_elements(); ++c, ++i) { @@ -821,41 +543,41 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); LA_REQUIRE_THROWS(mesh.remove_vertices({1, 5, 2})); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); LA_REQUIRE_THROWS(mesh.remove_vertices({1, 5, 100})); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); LA_REQUIRE_THROWS(mesh.remove_facets({2, 1})); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); LA_REQUIRE_THROWS(mesh.remove_facets({0, 1, 100})); } @@ -864,17 +586,17 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_facets(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_facets() == 0); REQUIRE(mesh.get_num_corners() == 0); REQUIRE(mesh.get_num_vertices() == 10); @@ -882,13 +604,13 @@ void test_element_removal(bool with_edges) mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); mesh.clear_vertices(); REQUIRE(mesh.get_num_facets() == 0); @@ -902,14 +624,14 @@ void test_element_removal(bool with_edges) if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); mesh.remove_vertices([](Index) noexcept { return false; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 10); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); mesh.remove_vertices({}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 10); } @@ -918,33 +640,33 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_facets([](Index) noexcept { return false; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_facets() == 4); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_facets({}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_facets() == 4); } @@ -953,34 +675,34 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices([](Index v) noexcept { return v == 3; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 9); REQUIRE(mesh.get_num_facets() == 2); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices({3}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 9); REQUIRE(mesh.get_num_facets() == 2); } @@ -990,34 +712,34 @@ void test_element_removal(bool with_edges) MeshType mesh; if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(2, 3, 4); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices([](Index v) noexcept { return v == 3; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 9); REQUIRE(mesh.get_num_facets() == 2); mesh = MeshType(); if (with_edges) mesh.initialize_edges(); mesh.add_vertices(10); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_quad(0, 1, 2, 3); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_triangle(0, 1, 2); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.add_polygon({0, 1, 2, 3, 4}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices({3}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.get_num_vertices() == 9); REQUIRE(mesh.get_num_facets() == 2); } @@ -1092,7 +814,7 @@ void test_mesh_storage() for (Index f = 0; f < num_facets; ++f) { REQUIRE(mesh.get_facet_size(f) == facet_size); } - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.is_regular()); } } @@ -1116,7 +838,7 @@ void test_mesh_storage() for (Index f = 0; f < num_facets; ++f) { REQUIRE(mesh.get_facet_size(f) == f + facet_size); } - check_valid(mesh); + lagrange::testing::check_mesh(mesh); REQUIRE(mesh.is_hybrid()); } } @@ -1393,7 +1115,7 @@ void test_mesh_attribute() REQUIRE(mesh.has_attribute("normals_i")); REQUIRE(mesh.has_attribute("normals_x")); REQUIRE(mesh.template is_attribute_type("normals_v0")); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); auto attr_v0 = mesh.template ref_attribute("normals_v0").get_all(); REQUIRE(attr_v0.size() == mesh.get_num_vertices() * mesh.get_dimension()); @@ -1645,7 +1367,7 @@ void test_wrap_attribute_special() REQUIRE(mesh.get_corner_to_vertex().get_all().data() == corner_to_vertex.data()); REQUIRE(mesh.get_num_facets() == num_facets); REQUIRE(mesh.is_triangle_mesh()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Wrap buffer as const facets mesh.ref_corner_to_vertex().create_internal_copy(); @@ -1655,7 +1377,7 @@ void test_wrap_attribute_special() REQUIRE(mesh.get_num_facets() == num_facets); REQUIRE(mesh.is_triangle_mesh()); LA_REQUIRE_THROWS(mesh.ref_facet_vertices(0)); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } { @@ -1673,7 +1395,7 @@ void test_wrap_attribute_special() .data() == offsets.data()); REQUIRE(mesh.get_num_facets() == num_facets); REQUIRE(mesh.is_hybrid()); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Wrap as const facets + indices mesh = SurfaceMesh(); @@ -1682,7 +1404,7 @@ void test_wrap_attribute_special() REQUIRE(mesh.get_num_facets() == num_facets); REQUIRE(mesh.is_hybrid()); LA_REQUIRE_THROWS(mesh.ref_facet_vertices(0)); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } } @@ -2016,7 +1738,7 @@ void test_resize_attribute_basic() mesh.template create_attribute("corner", AttributeElement::Corner); mesh.template create_attribute("value", AttributeElement::Value); mesh.template create_attribute("indexed", AttributeElement::Indexed); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); const Index num_vertices = 10; const Index num_facets = 6; @@ -2033,7 +1755,7 @@ void test_resize_attribute_basic() REQUIRE( mesh.template get_indexed_attribute("indexed").indices().get_num_elements() == num_corners); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } { @@ -2062,7 +1784,7 @@ void test_resize_attribute_basic() REQUIRE( mesh.template get_indexed_attribute("indexed").indices().get_num_elements() == num_corners); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } // TODO: Wrap + resize other attr @@ -2099,7 +1821,7 @@ void test_edit_facets_with_edges() mesh.add_triangles(3, [](Index, span t) { std::fill(t.begin(), t.end(), Index(0)); }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } { // Add quad @@ -2107,7 +1829,7 @@ void test_edit_facets_with_edges() const Index indices[] = {0, 1, 2, 3, 0, 1, 2, 3}; mesh.add_quads(2, indices); mesh.add_quads(3, [](Index, span t) { std::fill(t.begin(), t.end(), Index(0)); }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } { // Add polygon @@ -2119,7 +1841,7 @@ void test_edit_facets_with_edges() mesh.add_polygons(3, 5, [](Index, span t) { std::fill(t.begin(), t.end(), Index(0)); }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } { // Add hybrid @@ -2132,7 +1854,7 @@ void test_edit_facets_with_edges() [](Index, lagrange::span t) noexcept { std::fill(t.begin(), t.end(), Index(0)); }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } } @@ -2627,7 +2349,7 @@ void test_element_index_resize() mesh.add_triangle(5, 6, 7); mesh.add_triangle(6, 7, 8); mesh.initialize_edges(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); auto vid = mesh.template create_attribute( "vid", @@ -2661,7 +2383,7 @@ void test_element_index_resize() "eid_i", AttributeElement::Indexed, AttributeUsage::EdgeIndex); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Initialize attribute values { @@ -2683,21 +2405,21 @@ void test_element_index_resize() auto eattr_i = mesh.template ref_indexed_attribute(eid_i).values().ref_all(); std::iota(eattr_i.begin(), eattr_i.end(), 0); } - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Resize attr mesh.remove_facets({1}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices({5}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Clear mesh mesh.clear_facets(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_vertices(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_edges(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } template @@ -2731,7 +2453,7 @@ void test_resize_attribute_type() mesh.add_triangle(5, 6, 7); mesh.add_triangle(6, 7, 8); mesh.initialize_edges(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); auto vid = mesh.template create_attribute("vid", AttributeElement::Vertex); auto fid = mesh.template create_attribute("fid", AttributeElement::Facet); @@ -2739,7 +2461,7 @@ void test_resize_attribute_type() auto eid = mesh.template create_attribute("eid", AttributeElement::Edge); auto iid = mesh.template create_attribute("iid", AttributeElement::Indexed); auto xid = mesh.template create_attribute("xid", AttributeElement::Value); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Ground truth copy of each attributes std::vector vgt; @@ -2775,7 +2497,7 @@ void test_resize_attribute_type() egt.assign(eattr.begin(), eattr.end()); igt.assign(attr_indices.begin(), attr_indices.end()); } - check_valid(mesh); + lagrange::testing::check_mesh(mesh); auto check_all_attr = [&]() { check_attr(mesh, fid, fgt); @@ -2816,29 +2538,29 @@ void test_resize_attribute_type() // Resize attr remove_facets_from_ground_truth([](Index f) noexcept { return f == 1; }); mesh.remove_facets({1}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); check_all_attr(); remove_facets_from_ground_truth([](Index f) noexcept { return f == 2; }); mesh.remove_facets([](Index f) noexcept { return f == 2; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); check_all_attr(); remove_vertices_from_ground_truth([](Index v) noexcept { return v == 6; }); mesh.remove_vertices([](Index v) noexcept { return v == 6; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); remove_vertices_from_ground_truth([](Index v) noexcept { return v == 5; }); mesh.remove_vertices({5}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Clear mesh mesh.clear_facets(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_vertices(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_edges(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } { @@ -2851,7 +2573,7 @@ void test_resize_attribute_type() mesh.add_triangle(5, 6, 7); mesh.add_triangle(6, 7, 8); mesh.initialize_edges(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); auto vid = mesh.template create_attribute("vid", AttributeElement::Vertex); auto fid = mesh.template create_attribute("fid", AttributeElement::Facet); @@ -2859,7 +2581,7 @@ void test_resize_attribute_type() auto eid = mesh.template create_attribute("eid", AttributeElement::Edge); auto iid = mesh.template create_attribute("iid", AttributeElement::Indexed); auto xid = mesh.template create_attribute("xid", AttributeElement::Value); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Initialize attribute values { @@ -2876,28 +2598,28 @@ void test_resize_attribute_type() auto xattr = mesh.template ref_attribute(xid).ref_all(); std::iota(xattr.begin(), xattr.end(), ValueType(0)); } - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // TODO: Implement "ground truth" attributes & remapping similar to the regular triangle // mesh (more complicated due to corner offsets). // Resize attr mesh.remove_facets({1}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_facets([](Index f) noexcept { return f == 2; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices([](Index v) noexcept { return v == 6; }); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.remove_vertices({5}); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); // Clear mesh mesh.clear_facets(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_vertices(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); mesh.clear_edges(); - check_valid(mesh); + lagrange::testing::check_mesh(mesh); } } @@ -3124,3 +2846,25 @@ TEST_CASE("SurfaceMesh: Shrink And Compress", "[next]") mesh.shrink_to_fit(); mesh.compress_if_regular(); } + +TEST_CASE("SurfaceMesh: sanity check", "[next]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + // Simple cube: + // 3 +---+ 2 + // | / | + // 0 +---+ 1 + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({1, 1, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(0, 2, 3); + mesh.initialize_edges(); + + lagrange::testing::check_mesh(mesh); +} diff --git a/modules/core/tests/test_triangulate_polygonal_facets.cpp b/modules/core/tests/test_triangulate_polygonal_facets.cpp index d0954204..11c3a533 100644 --- a/modules/core/tests/test_triangulate_polygonal_facets.cpp +++ b/modules/core/tests/test_triangulate_polygonal_facets.cpp @@ -60,11 +60,7 @@ void test_basic() for (fs::path filename : filenames) { lagrange::logger().debug("Input path: {}", ("open/core" / filename).string()); - fs::path input_path = lagrange::testing::get_data_path("open/core" / filename); - REQUIRE(fs::exists(input_path)); - - auto output = lagrange::io::load_mesh_obj(input_path.string()); - auto mesh = std::move(output.mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core" / filename); lagrange::logger().debug( "Loaded mesh with {} vertices and {} facets", mesh.get_num_vertices(), @@ -117,12 +113,10 @@ void test_2d() for (fs::path filename : filenames) { lagrange::logger().debug("Input path: {}", ("open/core" / filename).string()); - fs::path input_path = lagrange::testing::get_data_path("open/core" / filename); - REQUIRE(fs::exists(input_path)); auto mesh = [&] { // TODO: Write utils to go from 2d to 3d, and vice-versa (while preserving attributes) - auto mesh_3d = lagrange::io::load_mesh_obj(input_path.string()).mesh; + auto mesh_3d = lagrange::testing::load_surface_mesh("open/core" / filename); MeshType mesh_2d(2); mesh_2d.add_vertices(mesh_3d.get_num_vertices()); vertex_ref(mesh_2d) = vertex_view(mesh_3d).leftCols(2); @@ -147,17 +141,7 @@ void test_2d() // Triangulation does not insert new vertices REQUIRE(mesh.get_num_vertices() == old_num_vertices); - - // Because we edit the mesh in place, mesh.is_triangle_mesh() will *not* return true. Once - // we implement the method SurfaceMesh::compress_if_regular(), we should be able to use it - // instead. - bool all_triangles = true; - for (Index f = 0; f < mesh.get_num_facets(); ++f) { - if (mesh.get_facet_size(f) != 3) { - all_triangles = false; - } - } - REQUIRE(all_triangles); + REQUIRE(mesh.is_triangle_mesh()); lagrange::logger().debug( "Mesh after triangulation has {} vertices and {} facets", @@ -181,11 +165,7 @@ void test_triangle() fs::path filename = "bunny_simple.obj"; - fs::path input_path = lagrange::testing::get_data_path("open/core" / filename); - REQUIRE(fs::exists(input_path)); - - auto output = lagrange::io::load_mesh_obj(input_path.string()); - auto mesh = std::move(output.mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core" / filename); lagrange::logger().debug( "Loaded mesh with {} vertices and {} facets", mesh.get_num_vertices(), @@ -220,12 +200,9 @@ void test_attributes() fs::path filename = "poly/mixedFaringPart.obj"; - fs::path input_path = lagrange::testing::get_data_path("open/core" / filename); - REQUIRE(fs::exists(input_path)); - auto mesh = [&] { LAGRANGE_ZONE_SCOPED - return lagrange::io::load_mesh_obj(input_path.string()).mesh; + return lagrange::testing::load_surface_mesh("open/core" / filename); }(); lagrange::logger().debug( "Loaded mesh with {} vertices and {} facets", diff --git a/modules/core/tests/test_utils_hash.cpp b/modules/core/tests/test_utils_hash.cpp new file mode 100644 index 00000000..64df818d --- /dev/null +++ b/modules/core/tests/test_utils_hash.cpp @@ -0,0 +1,112 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include + +using namespace lagrange; + +namespace { + +template +void init_key(Key& k, const U& u, const V& v) +{ + k = Key(u, v); +} + +template +void init_key(T*& k, const T& u, const T& v) +{ + k = new T[2]; + k[0] = u; + k[1] = v; +} + +template +void init_key(std::array& k, const T& u, const T& v) +{ + k[0] = u; + k[1] = v; +} + +template +void destroy_key(Key&) +{} + +template +void destroy_key(T* k) +{ + delete[] k; +} + +} // namespace + +template +void test_ordered() +{ + Key k0, k1; + init_key(k0, 0, 1); + init_key(k1, 1, 0); + OrderedPairHash hasher; + auto h0 = hasher(k0); + auto h1 = hasher(k1); + REQUIRE(h0 != h1); // hash collisions should be vanishingly improbable + + std::unordered_set> s; + s.insert(k0); + s.insert(k1); + s.insert(k0); // double insert on purpose + + REQUIRE(s.size() == 2); + REQUIRE(s.find(k0) != s.end()); + REQUIRE(s.find(k1) != s.end()); + + Key k2, k3; + init_key(k2, 0, 0); + init_key(k3, 1, 1); + REQUIRE(s.find(k2) == s.end()); + REQUIRE(s.find(k3) == s.end()); + + destroy_key(k0); + destroy_key(k1); + destroy_key(k2); + destroy_key(k3); +} + +TEST_CASE("hash", "[hash]") +{ + SECTION("ordered-pair") + { + logger().info("Testing hashes of ordered std::pair"); + test_ordered>(); // warning if cross-comparison + } + + SECTION("ordered-array") + { + logger().info("Testing hashes of ordered std::array"); + test_ordered>(); + } + + SECTION("ordered-eigen-col-vector") + { + logger().info("Testing hashes of ordered Eigen 2D column vectors"); + test_ordered>(); + } + + SECTION("ordered-eigen-row-vector") + { + logger().info("Testing hashes of ordered Eigen 2D row vectors"); + test_ordered>(); + } +} diff --git a/modules/fs/CMakeLists.txt b/modules/fs/CMakeLists.txt index 7f07d564..840f90d1 100644 --- a/modules/fs/CMakeLists.txt +++ b/modules/fs/CMakeLists.txt @@ -19,7 +19,7 @@ target_link_libraries(lagrange_fs PUBLIC lagrange::core ) -set(LAGRANGE_FS_BACKEND "ghc" +set(LAGRANGE_FS_BACKEND "std" CACHE STRING "Which file system backend to use? Options are 'std', 'ghc' and 'boost'. Default is 'ghc'" ) @@ -29,7 +29,7 @@ set_property(CACHE LAGRANGE_FS_BACKEND PROPERTY STRINGS ${LAGRANGE_FS_BACKEND_CH string(TOLOWER ${LAGRANGE_FS_BACKEND} LAGRANGE_FS_BACKEND) if (LAGRANGE_FS_BACKEND STREQUAL "ghc") include(filesystem) - target_link_libraries(lagrange_fs PUBLIC filesystem::filesystem) + target_link_libraries(lagrange_fs PUBLIC ghcFilesystem::ghc_filesystem) target_compile_definitions(lagrange_fs PUBLIC LAGRANGE_USE_GHC_FS) elseif (LAGRANGE_FS_BACKEND STREQUAL "std") target_compile_features(lagrange_fs PUBLIC cxx_std_17) diff --git a/modules/io/CMakeLists.txt b/modules/io/CMakeLists.txt index 1c683b92..d3ecac08 100644 --- a/modules/io/CMakeLists.txt +++ b/modules/io/CMakeLists.txt @@ -14,15 +14,20 @@ lagrange_add_module() # 2. dependencies -lagrange_include_modules(core fs) +lagrange_include_modules(core fs scene) include(tinyobjloader) include(libigl) # TODO: remove libigl later include(mshio) +include(happly) +include(tinygltf) target_link_libraries(lagrange_io PUBLIC lagrange::core lagrange::fs + lagrange::scene tinyobjloader::tinyobjloader + tinygltf::tinygltf + happly::happly igl::core PRIVATE mshio::mshio @@ -45,3 +50,7 @@ endif() if(LAGRANGE_UNIT_TESTS) add_subdirectory(tests) endif() + +if(LAGRANGE_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/modules/io/examples/CMakeLists.txt b/modules/io/examples/CMakeLists.txt new file mode 100644 index 00000000..7f295a7e --- /dev/null +++ b/modules/io/examples/CMakeLists.txt @@ -0,0 +1,15 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +include(cli11) + +lagrange_add_example(mesh_convert mesh_convert.cpp) +target_link_libraries(mesh_convert lagrange::core lagrange::io CLI11::CLI11) diff --git a/modules/io/examples/mesh_convert.cpp b/modules/io/examples/mesh_convert.cpp new file mode 100644 index 00000000..f6d54c7e --- /dev/null +++ b/modules/io/examples/mesh_convert.cpp @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include +#include +#include +#include +#include + +#include + +int main(int argc, char** argv) +{ + struct + { + std::string input; + std::string output; + } args; + + CLI::App app{argv[0]}; + app.add_option("input", args.input, "Input mesh.")->required()->check(CLI::ExistingFile); + app.add_option("output", args.output, "Output mesh.")->required(); + CLI11_PARSE(app, argc, argv) + + lagrange::logger().set_level(spdlog::level::trace); + + using SceneType = lagrange::scene::SimpleScene32f3; + using MeshType = typename SceneType::MeshType; + + // Load scene + lagrange::logger().info("Loading input scene: {}", args.input); + auto scene = lagrange::io::load_simple_scene(args.input); + lagrange::logger().info( + "Input scene has {} meshes and {} instances", + scene.get_num_meshes(), + scene.compute_num_instances()); + + // Combine meshes + std::vector meshes; + scene.foreach_instances([&](auto instance) { + MeshType mesh = scene.get_mesh(instance.mesh_index); + auto vertices = vertex_ref(mesh); + vertices = (instance.transform * vertices.leftCols<3>().transpose()).transpose(); + if (instance.transform.linear().determinant() < 0) { + // TODO: We must flip facets due to transform with negative scale + } + meshes.emplace_back(std::move(mesh)); + }); + + lagrange::logger().info("Combining {} meshes together", meshes.size()); + auto mesh = lagrange::combine_meshes(lagrange::span(meshes)); + + lagrange::logger().info("Saving output mesh: {}", args.output); + lagrange::io::save_mesh(args.output, mesh); + + return 0; +} diff --git a/modules/io/include/lagrange/io/internal/load_assimp.h b/modules/io/include/lagrange/io/internal/load_assimp.h new file mode 100644 index 00000000..fe745586 --- /dev/null +++ b/modules/io/include/lagrange/io/internal/load_assimp.h @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#ifdef LAGRANGE_WITH_ASSIMP + +#include +#include +#include + +#include +#include +#include +#include + +namespace lagrange::io::internal { +/** + * Load an assimp scene. + */ +std::unique_ptr load_assimp( + const fs::path& filename, + unsigned int flags = aiProcess_JoinIdenticalVertices | aiProcess_CalcTangentSpace | + aiProcess_GenUVCoords | aiProcess_PopulateArmatureData); + +/** + * Convert an assimp mesh to a lagrange SurfaceMesh + */ +template +MeshType convert_mesh_assimp_to_lagrange(const aiMesh& mesh, const LoadOptions& options = {}); + +/** + * Load a mesh using assimp. If the scene contains multiple meshes, they will be combined into one. + */ +template +MeshType load_mesh_assimp(const aiScene& scene, const LoadOptions& options = {}); + +/** + * Load a simple scene with assimp. + */ +template +SceneType load_simple_scene_assimp(const aiScene& scene, const LoadOptions& options = {}); + +} + +#endif diff --git a/modules/io/include/lagrange/io/internal/load_gltf.h b/modules/io/include/lagrange/io/internal/load_gltf.h new file mode 100644 index 00000000..c8cd31e3 --- /dev/null +++ b/modules/io/include/lagrange/io/internal/load_gltf.h @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace lagrange::io::internal { + +/** + * Load a gltf model. + */ +tinygltf::Model load_tinygltf(const fs::path& filename); + +/** + * Convert a gltf mesh to a lagrange SurfaceMesh + */ +template +MeshType convert_mesh_tinygltf_to_lagrange( + const tinygltf::Model& model, + const tinygltf::Mesh& mesh, + const LoadOptions& options = {}); + +/** + * Load a mesh using gltf. If the scene contains multiple meshes, they will be merged into one. + */ +template +MeshType load_mesh_gltf(const tinygltf::Model& model, const LoadOptions& options = {}); + +/** + * Load a simple scene with gltf. + */ +template +SceneType load_simple_scene_gltf(const tinygltf::Model& model, const LoadOptions& options = {}); + +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/internal/load_obj.h b/modules/io/include/lagrange/io/internal/load_obj.h new file mode 100644 index 00000000..a02e41a3 --- /dev/null +++ b/modules/io/include/lagrange/io/internal/load_obj.h @@ -0,0 +1,61 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +#include + +namespace lagrange::io { +namespace internal { + +/** + * Output of the obj mesh loader + * + * @tparam Scalar Mesh scalar type + * @tparam Index Mesh index type + */ +template +struct ObjReaderResult +{ + /// Whether the load operation was successful. + bool success = true; + + /// Aggregated mesh containing all elements in the .obj file. To separate the different + /// entities, split the mesh facets based on object ids. + SurfaceMesh mesh; + + /// Materials associated with the mesh. + std::vector materials; + + /// Names of each object in the aggregate mesh. + std::vector names; +}; + +/** + * Loads a .obj mesh from a file. + * + * @param[in] filename The file to read data from. + * @param[in] options Optional configuration for the loader. + * + * @tparam MeshType Mesh type to load. + * + * @return Result of the load. + */ +template +auto load_mesh_obj(const fs::path& filename, const LoadOptions& options = {}) + -> ObjReaderResult; + +} // namespace internal +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/legacy/load_mesh.h b/modules/io/include/lagrange/io/legacy/load_mesh.h new file mode 100644 index 00000000..7aabeef0 --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/load_mesh.h @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +/* + * This file does not contain function definition because those require other expensive headers + * and we want to minimize the size of this header, as it's used in almost all tests. + * + * If you have any issues with a load_mesh function not being defined for your specific + * mesh type, you can #include instead. + */ + +#include +#include +#include +#include + +#include +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +template +std::unique_ptr load_mesh_basic(const fs::path& filename); + +template +std::vector> load_obj_meshes(const fs::path& filename); + +template +std::unique_ptr load_obj_mesh(const fs::path& filename); + +template < + typename MeshType, + std::enable_if_t::value>* = nullptr> +std::unique_ptr load_mesh(const fs::path& filename); + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/legacy/load_mesh.impl.h b/modules/io/include/lagrange/io/legacy/load_mesh.impl.h new file mode 100644 index 00000000..0b8dc022 --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/load_mesh.impl.h @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +template +std::unique_ptr load_mesh_basic(const fs::path& filename) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + typename MeshType::VertexArray vertices; + typename MeshType::FacetArray facets; + + igl::read_triangle_mesh(filename.string(), vertices, facets); + + return create_mesh(vertices, facets); +} +extern template std::unique_ptr load_mesh_basic(const fs::path&); +extern template std::unique_ptr load_mesh_basic(const fs::path&); + +template +std::vector> load_obj_meshes(const fs::path& filename) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + if (!std::is_same::value) { + logger().warn("Tinyobjloader compiled to use float, loss of precision may occur."); + } + + return load_mesh_ext(filename).meshes; +} +extern template std::vector> load_obj_meshes(const fs::path&); +extern template std::vector> load_obj_meshes(const fs::path&); + +template +std::unique_ptr load_obj_mesh(const fs::path& filename) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + auto res = load_obj_meshes(filename); + if (res.empty()) { + return nullptr; + } else if (res.size() == 1) { + return std::move(res[0]); + } else { + logger().debug("Combining {} meshes into one.", res.size()); + return combine_mesh_list(res, true); + } +} +extern template std::unique_ptr load_obj_mesh(const fs::path&); +extern template std::unique_ptr load_obj_mesh(const fs::path&); + +template < + typename MeshType, + std::enable_if_t::value>* /*= nullptr*/> +std::unique_ptr load_mesh(const fs::path& filename) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + if (filename.extension() == ".obj") { + return load_obj_mesh(filename); + } else if (filename.extension() == ".ply") { + return load_mesh_ply(filename); + } else { + return load_mesh_basic(filename); + } +} +extern template std::unique_ptr load_mesh(const fs::path&); +extern template std::unique_ptr load_mesh(const fs::path&); + + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/legacy/load_mesh_assimp.h b/modules/io/include/lagrange/io/legacy/load_mesh_assimp.h new file mode 100644 index 00000000..485c0cfd --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/load_mesh_assimp.h @@ -0,0 +1,207 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#ifdef LAGRANGE_WITH_ASSIMP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +// ==== Declarations ==== +std::unique_ptr load_scene_assimp(const lagrange::fs::path& filename); +std::unique_ptr load_scene_assimp_from_memory(const void* buffer, size_t size); + +template ::value>* = nullptr> +std::vector> load_mesh_assimp(const lagrange::fs::path& filename); +template +std::vector> load_mesh_assimp_from_memory( + const void* buffer, + size_t size); + +template +std::vector> extract_meshes_assimp(const aiScene* scene); +template +std::unique_ptr convert_mesh_assimp(const aiMesh* mesh); + +inline std::unique_ptr load_scene_assimp(const lagrange::fs::path& filename) +{ + Assimp::Importer importer; + const aiScene* scene = importer.ReadFile(filename.string(), 0); + if (!scene) { + logger().error("Error loading scene: {}", importer.GetErrorString()); + return nullptr; + } + + return std::unique_ptr(importer.GetOrphanedScene()); +} + +inline std::unique_ptr load_scene_assimp_from_memory(const void* buffer, size_t size) +{ + Assimp::Importer importer; + + // Note: we are asking assimp to triangulate the scene for us. + // Change this after we add support for n-gons. + const aiScene* scene = importer.ReadFileFromMemory(buffer, size, aiProcess_Triangulate); + + if (!scene) { + logger().error("Error loading scene: {}", importer.GetErrorString()); + return nullptr; + } + + return std::unique_ptr(importer.GetOrphanedScene()); +} + +template ::value>* /* = nullptr */> +std::vector> load_mesh_assimp(const lagrange::fs::path& filename) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + std::unique_ptr scene = load_scene_assimp(filename); + + return extract_meshes_assimp(scene.get()); +} + +template +std::vector> load_mesh_assimp_from_memory(const void* buffer, size_t size) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + std::unique_ptr scene = load_scene_assimp_from_memory(buffer, size); + + return extract_meshes_assimp(scene.get()); +} + +template +std::vector> extract_meshes_assimp(const aiScene* scene) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + std::vector> ret; + if (!scene) { + return ret; + } + for (unsigned int i = 0; i < scene->mNumMeshes; ++i) { + const aiMesh* mesh = scene->mMeshes[i]; + ret.emplace_back(convert_mesh_assimp(mesh)); + } + return ret; +} + +template +std::unique_ptr convert_mesh_assimp(const aiMesh* mesh) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + using VertexArray = typename MeshType::VertexArray; + using FacetArray = typename MeshType::FacetArray; + + // Check that all facets have the same size + unsigned int nvpf = 0; + bool triangulate = false; + for (unsigned int j = 0; j < mesh->mNumFaces; ++j) { + const aiFace& face = mesh->mFaces[j]; + if (nvpf == 0) { + nvpf = face.mNumIndices; + } else if (face.mNumIndices != nvpf) { + logger().warn("Facets with varying number of vertices detected, triangulating"); + nvpf = 3; + triangulate = true; + break; + } + } + if (FacetArray::ColsAtCompileTime != Eigen::Dynamic && FacetArray::ColsAtCompileTime != nvpf) { + logger().warn( + "FacetArray cannot hold facets with n!={} vertex per facet, triangulating", + FacetArray::ColsAtCompileTime); + triangulate = true; + nvpf = 3; + } + + // If triangulating a heterogeneous mesh, we need to count the number of facets + unsigned int num_output_facets = mesh->mNumFaces; + if (triangulate) { + num_output_facets = 0; + for (unsigned int j = 0; j < mesh->mNumFaces; ++j) { + const aiFace& face = mesh->mFaces[j]; + num_output_facets += face.mNumIndices - 2; + } + } + + VertexArray vertices(mesh->mNumVertices, 3); // should we support arbitrary dimension? + for (unsigned int j = 0; j < mesh->mNumVertices; ++j) { + const aiVector3D& vec = mesh->mVertices[j]; + vertices(j, 0) = vec.x; + vertices(j, 1) = vec.y; + vertices(j, 2) = vec.z; + } + + FacetArray faces(num_output_facets, nvpf); + for (unsigned int j = 0, f = 0; j < mesh->mNumFaces; ++j) { + const aiFace& face = mesh->mFaces[j]; + if (triangulate) { + for (unsigned int k = 2; k < face.mNumIndices; ++k) { + faces.row(f++) << face.mIndices[0], face.mIndices[k - 1], face.mIndices[k]; + } + } else { + for (unsigned int k = 0; k < face.mNumIndices; ++k) { + faces(j, k) = face.mIndices[k]; + } + } + assert(f <= num_output_facets); + } + + auto lagrange_mesh = create_mesh(std::move(vertices), std::move(faces)); + + if (mesh->HasTextureCoords(0)) { + typename MeshType::UVArray uvs(mesh->mNumVertices, 2); + typename MeshType::UVIndices uv_indices = lagrange_mesh->get_facets(); + + assert(mesh->GetNumUVChannels() == 1); // assume one UV channel + assert(mesh->mNumUVComponents[0] == 2); // assume two components (uv), not 3d uvw + for (unsigned int j = 0; j < mesh->mNumVertices; ++j) { + const aiVector3D& vec = mesh->mTextureCoords[0][j]; + uvs(j, 0) = vec.x; + uvs(j, 1) = vec.y; + } + + // uv indices are the same as facets + + lagrange_mesh->initialize_uv(std::move(uvs), std::move(uv_indices)); + map_indexed_attribute_to_corner_attribute(*lagrange_mesh, "uv"); + } + + return lagrange_mesh; +} + +} // namespace legacy +} // namespace lagrange::io + +#endif diff --git a/modules/io/include/lagrange/io/legacy/load_mesh_ext.h b/modules/io/include/lagrange/io/legacy/load_mesh_ext.h new file mode 100644 index 00000000..8a126d54 --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/load_mesh_ext.h @@ -0,0 +1,476 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +struct MeshLoaderParams +{ + /// When loading a mesh with mixed facet sizes, this parameter controls whether the polygonal + /// faces will be triangulated, or left as is and padded with INVALID. Note that if the mesh has + /// a compile-time face size, and cannot accommodate the maximum face size of the input mesh, + /// then it will be triangulated as well. If the input mesh has constant face size (e.g. a quad + /// mesh), and the mesh type can accommodate such facet type, then faces will also not be + /// triangulated, regardless of this parameter value. + bool triangulate = false; + + /// Normalize each object to a unit box around the origin. + bool normalize = false; + + bool load_normals = true; + bool load_uvs = true; + bool load_materials = true; + + /// Combines individual objects into a single mesh. Result contains a vector of size 1. + bool as_one_mesh = false; +}; + +template +struct MeshLoaderResult +{ + bool success = true; + std::vector> meshes; + std::vector materials; + std::vector mesh_names; +}; + +inline int fix_index(int index, size_t n, size_t global_offset) +{ + // Shifted absolute index + if (index > 0) return index - static_cast(global_offset) - 1; + // Relative index + if (index < 0) return static_cast(n) + index - static_cast(global_offset); + // 0 index is invalid in obj files + return -1; +} + +/// +/// Loads a .obj mesh from a stream. +/// +/// @param[in,out] input_stream The stream to read data from. +/// @param[in] params Optional parameters for the loader. +/// @param[in] material_reader Optional pointer to the material reader class. +/// +/// @tparam MeshType Mesh type. +/// +/// @return Result of the load. +/// +template +auto load_mesh_ext( + std::istream& input_stream, + const MeshLoaderParams& params = {}, + tinyobj::MaterialReader* material_reader = nullptr) -> MeshLoaderResult +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + using VertexArray = typename MeshType::VertexArray; + using FacetArray = typename MeshType::FacetArray; + + MeshLoaderResult result; + + if (!input_stream.good()) { + logger().error("Invalid input stream given"); + result.success = false; + return result; + } + + /// State of the loading state machine + /// Passed through tinyobj as void* user data (through function pointers) + struct Loader + { + Loader(const MeshLoaderParams& _params, MeshLoaderResult& _result) + : params(_params) + , result(_result) + {} + + const MeshLoaderParams& params; + MeshLoaderResult& result; + + std::vector vertices; + std::vector normals; + std::vector uvs; + std::vector indices; + std::vector material_ids; + + size_t vertex_offset = 0; + size_t normal_offset = 0; + size_t uv_offset = 0; + + std::string object_name = ""; + int current_material_id = 0; + + std::vector face_sizes; + int max_face_size = 0; + bool is_face_size_constant = false; + bool is_last_object = false; + + void triangulate() + { + std::vector new_indices; + std::vector new_face_sizes; + std::vector new_material_ids; + size_t indices_i = 0; + + // Loop over faces + for (auto face_index : range(face_sizes.size())) { + const auto vertex_in_face_num = face_sizes[face_index]; + const auto new_triangle_num = (vertex_in_face_num - 3) + 1; + + // Triangle fan conversion + for (auto i : range(new_triangle_num)) { + // Always use first vertex of origin polygon + tinyobj::index_t a; + a.vertex_index = indices[indices_i + 0].vertex_index; + a.normal_index = indices[indices_i + 0].normal_index; + a.texcoord_index = indices[indices_i + 0].texcoord_index; + + // Next two vertices shift by one in every iteration + tinyobj::index_t b; + b.vertex_index = indices[indices_i + i + 1].vertex_index; + b.normal_index = indices[indices_i + i + 1].normal_index; + b.texcoord_index = indices[indices_i + i + 1].texcoord_index; + tinyobj::index_t c; + c.vertex_index = indices[indices_i + i + 2].vertex_index; + c.normal_index = indices[indices_i + i + 2].normal_index; + c.texcoord_index = indices[indices_i + i + 2].texcoord_index; + + new_indices.push_back(a); + new_indices.push_back(b); + new_indices.push_back(c); + new_face_sizes.push_back(3); + + if (face_index < material_ids.size()) { + new_material_ids.push_back(material_ids[face_index]); + } + } + + indices_i += vertex_in_face_num; + } + + // Replace old polygon indices with triangle fan indices + indices = new_indices; + face_sizes = new_face_sizes; + material_ids = new_material_ids; + max_face_size = 3; + is_face_size_constant = true; + } + + } mesh_loader(params, result); + + + // Tinyobj callbacks (triggered when a line is parsed) + // callbacks are function pointers with void * user_data, hence no capture. + const auto vertex_cb = [](void* user_data, + tinyobj::real_t x, + tinyobj::real_t y, + tinyobj::real_t z, + tinyobj::real_t w) { + Loader& loader = *(reinterpret_cast(user_data)); + loader.vertices.insert(loader.vertices.end(), {x, y, z}); + (void)(w); // Avoid unused parameter warning. + }; + + const auto normal_cb = + [](void* user_data, tinyobj::real_t x, tinyobj::real_t y, tinyobj::real_t z) { + Loader& loader = *(reinterpret_cast(user_data)); + if (loader.params.load_normals) { + loader.normals.insert(loader.normals.end(), {x, y, z}); + } + }; + + const auto texcoord_cb = + [](void* user_data, tinyobj::real_t x, tinyobj::real_t y, tinyobj::real_t z) { + Loader& loader = *(reinterpret_cast(user_data)); + if (loader.params.load_uvs) { + loader.uvs.insert(loader.uvs.end(), {x, y}); + } + (void)(z); // Avoid unused parameter warning. + }; + + const auto usemtl_cb = [](void* user_data, const char* name, int material_id) { + Loader& loader = *(reinterpret_cast(user_data)); + loader.current_material_id = material_id; + (void)(name); // Avoid unused parameter warning. + }; + + const auto mtllib_cb = + [](void* user_data, const tinyobj::material_t* materials, int num_materials) { + Loader& loader = *(reinterpret_cast(user_data)); + loader.result.materials.insert( + loader.result.materials.end(), + materials, + materials + num_materials); + }; + + const auto index_cb = [](void* user_data, tinyobj::index_t* indices, int num_indices) { + Loader& loader = *(reinterpret_cast(user_data)); + auto num_vertices = loader.vertices.size() / 3; + auto num_normals = loader.normals.size() / 3; + auto num_uvs = loader.uvs.size() / 2; + for (auto i = 0; i < num_indices; i++) { + auto index = indices[i]; + index.vertex_index = fix_index(index.vertex_index, num_vertices, loader.vertex_offset); + assert(index.vertex_index >= 0 && index.vertex_index < static_cast(num_vertices)); + if (loader.params.load_normals && num_normals) { + index.normal_index = + fix_index(index.normal_index, num_normals, loader.normal_offset); + assert( + index.normal_index >= 0 && index.normal_index < static_cast(num_normals)); + } else { + index.normal_index = -1; + } + if (loader.params.load_uvs && num_uvs) { + index.texcoord_index = fix_index(index.texcoord_index, num_uvs, loader.uv_offset); + assert( + index.texcoord_index >= 0 && index.texcoord_index < static_cast(num_uvs)); + } else { + index.texcoord_index = -1; + } + loader.indices.push_back(index); + } + + loader.face_sizes.push_back(static_cast(num_indices)); + if (loader.params.load_materials) { + loader.material_ids.push_back(loader.current_material_id); + } + + if (loader.max_face_size != 0 && num_indices != loader.max_face_size) { + loader.is_face_size_constant = false; + } + loader.max_face_size = std::max(num_indices, loader.max_face_size); + }; + + const auto object_cb = [](void* user_data, const char* name) { + Loader& loader = *(reinterpret_cast(user_data)); + + size_t num_coords; + size_t max_face_size; + const auto DIM = 3; + const auto UV_DIM = 2; + + if (VertexArray::ColsAtCompileTime == Eigen::Dynamic) { + num_coords = DIM; + } else { + num_coords = VertexArray::ColsAtCompileTime; + } + if (FacetArray::ColsAtCompileTime == Eigen::Dynamic) { + max_face_size = size_t(loader.max_face_size); + if (!loader.is_face_size_constant && loader.params.triangulate) { + max_face_size = 3; + } + } else { + max_face_size = size_t(FacetArray::ColsAtCompileTime); + } + + if (loader.params.as_one_mesh && !loader.is_last_object) { + return; + } + + // First object begins + if (loader.vertices.size() == 0) { + loader.object_name = name; + return; + } + + // Triangulate if needed + if (max_face_size < safe_cast(loader.max_face_size)) { + loader.triangulate(); + } + + auto num_faces = loader.face_sizes.size(); + VertexArray vertices(loader.vertices.size() / DIM, num_coords); + FacetArray faces(num_faces, max_face_size); + + typename MeshType::UVArray uvs; + typename MeshType::UVIndices uv_indices; + typename MeshType::AttributeArray corner_normals; + if (!loader.uvs.empty()) { + uvs.resize(loader.uvs.size() / UV_DIM, UV_DIM); + uv_indices.resize(num_faces, max_face_size); + } + if (!loader.normals.empty()) { + corner_normals.resize(num_faces * max_face_size, num_coords); + } + + // Copy vertices + for (auto i : range(loader.vertices.size() / DIM)) { + for (auto k : range(num_coords)) { + vertices(i, k) = safe_cast(loader.vertices[DIM * i + k]); + } + } + + // Copy uvs + for (auto i : range(loader.uvs.size() / UV_DIM)) { + for (auto k : range(UV_DIM)) { + uvs(i, k) = safe_cast(loader.uvs[UV_DIM * i + k]); + } + } + + // Copy indices + size_t indices_i = 0; + for (auto face_index : range(loader.face_sizes.size())) { + const auto face_size = loader.face_sizes[face_index]; + + for (auto vertex_in_face : range(face_size)) { + const auto& index = loader.indices[indices_i]; + faces(face_index, vertex_in_face) = + safe_cast(index.vertex_index); + + if (index.normal_index != -1) { + const auto row_index = face_index * max_face_size + vertex_in_face; + for (auto k : range(num_coords)) { + corner_normals(row_index, k) = safe_cast( + loader.normals[DIM * index.normal_index + k]); + } + } + + if (!loader.uvs.empty()) { + uv_indices(face_index, vertex_in_face) = + safe_cast(index.texcoord_index); + } + + indices_i++; + } + + // Padding with invalid for mixed triangle/quad or arbitrary polygon meshes + for (auto pad = size_t(face_size); pad < size_t(max_face_size); pad++) { + faces(face_index, pad) = invalid(); + if (!loader.uvs.empty()) { + uv_indices(face_index, pad) = invalid(); + } + if (!loader.normals.empty()) { + corner_normals.row(face_index * max_face_size + pad).setZero(); + } + } + } + + auto mesh = create_mesh(vertices, faces); + + if (!loader.uvs.empty()) { + mesh->initialize_uv(uvs, uv_indices); + + // TODO: The loader should not do this mapping index -> corner + map_indexed_attribute_to_corner_attribute(*mesh, "uv"); + } + + if (!loader.normals.empty()) { + mesh->add_corner_attribute("normal"); + mesh->import_corner_attribute("normal", corner_normals); + } + + if (loader.result.materials.size() > 0 && loader.material_ids.size() == num_faces) { + // Because AttributeArray is not integral type, this converts to float type + Eigen::Map map(loader.material_ids.data(), loader.material_ids.size()); + mesh->add_facet_attribute("material_id"); + mesh->set_facet_attribute("material_id", map.cast()); + } + + loader.result.meshes.emplace_back(std::move(mesh)); + + loader.result.mesh_names.push_back(loader.object_name); + + loader.vertex_offset += loader.vertices.size() / DIM; + loader.normal_offset += loader.normals.size() / DIM; + loader.uv_offset += loader.uvs.size() / UV_DIM; + + loader.vertices.clear(); + loader.uvs.clear(); + loader.normals.clear(); + loader.indices.clear(); + loader.face_sizes.clear(); + loader.material_ids.clear(); + loader.max_face_size = 0; + loader.is_face_size_constant = true; + loader.object_name = name; + }; + + tinyobj::callback_t callback; + callback.vertex_cb = vertex_cb; + callback.index_cb = index_cb; + callback.object_cb = object_cb; + + if (params.load_normals) callback.normal_cb = normal_cb; + + if (params.load_uvs) callback.texcoord_cb = texcoord_cb; + + if (params.load_materials) { + callback.mtllib_cb = mtllib_cb; + callback.usemtl_cb = usemtl_cb; + } + + std::string warning_message; + std::string error_message; + result.success = tinyobj::LoadObjWithCallback( + input_stream, + callback, + &mesh_loader, + params.load_materials ? material_reader : nullptr, + &warning_message, + &error_message); + + mesh_loader.is_last_object = true; + object_cb(&mesh_loader, ""); + + if (!error_message.empty()) { + logger().error("Load mesh warning:\n{}", error_message); + } + if (!warning_message.empty()) { + logger().warn("Load mesh error:\n{}", warning_message); + } + + if (params.normalize) { + normalize_meshes(mesh_loader.result.meshes); + } + + return result; +} + +/// Load .obj from disk +template +MeshLoaderResult load_mesh_ext( + const lagrange::fs::path& filename, + const MeshLoaderParams& params = {}, + tinyobj::MaterialReader* material_reader = nullptr) +{ + lagrange::fs::ifstream stream(filename); + if (!stream.good()) { + MeshLoaderResult result; + logger().error("Cannot open file: \"{}\"", filename.string()); + result.success = false; + return result; + } + tinyobj::MaterialFileReader default_reader(filename.parent_path().string()); + if (params.load_materials && !material_reader) { + material_reader = &default_reader; + } + return load_mesh_ext(stream, params, material_reader); +} + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/legacy/load_mesh_ply.h b/modules/io/include/lagrange/io/legacy/load_mesh_ply.h new file mode 100644 index 00000000..5097a844 --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/load_mesh_ply.h @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +#include +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/// Load .ply with color information (if available) +template +std::unique_ptr load_mesh_ply(const fs::path& filename) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + using VertexArray = typename MeshType::VertexArray; + using FacetArray = typename MeshType::FacetArray; + using AttributeArray = typename MeshType::AttributeArray; + using Index = typename MeshType::Index; + + VertexArray V; + FacetArray F; + Eigen::Matrix E; + AttributeArray N; + AttributeArray UV; + + AttributeArray VD; + std::vector VDheader; + + AttributeArray FD; + std::vector FDheader; + + AttributeArray ED; + std::vector EDheader; + std::vector comments; + + // TODO: libigl's wrapper around tinyply doesn't allow to load attributes of different tyeps + // (e.g. char for colors + float for normals). + igl::readPLY( + filename.string(), + V, + F, + E, + N, + UV, + VD, + VDheader, + FD, + FDheader, + ED, + EDheader, + comments); + + auto mesh = create_mesh(std::move(V), std::move(F)); + if (N.rows() == mesh->get_num_vertices()) { + logger().debug("Setting vertex normal"); + mesh->add_vertex_attribute("normal"); + mesh->import_vertex_attribute("normal", N); + } + + auto has_attribute = [&](const std::string& name) { + return std::find(VDheader.begin(), VDheader.end(), name) != VDheader.end(); + }; + + if (VD.rows() == mesh->get_num_vertices()) { + if (has_attribute("red") && has_attribute("green") && has_attribute("blue")) { + bool has_alpha = has_attribute("alpha"); + int n = (has_alpha ? 4 : 3); + AttributeArray color(mesh->get_num_vertices(), n); + color.setZero(); + for (size_t i = 0; i < VDheader.size(); ++i) { + if (VDheader[i] == "red") { + la_runtime_assert(n >= 1); + color.col(0) = VD.col(i); + } else if (VDheader[i] == "green") { + la_runtime_assert(n >= 2); + color.col(1) = VD.col(i); + } else if (VDheader[i] == "blue") { + la_runtime_assert(n >= 3); + color.col(2) = VD.col(i); + } else if (VDheader[i] == "alpha") { + la_runtime_assert(n >= 4); + color.col(3) = VD.col(i); + } else { + logger().warn("Unknown vertex attribute: {}", VDheader[i]); + } + } + logger().debug("Setting vertex color"); + mesh->add_vertex_attribute("color"); + mesh->import_vertex_attribute("color", color); + } + } + + return mesh; +} + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/legacy/save_mesh.h b/modules/io/include/lagrange/io/legacy/save_mesh.h new file mode 100644 index 00000000..6040df26 --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/save_mesh.h @@ -0,0 +1,292 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +namespace internal { +template +void save_mesh_2D(const fs::path& filename, const MeshType& mesh) +{ + fs::ofstream fout(filename); + const auto& vertices = mesh.get_vertices(); + for (auto i : range(mesh.get_num_vertices())) { + fout << "v " << vertices(i, 0) << " " << vertices(i, 1) << std::endl; + } + + const auto& facets = mesh.get_facets(); + const auto vertex_per_facet = mesh.get_vertex_per_facet(); + for (auto i : range(mesh.get_num_facets())) { + fout << "f"; + for (auto j : range(vertex_per_facet)) { + fout << " " << facets(i, j) + 1; + } + fout << std::endl; + } + fout.close(); +} + +template +void save_mesh_basic(const fs::path& filename, const MeshType& mesh) +{ + if (mesh.get_dim() == 2) { + save_mesh_2D(filename, mesh); + } else { + const auto& vertices = mesh.get_vertices(); + const auto& facets = mesh.get_facets(); + igl::writeOBJ(filename.string(), vertices, facets); + } +} + +template +void extract_attribute( + const MeshType& mesh, + const std::string& attr_name, + Eigen::MatrixBase& values, + Eigen::MatrixBase& indices) +{ + const auto& facets = mesh.get_facets(); + if (mesh.has_indexed_attribute(attr_name)) { + auto attr = mesh.get_indexed_attribute(attr_name); + values = std::get<0>(attr); + indices = std::get<1>(attr); + } else if (mesh.has_corner_attribute(attr_name)) { + values = mesh.get_corner_attribute(attr_name); + indices.derived().resize(facets.rows(), facets.cols()); + typename MeshType::Index count = 0; + for (auto i : range(facets.rows())) { + for (auto j : range(facets.cols())) { + indices(i, j) = count; + count++; + } + } + } else if (mesh.has_vertex_attribute(attr_name)) { + values = mesh.get_vertex_attribute(attr_name); + indices = facets; + } +} + +template +void save_mesh_obj(const fs::path& filename, const MeshType& mesh) +{ + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + + const auto& vertices = mesh.get_vertices(); + const auto& facets = mesh.get_facets(); + + Eigen::Matrix CN; + Eigen::Matrix FN; + Eigen::Matrix TC; + Eigen::Matrix FTC; + + extract_attribute(mesh, "uv", TC, FTC); + extract_attribute(mesh, "normal", CN, FN); + + igl::writeOBJ(filename.string(), vertices, facets, CN, FN, TC, FTC); +} + +template +void save_mesh_vtk( + const fs::path& filename, + const MeshType& mesh, + const std::vector& face_attrib_names, + const std::vector& vertex_attrib_names) +{ + using AttributeArray = typename MeshType::AttributeArray; + + auto write_connectivity = [&](std::ostream& fl) { + la_runtime_assert(fl); + // Not a hard requirement. But since this is just a debugging tool + // let's just enforce it for now. + la_runtime_assert(mesh.get_vertex_per_facet() == 3); + + /* + * Write the vtk file. + */ + + // write the header + fl << "# vtk DataFile Version 2.0\n"; + fl << "Lagrange output mesh\n"; + fl << "ASCII\n"; + fl << "DATASET UNSTRUCTURED_GRID\n"; + fl << "\n"; + + // write the vertices + fl << "POINTS " << mesh.get_num_vertices() << " float\n"; + for (auto vnidx : range(mesh.get_num_vertices())) { + auto xyz = mesh.get_vertices().row(vnidx).eval(); + if (xyz.cols() == 3) { + fl << xyz(0) << " " << xyz(1) << " " << xyz(2) << "\n"; + } else if (xyz.cols() == 2) { + fl << xyz(0) << " " << xyz(1) << " " << 0 << "\n"; + } else { + throw std::runtime_error("This dimension is not supported"); + } + } + fl << "\n"; + + // + // write the faces + // + + // count their total number of vertices. + fl << "CELLS " << mesh.get_num_facets() << " " + << mesh.get_num_facets() * (mesh.get_vertex_per_facet() + 1) << "\n"; + for (auto fn : range(mesh.get_num_facets())) { + fl << mesh.get_vertex_per_facet() << " "; + for (auto voffset : range(mesh.get_vertex_per_facet())) { // + fl << mesh.get_facets()(fn, voffset) << " "; + } + fl << "\n"; + } + fl << "\n"; + + // write the face types + fl << "CELL_TYPES " << mesh.get_num_facets() << "\n"; + for (auto f : range(mesh.get_num_facets())) { + (void)f; + // fl << "7 \n"; // VTK POLYGON + fl << "5 \n"; // VTK TRIANGLE + } + fl << "\n"; + }; // end of write connectivity + + auto write_vertex_data_header = [&](std::ostream& fl) { + fl << "POINT_DATA " << mesh.get_num_vertices() << " \n"; + }; // end of write_vert_data_header + + auto write_cell_data_header = [&](std::ostream& fl) { + fl << "CELL_DATA " << mesh.get_num_facets() << " \n"; + }; // end of write_cell_header + + auto write_data = + [](std::ostream& fl, const std::string attrib_name, const AttributeArray& attrib) { + fl << "SCALARS " << attrib_name << " float " << attrib.cols() << "\n"; + fl << "LOOKUP_TABLE default \n"; + for (auto row : range(attrib.rows())) { + for (auto col : range(attrib.cols())) { + fl << attrib(row, col) << " "; + } // end of col + fl << "\n"; + } // end of row + fl << "\n"; + }; // end of write_data() + + // Open the file + fs::ofstream fl(filename, fs::fstream::out); + fl.precision(12); + fl.flags(fs::fstream::scientific); + la_runtime_assert(fl.is_open()); + + // write the connectivity + write_connectivity(fl); + + // Writing the face attribs + { + // Check if the mesh has any of the requested attribs + bool has_any_facet_attrib = false; + for (const auto& attrib_name : face_attrib_names) { + if (mesh.has_facet_attribute(attrib_name)) { + has_any_facet_attrib = true; + break; + } + } + // Write the header if it does + if (has_any_facet_attrib) { + write_cell_data_header(fl); + } + // Now write the data + for (const auto& attrib_name : face_attrib_names) { + if (mesh.has_facet_attribute(attrib_name)) { + write_data(fl, attrib_name, mesh.get_facet_attribute(attrib_name)); + } + } + + } // end of writing face attribs + + // Writing the vertex attribs + { + // Check if the mesh has any of the requested attribs + bool has_any_vertex_attrib = false; + for (const auto& attrib_name : vertex_attrib_names) { + if (mesh.has_vertex_attribute(attrib_name)) { + has_any_vertex_attrib = true; + break; + } + } + + // Write the header if it does + if (has_any_vertex_attrib) { + write_vertex_data_header(fl); + } + + // Now write the data + for (const auto& attrib_name : vertex_attrib_names) { + if (mesh.has_vertex_attribute(attrib_name)) { + write_data(fl, attrib_name, mesh.get_vertex_attribute(attrib_name)); + } + } + + } // end of writing vertex attribs + +} // end of write vtk + +} // namespace internal + + +template +void save_mesh(const fs::path& filename, const MeshType& mesh) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + if (filename.extension() == ".obj") { + internal::save_mesh_obj(filename, mesh); + } else if (filename.extension() == ".vtk") { + internal::save_mesh_vtk( + filename, + mesh, + mesh.get_facet_attribute_names(), + mesh.get_vertex_attribute_names()); + } else if (filename.extension() == ".ply") { + save_mesh_ply(filename, mesh); + } else { + internal::save_mesh_basic(filename, mesh); + } +} + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/legacy/save_mesh_ply.h b/modules/io/include/lagrange/io/legacy/save_mesh_ply.h new file mode 100644 index 00000000..c37c1023 --- /dev/null +++ b/modules/io/include/lagrange/io/legacy/save_mesh_ply.h @@ -0,0 +1,101 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +#include +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/// Save .ply with color information (if available) +template < + typename MeshType, + std::enable_if_t::value>* = nullptr> +void save_mesh_ply( + const fs::path& filename, + const MeshType& mesh, + FileEncoding encoding = FileEncoding::Binary) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + using VertexArray = typename MeshType::VertexArray; + using AttributeArray = typename MeshType::AttributeArray; + using Index = unsigned int; + + const VertexArray& V = mesh.get_vertices(); + const auto F = mesh.get_facets().template cast(); + Eigen::Matrix E; + AttributeArray N; + AttributeArray UV; + + Eigen::Matrix VD; + std::vector VDheader; + + AttributeArray FD; + std::vector FDheader; + + AttributeArray ED; + std::vector EDheader; + std::vector comments; + + if (mesh.has_vertex_attribute("normal")) { + N = mesh.get_vertex_attribute("normal"); + } + + if (mesh.has_vertex_attribute("color")) { + VDheader = {"red", "green", "blue"}; + auto C = mesh.get_vertex_attribute("color"); + if (C.maxCoeff() <= 1.0 && C.maxCoeff() > 0.0) { + logger().warn("Max color value is > 0.0 but <= 1.0, but colors are saved as char. " + "Please convert your colors to the range [0, 255]."); + } + VD = C.template cast(); + if (VD.cols() > 3) { + VDheader.push_back("alpha"); + } + la_runtime_assert(safe_cast(VD.cols()) == VDheader.size()); + } + + igl::writePLY( + filename.string(), + V, + F, + E, + N, + UV, + VD, + VDheader, + FD, + FDheader, + ED, + EDheader, + comments, + encoding == FileEncoding::Binary ? igl::FileEncoding::Binary : igl::FileEncoding::Ascii); +} + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh.h b/modules/io/include/lagrange/io/load_mesh.h index ccd8c080..0b54e674 100644 --- a/modules/io/include/lagrange/io/load_mesh.h +++ b/modules/io/include/lagrange/io/load_mesh.h @@ -11,35 +11,26 @@ */ #pragma once -/* -* This file does not contain function definition because those require other expensive headers -* and we want to minimize the size of this header, as it's used in almost all tests. -* -* If you have any issues with a load_mesh function not being defined for your specific -* mesh type, you can #include instead. -*/ - -#include +#include #include -#include - -#include -#include +#include +#include -namespace lagrange { -namespace io { +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#include +#endif -template -std::unique_ptr load_mesh_basic(const fs::path& filename); +namespace lagrange::io { -template -std::vector> load_obj_meshes(const fs::path& filename); +// The input stream version of load_mesh does not exist because it cannot determine the file type. +// If you want to load a mesh from an input stream, use a specific `load_mesh_type` function. -template -std::unique_ptr load_obj_mesh(const fs::path& filename); - -template -std::unique_ptr load_mesh(const fs::path& filename); +/** + * Load a mesh from a file. The loader will be chosen depending on the file extension. + */ +template < + typename MeshType, + std::enable_if_t::value>* = nullptr> +MeshType load_mesh(const fs::path& filename, const LoadOptions& = {}); -} // namespace io -} // namespace lagrange +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh.impl.h b/modules/io/include/lagrange/io/load_mesh.impl.h index bb6a062a..bf98e334 100644 --- a/modules/io/include/lagrange/io/load_mesh.impl.h +++ b/modules/io/include/lagrange/io/load_mesh.impl.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Adobe. All rights reserved. + * Copyright 2017 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -11,85 +11,8 @@ */ #pragma once -#include +// remove this file when we drop support for legacy mesh -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace lagrange { -namespace io { - -template -std::unique_ptr load_mesh_basic(const fs::path& filename) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - typename MeshType::VertexArray vertices; - typename MeshType::FacetArray facets; - - igl::read_triangle_mesh(filename.string(), vertices, facets); - - return create_mesh(vertices, facets); -} -extern template std::unique_ptr load_mesh_basic(const fs::path&); -extern template std::unique_ptr load_mesh_basic(const fs::path&); - -template -std::vector> load_obj_meshes(const fs::path& filename) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - if (!std::is_same::value) { - logger().warn("Tinyobjloader compiled to use float, loss of precision may occur."); - } - - return load_mesh_ext(filename).meshes; -} -extern template std::vector> load_obj_meshes(const fs::path&); -extern template std::vector> load_obj_meshes(const fs::path&); - -template -std::unique_ptr load_obj_mesh(const fs::path& filename) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - auto res = load_obj_meshes(filename); - if (res.empty()) { - return nullptr; - } else if (res.size() == 1) { - return std::move(res[0]); - } else { - logger().debug("Combining {} meshes into one.", res.size()); - return combine_mesh_list(res, true); - } -} -extern template std::unique_ptr load_obj_mesh(const fs::path&); -extern template std::unique_ptr load_obj_mesh(const fs::path&); - -template -std::unique_ptr load_mesh(const fs::path& filename) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - if (filename.extension() == ".obj") { - return load_obj_mesh(filename); - } else if (filename.extension() == ".ply") { - return load_mesh_ply(filename); - } else { - return load_mesh_basic(filename); - } -} -extern template std::unique_ptr load_mesh(const fs::path&); -extern template std::unique_ptr load_mesh(const fs::path&); - - -} // namespace io -} // namespace lagrange +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#include +#endif diff --git a/modules/io/include/lagrange/io/load_mesh_assimp.h b/modules/io/include/lagrange/io/load_mesh_assimp.h index 5d1d0d33..4ad379a5 100644 --- a/modules/io/include/lagrange/io/load_mesh_assimp.h +++ b/modules/io/include/lagrange/io/load_mesh_assimp.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Adobe. All rights reserved. + * Copyright 2023 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -13,188 +13,29 @@ #ifdef LAGRANGE_WITH_ASSIMP +#include #include -#include -#include +#include +#include -#include -#include -#include -#include -#include - -#include -#include - -namespace lagrange { -namespace io { - -// ==== Declarations ==== -std::unique_ptr load_scene_assimp(const lagrange::fs::path& filename); -std::unique_ptr load_scene_assimp_from_memory(const void* buffer, size_t size); - -template -std::vector> load_mesh_assimp(const lagrange::fs::path& filename); -template -std::vector> load_mesh_assimp_from_memory( - const void* buffer, - size_t size); - -template -std::vector> extract_meshes_assimp(const aiScene* scene); -template -std::unique_ptr convert_mesh_assimp(const aiMesh* mesh); - -inline std::unique_ptr load_scene_assimp(const lagrange::fs::path& filename) -{ - Assimp::Importer importer; - const aiScene* scene = importer.ReadFile(filename.string(), 0); - if (!scene) { - logger().error("Error loading scene: {}", importer.GetErrorString()); - return nullptr; - } - - return std::unique_ptr(importer.GetOrphanedScene()); -} - -inline std::unique_ptr load_scene_assimp_from_memory(const void* buffer, size_t size) -{ - Assimp::Importer importer; - - // Note: we are asking assimp to triangulate the scene for us. - // Change this after we add support for n-gons. - const aiScene* scene = importer.ReadFileFromMemory(buffer, size, aiProcess_Triangulate); - - if (!scene) { - logger().error("Error loading scene: {}", importer.GetErrorString()); - return nullptr; - } - - return std::unique_ptr(importer.GetOrphanedScene()); -} - -template -std::vector> load_mesh_assimp(const lagrange::fs::path& filename) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - std::unique_ptr scene = load_scene_assimp(filename); - - return extract_meshes_assimp(scene.get()); -} - -template -std::vector> load_mesh_assimp_from_memory(const void* buffer, size_t size) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - std::unique_ptr scene = load_scene_assimp_from_memory(buffer, size); - - return extract_meshes_assimp(scene.get()); -} - -template -std::vector> extract_meshes_assimp(const aiScene* scene) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - std::vector> ret; - if (!scene) { - return ret; - } - for (unsigned int i = 0; i < scene->mNumMeshes; ++i) { - const aiMesh* mesh = scene->mMeshes[i]; - ret.emplace_back(convert_mesh_assimp(mesh)); - } - return ret; -} - -template -std::unique_ptr convert_mesh_assimp(const aiMesh* mesh) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - using VertexArray = typename MeshType::VertexArray; - using FacetArray = typename MeshType::FacetArray; - - // Check that all facets have the same size - unsigned int nvpf = 0; - bool triangulate = false; - for (unsigned int j = 0; j < mesh->mNumFaces; ++j) { - const aiFace& face = mesh->mFaces[j]; - if (nvpf == 0) { - nvpf = face.mNumIndices; - } else if (face.mNumIndices != nvpf) { - logger().warn("Facets with varying number of vertices detected, triangulating"); - nvpf = 3; - triangulate = true; - break; - } - } - if (FacetArray::ColsAtCompileTime != Eigen::Dynamic && FacetArray::ColsAtCompileTime != nvpf) { - logger().warn( - "FacetArray cannot hold facets with n!={} vertex per facet, triangulating", - FacetArray::ColsAtCompileTime); - triangulate = true; - nvpf = 3; - } - - // If triangulating a heterogeneous mesh, we need to count the number of facets - unsigned int num_output_facets = mesh->mNumFaces; - if (triangulate) { - num_output_facets = 0; - for (unsigned int j = 0; j < mesh->mNumFaces; ++j) { - const aiFace& face = mesh->mFaces[j]; - num_output_facets += face.mNumIndices - 2; - } - } - - VertexArray vertices(mesh->mNumVertices, 3); // should we support arbitrary dimension? - for (unsigned int j = 0; j < mesh->mNumVertices; ++j) { - const aiVector3D& vec = mesh->mVertices[j]; - vertices(j, 0) = vec.x; - vertices(j, 1) = vec.y; - vertices(j, 2) = vec.z; - } - - FacetArray faces(num_output_facets, nvpf); - for (unsigned int j = 0, f = 0; j < mesh->mNumFaces; ++j) { - const aiFace& face = mesh->mFaces[j]; - if (triangulate) { - for (unsigned int k = 2; k < face.mNumIndices; ++k) { - faces.row(f++) << face.mIndices[0], face.mIndices[k - 1], face.mIndices[k]; - } - } else { - for (unsigned int k = 0; k < face.mNumIndices; ++k) { - faces(j, k) = face.mIndices[k]; - } - } - assert(f <= num_output_facets); - } - - auto lagrange_mesh = create_mesh(std::move(vertices), std::move(faces)); - - if (mesh->HasTextureCoords(0)) { - typename MeshType::UVArray uvs(mesh->mNumVertices, 2); - typename MeshType::UVIndices uv_indices = lagrange_mesh->get_facets(); - - assert(mesh->GetNumUVChannels() == 1); // assume one UV channel - assert(mesh->mNumUVComponents[0] == 2); // assume two components (uv), not 3d uvw - for (unsigned int j = 0; j < mesh->mNumVertices; ++j) { - const aiVector3D& vec = mesh->mTextureCoords[0][j]; - uvs(j, 0) = vec.x; - uvs(j, 1) = vec.y; - } - - // uv indices are the same as facets +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif - lagrange_mesh->initialize_uv(std::move(uvs), std::move(uv_indices)); - map_indexed_attribute_to_corner_attribute(*lagrange_mesh, "uv"); - } +namespace lagrange::io { - return lagrange_mesh; -} +/** + * Load a mesh using assimp. If the scene contains multiple meshes, they will be combined into one. + * + * @param[in] filename input filename + * @param[in] options + * + * @return loaded mesh + */ +template ::value>* = nullptr> +MeshType load_mesh_assimp(const fs::path& filename, const LoadOptions& options = {}); -} // namespace io -} // namespace lagrange +} // namespace lagrange::io #endif diff --git a/modules/io/include/lagrange/io/load_mesh_ext.h b/modules/io/include/lagrange/io/load_mesh_ext.h index 5d2eb291..7991461c 100644 --- a/modules/io/include/lagrange/io/load_mesh_ext.h +++ b/modules/io/include/lagrange/io/load_mesh_ext.h @@ -1,5 +1,5 @@ /* - * Copyright 2020 Adobe. All rights reserved. + * Copyright 2017 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -11,464 +11,8 @@ */ #pragma once -#include -#include -#include +// remove this file when we drop support for legacy mesh -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace lagrange { -namespace io { - -struct MeshLoaderParams -{ - /// When loading a mesh with mixed facet sizes, this parameter controls whether the polygonal - /// faces will be triangulated, or left as is and padded with INVALID. Note that if the mesh has - /// a compile-time face size, and cannot accommodate the maximum face size of the input mesh, - /// then it will be triangulated as well. If the input mesh has constant face size (e.g. a quad - /// mesh), and the mesh type can accommodate such facet type, then faces will also not be - /// triangulated, regardless of this parameter value. - bool triangulate = false; - - /// Normalize each object to a unit box around the origin. - bool normalize = false; - - bool load_normals = true; - bool load_uvs = true; - bool load_materials = true; - - /// Combines individual objects into a single mesh. Result contains a vector of size 1. - bool as_one_mesh = false; -}; - -template -struct MeshLoaderResult -{ - bool success = true; - std::vector> meshes; - std::vector materials; - std::vector mesh_names; -}; - -inline int fix_index(int index, size_t n, size_t global_offset) -{ - // Shifted absolute index - if (index > 0) return index - static_cast(global_offset) - 1; - // Relative index - if (index < 0) return static_cast(n) + index - static_cast(global_offset); - // 0 index is invalid in obj files - return -1; -} - -/// -/// Loads a .obj mesh from a stream. -/// -/// @param[in,out] input_stream The stream to read data from. -/// @param[in] params Optional parameters for the loader. -/// @param[in] material_reader Optional pointer to the material reader class. -/// -/// @tparam MeshType Mesh type. -/// -/// @return Result of the load. -/// -template -auto load_mesh_ext( - std::istream& input_stream, - const MeshLoaderParams& params = {}, - tinyobj::MaterialReader* material_reader = nullptr) -> MeshLoaderResult -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - using VertexArray = typename MeshType::VertexArray; - using FacetArray = typename MeshType::FacetArray; - - MeshLoaderResult result; - - if (!input_stream.good()) { - logger().error("Invalid input stream given"); - result.success = false; - return result; - } - - /// State of the loading state machine - /// Passed through tinyobj as void* user data (through function pointers) - struct Loader - { - Loader(const MeshLoaderParams& _params, MeshLoaderResult& _result) - : params(_params) - , result(_result) - {} - - const MeshLoaderParams& params; - MeshLoaderResult& result; - - std::vector vertices; - std::vector normals; - std::vector uvs; - std::vector indices; - std::vector material_ids; - - size_t vertex_offset = 0; - size_t normal_offset = 0; - size_t uv_offset = 0; - - std::string object_name = ""; - int current_material_id = 0; - - std::vector face_sizes; - int max_face_size = 0; - bool is_face_size_constant = false; - bool is_last_object = false; - - void triangulate() - { - std::vector new_indices; - std::vector new_face_sizes; - std::vector new_material_ids; - size_t indices_i = 0; - - // Loop over faces - for (auto face_index : range(face_sizes.size())) { - const auto vertex_in_face_num = face_sizes[face_index]; - const auto new_triangle_num = (vertex_in_face_num - 3) + 1; - - // Triangle fan conversion - for (auto i : range(new_triangle_num)) { - // Always use first vertex of origin polygon - tinyobj::index_t a; - a.vertex_index = indices[indices_i + 0].vertex_index; - a.normal_index = indices[indices_i + 0].normal_index; - a.texcoord_index = indices[indices_i + 0].texcoord_index; - - // Next two vertices shift by one in every iteration - tinyobj::index_t b; - b.vertex_index = indices[indices_i + i + 1].vertex_index; - b.normal_index = indices[indices_i + i + 1].normal_index; - b.texcoord_index = indices[indices_i + i + 1].texcoord_index; - tinyobj::index_t c; - c.vertex_index = indices[indices_i + i + 2].vertex_index; - c.normal_index = indices[indices_i + i + 2].normal_index; - c.texcoord_index = indices[indices_i + i + 2].texcoord_index; - - new_indices.push_back(a); - new_indices.push_back(b); - new_indices.push_back(c); - new_face_sizes.push_back(3); - - if (face_index < material_ids.size()) { - new_material_ids.push_back(material_ids[face_index]); - } - } - - indices_i += vertex_in_face_num; - } - - // Replace old polygon indices with triangle fan indices - indices = new_indices; - face_sizes = new_face_sizes; - material_ids = new_material_ids; - max_face_size = 3; - is_face_size_constant = true; - } - - } mesh_loader(params, result); - - - // Tinyobj callbacks (triggered when a line is parsed) - // callbacks are function pointers with void * user_data, hence no capture. - const auto vertex_cb = [](void* user_data, - tinyobj::real_t x, - tinyobj::real_t y, - tinyobj::real_t z, - tinyobj::real_t w) { - Loader& loader = *(reinterpret_cast(user_data)); - loader.vertices.insert(loader.vertices.end(), {x, y, z}); - (void)(w); // Avoid unused parameter warning. - }; - - const auto normal_cb = - [](void* user_data, tinyobj::real_t x, tinyobj::real_t y, tinyobj::real_t z) { - Loader& loader = *(reinterpret_cast(user_data)); - if (loader.params.load_normals) { - loader.normals.insert(loader.normals.end(), {x, y, z}); - } - }; - - const auto texcoord_cb = - [](void* user_data, tinyobj::real_t x, tinyobj::real_t y, tinyobj::real_t z) { - Loader& loader = *(reinterpret_cast(user_data)); - if (loader.params.load_uvs) { - loader.uvs.insert(loader.uvs.end(), {x, y}); - } - (void)(z); // Avoid unused parameter warning. - }; - - const auto usemtl_cb = [](void* user_data, const char* name, int material_id) { - Loader& loader = *(reinterpret_cast(user_data)); - loader.current_material_id = material_id; - (void)(name); // Avoid unused parameter warning. - }; - - const auto mtllib_cb = - [](void* user_data, const tinyobj::material_t* materials, int num_materials) { - Loader& loader = *(reinterpret_cast(user_data)); - loader.result.materials.insert( - loader.result.materials.end(), - materials, - materials + num_materials); - }; - - const auto index_cb = [](void* user_data, tinyobj::index_t* indices, int num_indices) { - Loader& loader = *(reinterpret_cast(user_data)); - auto num_vertices = loader.vertices.size() / 3; - auto num_normals = loader.normals.size() / 3; - auto num_uvs = loader.uvs.size() / 2; - for (auto i = 0; i < num_indices; i++) { - auto index = indices[i]; - index.vertex_index = - fix_index(index.vertex_index, num_vertices, loader.vertex_offset); - assert(index.vertex_index >= 0 && index.vertex_index < static_cast(num_vertices)); - if (loader.params.load_normals && num_normals) { - index.normal_index = - fix_index(index.normal_index, num_normals, loader.normal_offset); - assert(index.normal_index >= 0 && index.normal_index < static_cast(num_normals)); - } else { - index.normal_index = -1; - } - if (loader.params.load_uvs && num_uvs) { - index.texcoord_index = - fix_index(index.texcoord_index, num_uvs, loader.uv_offset); - assert(index.texcoord_index >= 0 && index.texcoord_index < static_cast(num_uvs)); - } else { - index.texcoord_index = -1; - } - loader.indices.push_back(index); - } - - loader.face_sizes.push_back(static_cast(num_indices)); - if (loader.params.load_materials) { - loader.material_ids.push_back(loader.current_material_id); - } - - if (loader.max_face_size != 0 && num_indices != loader.max_face_size) { - loader.is_face_size_constant = false; - } - loader.max_face_size = std::max(num_indices, loader.max_face_size); - }; - - const auto object_cb = [](void* user_data, const char* name) { - Loader& loader = *(reinterpret_cast(user_data)); - - size_t num_coords; - size_t max_face_size; - const auto DIM = 3; - const auto UV_DIM = 2; - - if (VertexArray::ColsAtCompileTime == Eigen::Dynamic) { - num_coords = DIM; - } else { - num_coords = VertexArray::ColsAtCompileTime; - } - if (FacetArray::ColsAtCompileTime == Eigen::Dynamic) { - max_face_size = size_t(loader.max_face_size); - if (!loader.is_face_size_constant && loader.params.triangulate) { - max_face_size = 3; - } - } else { - max_face_size = size_t(FacetArray::ColsAtCompileTime); - } - - if (loader.params.as_one_mesh && !loader.is_last_object) { - return; - } - - // First object begins - if (loader.vertices.size() == 0) { - loader.object_name = name; - return; - } - - // Triangulate if needed - if (max_face_size < safe_cast(loader.max_face_size)) { - loader.triangulate(); - } - - auto num_faces = loader.face_sizes.size(); - VertexArray vertices(loader.vertices.size() / DIM, num_coords); - FacetArray faces(num_faces, max_face_size); - - typename MeshType::UVArray uvs; - typename MeshType::UVIndices uv_indices; - typename MeshType::AttributeArray corner_normals; - if (!loader.uvs.empty()) { - uvs.resize(loader.uvs.size() / UV_DIM, UV_DIM); - uv_indices.resize(num_faces, max_face_size); - } - if (!loader.normals.empty()) { - corner_normals.resize(num_faces * max_face_size, num_coords); - } - - // Copy vertices - for (auto i : range(loader.vertices.size() / DIM)) { - for (auto k : range(num_coords)) { - vertices(i, k) = safe_cast(loader.vertices[DIM * i + k]); - } - } - - // Copy uvs - for (auto i : range(loader.uvs.size() / UV_DIM)) { - for (auto k : range(UV_DIM)) { - uvs(i, k) = safe_cast(loader.uvs[UV_DIM * i + k]); - } - } - - // Copy indices - size_t indices_i = 0; - for (auto face_index : range(loader.face_sizes.size())) { - const auto face_size = loader.face_sizes[face_index]; - - for (auto vertex_in_face : range(face_size)) { - const auto& index = loader.indices[indices_i]; - faces(face_index, vertex_in_face) = - safe_cast(index.vertex_index); - - if (index.normal_index != -1) { - const auto row_index = face_index * max_face_size + vertex_in_face; - for (auto k : range(num_coords)) { - corner_normals(row_index, k) = safe_cast( - loader.normals[DIM * index.normal_index + k]); - } - } - - if (!loader.uvs.empty()) { - uv_indices(face_index, vertex_in_face) = - safe_cast(index.texcoord_index); - } - - indices_i++; - } - - // Padding with invalid for mixed triangle/quad or arbitrary polygon meshes - for (auto pad = size_t(face_size); pad < size_t(max_face_size); pad++) { - faces(face_index, pad) = invalid(); - if (!loader.uvs.empty()) { - uv_indices(face_index, pad) = invalid(); - } - if (!loader.normals.empty()) { - corner_normals.row(face_index * max_face_size + pad).setZero(); - } - } - } - - auto mesh = create_mesh(vertices, faces); - - if (!loader.uvs.empty()) { - mesh->initialize_uv(uvs, uv_indices); - - // TODO: The loader should not do this mapping index -> corner - map_indexed_attribute_to_corner_attribute(*mesh, "uv"); - } - - if (!loader.normals.empty()) { - mesh->add_corner_attribute("normal"); - mesh->import_corner_attribute("normal", corner_normals); - } - - if (loader.result.materials.size() > 0 && loader.material_ids.size() == num_faces) { - // Because AttributeArray is not integral type, this converts to float type - Eigen::Map map(loader.material_ids.data(), loader.material_ids.size()); - mesh->add_facet_attribute("material_id"); - mesh->set_facet_attribute("material_id", map.cast()); - } - - loader.result.meshes.emplace_back(std::move(mesh)); - - loader.result.mesh_names.push_back(loader.object_name); - - loader.vertex_offset += loader.vertices.size() / DIM; - loader.normal_offset += loader.normals.size() / DIM; - loader.uv_offset += loader.uvs.size() / UV_DIM; - - loader.vertices.clear(); - loader.uvs.clear(); - loader.normals.clear(); - loader.indices.clear(); - loader.face_sizes.clear(); - loader.material_ids.clear(); - loader.max_face_size = 0; - loader.is_face_size_constant = true; - loader.object_name = name; - }; - - tinyobj::callback_t callback; - callback.vertex_cb = vertex_cb; - callback.index_cb = index_cb; - callback.object_cb = object_cb; - - if (params.load_normals) callback.normal_cb = normal_cb; - - if (params.load_uvs) callback.texcoord_cb = texcoord_cb; - - if (params.load_materials) { - callback.mtllib_cb = mtllib_cb; - callback.usemtl_cb = usemtl_cb; - } - - std::string warning_message; - std::string error_message; - result.success = tinyobj::LoadObjWithCallback( - input_stream, - callback, - &mesh_loader, - params.load_materials ? material_reader : nullptr, - &warning_message, - &error_message); - - mesh_loader.is_last_object = true; - object_cb(&mesh_loader, ""); - - if (!error_message.empty()) { - logger().error("Load mesh warning:\n{}", error_message); - } - if (!warning_message.empty()) { - logger().warn("Load mesh error:\n{}", warning_message); - } - - if (params.normalize) { - normalize_meshes(mesh_loader.result.meshes); - } - - return result; -} - -/// Load .obj from disk -template -MeshLoaderResult load_mesh_ext( - const lagrange::fs::path& filename, - const MeshLoaderParams& params = {}, - tinyobj::MaterialReader* material_reader = nullptr) -{ - lagrange::fs::ifstream stream(filename); - if (!stream.good()) { - MeshLoaderResult result; - logger().error("Cannot open file: \"{}\"", filename.string()); - result.success = false; - return result; - } - tinyobj::MaterialFileReader default_reader(filename.parent_path().string()); - if (params.load_materials && !material_reader) { - material_reader = &default_reader; - } - return load_mesh_ext(stream, params, material_reader); -} - -} // namespace io -} // namespace lagrange +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#include +#endif diff --git a/modules/io/include/lagrange/io/load_mesh_gltf.h b/modules/io/include/lagrange/io/load_mesh_gltf.h new file mode 100644 index 00000000..38c5472e --- /dev/null +++ b/modules/io/include/lagrange/io/load_mesh_gltf.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +namespace lagrange::io { + +/** + * Loads a mesh from a file in glTF or GLB format. If the scene contains multiple meshes, they will + * be merged into one. + * + * @param[in] filename Input filename. + * @param[in] options Load options. + * + * @tparam MeshType Mesh type to load. + * + * @return Loaded mesh. + */ +template +MeshType load_mesh_gltf(const fs::path& filename, const LoadOptions& options = {}); + +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_msh.h b/modules/io/include/lagrange/io/load_mesh_msh.h index 54d94e37..74b6ffb3 100644 --- a/modules/io/include/lagrange/io/load_mesh_msh.h +++ b/modules/io/include/lagrange/io/load_mesh_msh.h @@ -13,30 +13,38 @@ #include #include +#include namespace lagrange::io { /** - * Load a .msh file. + * Loads a mesh from a stream in MSH format. * - * @param[in] input_stream Input stream + * @param[in] input_stream Input stream. + * @param[in] options Load options. * - * @return The loaded surface mesh. + * @tparam MeshType Mesh type to load. + * + * @return Loaded mesh. */ template -MeshType load_mesh_msh(std::istream& input_stream); +MeshType load_mesh_msh(std::istream& input_stream, const LoadOptions& options = {}); /** * @overload * - * @param[in] filename Input mesh filename. + * Loads a mesh from a file in MSH format. + * + * @param[in] filename Input filename. + * @param[in] options Load options. + * + * @see load_mesh_msh(std::istream&) for more info. + * + * @tparam MeshType Mesh type to load. * - * @see load_mesh_msh(std::istream&) for more info. + * @return Loaded mesh. */ template -MeshType load_mesh_msh(const fs::path& filename) { - fs::ifstream fin(filename); - return load_mesh_msh(fin); -} +MeshType load_mesh_msh(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_obj.h b/modules/io/include/lagrange/io/load_mesh_obj.h index fd40a984..a5efa644 100644 --- a/modules/io/include/lagrange/io/load_mesh_obj.h +++ b/modules/io/include/lagrange/io/load_mesh_obj.h @@ -13,79 +13,21 @@ #include #include +#include -// TODO: Hide in .cpp -#include +namespace lagrange::io { -#include -#include - -namespace lagrange { -namespace io { - -/// -/// Config options for the obj mesh loader. -/// -struct ObjReaderOptions -{ - /// Triangulate any polygonal facet with > 3 vertices - bool triangulate = false; - - /// Load vertex normals as indexed attributes - bool load_normals = true; - - /// Load texture coordinates as indexed attributes - bool load_uvs = true; - - /// Load material ids as facet attributes - bool load_materials = true; - - /// Load vertex colors as vertex attributes - bool load_vertex_colors = false; - - /// Load object id as facet attributes - bool load_object_id = true; - - /// Search path for .mtl files. By default, searches the same folder as the provided filename. - std::string mtl_search_path; -}; - -/// -/// Output of the obj mesh loader. -/// -/// @tparam Scalar Mesh scalar type. -/// @tparam Index Mesh index type. -/// -template -struct ObjReaderResult -{ - /// Whether the load operation was successful. - bool success = true; - - /// Aggregated mesh containing all elements in the .obj file. To separate the different - /// entities, split the mesh facets based on object ids. - SurfaceMesh mesh; - - /// Materials associated with the mesh. - std::vector materials; - - /// Names of each object in the aggregate mesh. - std::vector names; -}; - -/// -/// Loads a .obj mesh from a file. -/// -/// @param[in] filename The file to read data from. -/// @param[in] options Optional configuration for the loader. -/// -/// @tparam MeshType Mesh type to load. -/// -/// @return Result of the load. -/// +/** + * Loads a mesh from a file in MSH format. + * + * @param[in] filename Input filename. + * @param[in] options Load options. + * + * @tparam MeshType Mesh type to load. + * + * @return Loaded mesh. + */ template -auto load_mesh_obj(const fs::path& filename, const ObjReaderOptions& options = {}) - -> ObjReaderResult; +MeshType load_mesh_obj(const fs::path& filename, const LoadOptions& options = {}); -} // namespace io -} // namespace lagrange +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_ply.h b/modules/io/include/lagrange/io/load_mesh_ply.h index e26ebef2..9e177da2 100644 --- a/modules/io/include/lagrange/io/load_mesh_ply.h +++ b/modules/io/include/lagrange/io/load_mesh_ply.h @@ -1,5 +1,5 @@ /* - * Copyright 2020 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -11,108 +11,38 @@ */ #pragma once +#include #include +#include -#include -#include +namespace lagrange::io { -// clang-format off -#include -#include -#include -// clang-format on - -#include -#include - -namespace lagrange { -namespace io { - -/// Load .ply with color information (if available) +/** + * Loads a mesh from a stream in PLY format. + * + * @param[in,out] input_stream Input stream. + * @param[in] options Load options. + * + * @tparam MeshType Mesh type to load. + * + * @return Loaded mesh. + */ template -std::unique_ptr load_mesh_ply(const fs::path& filename) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - using VertexArray = typename MeshType::VertexArray; - using FacetArray = typename MeshType::FacetArray; - using AttributeArray = typename MeshType::AttributeArray; - using Index = typename MeshType::Index; - - VertexArray V; - FacetArray F; - Eigen::Matrix E; - AttributeArray N; - AttributeArray UV; - - AttributeArray VD; - std::vector VDheader; - - AttributeArray FD; - std::vector FDheader; - - AttributeArray ED; - std::vector EDheader; - std::vector comments; +MeshType load_mesh_ply(std::istream& input_stream, const LoadOptions& options = {}); - // TODO: libigl's wrapper around tinyply doesn't allow to load attributes of different tyeps - // (e.g. char for colors + float for normals). - igl::readPLY( - filename.string(), - V, - F, - E, - N, - UV, - VD, - VDheader, - FD, - FDheader, - ED, - EDheader, - comments); - - auto mesh = create_mesh(std::move(V), std::move(F)); - if (N.rows() == mesh->get_num_vertices()) { - logger().debug("Setting vertex normal"); - mesh->add_vertex_attribute("normal"); - mesh->import_vertex_attribute("normal", N); - } - - auto has_attribute = [&](const std::string& name) { - return std::find(VDheader.begin(), VDheader.end(), name) != VDheader.end(); - }; - - if (VD.rows() == mesh->get_num_vertices()) { - if (has_attribute("red") && has_attribute("green") && has_attribute("blue")) { - bool has_alpha = has_attribute("alpha"); - int n = (has_alpha ? 4 : 3); - AttributeArray color(mesh->get_num_vertices(), n); - color.setZero(); - for (size_t i = 0; i < VDheader.size(); ++i) { - if (VDheader[i] == "red") { - la_runtime_assert(n >= 1); - color.col(0) = VD.col(i); - } else if (VDheader[i] == "green") { - la_runtime_assert(n >= 2); - color.col(1) = VD.col(i); - } else if (VDheader[i] == "blue") { - la_runtime_assert(n >= 3); - color.col(2) = VD.col(i); - } else if (VDheader[i] == "alpha") { - la_runtime_assert(n >= 4); - color.col(3) = VD.col(i); - } else { - logger().warn("Unknown vertex attribute: {}", VDheader[i]); - } - } - logger().debug("Setting vertex color"); - mesh->add_vertex_attribute("color"); - mesh->import_vertex_attribute("color", color); - } - } - - return mesh; -} +/** + * @overload + * + * Loads a mesh from a file in PLY format. + * + * @param[in] filename Input filename. + * @param[in] options Load options. + * + * @tparam MeshType Mesh type to load. + * + * @return Loaded mesh. + */ +template +MeshType load_mesh_ply(const fs::path& filename, const LoadOptions& options = {}); -} // namespace io -} // namespace lagrange +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_simple_scene.h b/modules/io/include/lagrange/io/load_simple_scene.h new file mode 100644 index 00000000..827b07b4 --- /dev/null +++ b/modules/io/include/lagrange/io/load_simple_scene.h @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +namespace lagrange::io { + +/** + * Load a simple scene. + * + * @param[in] filename path to file + * @param[in] options + * + * @return SceneType scene. + */ +template +SceneType load_simple_scene(const fs::path& filename, const LoadOptions& options = {}); + +} diff --git a/modules/io/include/lagrange/io/load_simple_scene_assimp.h b/modules/io/include/lagrange/io/load_simple_scene_assimp.h new file mode 100644 index 00000000..b2e113cf --- /dev/null +++ b/modules/io/include/lagrange/io/load_simple_scene_assimp.h @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#ifdef LAGRANGE_WITH_ASSIMP + +#include +#include +#include + +namespace lagrange::io { + +/** + * Load a simple scene using assimp. + * + * @param[in] filename input file name + * @param[in] options + * + * @return loaded scene + */ +template +SceneType load_simple_scene_assimp(const fs::path& filename, const LoadOptions& options = {}); + +} // namespace lagrange::io + +#endif diff --git a/modules/io/include/lagrange/io/load_simple_scene_gltf.h b/modules/io/include/lagrange/io/load_simple_scene_gltf.h new file mode 100644 index 00000000..469615fa --- /dev/null +++ b/modules/io/include/lagrange/io/load_simple_scene_gltf.h @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +namespace lagrange::io { + +/** + * Load a simple scene with gltf.. + * + * @param[in] filename input file + * @param[in] options + * + * @return scene + */ +template +SceneType load_simple_scene_gltf(const fs::path& filename, const LoadOptions& options = {}); + +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/save_mesh.h b/modules/io/include/lagrange/io/save_mesh.h index fe014792..156698e3 100644 --- a/modules/io/include/lagrange/io/save_mesh.h +++ b/modules/io/include/lagrange/io/save_mesh.h @@ -1,5 +1,5 @@ /* - * Copyright 2017 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -11,282 +11,27 @@ */ #pragma once -#include -#include -#include - -// clang-format off -#include -#include -#include -// clang-format on - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - +#include #include +#include -namespace lagrange { -namespace io { - -namespace internal { -template -void save_mesh_2D(const fs::path& filename, const MeshType& mesh) -{ - fs::ofstream fout(filename); - const auto& vertices = mesh.get_vertices(); - for (auto i : range(mesh.get_num_vertices())) { - fout << "v " << vertices(i, 0) << " " << vertices(i, 1) << std::endl; - } - - const auto& facets = mesh.get_facets(); - const auto vertex_per_facet = mesh.get_vertex_per_facet(); - for (auto i : range(mesh.get_num_facets())) { - fout << "f"; - for (auto j : range(vertex_per_facet)) { - fout << " " << facets(i, j) + 1; - } - fout << std::endl; - } - fout.close(); -} - -template -void save_mesh_basic(const fs::path& filename, const MeshType& mesh) -{ - if (mesh.get_dim() == 2) { - save_mesh_2D(filename, mesh); - } else { - const auto& vertices = mesh.get_vertices(); - const auto& facets = mesh.get_facets(); - igl::writeOBJ(filename.string(), vertices, facets); - } -} - -template -void extract_attribute( - const MeshType& mesh, - const std::string& attr_name, - Eigen::MatrixBase& values, - Eigen::MatrixBase& indices) -{ - const auto& facets = mesh.get_facets(); - if (mesh.has_indexed_attribute(attr_name)) { - auto attr = mesh.get_indexed_attribute(attr_name); - values = std::get<0>(attr); - indices = std::get<1>(attr); - } else if (mesh.has_corner_attribute(attr_name)) { - values = mesh.get_corner_attribute(attr_name); - indices.derived().resize(facets.rows(), facets.cols()); - typename MeshType::Index count = 0; - for (auto i : range(facets.rows())) { - for (auto j : range(facets.cols())) { - indices(i, j) = count; - count++; - } - } - } else if (mesh.has_vertex_attribute(attr_name)) { - values = mesh.get_vertex_attribute(attr_name); - indices = facets; - } -} - -template -void save_mesh_obj(const fs::path& filename, const MeshType& mesh) -{ - using Scalar = typename MeshType::Scalar; - using Index = typename MeshType::Index; - - const auto& vertices = mesh.get_vertices(); - const auto& facets = mesh.get_facets(); - - Eigen::Matrix CN; - Eigen::Matrix FN; - Eigen::Matrix TC; - Eigen::Matrix FTC; - - extract_attribute(mesh, "uv", TC, FTC); - extract_attribute(mesh, "normal", CN, FN); +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif - igl::writeOBJ(filename.string(), vertices, facets, CN, FN, TC, FTC); -} +namespace lagrange::io { -template -void save_mesh_vtk( +/** + * Save a mesh to a file. + * + * @param[in] filename path to output + * @param[in] mesh mesh to save + * @param[in] encoding If supported, pick whether to save binary or plain text. + */ +template +void save_mesh( const fs::path& filename, - const MeshType& mesh, - const std::vector& face_attrib_names, - const std::vector& vertex_attrib_names) -{ - using AttributeArray = typename MeshType::AttributeArray; - - auto write_connectivity = [&](std::ostream& fl) { - la_runtime_assert(fl); - // Not a hard requirement. But since this is just a debugging tool - // let's just enforce it for now. - la_runtime_assert(mesh.get_vertex_per_facet() == 3); - - /* - * Write the vtk file. - */ - - // write the header - fl << "# vtk DataFile Version 2.0\n"; - fl << "Lagrange output mesh\n"; - fl << "ASCII\n"; - fl << "DATASET UNSTRUCTURED_GRID\n"; - fl << "\n"; + const SurfaceMesh& mesh, + const SaveOptions& options = {}); - // write the vertices - fl << "POINTS " << mesh.get_num_vertices() << " float\n"; - for (auto vnidx : range(mesh.get_num_vertices())) { - auto xyz = mesh.get_vertices().row(vnidx).eval(); - if (xyz.cols() == 3) { - fl << xyz(0) << " " << xyz(1) << " " << xyz(2) << "\n"; - } else if (xyz.cols() == 2) { - fl << xyz(0) << " " << xyz(1) << " " << 0 << "\n"; - } else { - throw std::runtime_error("This dimension is not supported"); - } - } - fl << "\n"; - - // - // write the faces - // - - // count their total number of vertices. - fl << "CELLS " << mesh.get_num_facets() << " " - << mesh.get_num_facets() * (mesh.get_vertex_per_facet() + 1) << "\n"; - for (auto fn : range(mesh.get_num_facets())) { - fl << mesh.get_vertex_per_facet() << " "; - for (auto voffset : range(mesh.get_vertex_per_facet())) { // - fl << mesh.get_facets()(fn, voffset) << " "; - } - fl << "\n"; - } - fl << "\n"; - - // write the face types - fl << "CELL_TYPES " << mesh.get_num_facets() << "\n"; - for (auto f : range(mesh.get_num_facets())) { - (void)f; - // fl << "7 \n"; // VTK POLYGON - fl << "5 \n"; // VTK TRIANGLE - } - fl << "\n"; - }; // end of write connectivity - - auto write_vertex_data_header = [&](std::ostream& fl) { - fl << "POINT_DATA " << mesh.get_num_vertices() << " \n"; - }; // end of write_vert_data_header - - auto write_cell_data_header = [&](std::ostream& fl) { - fl << "CELL_DATA " << mesh.get_num_facets() << " \n"; - }; // end of write_cell_header - - auto write_data = - [](std::ostream& fl, const std::string attrib_name, const AttributeArray& attrib) { - fl << "SCALARS " << attrib_name << " float " << attrib.cols() << "\n"; - fl << "LOOKUP_TABLE default \n"; - for (auto row : range(attrib.rows())) { - for (auto col : range(attrib.cols())) { - fl << attrib(row, col) << " "; - } // end of col - fl << "\n"; - } // end of row - fl << "\n"; - }; // end of write_data() - - // Open the file - fs::ofstream fl(filename, fs::fstream::out); - fl.precision(12); - fl.flags(fs::fstream::scientific); - la_runtime_assert(fl.is_open()); - - // write the connectivity - write_connectivity(fl); - - // Writing the face attribs - { - // Check if the mesh has any of the requested attribs - bool has_any_facet_attrib = false; - for (const auto& attrib_name : face_attrib_names) { - if (mesh.has_facet_attribute(attrib_name)) { - has_any_facet_attrib = true; - break; - } - } - // Write the header if it does - if (has_any_facet_attrib) { - write_cell_data_header(fl); - } - // Now write the data - for (const auto& attrib_name : face_attrib_names) { - if (mesh.has_facet_attribute(attrib_name)) { - write_data(fl, attrib_name, mesh.get_facet_attribute(attrib_name)); - } - } - - } // end of writing face attribs - - // Writing the vertex attribs - { - // Check if the mesh has any of the requested attribs - bool has_any_vertex_attrib = false; - for (const auto& attrib_name : vertex_attrib_names) { - if (mesh.has_vertex_attribute(attrib_name)) { - has_any_vertex_attrib = true; - break; - } - } - - // Write the header if it does - if (has_any_vertex_attrib) { - write_vertex_data_header(fl); - } - - // Now write the data - for (const auto& attrib_name : vertex_attrib_names) { - if (mesh.has_vertex_attribute(attrib_name)) { - write_data(fl, attrib_name, mesh.get_vertex_attribute(attrib_name)); - } - } - - } // end of writing vertex attribs - -} // end of write vtk - -} // namespace internal - - -template -void save_mesh(const fs::path& filename, const MeshType& mesh) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - if (filename.extension() == ".obj") { - internal::save_mesh_obj(filename, mesh); - } else if (filename.extension() == ".vtk") { - internal::save_mesh_vtk( - filename, - mesh, - mesh.get_facet_attribute_names(), - mesh.get_vertex_attribute_names()); - } else if (filename.extension() == ".ply") { - save_mesh_ply(filename, mesh); - } else { - internal::save_mesh_basic(filename, mesh); - } } - -} // namespace io -} // namespace lagrange diff --git a/modules/io/include/lagrange/io/save_mesh_gltf.h b/modules/io/include/lagrange/io/save_mesh_gltf.h new file mode 100644 index 00000000..04ba71c3 --- /dev/null +++ b/modules/io/include/lagrange/io/save_mesh_gltf.h @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +namespace lagrange::io { + +/** + * Saves a mesh to a file in glTF or GLB format. + * + * @param[in] filename Output filename. + * @param[in] mesh Mesh to write. + * @param[in] options Save options. + * + * @tparam Scalar Mesh scalar type. + * @tparam Index Mesh index type. + */ +template +void save_mesh_gltf( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options = {}); + + +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/save_mesh_msh.h b/modules/io/include/lagrange/io/save_mesh_msh.h index f2b7cc72..fba52ca4 100644 --- a/modules/io/include/lagrange/io/save_mesh_msh.h +++ b/modules/io/include/lagrange/io/save_mesh_msh.h @@ -13,28 +13,15 @@ #include #include - -#include -#include +#include namespace lagrange::io { /** - * Config options for saving mesh in .msh format. - */ -struct MshSaverOptions -{ - bool binary = true; ///< Use binary encoding. - std::vector attr_ids; ///< Set of attributes to output. -}; - - -/** - * Saves a mesh to a stream in MSH format. If the mesh cannot be saved, an - * exception is raised (e.g., invalid output stream, incorrect mesh dimension, - * or facet size < 3). + * Saves a mesh to a stream in MSH format. If the mesh cannot be saved, an exception is raised + * (e.g., invalid output stream, incorrect mesh dimension, or facet size < 3). * - * @param[in,out] output_stream Output stream to write to. + * @param[in,out] output_stream Output stream. * @param[in] mesh Input mesh. * @param[in] options Option settings. * @@ -45,23 +32,28 @@ template void save_mesh_msh( std::ostream& output_stream, const SurfaceMesh& mesh, - const MshSaverOptions& options = {}); + const SaveOptions& options = {}); /** * @overload * - * @param[in] filename Output filename. + * Saves a mesh to a stream in MSH format. If the mesh cannot be saved, an exception is raised + * (e.g., incorrect mesh dimension, or facet size < 3). + * + * @param[in] filename Output filename. + * @param[in] mesh Mesh to write. + * @param[in] options Save options. + * + * @see save_mesh_msh(std::ostream&, const SurfaceMesh&, const MshOptions&) for more + * info. * - * @see save_mesh_msh(std::ostream&, const SurfaceMesh&, const MshOptions&) for more info. + * @tparam Scalar Mesh scalar type. + * @tparam Index Mesh index type. */ template void save_mesh_msh( const fs::path& filename, const SurfaceMesh& mesh, - const MshSaverOptions& options = {}) -{ - fs::ofstream fout(filename); - save_mesh_msh(fout, mesh, options); -} + const SaveOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/save_mesh_obj.h b/modules/io/include/lagrange/io/save_mesh_obj.h index c6ed5fe0..0e17b5e5 100644 --- a/modules/io/include/lagrange/io/save_mesh_obj.h +++ b/modules/io/include/lagrange/io/save_mesh_obj.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -21,30 +22,40 @@ namespace lagrange { namespace io { /// -/// Saves a .obj mesh to a stream. If the mesh cannot be saved, an exception is raised (e.g., -/// invalid output stream, incorrect mesh dimension, or facet size < 3). +/// Saves a mesh to a stream in OBJ format. If the mesh cannot be saved, an exception is raised +/// (e.g., invalid output stream, incorrect mesh dimension, or facet size < 3). /// -/// @param[in,out] output_stream Output stream to write to. -/// @param[in] mesh Mesh to write as an obj. +/// @param[in,out] output_stream Output stream. +/// @param[in] mesh Mesh to write. +/// @param[in] options Save options. /// /// @tparam Scalar Mesh scalar type. /// @tparam Index Mesh index type. /// template -void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh& mesh); +void save_mesh_obj( + std::ostream& output_stream, + const SurfaceMesh& mesh, + const SaveOptions& options = {}); /// -/// Saves a .obj mesh to a file. If the mesh cannot be saved, an exception is raised (e.g., invalid -/// output stream, incorrect mesh dimension, or facet size < 3). +/// @overload /// -/// @param[in] filename Output filename to write to. -/// @param[in] mesh Mesh to write as an obj. +/// Saves a mesh to a file in OBJ format. If the mesh cannot be saved, an exception is raised (e.g., +/// incorrect mesh dimension, or facet size < 3). +/// +/// @param[in] filename Output filename. +/// @param[in] mesh Mesh to write. +/// @param[in] options Save options. /// /// @tparam Scalar Mesh scalar type. /// @tparam Index Mesh index type. /// template -void save_mesh_obj(const fs::path& filename, const SurfaceMesh& mesh); +void save_mesh_obj( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options = {}); } // namespace io } // namespace lagrange diff --git a/modules/io/include/lagrange/io/save_mesh_ply.h b/modules/io/include/lagrange/io/save_mesh_ply.h index 81221f2d..9376ac45 100644 --- a/modules/io/include/lagrange/io/save_mesh_ply.h +++ b/modules/io/include/lagrange/io/save_mesh_ply.h @@ -1,5 +1,5 @@ /* - * Copyright 2020 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -11,84 +11,44 @@ */ #pragma once +#include #include #include -#include -#include -#include - -// clang-format off -#include -#include -#include -// clang-format on - -#include -#include - -namespace lagrange { -namespace io { - -/// Save .ply with color information (if available) -template -void save_mesh_ply(const fs::path &filename, const MeshType &mesh, FileEncoding encoding = FileEncoding::Binary) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - using VertexArray = typename MeshType::VertexArray; - using AttributeArray = typename MeshType::AttributeArray; - using Index = unsigned int; - - const VertexArray &V = mesh.get_vertices(); - const auto F = mesh.get_facets().template cast(); - Eigen::Matrix E; - AttributeArray N; - AttributeArray UV; - - Eigen::Matrix VD; - std::vector VDheader; - - AttributeArray FD; - std::vector FDheader; - - AttributeArray ED; - std::vector EDheader; - std::vector comments; - - if (mesh.has_vertex_attribute("normal")) { - N = mesh.get_vertex_attribute("normal"); - } - - if (mesh.has_vertex_attribute("color")) { - VDheader = {"red", "green", "blue"}; - auto C = mesh.get_vertex_attribute("color"); - if (C.maxCoeff() <= 1.0 && C.maxCoeff() > 0.0) { - logger().warn("Max color value is > 0.0 but <= 1.0, but colors are saved as char. " - "Please convert your colors to the range [0, 255]."); - } - VD = C.template cast(); - if (VD.cols() > 3) { - VDheader.push_back("alpha"); - } - la_runtime_assert(safe_cast(VD.cols()) == VDheader.size()); - } - - igl::writePLY( - filename.string(), - V, - F, - E, - N, - UV, - VD, - VDheader, - FD, - FDheader, - ED, - EDheader, - comments, - encoding == FileEncoding::Binary ? igl::FileEncoding::Binary : igl::FileEncoding::Ascii); -} - -} // namespace io -} // namespace lagrange +namespace lagrange::io { + +/// +/// Saves a mesh to a stream in PLY format. +/// +/// @param[in,out] output_stream Output stream. +/// @param[in] mesh Mesh to write. +/// @param[in] options Save options. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// +template +void save_mesh_ply( + std::ofstream& output_stream, + const SurfaceMesh& mesh, + const SaveOptions& options = {}); + +/// +/// @overload +/// +/// Saves a mesh to a file in PLY format. +/// +/// @param[in] filename Output filename. +/// @param[in] mesh Mesh to write. +/// @param[in] options Save options. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// +template +void save_mesh_ply( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options = {}); + +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/save_simple_scene.h b/modules/io/include/lagrange/io/save_simple_scene.h new file mode 100644 index 00000000..fee811b3 --- /dev/null +++ b/modules/io/include/lagrange/io/save_simple_scene.h @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +namespace lagrange::io { + +/** + * Save a mesh to a file. + * + * @param[in] filename path to output + * @param[in] scene mesh to save + * @param[in] options SaveOptions, check the struct for more details. + */ +template +void save_simple_scene( + const fs::path& filename, + const scene::SimpleScene& scene, + const SaveOptions& options = {}); + +} diff --git a/modules/io/include/lagrange/io/save_simple_scene_gltf.h b/modules/io/include/lagrange/io/save_simple_scene_gltf.h new file mode 100644 index 00000000..73e020d0 --- /dev/null +++ b/modules/io/include/lagrange/io/save_simple_scene_gltf.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +namespace lagrange::io { + +/** + * Save a simple scene to a gltf or glb file. + * + * @param filename path to output file + * @param scene Scene to save + * @param options SaveOptions, check the struct for more details. + */ +template +void save_simple_scene_gltf( + const fs::path& filename, + const scene::SimpleScene& scene, + const SaveOptions& options = {}); + + +} // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/types.h b/modules/io/include/lagrange/io/types.h index 1d6cc388..c7307b35 100644 --- a/modules/io/include/lagrange/io/types.h +++ b/modules/io/include/lagrange/io/types.h @@ -11,10 +11,79 @@ */ #pragma once +#include + +#include + namespace lagrange { namespace io { enum class FileEncoding { Binary, Ascii }; +/** + * Options used when saving a mesh or a scene. + * Note that not all options are supported for all backends or filetypes. + */ +struct SaveOptions +{ + /** + * Whether to encode the file as plain text or binary. + * Some filetypes only support Ascii and will ignore this parameter. + */ + FileEncoding encoding = FileEncoding::Binary; + + /** + * Which attributes to save with the mesh? + */ + enum class OutputAttributes { + /// All attributes (default) + All, + + /// Only attributes listed in `selected_attributes` + SelectedOnly, + }; + OutputAttributes output_attributes = OutputAttributes::All; + + /// Attributes to output, usage depends on the above. + std::vector selected_attributes; +}; + +/** + * Options used when loading a mesh or a scene. + * Note that not all options are supported for all backends or filetypes. + */ +struct LoadOptions +{ + /// Triangulate any polygonal facet with > 3 vertices + bool triangulate = false; + + /// Load vertex normals as indexed attribute + bool load_normals = true; + + /// Load tangents and bitangent as indexed attribute + bool load_tangents = true; + + /// Load texture coordinates as indexed attribute + bool load_uvs = true; + + /// Load skinning weights attributes (joints id and weight). + bool load_weights = true; + + /// Load material ids as facet attribute + bool load_materials = true; + + /// Load vertex colors as vertex attribute + bool load_vertex_colors = false; + + /// Load object id as facet attribute + bool load_object_id = true; + + /** + * Search path for related files, such as .mtl, .bin, or image textures. + * By default, searches the same folder as the provided filename. + */ + fs::path search_path; +}; + } // namespace io } // namespace lagrange diff --git a/modules/io/src/internal/load_obj.cpp b/modules/io/src/internal/load_obj.cpp new file mode 100644 index 00000000..415dd913 --- /dev/null +++ b/modules/io/src/internal/load_obj.cpp @@ -0,0 +1,241 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +#include +#include + +namespace lagrange::io { +namespace internal { + +template +auto load_mesh_obj(const fs::path& filename, const LoadOptions& options) + -> ObjReaderResult +{ + ObjReaderResult result; + + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + using SignedIndex = typename MeshType::SignedIndex; + + tinyobj::ObjReader reader; + { + logger().trace("[load_mesh_obj] Parsing obj file: {}", filename.string()); + tinyobj::ObjReaderConfig config; + config.triangulate = options.triangulate; + config.vertex_color = options.load_vertex_colors; + config.mtl_search_path = options.search_path.string(); + reader.ParseFromFile(filename.string(), config); + } + + result.success = reader.Valid(); + if (!reader.Warning().empty()) { + auto lines = string_split(reader.Warning(), '\n'); + for (const auto& msg : lines) { + logger().warn("[load_mesh_obj] {}", msg); + } + } + if (!reader.Error().empty()) { + auto lines = string_split(reader.Error(), '\n'); + for (const auto& msg : lines) { + logger().error("[load_mesh_obj] {}", msg); + } + } + if (!reader.Valid()) { + return result; + } + + logger().trace("[load_mesh_obj] Copying data into a mesh"); + + auto& mesh = result.mesh; + const Index dim = 3; + const Index uv_dim = 2; + const auto& attrib = reader.GetAttrib(); + const auto& shapes = reader.GetShapes(); + if (options.load_materials) { + result.materials = reader.GetMaterials(); + } + + // Copy vertices + logger().trace("[load_mesh_obj] Copying vertices"); + la_runtime_assert(attrib.vertices.size() % dim == 0); + const Index num_vertices = safe_cast(attrib.vertices.size()) / dim; + mesh.add_vertices(num_vertices, [&](Index v, span p) { + la_debug_assert(dim == p.size()); + std::copy_n(attrib.vertices.begin() + static_cast(v * dim), dim, p.begin()); + }); + + // Copy texcoord values + IndexedAttribute* uv_attr = nullptr; + if (options.load_uvs && !attrib.texcoords.empty()) { + logger().trace("[load_mesh_obj] Copying uvs"); + la_runtime_assert(attrib.texcoords.size() % uv_dim == 0); + auto id = mesh.template create_attribute( + AttributeName::texcoord, + AttributeElement::Indexed, + AttributeUsage::UV, + uv_dim); + uv_attr = &mesh.template ref_indexed_attribute(id); + uv_attr->values().resize_elements(attrib.texcoords.size() / uv_dim); + std::copy( + attrib.texcoords.begin(), + attrib.texcoords.end(), + uv_attr->values().ref_all().begin()); + } + + // Copy normal values + IndexedAttribute* nrm_attr = nullptr; + if (options.load_normals && !attrib.normals.empty()) { + logger().trace("[load_mesh_obj] Copying normals"); + la_runtime_assert(attrib.normals.size() % dim == 0); + auto id = mesh.template create_attribute( + AttributeName::normal, + AttributeElement::Indexed, + AttributeUsage::Normal, + dim); + nrm_attr = &mesh.template ref_indexed_attribute(id); + nrm_attr->values().resize_elements(attrib.normals.size() / dim); + std::copy( + attrib.normals.begin(), + attrib.normals.end(), + nrm_attr->values().ref_all().begin()); + } + + // Copy vertex colors + if (options.load_vertex_colors && !attrib.colors.empty()) { + logger().trace("[load_mesh_obj] Copying vertex colors"); + la_runtime_assert(mesh.get_num_vertices() == attrib.colors.size() / dim); + auto id = mesh.template create_attribute( + AttributeName::color, + AttributeElement::Vertex, + AttributeUsage::Color, + dim); + auto& attr = mesh.template ref_attribute(id); + std::copy(attrib.colors.begin(), attrib.colors.end(), attr.ref_all().begin()); + } + + // Reserve facet indices + logger().trace("[load_mesh_obj] Reserve facet indices"); + std::vector facet_sizes; + std::vector facet_counts; + for (const auto& shape : shapes) { + facet_sizes.insert( + facet_sizes.end(), + shape.mesh.num_face_vertices.begin(), + shape.mesh.num_face_vertices.end()); + facet_counts.push_back(shape.mesh.num_face_vertices.size()); + result.names.push_back(shape.name); + } + mesh.add_hybrid(facet_sizes); + + // Initialize material id attr + Attribute* mat_attr = nullptr; + if (options.load_materials) { + auto id = mesh.template create_attribute( + AttributeName::material_id, + AttributeElement::Facet, + AttributeUsage::Scalar); + mat_attr = &mesh.template ref_attribute(id); + } + + // Initialize object id + Attribute* id_attr = nullptr; + if (options.load_object_id) { + auto id = mesh.template create_attribute( + AttributeName::object_id, + AttributeElement::Facet, + AttributeUsage::Scalar); + id_attr = &mesh.template ref_attribute(id); + } + + span vtx_indices = mesh.ref_corner_to_vertex().ref_all(); + span uv_indices = (uv_attr ? uv_attr->indices().ref_all() : span{}); + span nrm_indices = (nrm_attr ? nrm_attr->indices().ref_all() : span{}); + + logger().trace("[load_mesh_obj] Copy facet indices"); + std::partial_sum(facet_counts.begin(), facet_counts.end(), facet_counts.begin()); + tbb::parallel_for(Index(0), Index(shapes.size()), [&](Index i) { + const auto& shape = shapes[i]; + const Index first_facet = (i == 0 ? 0 : facet_counts[i - 1]); + const auto local_num_facets = safe_cast(shape.mesh.num_face_vertices.size()); + + // Copy material id + if (mat_attr) { + la_debug_assert(local_num_facets <= mesh.get_num_facets()); + la_debug_assert(local_num_facets <= mat_attr->get_num_elements()); + auto span = mat_attr->ref_middle(first_facet, local_num_facets); + if (shape.mesh.material_ids.empty()) { + std::fill(span.begin(), span.end(), SignedIndex(-1)); + } else { + la_runtime_assert(shape.mesh.material_ids.size() == local_num_facets); + std::transform( + shape.mesh.material_ids.begin(), + shape.mesh.material_ids.end(), + span.begin(), + [](auto x) -> SignedIndex { return safe_cast(x); }); + } + } + + // Copy object id + if (id_attr) { + auto span = id_attr->ref_middle(first_facet, local_num_facets); + std::fill(span.begin(), span.end(), i); + } + + // Copy indices + for (Index f = 0, local_corner = 0; f < local_num_facets; ++f) { + const Index first_corner = mesh.get_facet_corner_begin(first_facet + f); + const Index last_corner = mesh.get_facet_corner_end(first_facet + f); + + for (Index c = first_corner; c < last_corner; ++c, ++local_corner) { + const auto& index = shape.mesh.indices[local_corner]; + vtx_indices[c] = safe_cast(index.vertex_index); + if (!uv_indices.empty()) { + uv_indices[c] = safe_cast(index.texcoord_index); + } + if (!nrm_indices.empty()) { + nrm_indices[c] = safe_cast(index.normal_index); + } + } + } + + // TODO: Support smoothing groups + subd tags + }); + + logger().trace("[load_mesh_obj] Loading complete"); + + return result; +} + +#define LA_X_load_mesh(_, Scalar, Index) \ + template ObjReaderResult load_mesh_obj>( \ + const fs::path& filename, \ + const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh, 0) + +} // namespace internal +} // namespace lagrange::io diff --git a/modules/io/src/legacy_load_mesh.cpp b/modules/io/src/legacy_load_mesh.cpp new file mode 100644 index 00000000..a0c391f5 --- /dev/null +++ b/modules/io/src/legacy_load_mesh.cpp @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +namespace lagrange::io { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +template std::unique_ptr load_mesh_basic(const fs::path&); +template std::unique_ptr load_mesh_basic(const fs::path&); + +template std::vector> load_obj_meshes(const fs::path&); +template std::vector> load_obj_meshes(const fs::path&); + +template std::unique_ptr load_obj_mesh(const fs::path&); +template std::unique_ptr load_obj_mesh(const fs::path&); + +template std::unique_ptr load_mesh(const fs::path&); +template std::unique_ptr load_mesh(const fs::path&); +template std::unique_ptr load_mesh(const fs::path&); +template std::unique_ptr> load_mesh(const fs::path&); + +} // namespace legacy +} // namespace lagrange::io diff --git a/modules/io/src/load_assimp.cpp b/modules/io/src/load_assimp.cpp new file mode 100644 index 00000000..e18b5299 --- /dev/null +++ b/modules/io/src/load_assimp.cpp @@ -0,0 +1,322 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#ifdef LAGRANGE_WITH_ASSIMP + +// this .cpp provides implementations for functions defined in those headers: +#include +#include +#include +// ==== + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +// ===================================== +// internal/load_assimp.h +// ===================================== +namespace lagrange::io { +namespace internal { + +std::unique_ptr load_assimp(const fs::path& filename, unsigned int flags) +{ + Assimp::Importer importer; + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); + const aiScene* scene = importer.ReadFile(filename.string(), flags); + + if (!scene) { + throw std::runtime_error(importer.GetErrorString()); + } + + return std::unique_ptr(importer.GetOrphanedScene()); +} + + +template +MeshType convert_mesh_assimp_to_lagrange(const aiMesh& aimesh, const LoadOptions& options) +{ + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + constexpr unsigned int dim = 3; + constexpr unsigned int color_dim = 4; + constexpr unsigned int uv_dim = 2; + + MeshType lmesh; + + lmesh.add_vertices(aimesh.mNumVertices, [&](Index v, span p) -> void { + for (unsigned int i = 0; i < dim; ++i) { + p[i] = Scalar(aimesh.mVertices[v][i]); + } + }); + + lmesh.add_hybrid( + aimesh.mNumFaces, + [&aimesh](Index f) -> Index { return aimesh.mFaces[f].mNumIndices; }, + [&aimesh](Index f, span t) -> void { + const aiFace& face = aimesh.mFaces[f]; + la_debug_assert(t.size() == face.mNumIndices); + for (unsigned int i = 0; i < face.mNumIndices; ++i) { + t[i] = Index(face.mIndices[i]); + } + }); + + const unsigned int num_uv_channels = aimesh.GetNumUVChannels(); + if (options.load_uvs && num_uv_channels > 0) { + for (unsigned int uv_set = 0; uv_set < num_uv_channels; ++uv_set) { + std::string name; + if (aimesh.HasTextureCoordsName(uv_set)) { + name = std::string(aimesh.GetTextureCoordsName(uv_set)->C_Str()); + } else if (num_uv_channels > 1) { + name = std::string(AttributeName::texcoord) + "_" + std::to_string(uv_set); + } else { + name = AttributeName::texcoord; + } + + auto id = lmesh.template create_attribute( + name, + AttributeElement::Vertex, + AttributeUsage::UV, + uv_dim); + auto uv_attr = lmesh.template ref_attribute(id).ref_all(); + for (unsigned int i = 0; i < aimesh.mNumVertices; ++i) { + const aiVector3D& vec = aimesh.mTextureCoords[uv_set][i]; + for (unsigned int j = 0; j < uv_dim; ++j) { + uv_attr[i * uv_dim + j] = Scalar(vec[j]); + } + } + } + } + + if (aimesh.HasBones() && options.load_weights) { + using BoneIndexType = uint32_t; + using BoneWeightScalar = float; + Eigen::Matrix weights( + aimesh.mNumVertices, + aimesh.mNumBones); + weights.setZero(); + + for (unsigned int i = 0; i < aimesh.mNumBones; ++i) { + const aiBone* bone = aimesh.mBones[i]; + for (unsigned int j = 0; j < bone->mNumWeights; ++j) { + const aiVertexWeight& weight = bone->mWeights[j]; + weights(weight.mVertexId, i) = BoneWeightScalar(weight.mWeight); + } + } + + // weights matrix is |V| x |bones| + lagrange::internal:: + weights_to_indexed_mesh_attribute( + lmesh, + weights, + 4); + } + + if (aimesh.HasTangentsAndBitangents() && options.load_tangents) { + auto id_tangent = lmesh.template create_attribute( + AttributeName::tangent, + AttributeElement::Vertex, + AttributeUsage::Vector, + dim); + auto id_bitangent = lmesh.template create_attribute( + AttributeName::bitangent, + AttributeElement::Vertex, + AttributeUsage::Vector, + dim); + auto tangent_attr = lmesh.template ref_attribute(id_tangent).ref_all(); + auto bitangent_attr = lmesh.template ref_attribute(id_bitangent).ref_all(); + + for (unsigned int i = 0; i < aimesh.mNumVertices; ++i) { + const aiVector3D& t = aimesh.mTangents[i]; + for (unsigned int j = 0; j < dim; j++) { + tangent_attr[i * dim + j] = t[j]; + } + const aiVector3D& bt = aimesh.mBitangents[i]; + for (unsigned int j = 0; j < dim; j++) { + bitangent_attr[i * dim + j] = bt[j]; + } + } + } + + if (aimesh.HasNormals() && options.load_normals) { + auto id = lmesh.template create_attribute( + AttributeName::normal, + AttributeElement::Vertex, + AttributeUsage::Normal, + dim); + auto normal_attr = lmesh.template ref_attribute(id).ref_all(); + + for (unsigned int i = 0; i < aimesh.mNumVertices; ++i) { + const aiVector3D& t = aimesh.mNormals[i]; + for (unsigned int j = 0; j < dim; j++) { + normal_attr[i * dim + j] = t[j]; + } + } + } + + const unsigned int num_color_channels = aimesh.GetNumColorChannels(); + if (options.load_vertex_colors && num_color_channels > 0) { + for (unsigned int color_set = 0; color_set < num_color_channels; ++color_set) { + std::string name; + if (num_color_channels > 1) { + name = std::string(AttributeName::color) + "_" + std::to_string(color_set); + } else { + name = AttributeName::color; + } + auto id = lmesh.template create_attribute( + name, + AttributeElement::Vertex, + AttributeUsage::Color, + color_dim); + auto color_attr = lmesh.template ref_attribute(id).ref_all(); + for (unsigned int i = 0; i < aimesh.mNumVertices; ++i) { + const aiColor4D& color = aimesh.mColors[color_set][i]; + for (unsigned int j = 0; j < color_dim; j++) { + color_attr[i * color_dim + j] = color[j]; + } + } + } + } + + return lmesh; +} +#define LA_X_convert_mesh_assimp_to_lagrange(_, S, I) \ + template SurfaceMesh convert_mesh_assimp_to_lagrange( \ + const aiMesh& mesh, \ + const LoadOptions& options); +LA_SURFACE_MESH_X(convert_mesh_assimp_to_lagrange, 0); +#undef LA_X_convert_mesh_assimp_to_lagrange + + +template +MeshType load_mesh_assimp(const aiScene& scene, const LoadOptions& options) +{ + la_runtime_assert(scene.mNumMeshes > 0); + if (scene.mNumMeshes == 1) { + return convert_mesh_assimp_to_lagrange(*scene.mMeshes[0], options); + } else { + std::vector meshes(scene.mNumMeshes); + for (unsigned int i = 0; i < scene.mNumMeshes; ++i) { + meshes[i] = convert_mesh_assimp_to_lagrange(*scene.mMeshes[i], options); + } + bool preserve_attributes = true; + return combine_meshes( + meshes, + preserve_attributes); + } + +} +#define LA_X_load_mesh_assimp(_, S, I) \ + template SurfaceMesh load_mesh_assimp( \ + const aiScene& mesh, \ + const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh_assimp, 0); +#undef LA_X_load_mesh_assimp + + +template +SceneType load_simple_scene_assimp(const aiScene& scene, const LoadOptions& options) +{ + using MeshType = typename SceneType::MeshType; + using AffineTransform = typename SceneType::AffineTransform; + + // TODO: handle 2d SimpleScene + + SceneType lscene; + + for (unsigned int i = 0; i < scene.mNumMeshes; ++i) { + // By adding in the same order, we can assume that assimp's indexing is the same + // as in the lagrange scene. We use this later. + lscene.add_mesh(convert_mesh_assimp_to_lagrange(*scene.mMeshes[i], options)); + } + std::function visit_node; + visit_node = [&](aiNode* node, const AffineTransform& parent_transform) -> void { + AffineTransform node_transform; + if constexpr (SceneType::Dim == 3) { + auto& t = node->mTransformation; + // clang-format off + node_transform.matrix() << + t.a1, t.a2, t.a3, t.a4, + t.b1, t.b2, t.b3, t.b4, + t.c1, t.c2, t.c3, t.c4, + t.d1, t.d2, t.d3, t.d4; + //clang-format on + } else { + // TODO: convert 3d transforms into 2d + logger().warn("Ignoring 3d node transform while loading 2d scene"); + } + AffineTransform global_transform = parent_transform * node_transform; + + for (unsigned int i = 0; i < node->mNumMeshes; ++i) { + unsigned int mesh_idx = node->mMeshes[i]; + lscene.add_instance({mesh_idx, global_transform}); + } + + for (unsigned int i = 0; i < node->mNumChildren; ++i) { + visit_node(node->mChildren[i], global_transform); + } + }; + visit_node(scene.mRootNode, AffineTransform::Identity()); + return lscene; +} +#define LA_X_load_simple_scene_assimp(_, S, I, D) \ + template scene::SimpleScene load_simple_scene_assimp(const aiScene& scene, const LoadOptions& options); +LA_SIMPLE_SCENE_X(load_simple_scene_assimp, 0); +#undef LA_X_load_simple_scene_assimp + +} // namespace lagrange::io::internal + + +// ===================================== +// load_mesh_assimp.h +// ===================================== +template ::value>* /*= nullptr*/> +MeshType load_mesh_assimp(const fs::path& filename, const LoadOptions& options) { + std::unique_ptr scene = internal::load_assimp(filename); + return internal::load_mesh_assimp(*scene, options); +} + +#define LA_X_load_mesh_assimp(_, S, I) \ + template SurfaceMesh load_mesh_assimp(const fs::path& filename, const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh_assimp, 0); +#undef LA_X_load_mesh_assimp + + +// ===================================== +// load_simple_scene_assimp.h +// ===================================== +template +SceneType load_simple_scene_assimp(const fs::path& filename, const LoadOptions& options) +{ + std::unique_ptr scene = internal::load_assimp(filename); + return internal::load_simple_scene_assimp(*scene, options); +} + +#define LA_X_load_simple_scene_assimp(_, S, I, D) \ + template scene::SimpleScene load_simple_scene_assimp(const fs::path& filename, const LoadOptions& options); +LA_SIMPLE_SCENE_X(load_simple_scene_assimp, 0); +#undef LA_X_load_simple_scene_assimp + +} // namespace lagrange::io + +#endif diff --git a/modules/io/src/load_gltf.cpp b/modules/io/src/load_gltf.cpp new file mode 100644 index 00000000..9e923e32 --- /dev/null +++ b/modules/io/src/load_gltf.cpp @@ -0,0 +1,432 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// this .cpp provides implementations for functions defined in those headers: +#include +#include +#include +// ==== + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace lagrange::io { +namespace internal { + +namespace { + +template +std::vector load_buffer_data_internal( + const tinygltf::Model& model, + const tinygltf::Accessor& accessor) +{ + const tinygltf::BufferView& buffer_view = model.bufferViews[accessor.bufferView]; + const tinygltf::Buffer& buffer = model.buffers[buffer_view.buffer]; + + int size = 1; + if (accessor.type != TINYGLTF_TYPE_SCALAR) { + size = accessor.type; + } + + std::vector data(accessor.count * size); + + const size_t start = accessor.byteOffset + buffer_view.byteOffset; + if (buffer_view.byteStride) { + for (size_t i = 0; i < accessor.count; ++i) { + size_t buf_idx = start + buffer_view.byteStride * i; + std::memcpy(&data.at(i * size), &buffer.data.at(buf_idx), size * sizeof(Orig_t)); + } + } else { + std::memcpy(&data.at(0), &buffer.data.at(start), accessor.count * size * sizeof(Orig_t)); + } + + std::vector ret; + std::transform(data.begin(), data.end(), std::back_inserter(ret), [&](Orig_t x) { + if (accessor.normalized) { + // If needed, convert normalized values into float or double. Details here: + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { + return Target_t(std::max(x / 127.0, -1.0)); + } else if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { + return Target_t(x / 255.0); + } else if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_SHORT) { + return Target_t(std::max(x / 32767.0, -1.0)); + } else if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + return Target_t(x / 65535.0); + } else { + la_runtime_assert("Invalid normalized/componentType pair!"); + return Target_t(0); // happy compiler + } + } else { + return Target_t(x); + } + + }); + return ret; +} + +template +std::vector load_buffer_data_as(const tinygltf::Model& model, const tinygltf::Accessor& accessor) +{ + // clang-format off + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_BYTE: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_SHORT: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_INT: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_FLOAT: + return load_buffer_data_internal(model, accessor); + case TINYGLTF_COMPONENT_TYPE_DOUBLE: + return load_buffer_data_internal(model, accessor); + default: + throw std::runtime_error("Unexpected component type"); + } + // clang-format on + return std::vector(); // make compiler happy +} +} // namespace + +// ===================================== +// internal/load_gltf.h +// ===================================== +tinygltf::Model load_tinygltf(const fs::path& filename) +{ + tinygltf::Model model; + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + bool ret = loader.LoadASCIIFromFile(&model, &err, &warn, filename.string()); + + if (!warn.empty()) logger().warn("%s", warn.c_str()); + if (!ret || !err.empty()) { + throw std::runtime_error(err); + } + + return model; +} + +template +MeshType convert_mesh_tinygltf_to_lagrange( + const tinygltf::Model& model, + const tinygltf::Mesh& mesh, + const LoadOptions& options) +{ + using Index = typename MeshType::Index; + using Scalar = typename MeshType::Scalar; + + // each gltf mesh is made of one or more primitives. + // Different primitives can reference different materials and data buffers. + std::vector lmeshes; + + for (size_t i = 0; i < mesh.primitives.size(); ++i) { + MeshType lmesh; + const tinygltf::Primitive& primitive = mesh.primitives[i]; + la_runtime_assert(primitive.mode == TINYGLTF_MODE_TRIANGLES); + + // read vertices + auto it = primitive.attributes.find("POSITION"); + la_runtime_assert(it != primitive.attributes.end(), "missing positions"); + { + const tinygltf::Accessor& accessor = model.accessors[it->second]; + const size_t num_vertices = accessor.count; + la_debug_assert(accessor.type == TINYGLTF_TYPE_VEC3); + std::vector coords = load_buffer_data_as(model, accessor); + lmesh.add_vertices(Index(num_vertices), coords); + } + + // read faces + { + const tinygltf::Accessor& accessor = model.accessors[primitive.indices]; + const size_t num_indices = accessor.count; + const size_t num_facets = num_indices / 3; // because triangle + la_debug_assert(accessor.type == TINYGLTF_TYPE_SCALAR); + + std::vector indices = load_buffer_data_as(model, accessor); + lmesh.add_triangles(Index(num_facets), indices); + } + + // read other attributes + for (auto& attribute : primitive.attributes) { + if (starts_with(attribute.first, "POSITION")) continue; // already done + + const tinygltf::Accessor& accessor = model.accessors[attribute.second]; + + int size = 1; + if (accessor.type != TINYGLTF_TYPE_SCALAR) { + size = accessor.type; + } + std::vector data = load_buffer_data_as(model, accessor); + la_debug_assert(data.size() == accessor.count * size); + + const std::string& name = attribute.first; + std::string name_lowercase = attribute.first; + std::transform( + name_lowercase.begin(), + name_lowercase.end(), + name_lowercase.begin(), + [](unsigned char c) { return std::tolower(c); }); + + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + if (name == "POSITION") { + la_runtime_assert(false); // done earlier, should never get here + } else if (name == "NORMAL" && options.load_normals) { + lmesh.template create_attribute( + name_lowercase, + AttributeElement::Vertex, + AttributeUsage::Normal, + size, + data); + } else if (name == "TANGENT" && options.load_tangents) { + lmesh.template create_attribute( + name_lowercase, + AttributeElement::Vertex, + AttributeUsage::Tangent, + size, + data); + } else if (starts_with(name, "COLOR") && options.load_vertex_colors) { + lmesh.template create_attribute( + name_lowercase, + AttributeElement::Vertex, + AttributeUsage::Color, + size, + data); + } else if (starts_with(name, "JOINTS") && options.load_weights) { + la_runtime_assert(accessor.type == TINYGLTF_TYPE_VEC4); + la_runtime_assert( + accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE || + accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT); + // note: should we change to integer type? + lmesh.template create_attribute( + AttributeName::indexed_joint, + AttributeElement::Vertex, + AttributeUsage::Vector, + size, + data); + } else if (starts_with(name, "WEIGHTS") && options.load_weights) { + la_runtime_assert(accessor.type == TINYGLTF_TYPE_VEC4); + la_runtime_assert( + accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT || + accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE || + accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT); + lmesh.template create_attribute( + AttributeName::indexed_weight, + AttributeElement::Vertex, + AttributeUsage::Vector, + size, + data); + } else if (starts_with(name, "TEXCOORD") && options.load_uvs) { + lmesh.template create_attribute( + AttributeName::texcoord, + AttributeElement::Vertex, + AttributeUsage::UV, + size, + data); + } else { + logger().error("Unknown mesh property {}!", name); + } + } + + + // for future reference, material is here. No need in this function. + // const tinygltf::Material& mat = model.materials[primitive.material]; + + lmeshes.push_back(lmesh); + } + + if (lmeshes.size() == 1) { + return lmeshes.front(); + } else { + constexpr bool preserve_attributes = true; + return combine_meshes(lmeshes, preserve_attributes); + } +} +#define LA_X_convert_mesh_tinygltf_to_lagrange(_, S, I) \ + template SurfaceMesh convert_mesh_tinygltf_to_lagrange( \ + const tinygltf::Model& model, \ + const tinygltf::Mesh& mesh, \ + const LoadOptions& options); +LA_SURFACE_MESH_X(convert_mesh_tinygltf_to_lagrange, 0); +#undef LA_X_convert_mesh_tinygltf_to_lagrange + +template +MeshType load_mesh_gltf(const tinygltf::Model& model, const LoadOptions& options) +{ + la_runtime_assert(!model.meshes.empty()); + if (model.meshes.size() == 1) { + return convert_mesh_tinygltf_to_lagrange(model, model.meshes[0], options); + } else { + std::vector meshes(model.meshes.size()); + for (size_t i = 0; i < model.meshes.size(); ++i) { + meshes[i] = convert_mesh_tinygltf_to_lagrange(model, model.meshes[i], options); + } + constexpr bool preserve_attributes = true; + return lagrange::combine_meshes( + meshes, + preserve_attributes); + } +} +#define LA_X_load_mesh_gltf(_, S, I) \ + template SurfaceMesh load_mesh_gltf( \ + const tinygltf::Model& model, \ + const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh_gltf, 0); +#undef LA_X_load_mesh_gltf + +template +SceneType load_simple_scene_gltf(const tinygltf::Model& model, const LoadOptions& options) +{ + using MeshType = typename SceneType::MeshType; + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + using AffineTransform = typename SceneType::AffineTransform; + + // TODO: handle 2d SimpleScene + + SceneType lscene; + + for (const tinygltf::Mesh& mesh : model.meshes) { + // By adding in the same order, we can assume that tinygltf's indexing is the same + // as in the lagrange scene. We use this later. + lscene.add_mesh(convert_mesh_tinygltf_to_lagrange(model, mesh, options)); + } + + std::function + visit_node; + visit_node = [&](const tinygltf::Model& model, + const tinygltf::Node& node, + const AffineTransform& parent_transform) -> void { + AffineTransform node_transform = AffineTransform::Identity(); + if constexpr (SceneType::Dim == 3) { + if (!node.matrix.empty()) { + // gltf stores in column-major order, so we should be good + for (size_t i = 0; i < 16; ++i) { + node_transform.data()[i] = Scalar(node.matrix[i]); + } + } else { + AffineTransform translation = AffineTransform::Identity(); + AffineTransform rotation = AffineTransform::Identity(); + AffineTransform scale = AffineTransform::Identity(); + if (!node.translation.empty()) { + translation = Eigen::Translation( + Scalar(node.translation[0]), + Scalar(node.translation[1]), + Scalar(node.translation[2])); + } + if (!node.rotation.empty()) { + rotation = Eigen::Quaternion( + Scalar(node.rotation[3]), + Scalar(node.rotation[0]), + Scalar(node.rotation[1]), + Scalar(node.rotation[2])); + } + if (!node.scale.empty()) { + scale = Eigen::Scaling( + Scalar(node.scale[0]), + Scalar(node.scale[1]), + Scalar(node.scale[2])); + } + node_transform = translation * rotation * scale; + } + } else { + // TODO: convert 3d transforms into 2d + logger().warn("Ignoring 3d node transform while loading 2d scene"); + } + + AffineTransform global_transform = parent_transform * node_transform; + if (node.mesh != -1) { + lscene.add_instance({Index(node.mesh), global_transform}); + } + + for (int child_idx : node.children) { + visit_node(model, model.nodes[child_idx], global_transform); + } + }; + + if (model.scenes.empty()) { + logger().warn("glTF does not contain any scene."); + } else { + int scene_id = model.defaultScene; + if (scene_id < 0) { + logger().warn("No default scene selected. Using the first available scene."); + scene_id = 0; + } + for (int node_idx : model.scenes[scene_id].nodes) { + AffineTransform root_transform = AffineTransform::Identity(); + visit_node(model, model.nodes[node_idx], root_transform); + } + } + + return lscene; +} + +#define LA_X_load_simple_scene_gltf(_, S, I, D) \ + template scene::SimpleScene load_simple_scene_gltf( \ + const tinygltf::Model& model, \ + const LoadOptions& options); +LA_SIMPLE_SCENE_X(load_simple_scene_gltf, 0); +#undef LA_X_load_simple_scene_gltf + + +} // namespace internal + + +// ===================================== +// load_mesh_gltf.h +// ===================================== +template +MeshType load_mesh_gltf(const fs::path& filename, const LoadOptions& options) +{ + tinygltf::Model model = internal::load_tinygltf(filename); + return internal::load_mesh_gltf(model, options); +} +#define LA_X_load_mesh_gltf(_, S, I) \ + template SurfaceMesh load_mesh_gltf(const fs::path& filename, const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh_gltf, 0); +#undef LA_X_load_mesh_gltf + + +// ===================================== +// load_simple_scene_gltf.h +// ===================================== +template +SceneType load_simple_scene_gltf(const fs::path& filename, const LoadOptions& options) +{ + tinygltf::Model model = internal::load_tinygltf(filename); + return internal::load_simple_scene_gltf(model, options); +} +#define LA_X_load_simple_scene_gltf(_, S, I, D) \ + template scene::SimpleScene load_simple_scene_gltf( \ + const fs::path& filename, \ + const LoadOptions& options); +LA_SIMPLE_SCENE_X(load_simple_scene_gltf, 0); +#undef LA_X_load_simple_scene_gltf + +} // namespace lagrange::io diff --git a/modules/io/src/load_mesh.cpp b/modules/io/src/load_mesh.cpp index f5f991aa..d92dc007 100644 --- a/modules/io/src/load_mesh.cpp +++ b/modules/io/src/load_mesh.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2021 Adobe. All rights reserved. + * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,23 +9,48 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -namespace lagrange { -namespace io { +#include +#include +#include -template std::unique_ptr load_mesh_basic(const fs::path&); -template std::unique_ptr load_mesh_basic(const fs::path&); +#include +#include +#include +#include +#ifdef LAGRANGE_WITH_ASSIMP +#include +#endif -template std::vector> load_obj_meshes(const fs::path&); -template std::vector> load_obj_meshes(const fs::path&); +namespace lagrange::io { -template std::unique_ptr load_obj_mesh(const fs::path&); -template std::unique_ptr load_obj_mesh(const fs::path&); - -template std::unique_ptr load_mesh(const fs::path&); -template std::unique_ptr load_mesh(const fs::path&); -template std::unique_ptr load_mesh(const fs::path&); -template std::unique_ptr> load_mesh(const fs::path&); +template < + typename MeshType, + std::enable_if_t::value>* /* = nullptr*/> +MeshType load_mesh(const fs::path& filename, const LoadOptions& options) +{ + if (filename.extension() == ".obj") { + return load_mesh_obj(filename, options); + } else if (filename.extension() == ".ply") { + return load_mesh_ply(filename, options); + } else if (filename.extension() == ".msh") { + return load_mesh_msh(filename, options); + } else if (filename.extension() == ".gltf" || filename.extension() == ".glb") { + return load_mesh_gltf(filename, options); + } else { +#ifdef LAGRANGE_WITH_ASSIMP + return load_mesh_assimp(filename, options); +#else + throw std::runtime_error( + "Unsupported format. You may want to compile with LAGRANGE_WITH_ASSIMP=ON"); +#endif + } } -} // namespace lagrange + +#define LA_X_load_mesh(_, S, I) \ + template SurfaceMesh load_mesh, nullptr>( \ + const fs::path& filename, \ + const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh, 0); + +} // namespace lagrange::io diff --git a/modules/io/src/load_mesh_msh.cpp b/modules/io/src/load_mesh_msh.cpp index 9fe8e589..343f1f12 100644 --- a/modules/io/src/load_mesh_msh.cpp +++ b/modules/io/src/load_mesh_msh.cpp @@ -15,8 +15,8 @@ #include #include #include -#include #include +#include #include @@ -24,6 +24,8 @@ namespace lagrange::io { +namespace { + /** * @private * @@ -69,10 +71,12 @@ void extract_facets(const mshio::MshSpec& spec, SurfaceMesh& mesh element_block.num_elements_in_block, static_cast(nodes_per_element), [&](Index fid, span f) { - std::copy_n( - element_block.data.begin() + fid * (nodes_per_element + 1) + 1, - nodes_per_element, - f.begin()); + Index offset = fid * (nodes_per_element + 1) + 1; + std::transform( + element_block.data.begin() + offset, + element_block.data.begin() + offset + nodes_per_element, + f.begin(), + [](int vid) { return static_cast(vid - 1); }); }); } } @@ -86,7 +90,8 @@ template void extract_attribute( const mshio::Data& data, SurfaceMesh& mesh, - AttributeElement element_type) + AttributeElement element_type, + const LoadOptions& options) { la_runtime_assert(data.header.string_tags.size() > 0); la_runtime_assert(data.header.int_tags.size() > 2); @@ -97,10 +102,13 @@ void extract_attribute( AttributeUsage usage = AttributeUsage::Scalar; if (attr_name == "@normal") { + if (!options.load_normals) return; usage = AttributeUsage::Normal; } else if (attr_name == "@uv") { + if (!options.load_uvs) return; usage = AttributeUsage::UV; } else if (attr_name == "@color") { + if (!options.load_vertex_colors) return; usage = AttributeUsage::Color; } else if (num_fields > 1) { usage = AttributeUsage::Vector; @@ -137,10 +145,13 @@ void extract_attribute( * @private */ template -void extract_vertex_attributes(const mshio::MshSpec& spec, SurfaceMesh& mesh) +void extract_vertex_attributes( + const mshio::MshSpec& spec, + SurfaceMesh& mesh, + const LoadOptions& options) { for (const auto& data : spec.node_data) { - extract_attribute(data, mesh, AttributeElement::Vertex); + extract_attribute(data, mesh, AttributeElement::Vertex, options); } } @@ -148,10 +159,13 @@ void extract_vertex_attributes(const mshio::MshSpec& spec, SurfaceMesh -void extract_facet_attributes(const mshio::MshSpec& spec, SurfaceMesh& mesh) +void extract_facet_attributes( + const mshio::MshSpec& spec, + SurfaceMesh& mesh, + const LoadOptions& options) { for (const auto& data : spec.element_data) { - extract_attribute(data, mesh, AttributeElement::Facet); + extract_attribute(data, mesh, AttributeElement::Facet, options); } } @@ -159,33 +173,43 @@ void extract_facet_attributes(const mshio::MshSpec& spec, SurfaceMesh -void extract_corner_attributes(const mshio::MshSpec& spec, SurfaceMesh& mesh) +void extract_corner_attributes( + const mshio::MshSpec& spec, + SurfaceMesh& mesh, + const LoadOptions& options) { for (const auto& data : spec.element_node_data) { - extract_attribute(data, mesh, AttributeElement::Corner); + extract_attribute(data, mesh, AttributeElement::Corner, options); } } +} // namespace + template -MeshType load_mesh_msh(std::istream& input_stream) +MeshType load_mesh_msh(std::istream& input_stream, const LoadOptions& options) { mshio::MshSpec spec = mshio::load_msh(input_stream); MeshType mesh; extract_vertices(spec, mesh); extract_facets(spec, mesh); - extract_vertex_attributes(spec, mesh); - extract_facet_attributes(spec, mesh); - extract_corner_attributes(spec, mesh); + extract_vertex_attributes(spec, mesh, options); + extract_facet_attributes(spec, mesh, options); + extract_corner_attributes(spec, mesh, options); return mesh; } +template +MeshType load_mesh_msh(const fs::path& filename, const LoadOptions& options) +{ + fs::ifstream fin(filename); + return load_mesh_msh(fin, options); +} + #define LA_X_load_mesh_msh(_, S, I) \ - template SurfaceMesh load_mesh_msh(std::istream& input_stream); + template SurfaceMesh load_mesh_msh(const fs::path& filename, const LoadOptions& options); LA_SURFACE_MESH_X(load_mesh_msh, 0) #undef LA_X_load_mesh_msh - } // namespace lagrange::io - diff --git a/modules/io/src/load_mesh_obj.cpp b/modules/io/src/load_mesh_obj.cpp index e70113ff..ecb75d6b 100644 --- a/modules/io/src/load_mesh_obj.cpp +++ b/modules/io/src/load_mesh_obj.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2023 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,230 +9,27 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include +#include -#include -#include #include #include -#include -#include -#include +#include +#include -// clang-format off -#include -#include -#include -// clang-format on - -#include -#include - -namespace lagrange { -namespace io { +namespace lagrange::io { template -auto load_mesh_obj(const fs::path& filename, const ObjReaderOptions& options) - -> ObjReaderResult +MeshType load_mesh_obj(const fs::path& filename, const LoadOptions& options) { - ObjReaderResult result; - - using Scalar = typename MeshType::Scalar; - using Index = typename MeshType::Index; - using SignedIndex = typename MeshType::SignedIndex; - - tinyobj::ObjReader reader; - { - logger().trace("[load_mesh_obj] Parsing obj file: {}", filename.string()); - tinyobj::ObjReaderConfig config; - config.triangulate = options.triangulate; - config.vertex_color = options.load_vertex_colors; - config.mtl_search_path = options.mtl_search_path; - reader.ParseFromFile(filename.string(), config); + auto ret = internal::load_mesh_obj(filename, options); + if (!ret.success) { + throw Error(fmt::format("Failed to load mesh from file: '{}'", filename.string())); } - - result.success = reader.Valid(); - if (!reader.Warning().empty()) { - auto lines = string_split(reader.Warning(), '\n'); - for (const auto& msg : lines) { - logger().warn("[load_mesh_obj] {}", msg); - } - } - if (!reader.Error().empty()) { - auto lines = string_split(reader.Error(), '\n'); - for (const auto& msg : lines) { - logger().error("[load_mesh_obj] {}", msg); - } - } - if (!reader.Valid()) { - return result; - } - - logger().trace("[load_mesh_obj] Copying data into a mesh"); - - auto& mesh = result.mesh; - const Index dim = 3; - const Index uv_dim = 2; - const auto& attrib = reader.GetAttrib(); - const auto& shapes = reader.GetShapes(); - if (options.load_materials) { - result.materials = reader.GetMaterials(); - } - - // Copy vertices - logger().trace("[load_mesh_obj] Copying vertices"); - la_runtime_assert(attrib.vertices.size() % dim == 0); - const Index num_vertices = safe_cast(attrib.vertices.size()) / dim; - mesh.add_vertices(num_vertices, [&](Index v, span p) { - la_debug_assert(dim == p.size()); - std::copy_n(attrib.vertices.begin() + static_cast(v * dim), dim, p.begin()); - }); - - // Copy texcoord values - IndexedAttribute* uv_attr = nullptr; - if (options.load_uvs && !attrib.texcoords.empty()) { - logger().trace("[load_mesh_obj] Copying uvs"); - la_runtime_assert(attrib.texcoords.size() % uv_dim == 0); - auto id = mesh.template create_attribute( - "uv", - AttributeElement::Indexed, - AttributeUsage::UV, - uv_dim); - uv_attr = &mesh.template ref_indexed_attribute(id); - uv_attr->values().resize_elements(attrib.texcoords.size() / uv_dim); - std::copy( - attrib.texcoords.begin(), - attrib.texcoords.end(), - uv_attr->values().ref_all().begin()); - } - - // Copy normal values - IndexedAttribute* nrm_attr = nullptr; - if (options.load_normals && !attrib.normals.empty()) { - logger().trace("[load_mesh_obj] Copying normals"); - la_runtime_assert(attrib.normals.size() % dim == 0); - auto id = mesh.template create_attribute( - "normal", - AttributeElement::Indexed, - AttributeUsage::Normal, - dim); - nrm_attr = &mesh.template ref_indexed_attribute(id); - nrm_attr->values().resize_elements(attrib.normals.size() / dim); - std::copy( - attrib.normals.begin(), - attrib.normals.end(), - nrm_attr->values().ref_all().begin()); - } - - // Copy vertex colors - if (options.load_vertex_colors && !attrib.colors.empty()) { - logger().trace("[load_mesh_obj] Copying vertex colors"); - la_runtime_assert(mesh.get_num_vertices() == attrib.colors.size() / dim); - auto id = mesh.template create_attribute( - "color", - AttributeElement::Vertex, - AttributeUsage::Color, - dim); - auto& attr = mesh.template ref_attribute(id); - std::copy(attrib.colors.begin(), attrib.colors.end(), attr.ref_all().begin()); - } - - // Reserve facet indices - logger().trace("[load_mesh_obj] Reserve facet indices"); - std::vector facet_sizes; - for (const auto& shape : shapes) { - facet_sizes.insert( - facet_sizes.end(), - shape.mesh.num_face_vertices.begin(), - shape.mesh.num_face_vertices.end()); - result.names.push_back(shape.name); - } - mesh.add_hybrid(facet_sizes); - - // Initialize material id attr - Attribute* mat_attr = nullptr; - if (options.load_materials) { - auto id = mesh.template create_attribute( - "material_id", - AttributeElement::Facet, - AttributeUsage::Scalar); - mat_attr = &mesh.template ref_attribute(id); - } - - // Initialize object id - Attribute* id_attr = nullptr; - if (options.load_object_id) { - auto id = mesh.template create_attribute( - "object_id", - AttributeElement::Facet, - AttributeUsage::Scalar); - id_attr = &mesh.template ref_attribute(id); - } - - span vtx_indices = mesh.ref_corner_to_vertex().ref_all(); - span uv_indices = (uv_attr ? uv_attr->indices().ref_all() : span{}); - span nrm_indices = (nrm_attr ? nrm_attr->indices().ref_all() : span{}); - - logger().trace("[load_mesh_obj] Copy facet indices"); - std::partial_sum(facet_sizes.begin(), facet_sizes.end(), facet_sizes.begin()); - tbb::parallel_for(Index(0), Index(shapes.size()), [&](Index i) { - const auto& shape = shapes[i]; - const Index first_facet = (i == 0 ? 0 : facet_sizes[i - 1]); - const auto local_num_facets = safe_cast(shape.mesh.num_face_vertices.size()); - - // Copy material id - if (mat_attr) { - la_debug_assert(local_num_facets <= mesh.get_num_facets()); - la_debug_assert(local_num_facets <= mat_attr->get_num_elements()); - auto span = mat_attr->ref_middle(first_facet, local_num_facets); - if (shape.mesh.material_ids.empty()) { - std::fill(span.begin(), span.end(), SignedIndex(-1)); - } else { - la_runtime_assert(shape.mesh.material_ids.size() == local_num_facets); - std::transform( - shape.mesh.material_ids.begin(), - shape.mesh.material_ids.end(), - span.begin(), - [](auto x) -> SignedIndex { return safe_cast(x); }); - } - } - - // Copy object id - if (id_attr) { - auto span = id_attr->ref_middle(first_facet, local_num_facets); - std::fill(span.begin(), span.end(), i); - } - - // Copy indices - for (Index f = 0, local_corner = 0; f < local_num_facets; ++f) { - const Index first_corner = mesh.get_facet_corner_begin(first_facet + f); - const Index last_corner = mesh.get_facet_corner_end(first_facet + f); - - for (Index c = first_corner; c < last_corner; ++c, ++local_corner) { - const auto& index = shape.mesh.indices[local_corner]; - vtx_indices[c] = safe_cast(index.vertex_index); - if (!uv_indices.empty()) { - uv_indices[c] = safe_cast(index.texcoord_index); - } - if (!nrm_indices.empty()) { - nrm_indices[c] = safe_cast(index.normal_index); - } - } - } - - // TODO: Support smoothing groups + subd tags - }); - - logger().trace("[load_mesh_obj] Loading complete"); - - return result; + return std::move(ret.mesh); } -#define LA_X_load_mesh(_, Scalar, Index) \ - template ObjReaderResult load_mesh_obj>( \ - const fs::path& filename, \ - const ObjReaderOptions& options); -LA_SURFACE_MESH_X(load_mesh, 0) +#define LA_X_load_mesh_obj(_, S, I) \ + template SurfaceMesh load_mesh_obj(const fs::path& filename, const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh_obj, 0) -} // namespace io -} // namespace lagrange +} // namespace lagrange::io diff --git a/modules/io/src/load_mesh_ply.cpp b/modules/io/src/load_mesh_ply.cpp new file mode 100644 index 00000000..197993ac --- /dev/null +++ b/modules/io/src/load_mesh_ply.cpp @@ -0,0 +1,92 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +namespace lagrange::io { + +template +MeshType load_mesh_ply(std::istream& input_stream, const LoadOptions& options) +{ + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + using VertexArray = Eigen::Matrix; + using FacetArray = Eigen::Matrix; + using AttributeArray = Eigen::Matrix; + + VertexArray V; + FacetArray F; + Eigen::Matrix E; + AttributeArray N; + AttributeArray UV; + + AttributeArray VD; + std::vector VDheader; + + AttributeArray FD; + std::vector FDheader; + + AttributeArray ED; + std::vector EDheader; + std::vector comments; + + // TODO: libigl's wrapper around tinyply doesn't allow to load attributes of different tyeps + // (e.g. char for colors + float for normals). + bool success = igl:: + readPLY(input_stream, V, F, E, N, UV, VD, VDheader, FD, FDheader, ED, EDheader, comments); + if (!success) throw std::runtime_error("Error reading ply!"); + + MeshType mesh; + mesh.add_vertices(V.rows()); + mesh.add_triangles(F.rows()); + vertex_ref(mesh) = V; + facet_ref(mesh) = F; + + // TODO we should replace this whole file with happly, rather than implementing the stuff below. + + if (Scalar(UV.rows()) == mesh.get_num_vertices() && options.load_uvs) { + // TODO load uvs + } + + if (Scalar(N.rows()) == mesh.get_num_vertices() && options.load_normals) { + // TODO load normals + } + + if (Scalar(VD.rows()) == mesh.get_num_vertices() && options.load_vertex_colors) { + // TODO load vertex colors + } + + return mesh; +} + +template +MeshType load_mesh_ply(const fs::path& filename, const LoadOptions& options) +{ + fs::ifstream fin(filename); + return load_mesh_ply(fin, options); +} + +#define LA_X_load_mesh_ply(_, S, I) \ + template SurfaceMesh load_mesh_ply(const fs::path& filename, const LoadOptions& options); +LA_SURFACE_MESH_X(load_mesh_ply, 0) + +} // namespace lagrange::io diff --git a/modules/io/src/load_simple_scene.cpp b/modules/io/src/load_simple_scene.cpp new file mode 100644 index 00000000..79f3a72f --- /dev/null +++ b/modules/io/src/load_simple_scene.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include +#include +#include +#include + +#ifdef LAGRANGE_WITH_ASSIMP +#include +#endif + +namespace lagrange::io { + +template +SceneType load_simple_scene( + const fs::path& filename, const LoadOptions& options) +{ + if (filename.extension() == ".gltf" || filename.extension() == ".glb") { + return load_simple_scene_gltf(filename, options); + } else { +#ifdef LAGRANGE_WITH_ASSIMP + return load_simple_scene_assimp(filename, options); +#else + logger().error("Unsupported format. You may want to compile with LAGRANGE_WITH_ASSIMP=ON."); +#endif + } + return SceneType(); +} +#define LA_X_load_simple_scene(_, S, I, D) \ + template scene::SimpleScene load_simple_scene( \ + const fs::path& filename, \ + const LoadOptions& options); +LA_SIMPLE_SCENE_X(load_simple_scene, 0); + +} diff --git a/modules/io/src/save_gltf.cpp b/modules/io/src/save_gltf.cpp new file mode 100644 index 00000000..d7e2bbd6 --- /dev/null +++ b/modules/io/src/save_gltf.cpp @@ -0,0 +1,391 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// this .cpp provides implementations for functions defined in those headers: +#include +#include +// ==== + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace lagrange::io { + +// +// Important: +// gltf standard requires all accessors of the same primitive to have the same count and indexing. +// This means all attributes must be indexed in the same way. This includes including positions, +// UVs, normals, etc. +// Lagrange supports indexing attributes in different ways. For example, on a cube, we can have 8 +// vertices, but 14 UV coordinates, but this would be invalid in the gltf export. +// +// So in the functions below we call `unify_index_buffer`. This can affect your vertex indexing, +// and can affect performance. +// + +namespace { +void save_gltf(const fs::path& filename, const tinygltf::Model& model, const SaveOptions& options) { + tinygltf::TinyGLTF loader; + constexpr bool embed_images = false; + constexpr bool embed_buffers = true; + constexpr bool pretty_print = true; + loader.WriteGltfSceneToFile( + &model, + filename.string(), + embed_images, + embed_buffers, + pretty_print, + options.encoding == FileEncoding::Binary); +} + +// returns data, tmp +template +span get_attribute_as(span data, std::vector& tmp) { + if constexpr(std::is_same_v) { + return data; + } else { + tmp.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + tmp[i] = to_t(data[i]); + } + return tmp; + } +} + +// returns buffer_index, byte_offset, byte_length +template +std::tuple write_to_buffer(tinygltf::Model& model, span data) +{ + constexpr size_t buffer_max_size = ((size_t)2 << 32) - 1; // 2^32 - 1 + + if (model.buffers.empty()) { + model.buffers.push_back(tinygltf::Buffer()); + } + + tinygltf::Buffer& buffer = model.buffers.back(); + size_t byte_length = data.size_bytes(); + size_t byte_offset = buffer.data.size(); + if (byte_offset + byte_length > buffer_max_size) { + // a single data block cannot exceed the maximum buffer size + la_runtime_assert(byte_length < buffer_max_size); + model.buffers.push_back(tinygltf::Buffer()); + buffer = model.buffers.back(); + byte_offset = 0; + } + buffer.data.resize(byte_offset + byte_length); + std::memcpy(&buffer.data.at(byte_offset), data.begin(), byte_length); + + return {int(model.buffers.size()) - 1, byte_offset, byte_length}; +} +} // namespace + +// ===================================== +// save_mesh_gltf.h +// ===================================== +template +void save_mesh_gltf( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options) +{ + scene::SimpleScene scene; + using AffineTransform = typename decltype(scene)::AffineTransform; + + AffineTransform t = AffineTransform::Identity(); + scene.add_instance({scene.add_mesh(mesh), t}); + + save_simple_scene_gltf(filename, scene, options); +} + +#define LA_X_save_mesh_gltf(_, S, I) \ + template void save_mesh_gltf( \ + const fs::path& filename, \ + const SurfaceMesh& mesh, \ + const SaveOptions& options); +LA_SURFACE_MESH_X(save_mesh_gltf, 0) +#undef LA_X_save_mesh_gltf + +// ===================================== +// save_simple_scene_gltf.h +// ===================================== +template +void save_simple_scene_gltf( + const fs::path& filename, + const scene::SimpleScene& lscene, + const SaveOptions& options) +{ + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html + + tinygltf::Model model; + model.asset.generator = "Lagrange"; + model.asset.version = "2.0"; + + model.scenes.push_back(tinygltf::Scene()); + model.defaultScene = 0; + tinygltf::Scene& scene = model.scenes.front(); + + // gltf requires a material, so we create a dummy one + model.materials.push_back(tinygltf::Material()); + + for (Index i = 0; i < lscene.get_num_meshes(); ++i) { + // There is a paragraph about this step at the beginning of this file. + // TODO: maybe make this optional + auto lmesh = unify_index_buffer(lscene.get_mesh(i)); + + la_runtime_assert(lmesh.get_vertex_per_facet() == 3); // only support triangles + + tinygltf::Mesh mesh; + mesh.primitives.push_back(tinygltf::Primitive()); + tinygltf::Primitive& primitive = mesh.primitives.front(); + primitive.mode = TINYGLTF_MODE_TRIANGLES; + primitive.material = 0; + + + { // positions + std::vector tmp; + span data = get_attribute_as(lmesh.get_vertex_to_position().get_all(), tmp); + auto[buffer_index, byte_offset, byte_length] = write_to_buffer(model, data); + + tinygltf::BufferView buffer_view; + buffer_view.buffer = buffer_index; + buffer_view.byteLength = byte_length; + buffer_view.byteOffset = byte_offset; + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + int buffer_view_index = int(model.bufferViews.size()); + model.bufferViews.push_back(buffer_view); + + tinygltf::Accessor accessor; + accessor.bufferView = buffer_view_index; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.type = TINYGLTF_TYPE_VEC3; + accessor.count = lmesh.get_num_vertices(); + auto V = vertex_view(lmesh); + auto bb_max = V.colwise().maxCoeff(); + auto bb_min = V.colwise().minCoeff(); + accessor.maxValues = {double(bb_max(0)), double(bb_max(1)), double(bb_max(2))}; + accessor.minValues = {double(bb_min(0)), double(bb_min(1)), double(bb_min(2))}; + int accessor_index = int(model.accessors.size()); + model.accessors.push_back(accessor); + + primitive.attributes["POSITION"] = accessor_index; + } + + { // face indices + std::vector tmp; + span data = get_attribute_as(lmesh.get_corner_to_vertex().get_all(), tmp); + + auto[buffer_index, byte_offset, byte_length] = write_to_buffer(model, data); + + tinygltf::BufferView buffer_view; + buffer_view.buffer = buffer_index; + buffer_view.byteLength = byte_length; + buffer_view.byteOffset = byte_offset; + buffer_view.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + int buffer_view_index = int(model.bufferViews.size()); + model.bufferViews.push_back(buffer_view); + + tinygltf::Accessor accessor; + accessor.bufferView = buffer_view_index; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; + accessor.type = TINYGLTF_TYPE_SCALAR; + accessor.count = lmesh.get_num_facets() * lmesh.get_vertex_per_facet(); + int accessor_index = int(model.accessors.size()); + model.accessors.push_back(accessor); + + primitive.indices = accessor_index; + } + + { // other attributes + bool found_normal = false; + bool found_tangent = false; + int texcoord_count = 0; + int color_count = 0; + seq_foreach_named_attribute_read(lmesh, [&](std::string_view name, auto&& attr) { + // TODO: change this for the attribute visitor that takes id and simplify this. + if (lmesh.attr_name_is_reserved(name)) return; + AttributeId id = lmesh.get_attribute_id(name); + if (options.output_attributes == SaveOptions::OutputAttributes::SelectedOnly) { + if (std::find( + options.selected_attributes.begin(), + options.selected_attributes.end(), + id) == options.selected_attributes.end()) { + return; + } + } + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + + if constexpr (AttributeType::IsIndexed) { + const auto& indices = attr.indices(); + if (vector_view(indices) != vector_view(lmesh.get_corner_to_vertex())) { + // Indexed attributes are supported IF their indexing matches the vertices. + // This should be the case if you call `unify_index_buffers` + // TODO: ^ it's not. + logger().warn("Skipping attribute {}: unsupported non-indexed", name); + return; + } + } + + const auto& values = [&]() -> const auto& { + if constexpr (AttributeType::IsIndexed) { + return attr.values(); + } else { + return attr; + } + }(); + + + tinygltf::Accessor accessor; + if constexpr(std::is_same_v) { + accessor.componentType = TINYGLTF_COMPONENT_TYPE_BYTE; + } else if constexpr(std::is_same_v) { + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + } else if constexpr(std::is_same_v) { + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; + } else if constexpr(std::is_same_v) { + accessor.componentType = TINYGLTF_COMPONENT_TYPE_INT; + } else if constexpr(std::is_same_v) { + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; + } else if constexpr(std::is_same_v) { + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + } else if constexpr(std::is_same_v) { + // special case: convert double to float + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + } else { + logger().warn("Skipping attribute {}: unsupported type", name); + return; + } + + switch (attr.get_num_channels()) { + case 1: accessor.type = TINYGLTF_TYPE_SCALAR; break; + case 2: accessor.type = TINYGLTF_TYPE_VEC2; break; + case 3: accessor.type = TINYGLTF_TYPE_VEC3; break; + case 4: accessor.type = TINYGLTF_TYPE_VEC4; break; + // note that we have no way to know if the type should be MAT2 instead + case 9: accessor.type = TINYGLTF_TYPE_MAT3; break; + case 16: accessor.type = TINYGLTF_TYPE_MAT4; break; + default: + logger().warn( + "Skipping attribute {}: unsupported number of channels", + name); + return; + } + + int buffer_index; + size_t byte_offset, byte_length; + if constexpr(std::is_same_v) { + std::vector tmp; + span data = get_attribute_as(values.get_all(), tmp); + std::tie(buffer_index, byte_offset, byte_length) = write_to_buffer(model, data); + } else { + std::vector tmp; + span data = get_attribute_as(values.get_all(), tmp); + std::tie(buffer_index, byte_offset, byte_length) = write_to_buffer(model, data); + } + + tinygltf::BufferView buffer_view; + buffer_view.buffer = buffer_index; + buffer_view.byteLength = byte_length; + buffer_view.byteOffset = byte_offset; + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + int buffer_view_index = int(model.bufferViews.size()); + model.bufferViews.push_back(buffer_view); + + accessor.bufferView = buffer_view_index; + accessor.count = values.get_num_elements(); + int accessor_index = int(model.accessors.size()); + model.accessors.push_back(accessor); + + std::string name_uppercase; + std::transform( + name.begin(), + name.end(), + name_uppercase.begin(), + [](unsigned char c) { return std::toupper(c); }); + if (attr.get_usage() == AttributeUsage::Normal) { + if (found_normal) { + name_uppercase = "_" + name_uppercase; + logger().warn("Found multiple attributes for normal, saving {} as {}", name, name_uppercase); + } else { + found_normal = true; + name_uppercase = "NORMAL"; + } + } else if (attr.get_usage() == AttributeUsage::Tangent) { + if (found_tangent) { + name_uppercase = "_" + name_uppercase; + logger().warn("Found multiple attributes for tangent, saving {} as {}", name, name_uppercase); + } else { + found_tangent = true; + name_uppercase = "TANGENT"; + } + } else if (attr.get_usage() == AttributeUsage::Color) { + name_uppercase = "COLOR_" + std::to_string(color_count); + ++color_count; + } else if (attr.get_usage() == AttributeUsage::UV) { + name_uppercase = "TEXCOORD_" + std::to_string(texcoord_count); + ++texcoord_count; + } else { + // If no previous match, save with the current attribute name. + // Note that the gltf format is quite strict on the allowed names: + // POSITION, NORMAL, TANGENT< TEXCOORD_n, COLOR_n, JOINTS_n, and WEIGHTS_n. + // Custom attributes are allowed ONLY if they start with a leading understore, e.g. _TEMPERATURE. + // Custom attribute MUST NOT use unsigned int type. + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + } + + primitive.attributes[name_uppercase] = accessor_index; + }); + } + + model.meshes.push_back(mesh); + + for (Index j = 0; j < lscene.get_num_instances(i); ++j) { + const auto& instance = lscene.get_instance(i, j); + + tinygltf::Node node; + node.mesh = int(i); + if constexpr (Dimension == 3) { + if (!instance.transform.matrix().isIdentity()) { + node.matrix = std::vector( + instance.transform.data(), + instance.transform.data() + 16); + } + } else { + // TODO: convert 2d transforms to 3d + logger().warn("Ignoring 2d instance transforms while saving gltf scene"); + } + + model.nodes.push_back(node); + scene.nodes.push_back(int(model.nodes.size()) - 1); + } + } + + save_gltf(filename, model, options); +} + +#define LA_X_save_simple_scene_gltf(_, S, I, D) \ + template void save_simple_scene_gltf( \ + const fs::path& filename, \ + const scene::SimpleScene& scene, \ + const SaveOptions& options); +LA_SIMPLE_SCENE_X(save_simple_scene_gltf, 0); +#undef LA_X_save_simple_scene_gltf + +} // namespace lagrange::io diff --git a/modules/io/src/save_mesh.cpp b/modules/io/src/save_mesh.cpp new file mode 100644 index 00000000..1d3eee2a --- /dev/null +++ b/modules/io/src/save_mesh.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include + +namespace lagrange::io { + +template +void save_mesh(const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options) +{ + if (filename.extension() == ".obj") { + save_mesh_obj(filename, mesh, options); + } else if (filename.extension() == ".ply") { + save_mesh_ply(filename, mesh, options); + } else if (filename.extension() == ".msh") { + save_mesh_msh(filename, mesh, options); + } else if (filename.extension() == ".gltf" || filename.extension() == ".glb") { + la_runtime_assert( + (options.encoding == FileEncoding::Binary && filename.extension() == ".glb") || + (options.encoding == FileEncoding::Ascii && filename.extension() == ".gltf")); + save_mesh_gltf(filename, mesh, options); + } else { + la_runtime_assert(false, "Unrecognized filetype!"); + } +} + +#define LA_X_save_mesh(_, S, I) \ + template void save_mesh(const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options); +LA_SURFACE_MESH_X(save_mesh, 0); + +} diff --git a/modules/io/src/save_mesh_msh.cpp b/modules/io/src/save_mesh_msh.cpp index c0eca3c1..68105259 100644 --- a/modules/io/src/save_mesh_msh.cpp +++ b/modules/io/src/save_mesh_msh.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -86,11 +87,13 @@ void populate_elements(mshio::MshSpec& spec, const SurfaceMesh& m template void populate_indexed_attribute( mshio::MshSpec& /*spec*/, - const SurfaceMesh& /*mesh*/, - AttributeId /*id*/) + const SurfaceMesh& mesh, + AttributeId id) { - // TODO - throw Error("Saving indexed attribute in MSH format is not yet supported."); + // TODO add support. + // can reference save_gltf. gltf also does not support indexed attributes. + std::string_view name = mesh.get_attribute_name(id); + logger().warn("Skipping attribute {}: unsupported non-indexed", name); } template @@ -210,7 +213,7 @@ void populate_non_indexed_corner_attribute( element_node_data.entries.emplace_back(); auto& entry = element_node_data.entries.back(); entry.tag = static_cast(i + 1); - entry.num_nodes_per_element = vertex_per_facet; + entry.num_nodes_per_element = int(vertex_per_facet); entry.data.reserve(num_channels * vertex_per_facet); for (auto j : range(vertex_per_facet)) { for (auto k : range(num_channels)) { @@ -285,7 +288,7 @@ template void save_mesh_msh( std::ostream& output_stream, const SurfaceMesh& mesh, - const MshSaverOptions& options) + const SaveOptions& options) { if constexpr (sizeof(size_t) != 8) { logger().error("MSH format requries `size_t` to be 8 bytes!"); @@ -298,22 +301,45 @@ void save_mesh_msh( "Only triangle and quad mesh are supported for now."); mshio::MshSpec spec; - spec.mesh_format.file_type = options.binary ? 1 : 0; + spec.mesh_format.file_type = options.encoding == FileEncoding::Binary ? 1 : 0; populate_nodes(spec, mesh); populate_elements(spec, mesh); - for (auto id : options.attr_ids) { + std::vector ids; + if (options.output_attributes == SaveOptions::OutputAttributes::All) { + mesh.seq_foreach_attribute_id([&](std::string_view name, AttributeId id) { + if (!mesh.attr_name_is_reserved(name)) { + ids.push_back(id); + } + }); + ids.push_back(mesh.attr_id_vertex_to_positions()); + ids.push_back(mesh.attr_id_corner_to_vertex()); + } else { + ids = options.selected_attributes; + } + + for (auto id : ids) { populate_attribute(spec, mesh, id); } mshio::save_msh(output_stream, spec); } +template +void save_mesh_msh( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options) +{ + fs::ofstream fout(filename); + save_mesh_msh(fout, mesh, options); +} + #define LA_X_save_mesh_msh(_, S, I) \ template void save_mesh_msh( \ - std::ostream& output_stream, \ + const fs::path& filename, \ const SurfaceMesh& mesh, \ - const MshSaverOptions& options); + const SaveOptions& options); LA_SURFACE_MESH_X(save_mesh_msh, 0) #undef LA_X_save_mesh_msh diff --git a/modules/io/src/save_mesh_obj.cpp b/modules/io/src/save_mesh_obj.cpp index 7e691080..75d9aea5 100644 --- a/modules/io/src/save_mesh_obj.cpp +++ b/modules/io/src/save_mesh_obj.cpp @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include +#include #include #include @@ -27,7 +27,10 @@ namespace lagrange { namespace io { template -void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh& mesh) +void save_mesh_obj( + std::ostream& output_stream, + const SurfaceMesh& mesh, + const SaveOptions& options) { la_runtime_assert(output_stream, "Invalid output stream"); @@ -67,6 +70,17 @@ void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh std::string found_uv_name; const Attribute* uv_indices = nullptr; seq_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { + // TODO: change this for the attribute visitor that takes id and simplify this block. + if (options.output_attributes == SaveOptions::OutputAttributes::SelectedOnly) { + AttributeId id = mesh.get_attribute_id(name); + if (std::count( + options.selected_attributes.begin(), + options.selected_attributes.end(), + id)) { + return; + } + } + if (attr.get_usage() != AttributeUsage::UV) { return; } @@ -87,7 +101,7 @@ void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh } uv_indices = &attr.indices(); } else { - la_runtime_assert(false, "Writing non-indexed UV attr not supported yet"); + logger().warn("Writing non-indexed UV attr not supported yet"); } }); @@ -95,6 +109,17 @@ void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh std::string found_nrm_name; const Attribute* nrm_indices = nullptr; seq_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { + // TODO: change this for the attribute visitor that takes id and simplify this block. + if (options.output_attributes == SaveOptions::OutputAttributes::SelectedOnly) { + AttributeId id = mesh.get_attribute_id(name); + if (std::count( + options.selected_attributes.begin(), + options.selected_attributes.end(), + id)) { + return; + } + } + if (attr.get_usage() != AttributeUsage::Normal) { return; } @@ -115,7 +140,7 @@ void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh } nrm_indices = &attr.indices(); } else { - la_runtime_assert(false, "Writing non-indexed Normal attr not supported yet"); + logger().warn("Writing non-indexed Normal attr not supported yet"); } }); @@ -149,17 +174,24 @@ void save_mesh_obj(std::ostream& output_stream, const SurfaceMesh } template -void save_mesh_obj(const fs::path& filename, const SurfaceMesh& mesh) +void save_mesh_obj( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options) { fs::ofstream output_stream(filename); - save_mesh_obj(output_stream, mesh); + save_mesh_obj(output_stream, mesh, options); } -#define LA_X_save_mesh(_, Scalar, Index) \ - template void save_mesh_obj( \ - std::ostream& output_stream, \ - const SurfaceMesh& mesh); \ - template void save_mesh_obj(const fs::path& filename, const SurfaceMesh& mesh); +#define LA_X_save_mesh(_, Scalar, Index) \ + template void save_mesh_obj( \ + std::ostream& output_stream, \ + const SurfaceMesh& mesh, \ + const SaveOptions& options); \ + template void save_mesh_obj( \ + const fs::path& filename, \ + const SurfaceMesh& mesh, \ + const SaveOptions& options); LA_SURFACE_MESH_X(save_mesh, 0) } // namespace io diff --git a/modules/io/src/save_mesh_ply.cpp b/modules/io/src/save_mesh_ply.cpp new file mode 100644 index 00000000..17659276 --- /dev/null +++ b/modules/io/src/save_mesh_ply.cpp @@ -0,0 +1,184 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +// clang-format on + +namespace lagrange::io { + +namespace { + +template +std::vector to_vector(const Eigen::DenseBase& src) +{ + std::vector dst(src.size()); + Eigen::VectorX::Map(dst.data(), dst.size()) = src.template cast(); + return dst; +} + +} // namespace + +template +void save_mesh_ply( + std::ofstream& output_stream, + const SurfaceMesh& mesh, + const SaveOptions& options) +{ + la_runtime_assert(mesh.get_dimension() == 3); + + // Create an empty object + happly::PLYData ply; + + // Add mesh elements + ply.addElement("vertex", mesh.get_num_vertices()); + ply.addElement("face", mesh.get_num_facets()); + if (mesh.has_edges()) { + ply.addElement("edge", mesh.get_num_edges()); + } + + // Vertex positions + { + auto pos = vertex_view(mesh); + auto vx = to_vector(pos.col(0)); + auto vy = to_vector(pos.col(1)); + auto vz = to_vector(pos.col(2)); + ply.getElement("vertex").addProperty("x", vx); + ply.getElement("vertex").addProperty("y", vy); + ply.getElement("vertex").addProperty("z", vz); + } + + // TODO: Make this a utility function? + std::unordered_set allowed_ids; + if (options.output_attributes == SaveOptions::OutputAttributes::SelectedOnly) { + allowed_ids.insert(options.selected_attributes.begin(), options.selected_attributes.end()); + } else { + la_debug_assert(options.output_attributes == SaveOptions::OutputAttributes::All); + mesh.seq_foreach_attribute_id([&](auto id) { allowed_ids.insert(id); }); + } + + // Vertex normals + if (auto id = internal::find_matching_attribute( + mesh, + allowed_ids, + AttributeElement::Vertex, + AttributeUsage::Normal, + 3); + id != invalid_attribute_id()) { + logger().debug("Writing vertex normal attribute '{}'", mesh.get_attribute_name(id)); + auto nrm = attribute_matrix_view(mesh, id); + auto nx = to_vector(nrm.col(0)); + auto ny = to_vector(nrm.col(1)); + auto nz = to_vector(nrm.col(2)); + ply.getElement("vertex").addProperty("nx", nx); + ply.getElement("vertex").addProperty("ny", ny); + ply.getElement("vertex").addProperty("nz", nz); + } + + // Vertex texcoords + if (auto id = internal::find_matching_attribute( + mesh, + allowed_ids, + AttributeElement::Vertex, + AttributeUsage::UV, + 2); + id != invalid_attribute_id()) { + logger().debug("Writing vertex uv attribute '{}'", mesh.get_attribute_name(id)); + auto uv = attribute_matrix_view(mesh, id); + logger().info("uv rows {} / num vtx {}", uv.rows(), mesh.get_num_vertices()); + auto s = to_vector(uv.col(0)); + auto t = to_vector(uv.col(1)); + ply.getElement("vertex").addProperty("u", s); + ply.getElement("vertex").addProperty("v", t); + } + + // Vertex colors + if (auto id = internal::find_matching_attribute( + mesh, + allowed_ids, + AttributeElement::Vertex, + AttributeUsage::Color, + 3); + id != invalid_attribute_id()) { + logger().debug("Writing vertex color attribute '{}'", mesh.get_attribute_name(id)); + auto rgb = (attribute_matrix_view(mesh, id) * Scalar(255)).array().round().eval(); + auto r = to_vector(rgb.col(0)); + auto g = to_vector(rgb.col(1)); + auto b = to_vector(rgb.col(2)); + ply.getElement("vertex").addProperty("red", r); + ply.getElement("vertex").addProperty("green", g); + ply.getElement("vertex").addProperty("blue", b); + } + + // Facet indices + { + std::vector> facet_indices(mesh.get_num_facets()); + for (Index f = 0; f < mesh.get_num_facets(); ++f) { + facet_indices[f].reserve(mesh.get_facet_size(f)); + for (Index v : mesh.get_facet_vertices(f)) { + facet_indices[f].push_back(static_cast(v)); + } + } + ply.getElement("face").addListProperty("vertex_indices", facet_indices); + } + + // Edge indices + if (mesh.has_edges()) { + std::vector v1(mesh.get_num_edges()); + std::vector v2(mesh.get_num_edges()); + for (Index e = 0; e < mesh.get_num_edges(); ++e) { + auto verts = mesh.get_edge_vertices(e); + v1[e] = uint32_t(verts[0]); + v2[e] = uint32_t(verts[1]); + } + ply.getElement("edge").addProperty("vertex1", v1); + ply.getElement("edge").addProperty("vertex2", v2); + } + + // Write the object to file + happly::DataFormat format; + switch (options.encoding) { + case FileEncoding::Binary: format = happly::DataFormat::Binary; break; + case FileEncoding::Ascii: format = happly::DataFormat::ASCII; break; + default: format = happly::DataFormat::Binary; + } + ply.write(output_stream, format); +} + +template +void save_mesh_ply( + const fs::path& filename, + const SurfaceMesh& mesh, + const SaveOptions& options) +{ + fs::ofstream fout(filename); + save_mesh_ply(fout, mesh, options); +} + +#define LA_X_save_mesh_ply(_, Scalar, Index) \ + template void save_mesh_ply( \ + const fs::path& filename, \ + const SurfaceMesh& mesh, \ + const SaveOptions& options); +LA_SURFACE_MESH_X(save_mesh_ply, 0) + +} // namespace lagrange::io diff --git a/modules/io/src/save_simple_scene.cpp b/modules/io/src/save_simple_scene.cpp new file mode 100644 index 00000000..b3ae5879 --- /dev/null +++ b/modules/io/src/save_simple_scene.cpp @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + + +#include + +#include +#include + +#include +#include +#include +#include + +namespace lagrange::io { + +template +void save_simple_scene(const fs::path& filename, + const scene::SimpleScene& scene, + const SaveOptions& options) { + + if (filename.extension() == ".obj") { + // todo + throw std::runtime_error("Not implemented yet!"); + } else if (filename.extension() == ".ply") { + // todo + throw std::runtime_error("Not implemented yet!"); + } else if (filename.extension() == ".msh") { + // todo + throw std::runtime_error("Not implemented yet!"); + } else if (filename.extension() == ".gltf" || filename.extension() == ".glb") { + save_simple_scene_gltf(filename, scene, options); + } +} + +#define LA_X_save_simple_scene(_, S, I, D) \ + template void save_simple_scene( \ + const fs::path& filename, \ + const scene::SimpleScene& scene, \ + const SaveOptions& options); +LA_SIMPLE_SCENE_X(save_simple_scene, 0); +#undef LA_X_save_simple_scene + +} diff --git a/modules/io/tests/io_common.cpp b/modules/io/tests/io_common.cpp new file mode 100644 index 00000000..21282e35 --- /dev/null +++ b/modules/io/tests/io_common.cpp @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include + +#include +#include +#include + + +namespace lagrange::io::testing { + +SurfaceMesh32d create_surfacemesh_cube() +{ + std::unique_ptr legacy = create_cube(); + return to_surface_mesh_copy(*legacy); +} + +SurfaceMesh32d create_surfacemesh_sphere() +{ + std::unique_ptr legacy = create_sphere(); + return to_surface_mesh_copy(*legacy); +} + +} diff --git a/modules/io/tests/io_common.h b/modules/io/tests/io_common.h new file mode 100644 index 00000000..ea0c9e8f --- /dev/null +++ b/modules/io/tests/io_common.h @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include + +// TODO move this elsewhere + +namespace lagrange::io::testing { + +SurfaceMesh32d create_surfacemesh_cube(); + +SurfaceMesh32d create_surfacemesh_sphere(); + +} diff --git a/modules/io/tests/test_io.cpp b/modules/io/tests/test_io.cpp index 8e160ca4..d3a23809 100644 --- a/modules/io/tests/test_io.cpp +++ b/modules/io/tests/test_io.cpp @@ -17,6 +17,7 @@ #include #include #include +#include // clang-format off #include @@ -45,10 +46,7 @@ void test_load_save() for (int i = 0; i < 8; ++i) { fs::path filename = fmt::format("semi{}.obj", i + 1); - fs::path input_path = lagrange::testing::get_data_path("open/core/tilings" / filename); - REQUIRE(fs::exists(input_path)); - auto output = io::load_mesh_obj(input_path.string()); - auto mesh = std::move(output.mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core/tilings" / filename); logger().info( "Loaded tiling with {} vertices and {} facets", mesh.get_num_vertices(), @@ -57,17 +55,14 @@ void test_load_save() for (Index v = 0; v < mesh.get_num_vertices(); ++v) { mesh.ref_position(v)[2] = dist(gen); } - io::save_mesh_obj(filename, mesh); + io::save_mesh(filename, mesh); } std::array, 3> test_cases = { {{"hexagon", 6}, {"square", 4}, {"triangle", 3}}}; for (auto kv : test_cases) { fs::path filename = fmt::format("{}.obj", kv.first); - fs::path input_path = lagrange::testing::get_data_path("open/core/tilings" / filename); - REQUIRE(fs::exists(input_path)); - auto output = io::load_mesh_obj(input_path.string()); - auto mesh = std::move(output.mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core/tilings" / filename); logger().info( "Loaded tiling with {} vertices and {} facets", mesh.get_num_vertices(), @@ -77,7 +72,7 @@ void test_load_save() for (Index v = 0; v < mesh.get_num_vertices(); ++v) { mesh.ref_position(v)[2] = dist(gen); } - io::save_mesh_obj(filename, mesh); + io::save_mesh(filename, mesh); } } @@ -94,27 +89,21 @@ void test_benchmark_tiles() for (int i = 0; i < 8; ++i) { fs::path filename = fmt::format("semi{}.obj", i + 1); - fs::path input_path = lagrange::testing::get_data_path("open/core/tilings" / filename); - REQUIRE(fs::exists(input_path)); - auto output = io::load_mesh_obj(input_path.string()); - auto mesh = std::move(output.mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core/tilings" / filename); n += safe_cast(mesh.get_num_vertices()); REQUIRE(mesh.is_hybrid()); - io::save_mesh_obj(filename, mesh); + io::save_mesh(filename, mesh); } std::array, 3> test_cases = { {{"hexagon", 6}, {"square", 4}, {"triangle", 3}}}; for (auto kv : test_cases) { fs::path filename = fmt::format("{}.obj", kv.first); - fs::path input_path = lagrange::testing::get_data_path("open/core/tilings" / filename); - REQUIRE(fs::exists(input_path)); - auto output = io::load_mesh_obj(input_path.string()); - auto mesh = std::move(output.mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core/tilings" / filename); n += safe_cast(mesh.get_num_vertices()); REQUIRE(mesh.is_regular()); REQUIRE(mesh.get_vertex_per_facet() == safe_cast(kv.second)); - io::save_mesh_obj(filename, mesh); + io::save_mesh(filename, mesh); } return n; @@ -128,15 +117,11 @@ void test_io_blub() using MeshType = SurfaceMesh; using Scalar = typename MeshType::Scalar; - fs::path path = lagrange::testing::get_data_path("open/core/blub/blub.obj"); - REQUIRE(fs::exists(path)); - auto res = io::load_mesh_obj(path.string()); - REQUIRE(res.success); - auto mesh = std::move(res.mesh); - io::save_mesh_obj("blub.obj", mesh); + auto mesh = lagrange::testing::load_surface_mesh("open/core/blub/blub.obj"); + io::save_mesh("blub.obj", mesh); logger().info("Mesh #v {}, #f {}", mesh.get_num_vertices(), mesh.get_num_facets()); - auto& uv_attr = mesh.template get_indexed_attribute("uv"); + auto& uv_attr = mesh.template get_indexed_attribute(AttributeName::texcoord); logger().info("Mesh #uv {}", uv_attr.values().get_num_elements()); } @@ -151,7 +136,7 @@ void test_benchmark_large() // TODO: Add this guy to our unit test data fs::path path = "/Users/jedumas/cloud/adobe/shared/mesh_processing/Modeler - " "Qadremesher/SoylentGreen_FullRes.obj"; - auto mesh = std::move(io::load_mesh_obj(path.string()).mesh); + auto mesh = io::load_mesh(path.string()); logger().info("Mesh #v {}, #f {}", mesh.get_num_vertices(), mesh.get_num_facets()); return mesh.get_num_vertices(); // Uncomment to time obj save as well @@ -160,6 +145,32 @@ void test_benchmark_large() }; } +template +void test_obj_indexing() +{ + using namespace lagrange; + using MeshType = SurfaceMesh; + using Index = typename MeshType::Index; + + auto mesh = lagrange::testing::load_surface_mesh("open/core/index-test.obj"); + + for (Index f = 0; f < mesh.get_num_facets(); ++f) + { + const Index first_corner = mesh.get_facet_corner_begin(f); + const Index last_corner = mesh.get_facet_corner_end(f); + + bool all_zero = true; + for (Index c = first_corner; c < last_corner; ++c) + { + Index vertexIndex = mesh.get_corner_vertex(c); + all_zero = all_zero && (vertexIndex == 0); + } + + // Incorrect mesh indexing during obj load will result in uninitialized facets. + REQUIRE(!all_zero); + } +} + } // namespace TEST_CASE("Mesh IO: Load and Save", "[next]") @@ -185,3 +196,9 @@ TEST_CASE("Mesh IO: Benchmark Large", "[next][!benchmark]" LA_CORP_FLAG) #define LA_X_test_benchmark_large(_, S, I) test_benchmark_large(); LA_SURFACE_MESH_X(test_benchmark_large, 0) } + +TEST_CASE("Mesh IO: Index Test", "[next]") +{ +#define LA_X_test_obj_indexing(_, S, I) test_obj_indexing(); + LA_SURFACE_MESH_X(test_obj_indexing, 0) +} diff --git a/modules/io/tests/test_load_mesh_assimp.cpp b/modules/io/tests/test_legacy_load_mesh_assimp.cpp similarity index 83% rename from modules/io/tests/test_load_mesh_assimp.cpp rename to modules/io/tests/test_legacy_load_mesh_assimp.cpp index 65349832..3cc8329f 100644 --- a/modules/io/tests/test_load_mesh_assimp.cpp +++ b/modules/io/tests/test_legacy_load_mesh_assimp.cpp @@ -27,16 +27,16 @@ TEST_CASE("load_mesh_assimp", "[mesh][io]") { } TEST_CASE("load_scene_assimp", "[io]") { - auto scene = - lagrange::io::load_scene_assimp(lagrange::testing::get_data_path("open/core/drop_tri.obj")); + auto scene = lagrange::io::load_scene_assimp( + lagrange::testing::get_data_path("open/core/drop_tri.obj")); REQUIRE(scene != nullptr); REQUIRE(scene->mNumMeshes == 1); REQUIRE(scene->mMeshes[0]->mNumFaces > 0); } TEST_CASE("load fbx", "[io]") { - auto scene = - lagrange::io::load_scene_assimp(lagrange::testing::get_data_path("corp/io/rp_adanna_rigged_001_zup_t.fbx")); + auto scene = lagrange::io::load_scene_assimp( + lagrange::testing::get_data_path("corp/io/rp_adanna_rigged_001_zup_t.fbx")); REQUIRE(scene != nullptr); REQUIRE(scene->mNumMeshes == 1); // one mesh with one material, but multiple components auto* mesh = scene->mMeshes[0]; @@ -58,11 +58,12 @@ TEST_CASE("load glb", "[io]") { REQUIRE(scene->mNumMaterials == 2); REQUIRE(scene->mMaterials[mesh->mMaterialIndex]->mNumProperties > 0); - auto lmeshes = lagrange::io::extract_meshes_assimp(scene.get()); + auto lmeshes = + lagrange::io::legacy::extract_meshes_assimp(scene.get()); REQUIRE(lmeshes.size() == 1); using Index = lagrange::TriangleMesh3D::Index; - auto lmesh = lagrange::io::convert_mesh_assimp(mesh); + auto lmesh = lagrange::io::legacy::convert_mesh_assimp(mesh); REQUIRE(lmesh->get_num_vertices() == lagrange::safe_cast(mesh->mNumVertices)); REQUIRE(lmesh->get_num_facets() == lagrange::safe_cast(mesh->mNumFaces)); REQUIRE(lmesh->is_uv_initialized()); diff --git a/modules/io/tests/test_load_mesh_ext.cpp b/modules/io/tests/test_legacy_load_mesh_ext.cpp similarity index 95% rename from modules/io/tests/test_load_mesh_ext.cpp rename to modules/io/tests/test_legacy_load_mesh_ext.cpp index 3329fdd2..eb654f40 100644 --- a/modules/io/tests/test_load_mesh_ext.cpp +++ b/modules/io/tests/test_legacy_load_mesh_ext.cpp @@ -14,8 +14,8 @@ #include #include -#include -#include +#include +#include #include "test_load_mesh_data.h" @@ -38,12 +38,12 @@ TEST_CASE("MeshLoad Params", "[Mesh][Load]" LA_CORP_FLAG) for (bool load_normals : {true, false}) { for (bool load_materials : {true, false}) { for (bool load_uvs : {true, false}) { - lagrange::io::MeshLoaderParams params; + lagrange::io::legacy::MeshLoaderParams params; params.load_materials = load_materials; params.load_normals = load_normals; params.load_uvs = load_uvs; tinyobj::MaterialFileReader mtl_reader(lagrange::testing::get_data_path("corp/core/").string()); - auto result = lagrange::io::load_mesh_ext( + auto result = lagrange::io::legacy::load_mesh_ext( lagrange::testing::get_data_path(name), params, &mtl_reader); @@ -68,12 +68,12 @@ TEST_CASE("MeshLoad Params (open)", "[Mesh][Load]") for (bool load_normals : {true, false}) { for (bool load_materials : {true, false}) { for (bool load_uvs : {true, false}) { - lagrange::io::MeshLoaderParams params; + lagrange::io::legacy::MeshLoaderParams params; params.load_materials = load_materials; params.load_normals = load_normals; params.load_uvs = load_uvs; tinyobj::MaterialFileReader mtl_reader(lagrange::testing::get_data_path("open/core/").string()); - auto result = lagrange::io::load_mesh_ext( + auto result = lagrange::io::legacy::load_mesh_ext( lagrange::testing::get_data_path(name), params, &mtl_reader); @@ -96,9 +96,9 @@ TEST_CASE("MeshLoad Negative Indices", "[Mesh][Load]") { const std::string name = "open/core/dragon.obj"; for (bool load_normals : {true, false}) { - lagrange::io::MeshLoaderParams params; + lagrange::io::legacy::MeshLoaderParams params; params.load_normals = load_normals; - auto result = lagrange::io::load_mesh_ext( + auto result = lagrange::io::legacy::load_mesh_ext( lagrange::testing::get_data_path(name), params); REQUIRE(result.success); @@ -107,7 +107,7 @@ TEST_CASE("MeshLoad Negative Indices", "[Mesh][Load]") REQUIRE(result.meshes[0]->get_facets().minCoeff() >= 0); REQUIRE(result.meshes[0]->get_facets().maxCoeff() < result.meshes[0]->get_num_vertices()); // make sure the loading is consistent (i.e., doesn't contain garbage data) - auto result2 = lagrange::io::load_mesh_ext( + auto result2 = lagrange::io::legacy::load_mesh_ext( lagrange::testing::get_data_path(name), params); REQUIRE(result2.success); @@ -131,7 +131,7 @@ TEST_CASE("MeshLoad Negative Indices", "[Mesh][Load]") TEST_CASE("MeshLoad", "[Mesh][Load]") { using namespace lagrange; - using namespace lagrange::io; + using namespace lagrange::io::legacy; const std::string tmp_filename = "tmp.obj"; diff --git a/modules/io/tests/test_mesh_io.cpp b/modules/io/tests/test_legacy_mesh_io.cpp similarity index 89% rename from modules/io/tests/test_mesh_io.cpp rename to modules/io/tests/test_legacy_mesh_io.cpp index 777b42a1..1833c0ad 100644 --- a/modules/io/tests/test_mesh_io.cpp +++ b/modules/io/tests/test_legacy_mesh_io.cpp @@ -13,8 +13,8 @@ #include #include -#include -#include +#include +#include #include @@ -23,7 +23,7 @@ TEST_CASE("drop", "[mesh][io]") using namespace lagrange; auto mesh = lagrange::testing::load_mesh("open/core/drop_tri.obj"); lagrange::io::save_mesh("io_test_drop.obj", *mesh); - auto mesh2 = lagrange::io::load_mesh("io_test_drop.obj"); + auto mesh2 = lagrange::io::legacy::load_mesh("io_test_drop.obj"); fs::remove("io_test_drop.obj"); REQUIRE(mesh->get_num_vertices() == mesh2->get_num_vertices()); REQUIRE(mesh->get_num_facets() == mesh2->get_num_facets()); diff --git a/modules/io/tests/test_load_assimp.cpp b/modules/io/tests/test_load_assimp.cpp new file mode 100644 index 00000000..f5b76ae7 --- /dev/null +++ b/modules/io/tests/test_load_assimp.cpp @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#ifdef LAGRANGE_WITH_ASSIMP + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace lagrange; + +TEST_CASE("load_mesh_assimp", "[io]") { + auto mesh = io::load_mesh_assimp( + testing::get_data_path("open/core/drop_tri.obj")); + REQUIRE(mesh.get_num_facets() > 0); + REQUIRE(mesh.has_attribute(AttributeName::texcoord)); + REQUIRE(mesh.has_attribute(AttributeName::normal)); +} + +TEST_CASE("load_assimp_fbx", "[io]") +{ + auto scene = lagrange::io::internal::load_assimp( + lagrange::testing::get_data_path("corp/io/rp_adanna_rigged_001_zup_t.fbx")); + REQUIRE(scene != nullptr); + REQUIRE(scene->mNumMeshes == 1); // one mesh with one material, but multiple components + auto* mesh = scene->mMeshes[0]; + REQUIRE(mesh->mNumFaces == 12025); + REQUIRE(mesh->mNumBones == 88); // fbx has bone information + + REQUIRE(scene->mNumMaterials == 1); + REQUIRE(scene->mMaterials[mesh->mMaterialIndex]->mNumProperties > 0); + + auto lmesh = lagrange::io::internal::load_mesh_assimp(*scene); + REQUIRE(lmesh.get_num_facets() > 0); + REQUIRE(lmesh.get_num_vertices() > 0); + //REQUIRE(lmesh.has_attribute(AttributeName::texcoord)); + REQUIRE(lmesh.has_attribute(AttributeName::normal)); + REQUIRE(lmesh.has_attribute(AttributeName::indexed_joint)); + REQUIRE(lmesh.has_attribute(AttributeName::indexed_weight)); + REQUIRE( + internal::find_matching_attribute( + lmesh, + "", + BitField(AttributeElement::Vertex | AttributeElement::Indexed), + AttributeUsage::UV, + 2) != invalid_attribute_id()); +} + +TEST_CASE("load_assimp_glb", "[io]") +{ + auto scene = lagrange::io::internal::load_assimp( + lagrange::testing::get_data_path("open/core/blub/blub.glb")); + REQUIRE(scene != nullptr); + REQUIRE(scene->mNumMeshes == 1); + auto* mesh = scene->mMeshes[0]; + REQUIRE(mesh->mNumFaces > 0); + + REQUIRE(scene->mNumMaterials == 2); + REQUIRE(scene->mMaterials[mesh->mMaterialIndex]->mNumProperties > 0); + + using Index = lagrange::SurfaceMesh32f::Index; + auto lmesh = lagrange::io::internal::load_mesh_assimp(*scene); + REQUIRE(lmesh.get_num_vertices() == lagrange::safe_cast(mesh->mNumVertices)); + REQUIRE(lmesh.get_num_facets() == lagrange::safe_cast(mesh->mNumFaces)); + REQUIRE(lmesh.has_attribute(AttributeName::texcoord)); + REQUIRE(lmesh.has_attribute(AttributeName::normal)); +} + +TEST_CASE("load_simple_scene_assimp", "[io]") { + auto scene = lagrange::io::load_simple_scene_assimp( + lagrange::testing::get_data_path("open/io/three_cubes_instances.gltf")); + REQUIRE(scene.get_num_meshes() == 1); + REQUIRE(scene.get_num_instances(0) == 3); + + const auto& t1 = scene.get_instance(0, 0).transform; + const auto& t2 = scene.get_instance(0, 1).transform; + const auto& t3 = scene.get_instance(0, 2).transform; + REQUIRE(!t1.isApprox(t2)); + REQUIRE(!t2.isApprox(t3)); + REQUIRE(!t3.isApprox(t1)); +} + +#endif diff --git a/modules/io/tests/test_load_gltf.cpp b/modules/io/tests/test_load_gltf.cpp new file mode 100644 index 00000000..029ec8d8 --- /dev/null +++ b/modules/io/tests/test_load_gltf.cpp @@ -0,0 +1,80 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include +#include + +using namespace lagrange; + +// this file is a single gltf with embedded buffers +TEST_CASE("load_mesh_gltf", "[io]") { + auto mesh = io::load_mesh_gltf( + testing::get_data_path("open/io/three_cubes_instances.gltf")); + REQUIRE(mesh.get_num_vertices() == 24); + REQUIRE(mesh.get_num_facets() == 12); + REQUIRE(mesh.has_attribute(AttributeName::normal)); + REQUIRE(mesh.has_attribute(AttributeName::texcoord)); +} + +TEST_CASE("load_simple_scene_gltf", "[io]") { + auto scene = lagrange::io::load_simple_scene_gltf( + lagrange::testing::get_data_path("open/io/three_cubes_instances.gltf")); + REQUIRE(scene.get_num_meshes() == 1); + REQUIRE(scene.get_num_instances(0) == 3); + + const auto& t1 = scene.get_instance(0, 0).transform; + const auto& t2 = scene.get_instance(0, 1).transform; + const auto& t3 = scene.get_instance(0, 2).transform; + REQUIRE(!t1.isApprox(t2)); + REQUIRE(!t2.isApprox(t3)); + REQUIRE(!t3.isApprox(t1)); + +} + +// this file is a gltf with separate .bin and textures +TEST_CASE("load_mesh_gltf_animated_cube", "[io]") { + auto mesh = io::load_mesh_gltf( + testing::get_data_path("open/io/gltf_animated_cube/AnimatedCube.gltf")); + REQUIRE(mesh.get_num_vertices() == 36); + REQUIRE(mesh.get_num_facets() == 12); + REQUIRE(mesh.has_attribute(AttributeName::normal)); + REQUIRE(mesh.has_attribute(AttributeName::texcoord)); + +} + + +// this file contains a single mesh with two separate components. +TEST_CASE("load_gltf_avocado", "[io]") { + auto mesh = io::load_mesh_gltf( + testing::get_data_path("open/io/gltf_avocado/Avocado.gltf")); + REQUIRE(mesh.get_num_vertices() > 0); + REQUIRE(mesh.get_num_facets() > 0); + REQUIRE(mesh.has_attribute(AttributeName::normal)); + REQUIRE(mesh.has_attribute(AttributeName::texcoord)); + + auto scene = io::load_simple_scene_gltf( + testing::get_data_path("open/io/gltf_avocado/Avocado.gltf")); + REQUIRE(scene.get_num_meshes() == 1); + REQUIRE(scene.get_num_instances(0) == 1); +} + +// this file contains a model made of many different meshes (29!). +// There are no textures and no UVs, each component has a material with a different base color. +// note that this file has a total of 10413 degenerate triangles out of 75730. +TEST_CASE("load_gltf_engine", "[io]") { + auto mesh = io::load_mesh_gltf( + testing::get_data_path("open/io/gltf_engine/2CylinderEngine.gltf")); + REQUIRE(mesh.get_num_vertices() == 55843); + REQUIRE(mesh.get_num_facets() == 75730); + REQUIRE(mesh.has_attribute(AttributeName::normal)); +} diff --git a/modules/io/tests/test_load_mesh_data.h b/modules/io/tests/test_load_mesh_data.h index fe88b588..d24a7859 100644 --- a/modules/io/tests/test_load_mesh_data.h +++ b/modules/io/tests/test_load_mesh_data.h @@ -9,12 +9,6 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include - - -#include -#include const char* obj_cube_triangles = R"( # Blender v2.81 (sub 16) OBJ File: '' diff --git a/modules/io/tests/test_load_mesh_obj.cpp b/modules/io/tests/test_load_mesh_obj.cpp new file mode 100644 index 00000000..0bf55037 --- /dev/null +++ b/modules/io/tests/test_load_mesh_obj.cpp @@ -0,0 +1,15 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include + +// TODO diff --git a/modules/io/tests/test_load_mesh_ply.cpp b/modules/io/tests/test_load_mesh_ply.cpp new file mode 100644 index 00000000..ab83ccf1 --- /dev/null +++ b/modules/io/tests/test_load_mesh_ply.cpp @@ -0,0 +1,21 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include + +TEST_CASE("load_ply", "[io][ply]") { + using namespace lagrange; + auto mesh = + io::load_mesh_ply(testing::get_data_path("open/subdivision/sphere.ply")); + REQUIRE(mesh.get_num_vertices() == 42); + REQUIRE(mesh.get_num_facets() == 80); +} diff --git a/modules/io/tests/test_load_simple_scene.cpp b/modules/io/tests/test_load_simple_scene.cpp new file mode 100644 index 00000000..e69de29b diff --git a/modules/io/tests/test_msh.cpp b/modules/io/tests/test_msh.cpp index 7b018bc3..95cbc004 100644 --- a/modules/io/tests/test_msh.cpp +++ b/modules/io/tests/test_msh.cpp @@ -50,7 +50,7 @@ TEST_CASE("io/msh", "[mesh][io][msh]") const auto num_facets = m1.get_num_facets(); for (auto fid : range(num_facets)) { const auto f1 = m1.get_facet_vertices(fid); - const auto f2 = m1.get_facet_vertices(fid); + const auto f2 = m2.get_facet_vertices(fid); REQUIRE(f1.size() == f2.size()); for (auto i : range(f1.size())) { @@ -86,8 +86,9 @@ TEST_CASE("io/msh", "[mesh][io][msh]") mesh.add_triangle(0, 1, 3); mesh.add_triangle(1, 2, 3); std::stringstream data; - io::MshSaverOptions options; - options.binary = false; + io::SaveOptions options; + options.encoding = io::FileEncoding::Ascii; + options.output_attributes = io::SaveOptions::OutputAttributes::SelectedOnly; SECTION("With vertex attribute") { @@ -104,7 +105,7 @@ TEST_CASE("io/msh", "[mesh][io][msh]") buffer[2] = 2.; buffer[3] = 3.; - options.attr_ids.push_back(id); + options.selected_attributes.push_back(id); } SECTION("With facet attribute") @@ -122,7 +123,7 @@ TEST_CASE("io/msh", "[mesh][io][msh]") buffer[2] = 2.; buffer[3] = 3.; - options.attr_ids.push_back(id); + options.selected_attributes.push_back(id); } SECTION("With corner attribute") @@ -139,7 +140,7 @@ TEST_CASE("io/msh", "[mesh][io][msh]") for (auto i : range(buffer.size())) { buffer[i] = static_cast(i); } - options.attr_ids.push_back(id); + options.selected_attributes.push_back(id); } SECTION("With int data") @@ -157,7 +158,7 @@ TEST_CASE("io/msh", "[mesh][io][msh]") buffer[2] = 2; buffer[3] = 3; - options.attr_ids.push_back(id); + options.selected_attributes.push_back(id); } REQUIRE_NOTHROW(io::save_mesh_msh(data, mesh, options)); diff --git a/modules/io/tests/test_save_mesh.cpp b/modules/io/tests/test_save_mesh.cpp new file mode 100644 index 00000000..91f06646 --- /dev/null +++ b/modules/io/tests/test_save_mesh.cpp @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include "io_common.h" + +#include +#include + +#include + +using namespace lagrange; + +TEST_CASE("save_mesh", "[io]") +{ + auto cube_indexed = io::testing::create_surfacemesh_cube(); + auto cube = unify_index_buffer(cube_indexed); + io::SaveOptions opt; + opt.encoding = io::FileEncoding::Ascii; + REQUIRE_NOTHROW(io::save_mesh("test_cube.gltf", cube, opt)); + + opt.encoding = io::FileEncoding::Binary; + REQUIRE_NOTHROW(io::save_mesh("test_cube.glb", cube, opt)); + + REQUIRE_NOTHROW(io::save_mesh("test_cube.msh", cube)); + REQUIRE_NOTHROW(io::save_mesh("test_cube.obj", cube)); + REQUIRE_NOTHROW(io::save_mesh("test_cube.ply", cube)); +} diff --git a/modules/io/tests/test_save_simple_scene.cpp b/modules/io/tests/test_save_simple_scene.cpp new file mode 100644 index 00000000..731c7601 --- /dev/null +++ b/modules/io/tests/test_save_simple_scene.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include "io_common.h" + +#include +#include + +using namespace lagrange; + +scene::SimpleScene32d3 create_simple_scene() { + auto cube = io::testing::create_surfacemesh_cube(); + auto sphere = io::testing::create_surfacemesh_sphere(); + + scene::SimpleScene32d3 scene; + auto cube_idx = scene.add_mesh(std::move(cube)); + auto sphere_idx = scene.add_mesh(std::move(sphere)); + + using AffineTransform = typename decltype(scene)::AffineTransform; + AffineTransform t1 = AffineTransform::Identity(); + AffineTransform t2 = AffineTransform::Identity(); + AffineTransform t3 = AffineTransform::Identity(); + t1.translate(Eigen::Vector3d(0, -3, 0)); + t2.translate(Eigen::Vector3d(3, 0, 0)); + t3.translate(Eigen::Vector3d(-3, 0, 0)); + scene.add_instance({cube_idx, t1}); + scene.add_instance({cube_idx, t2}); + scene.add_instance({sphere_idx, t3}); + + return scene; +} + +TEST_CASE("save_simple_scene", "[io]") +{ + auto scene = create_simple_scene(); + io::SaveOptions opt; + opt.encoding = io::FileEncoding::Ascii; + REQUIRE_NOTHROW(io::save_simple_scene("scene.gltf", scene, opt)); +} diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt index a4078c2c..c9a73314 100644 --- a/modules/python/CMakeLists.txt +++ b/modules/python/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright 2022 Adobe. All rights reserved. +# Copyright 2020 Adobe. All rights reserved. # This file is licensed to you under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. You may obtain a copy # of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -10,20 +10,40 @@ # governing permissions and limitations under the License. # +# 0. Add $loader_path or $ORIGIN to rpath. +if(SKBUILD) + if(APPLE) + set(CMAKE_INSTALL_RPATH @loader_path) + elseif(UNIX) + set(CMAKE_INSTALL_RPATH $ORIGIN) + endif() +endif() + # 1. define module -include(nanobind) +include(nanobind tbb) nanobind_add_module(lagrange_python NB_STATIC) add_library(lagrange::python ALIAS lagrange_python) -set_target_properties(lagrange_python PROPERTIES FOLDER "${LAGRANGE_IDE_PREFIX}Lagrange/Modules") +set_target_properties(lagrange_python PROPERTIES + FOLDER "${LAGRANGE_IDE_PREFIX}Lagrange/Modules" + CXX_VISIBILITY_PRESET default) + +if(LAGRANGE_TOPLEVEL_PROJECT) + set_target_properties(lagrange_python PROPERTIES COMPILE_WARNING_AS_ERROR ON) +endif() + message(STATUS "Lagrange: creating target 'lagrange::python'") +if(TBB_PREFER_STATIC) + message(FATAL_ERROR "TBB must be compiled as shared library for python binding to work.") +endif() # 2. installation if(SKBUILD) # Install python extension directly at ${CMAKE_INSTALL_PREFIX}, which will # be set by `setup.py`. - install(TARGETS lagrange_python + install(TARGETS lagrange_python tbb ARCHIVE DESTINATION . COMPONENT Lagrange_Python_Runtime LIBRARY DESTINATION . COMPONENT Lagrange_Python_Runtime + RUNTIME DESTINATION . COMPONENT Lagrange_Python_Runtime ) add_custom_target(lagrange-python-install-runtime ${CMAKE_COMMAND} diff --git a/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h b/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h index 32de1352..65974bcd 100644 --- a/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h +++ b/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h @@ -189,21 +189,10 @@ class EmbreeRayCaster const Transform& trans = Transform::Identity(), RTCBuildQuality build_quality = RTC_BUILD_QUALITY_MEDIUM) { - m_meshes.push_back(std::move(std::make_unique>(mesh))); - m_transforms.push_back(trans); - m_mesh_build_qualities.push_back(build_quality); - m_visibility.push_back(true); - for (auto& f : m_filters) { // per-mesh, not per-instance - f.push_back(nullptr); - } - Index mesh_index = safe_cast(m_meshes.size() - 1); - la_runtime_assert(m_instance_index_ranges.size() > 0); - Index instance_index = m_instance_index_ranges.back(); - la_runtime_assert(instance_index == safe_cast(m_instance_to_user_mesh.size())); - m_instance_index_ranges.push_back(instance_index + 1); - m_instance_to_user_mesh.resize(instance_index + 1, mesh_index); - m_need_rebuild = true; - return mesh_index; + return add_raycasting_mesh( + std::make_unique>(mesh), + trans, + build_quality); } /** @@ -248,13 +237,10 @@ class EmbreeRayCaster std::shared_ptr mesh, RTCBuildQuality build_quality = RTC_BUILD_QUALITY_MEDIUM) { - la_runtime_assert(mesh->get_dim() == 3); - la_runtime_assert(mesh->get_vertex_per_facet() == 3); - la_runtime_assert(index < safe_cast(m_meshes.size())); - m_meshes[index] = std::move(std::make_unique>(mesh)); - m_mesh_build_qualities[index] = build_quality; - m_need_rebuild = true; // TODO: Make this more fine-grained so only the affected part of - // the Embree scene is updated + update_raycasting_mesh( + index, + std::make_unique>(mesh), + build_quality); } /** @@ -763,6 +749,43 @@ class EmbreeRayCaster /** Use the underlying BVH to find the point closest to a query point. */ ClosestPoint query_closest_point(const Point& p) const; +/** Add raycasting utilities **/ + Index add_raycasting_mesh( + std::unique_ptr mesh, + const Transform& trans = Transform::Identity(), + RTCBuildQuality build_quality = RTC_BUILD_QUALITY_MEDIUM) + { + m_meshes.push_back(std::move(mesh)); + m_transforms.push_back(trans); + m_mesh_build_qualities.push_back(build_quality); + m_visibility.push_back(true); + for (auto& f : m_filters) { // per-mesh, not per-instance + f.push_back(nullptr); + } + Index mesh_index = safe_cast(m_meshes.size() - 1); + la_runtime_assert(m_instance_index_ranges.size() > 0); + Index instance_index = m_instance_index_ranges.back(); + la_runtime_assert(instance_index == safe_cast(m_instance_to_user_mesh.size())); + m_instance_index_ranges.push_back(instance_index + 1); + m_instance_to_user_mesh.resize(instance_index + 1, mesh_index); + m_need_rebuild = true; + return mesh_index; + } + + void update_raycasting_mesh( + Index index, + std::unique_ptr mesh, + RTCBuildQuality build_quality = RTC_BUILD_QUALITY_MEDIUM) + { + la_runtime_assert(mesh->get_dim() == 3); + la_runtime_assert(mesh->get_vertex_per_facet() == 3); + la_runtime_assert(index < safe_cast(m_meshes.size())); + m_meshes[index] = std::move(mesh); + m_mesh_build_qualities[index] = build_quality; + m_need_rebuild = true; // TODO: Make this more fine-grained so only the affected part of + // the Embree scene is updated + } + protected: /** Get the Embree scene flags. */ virtual RTCSceneFlags get_scene_flags() const diff --git a/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h b/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h index 64c5c076..241135b8 100644 --- a/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h +++ b/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h @@ -9,7 +9,7 @@ #include -#include +#include #include #include diff --git a/modules/scene/CMakeLists.txt b/modules/scene/CMakeLists.txt new file mode 100644 index 00000000..1db24fd1 --- /dev/null +++ b/modules/scene/CMakeLists.txt @@ -0,0 +1,42 @@ +# +# Copyright 2022 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +# 1. define module +lagrange_add_module() +set_target_properties(lagrange_scene PROPERTIES POSITION_INDEPENDENT_CODE ON) + +if(LAGRANGE_TOPLEVEL_PROJECT) + set_target_properties(lagrange_scene PROPERTIES COMPILE_WARNING_AS_ERROR ON) +endif() + +# 2. dependencies +target_link_libraries(lagrange_scene + lagrange::core +) + +# 3. installation +if(LAGRANGE_INSTALL) + set_target_properties(lagrange_scene PROPERTIES EXPORT_NAME scene) + lagrange_install(lagrange_scene) +endif() + +# 3. unit tests and examples +if(LAGRANGE_UNIT_TESTS) + add_subdirectory(tests) +endif() + +# if(LAGRANGE_EXAMPLES) +# add_subdirectory(examples) +# endif() + +if(LAGRANGE_MODULE_PYTHON) + add_subdirectory(python) +endif() diff --git a/modules/scene/include/lagrange/scene/SimpleScene.h b/modules/scene/include/lagrange/scene/SimpleScene.h new file mode 100644 index 00000000..ebb7f08b --- /dev/null +++ b/modules/scene/include/lagrange/scene/SimpleScene.h @@ -0,0 +1,198 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +#include + +#include +#include + +namespace lagrange::scene { + +/// +/// A single mesh instance in a scene. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// @tparam Dimension Mesh dimension. Needed since we store the instance transform as a +/// stack-allocated matrix. +/// +template +struct MeshInstance +{ + /// Affine transformation matrix. + using AffineTransform = Eigen::Transform; + + /// Index of the referenced mesh in the scene. + Index mesh_index = invalid(); + + /// Instance transformation. + AffineTransform transform = AffineTransform::Identity(); + + /// Opaque user data. + std::any user_data = {}; + + /// Access dimension from outside the class + constexpr static size_t Dim = Dimension; +}; + +/// +/// Simple scene container for instanced meshes. +/// +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// @tparam Dimension Mesh dimension. +/// +template +class SimpleScene +{ +public: + /// Mesh type. + using MeshType = SurfaceMesh; + + /// Instance type. + using InstanceType = MeshInstance; + + /// Affine transform matrix. + using AffineTransform = typename InstanceType::AffineTransform; + + /// Access dimension from outside the class + constexpr static size_t Dim = Dimension; + +public: + /// + /// Gets the number of meshes in the scene. + /// + /// @return Number of meshes. + /// + Index get_num_meshes() const { return static_cast(m_meshes.size()); } + + /// + /// Gets the number of instances for a given mesh. + /// + /// @param[in] mesh_index Mesh index. + /// + /// @return Number of instances for a given mesh. + /// + Index get_num_instances(Index mesh_index) const + { + return static_cast(m_instances[mesh_index].size()); + } + + /// + /// Calculates the total number instances for all meshes in the scene. + /// + /// @return Total number of instances in the scene. + /// + Index compute_num_instances() const; + + /// + /// Gets a const reference to a mesh in the scene. + /// + /// @param[in] mesh_index Mesh index. + /// + /// @return Reference to the specified mesh. + /// + const MeshType& get_mesh(Index mesh_index) const { return m_meshes[mesh_index]; } + + /// + /// Gets a modifiable reference to a mesh in a scene. + /// + /// @param[in] mesh_index Mesh index. + /// + /// @return Reference to the specified mesh. + /// + MeshType& ref_mesh(Index mesh_index) { return m_meshes[mesh_index]; } + + /// + /// Get a const reference to a mesh instance in the scene. + /// + /// @param[in] mesh_index Index of the parent mesh in the scene. + /// @param[in] instance_index Local instance index respective to the parent mesh. + /// + /// @return Reference to the specified mesh instance. + /// + const InstanceType& get_instance(Index mesh_index, Index instance_index) const + { + return m_instances[mesh_index][instance_index]; + } + + /// + /// Pre-allocate a number of meshes in the scene. + /// + /// @param[in] num_meshes Number of meshes to reserve in the scene. + /// + void reserve_meshes(Index num_meshes); + + /// + /// Adds a mesh to the scene, possibly with existing instances. + /// + /// @param[in] mesh Mesh to be added to the scene. The object will be moved into the scene. + /// + /// @return Index of the newly added mesh in the scene. + /// + Index add_mesh(MeshType mesh); + + /// + /// Pre-allocate a number of instances for a given mesh. + /// + /// @param[in] mesh_index Mesh index. + /// @param[in] num_instances Number of instances to reserve for this mesh. + /// + void reserve_instances(Index mesh_index, Index num_instances); + + /// + /// Adds a new instance of an existing mesh. + /// + /// @param[in] instance Mesh instance to add to the scene. + /// + /// @return Index of the newly added instance, respective to the specified mesh. + /// + Index add_instance(InstanceType instance); + + /// + /// Iterates over all instances of a specific mesh. + /// + /// @param[in] mesh_index Mesh index on which to iterate over. + /// @param[in] func Callback function to call for each mesh instance. + /// + void foreach_instances_for_mesh(Index mesh_index, function_ref func) + const; + + /// + /// Iterates over all instances of the scene. + /// + /// @param[in] func Callback function to call for each mesh instance. + /// + void foreach_instances(function_ref func) const; + +protected: + /// List of meshes in the scene. + std::vector m_meshes; + + /// List of mesh instances in the scene. Stored as a list of instance per parent mesh. + std::vector> m_instances; +}; + +using SimpleScene32f2 = SimpleScene; +using SimpleScene32d2 = SimpleScene; +using SimpleScene64f2 = SimpleScene; +using SimpleScene64d2 = SimpleScene; +using SimpleScene32f3 = SimpleScene; +using SimpleScene32d3 = SimpleScene; +using SimpleScene64f3 = SimpleScene; +using SimpleScene64d3 = SimpleScene; + +} // namespace lagrange::scene diff --git a/modules/scene/include/lagrange/scene/SimpleSceneTypes.h b/modules/scene/include/lagrange/scene/SimpleSceneTypes.h new file mode 100644 index 00000000..9cdbecff --- /dev/null +++ b/modules/scene/include/lagrange/scene/SimpleSceneTypes.h @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +// clang-format off + +/// +/// [X Macro](https://en.wikipedia.org/wiki/X_Macro) arguments for the SimpleScene<> class. +/// +/// Since other modules might need to explicitly instantiate their own functions, this file is a +/// public header. +/// +/// Use in a .cpp as follows: +/// +/// @code +/// #include +/// #define LA_X_foo(_, Scalar, Index, Dim) template void my_function(const SimpleScene &); +/// LA_SIMPLE_SCENE_X(foo, 0) +/// @endcode +/// +/// The optional `data` argument can forwarded to other macros, in order to implement cartesian +/// products when instantiating nested types. +/// +/// @param mode Suffix to apply to the LA_X_* macro. +/// @param data Data to be passed around as the first argument of the X macro. +/// +/// @return Expansion of LA_X_##mode() for each type to be instantiated. +/// +#define LA_SIMPLE_SCENE_X(mode, data) \ + LA_X_##mode(data, float, uint32_t, 2u) \ + LA_X_##mode(data, double, uint32_t, 2u) \ + LA_X_##mode(data, float, uint64_t, 2u) \ + LA_X_##mode(data, double, uint64_t, 2u) \ + LA_X_##mode(data, float, uint32_t, 3u) \ + LA_X_##mode(data, double, uint32_t, 3u) \ + LA_X_##mode(data, float, uint64_t, 3u) \ + LA_X_##mode(data, double, uint64_t, 3u) + +// clang-format on diff --git a/modules/scene/python/CMakeLists.txt b/modules/scene/python/CMakeLists.txt new file mode 100644 index 00000000..781f5cac --- /dev/null +++ b/modules/scene/python/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +lagrange_add_python_binding() diff --git a/modules/scene/python/include/lagrange/python/scene.h b/modules/scene/python/include/lagrange/python/scene.h new file mode 100644 index 00000000..82ff3da2 --- /dev/null +++ b/modules/scene/python/include/lagrange/python/scene.h @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +// clang-format off +#include +#include +#include +// clang-format on + +namespace lagrange::python { +void populate_scene_module(nanobind::module_& m); +} diff --git a/modules/scene/python/src/bind_simple_scene.h b/modules/scene/python/src/bind_simple_scene.h new file mode 100644 index 00000000..fd8b9553 --- /dev/null +++ b/modules/scene/python/src/bind_simple_scene.h @@ -0,0 +1,134 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +// clang-format off +#include +#include +#include +#include +// clang-format on + +#include + +namespace lagrange::python { + +namespace nb = nanobind; + +template +void bind_simple_scene(nb::module_& m) +{ + using MeshInstance3D = lagrange::scene::MeshInstance; + nb::class_(m, "MeshInstance3D", "A single mesh instance in a scene") + .def(nb::init<>()) + .def_readwrite( + "mesh_index", + &MeshInstance3D::mesh_index) + .def_property( + "transform", + [](MeshInstance3D& self) { + auto& M = self.transform.matrix(); + using MatrixType = std::decay_t; + static_assert(!MatrixType::IsRowMajor, "Transformation matrix is not column major"); + + span data(M.data(), M.size()); + size_t shape[2]{static_cast(M.rows()), static_cast(M.cols())}; + int64_t stride[2]{1, 4}; + return span_to_tensor(data, shape, stride, nb::cast(&self)); + }, + [](MeshInstance3D& self, Tensor tensor) { + auto [values, shape, stride] = tensor_to_span(tensor); + auto& M = self.transform.matrix(); + using MatrixType = std::decay_t; + static_assert(!MatrixType::IsRowMajor, "Transformation matrix is not column major"); + + la_runtime_assert(is_dense(shape, stride)); + la_runtime_assert(check_shape(shape, 4, 4)); + if (stride[0] == 1) { + // Tensor is col major. + std::copy(values.begin(), values.end(), M.data()); + } else { + // Tensor is row major. + M(0, 0) = values[0]; + M(0, 1) = values[1]; + M(0, 2) = values[2]; + M(0, 3) = values[3]; + + M(1, 0) = values[4]; + M(1, 1) = values[5]; + M(1, 2) = values[6]; + M(1, 3) = values[7]; + + M(2, 0) = values[8]; + M(2, 1) = values[9]; + M(2, 2) = values[10]; + M(2, 3) = values[11]; + + M(3, 0) = values[12]; + M(3, 1) = values[13]; + M(3, 2) = values[14]; + M(3, 3) = values[15]; + } + }); + + using SimpleScene3D = lagrange::scene::SimpleScene; + nb::class_(m, "SimpleScene3D", "Simple scene container for instanced meshes") + .def(nb::init<>()) + .def_property_readonly( + "num_meshes", + &SimpleScene3D::get_num_meshes, + "Number of meshes in the scene") + .def( + "num_instances", + &SimpleScene3D::get_num_instances, + "mesh_index"_a) + .def_property_readonly( + "total_num_instances", + &SimpleScene3D::compute_num_instances, + "Total number of instances for all meshes in the scene") + .def( + "get_mesh", + &SimpleScene3D::get_mesh, + "mesh_index"_a) + .def( + "ref_mesh", + &SimpleScene3D::ref_mesh, + "mesh_index"_a) + .def( + "get_instance", + &SimpleScene3D::get_instance, + "mesh_index"_a, + "instance_index"_a) + .def( + "reserve_meshes", + &SimpleScene3D::reserve_meshes, + "num_meshes"_a) + .def( + "add_mesh", + &SimpleScene3D::add_mesh, + "mesh"_a) + .def( + "reserve_instances", + &SimpleScene3D::reserve_instances, + "mesh_index"_a, + "num_instances"_a) + .def( + "add_instance", + &SimpleScene3D::add_instance, + "instance"_a); +} + +} // namespace lagrange::python diff --git a/modules/scene/python/src/scene.cpp b/modules/scene/python/src/scene.cpp new file mode 100644 index 00000000..6b332367 --- /dev/null +++ b/modules/scene/python/src/scene.cpp @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include "bind_simple_scene.h" + +// clang-format off +#include +#include +#include +// clang-format on + + +namespace lagrange::python { + +void populate_scene_module(nb::module_& m) +{ + using Scalar = double; + using Index = uint32_t; + + bind_simple_scene(m); +} + +} // namespace lagrange::python diff --git a/modules/scene/python/tests/test_mesh_instance.py b/modules/scene/python/tests/test_mesh_instance.py new file mode 100644 index 00000000..3ab184dd --- /dev/null +++ b/modules/scene/python/tests/test_mesh_instance.py @@ -0,0 +1,31 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import pytest +import numpy as np + + +class TestMeshInstance: + def test_empty_instance(self): + instance = lagrange.scene.MeshInstance3D() + assert instance.mesh_index == lagrange.invalid_index + assert np.all(instance.transform == np.identity(4)) + + def test_update_instance(self): + instance = lagrange.scene.MeshInstance3D() + instance.mesh_index = 15 + assert instance.mesh_index == 15 + + M = np.arange(16).reshape((4, 4), order="C") + instance.transform = M + assert np.all(instance.transform == M) diff --git a/modules/scene/python/tests/test_simple_scene.py b/modules/scene/python/tests/test_simple_scene.py new file mode 100644 index 00000000..c429ad2e --- /dev/null +++ b/modules/scene/python/tests/test_simple_scene.py @@ -0,0 +1,69 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import pytest +import numpy as np + + +class TestSimpleScene: + def test_empty_scene(self): + scene = lagrange.scene.SimpleScene3D() + assert scene.num_meshes == 0 + assert scene.total_num_instances == 0 + + def create_scene(self): + mesh = lagrange.SurfaceMesh() + mesh.vertices = np.identity(3) + mesh.add_triangle(0, 1, 2) + + scene = lagrange.scene.SimpleScene3D() + assert scene.num_meshes == 0 + mesh_id = scene.add_mesh(mesh) + assert scene.num_meshes == 1 + assert scene.num_instances(mesh_id) == 0 + + instance = lagrange.scene.MeshInstance3D() + instance.mesh_index = mesh_id + instance_id = scene.add_instance(instance) + + return scene, mesh_id, instance_id + + def test_create_scene(self): + scene, mesh_id, instance_id = self.create_scene() + assert scene.num_meshes == 1 + assert scene.num_instances(mesh_id) == 1 + assert scene.total_num_instances == 1 + + assert scene.get_instance(mesh_id, instance_id).mesh_index == mesh_id + + def test_multiple_instances(self): + scene, mesh_id, instance_id = self.create_scene() + + instance2 = lagrange.scene.MeshInstance3D() + instance2.mesh_index = mesh_id + instance2.transform = np.array( + [ + [1, 2, 3, 1], + [0, 1, 0, 1], + [0, 0, 1, 1], + [0, 0, 0, 1], + ] + ) + instance_id2 = scene.add_instance(instance2) + + assert scene.num_instances(mesh_id) == 2 + assert scene.total_num_instances == 2 + + instance = scene.get_instance(mesh_id, instance_id2) + assert np.all(instance2.transform == instance.transform) + diff --git a/modules/scene/src/SimpleScene.cpp b/modules/scene/src/SimpleScene.cpp new file mode 100644 index 00000000..801d647f --- /dev/null +++ b/modules/scene/src/SimpleScene.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include + +namespace lagrange::scene { + +template +Index SimpleScene::compute_num_instances() const +{ + Index total = 0; + for (Index i = 0; i < get_num_meshes(); ++i) { + total += get_num_instances(i); + } + return total; +} + +template +void SimpleScene::reserve_meshes(Index num_meshes) +{ + m_meshes.reserve(num_meshes); + m_instances.reserve(num_meshes); +} + +template +Index SimpleScene::add_mesh(MeshType mesh) +{ + Index mesh_index = static_cast(m_meshes.size()); + m_meshes.emplace_back(std::move(mesh)); + m_instances.emplace_back(); + return mesh_index; +} + +template +void SimpleScene::reserve_instances(Index mesh_index, Index num_instances) +{ + m_instances[mesh_index].reserve(num_instances); +} + +template +Index SimpleScene::add_instance(InstanceType instance) +{ + la_runtime_assert(instance.mesh_index < static_cast(m_instances.size())); + Index instance_index = static_cast(m_instances[instance.mesh_index].size()); + m_instances[instance.mesh_index].emplace_back(std::move(instance)); + return instance_index; +} + +template +void SimpleScene::foreach_instances_for_mesh( + Index mesh_index, + function_ref func) const +{ + for (const auto& instance : m_instances[mesh_index]) { + func(instance); + } +} + +template +void SimpleScene::foreach_instances( + function_ref func) const +{ + for (Index mesh_index = 0; mesh_index < get_num_meshes(); ++mesh_index) { + foreach_instances_for_mesh(mesh_index, func); + } +} + +#define LA_X_simple_scene(_, Scalar, Index, Dim) template class SimpleScene; +LA_SIMPLE_SCENE_X(simple_scene, 0) + +} // namespace lagrange::scene diff --git a/modules/scene/tests/CMakeLists.txt b/modules/scene/tests/CMakeLists.txt new file mode 100644 index 00000000..560af728 --- /dev/null +++ b/modules/scene/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright 2022 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +lagrange_add_test() diff --git a/modules/scene/tests/test_scene.cpp b/modules/scene/tests/test_scene.cpp new file mode 100644 index 00000000..02e4ed73 --- /dev/null +++ b/modules/scene/tests/test_scene.cpp @@ -0,0 +1,78 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include + +#include + +#include + +namespace { + +template +void test_simple_scene_basic() +{ + using SceneType = lagrange::scene::SimpleScene; + using MeshType = lagrange::SurfaceMesh; + + SceneType scene; + MeshType mesh1, mesh2, mesh3; + + scene.reserve_meshes(3); + REQUIRE(scene.get_num_meshes() == 0); + + auto mesh_index1 = scene.add_mesh(mesh1); + REQUIRE(mesh_index1 == 0); + auto mesh_index2 = scene.add_mesh(mesh2); + REQUIRE(mesh_index2 == 1); + auto mesh_index3 = scene.add_mesh(mesh3); + REQUIRE(mesh_index3 == 2); + + scene.reserve_instances(mesh_index1, 3); + REQUIRE(scene.get_num_instances(mesh_index1) == 0); + + auto mesh1_instance1 = scene.add_instance({mesh_index1, {}, {}}); + auto mesh1_instance2 = scene.add_instance({mesh_index1, {}, {}}); + auto mesh1_instance3 = scene.add_instance({mesh_index1, {}, {}}); + + auto mesh2_instance1 = scene.add_instance({mesh_index2, {}, {}}); + auto mesh2_instance2 = scene.add_instance({mesh_index2, {}, {}}); + + auto mesh3_instance1 = scene.add_instance({mesh_index3, {}, {}}); + + REQUIRE(mesh1_instance1 == mesh2_instance1); + REQUIRE(mesh1_instance2 == mesh2_instance2); + REQUIRE(mesh1_instance1 == mesh3_instance1); + REQUIRE(mesh1_instance1 == 0); + REQUIRE(mesh1_instance2 == 1); + REQUIRE(mesh1_instance3 == 2); + REQUIRE(scene.compute_num_instances() == 6); + + REQUIRE(scene.get_mesh(mesh_index2).get_num_vertices() == 0); + scene.ref_mesh(mesh_index2).add_vertices(10); + REQUIRE(scene.get_mesh(mesh_index2).get_num_vertices() == 10); + REQUIRE(scene.get_mesh(mesh_index1).get_num_vertices() == 0); + REQUIRE(scene.get_mesh(mesh_index3).get_num_vertices() == 0); + + std::unordered_set valid_mesh_indices = {mesh_index1, mesh_index2, mesh_index3}; + scene.foreach_instances( + [&](const auto& instance) { REQUIRE(valid_mesh_indices.count(instance.mesh_index)); }); +} + +} // namespace + +TEST_CASE("SimpleScene: basic", "[scene]") +{ +#define LA_X_simple_scene_basic(_, Scalar, Index, Dim) \ + test_simple_scene_basic(); + LA_SIMPLE_SCENE_X(simple_scene_basic, 0) +} diff --git a/modules/testing/CMakeLists.txt b/modules/testing/CMakeLists.txt index e1e7ea32..50e549b1 100644 --- a/modules/testing/CMakeLists.txt +++ b/modules/testing/CMakeLists.txt @@ -69,6 +69,7 @@ if(LAGRANGE_USE_PCH) target_link_libraries(lagrange_testing_pch INTERFACE lagrange_core_pch) target_precompile_headers(lagrange_testing_pch INTERFACE + ) endif() diff --git a/modules/testing/include/lagrange/testing/check_mesh.h b/modules/testing/include/lagrange/testing/check_mesh.h new file mode 100644 index 00000000..5d9d8ac8 --- /dev/null +++ b/modules/testing/include/lagrange/testing/check_mesh.h @@ -0,0 +1,319 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +// clang-format off +#include +#include +#include +#include +// clang-format on + +#include +#include +#include +#include +#include +#include + +namespace lagrange::testing { + +// Check whether the function f : X --> Y restricted to the elements that maps onto Y, is +// surjective. Index.e. each element of [first, last) has an antecedent through f. +template +bool is_restriction_surjective(lagrange::span func, Index first, Index last) +{ + using namespace lagrange; + std::vector inv(last - first, invalid()); + for (Index x = 0; x < Index(func.size()); ++x) { + const Index y = func[x]; + if (y >= first && y < last) { + inv[y - first] = x; + } + } + // No element in the range [first, last) has no predecessor through the function + return std::none_of(inv.begin(), inv.end(), [&](Index y) { return y == invalid(); }); +} + +// Check whether the function f : X --> Y restricted to the elements that maps onto Y, is injective. +// Index.e. any two elements that maps onto [first, last) map to different values in [first, last). +template +bool is_restriction_injective(lagrange::span func, Index first, Index last) +{ + using namespace lagrange; + std::vector inv(last - first, invalid()); + for (Index x = 0; x < Index(func.size()); ++x) { + const Index y = func[x]; + if (y >= first && y < last) { + // Just found another x' that maps to the same value y + if (inv[y - first] != invalid() && inv[y - first] != x) { + return false; + } + inv[y - first] = x; + } + } + return true; +} + +// Check whether elements map within [first, last) +template +bool is_in_range(lagrange::span func, Index first, Index last) +{ + return std::all_of(func.begin(), func.end(), [&](Index y) { return first <= y && y < last; }); +} + +template +bool is_in_range_or_invalid(lagrange::span func, Index first, Index last) +{ + return std::all_of(func.begin(), func.end(), [&](Index y) { + return (first <= y && y < last) || y == lagrange::invalid(); + }); +} + +template +bool is_surjective(lagrange::span func, Index first, Index last) +{ + return is_in_range(func, first, last) && is_restriction_surjective(func, first, last); +} + +template +bool is_injective(lagrange::span func, Index first, Index last) +{ + return is_in_range(func, first, last) && is_restriction_injective(func, first, last); +} + +template +void check_mesh(const MeshType& mesh) +{ + using namespace lagrange; + using Index = typename MeshType::Index; + const Index nv = mesh.get_num_vertices(); + const Index nf = mesh.get_num_facets(); + const Index nc = mesh.get_num_corners(); + const Index ne = mesh.get_num_edges(); + + // Ensure that (V, F) is well-formed + for (Index f = 0; f < nf; ++f) { + const Index c0 = mesh.get_facet_corner_begin(f); + const Index c1 = mesh.get_facet_corner_end(f); + REQUIRE(c0 < c1); + for (Index c = c0; c < c1; ++c) { + const Index v = mesh.get_corner_vertex(c); + REQUIRE(mesh.get_corner_facet(c) == f); + REQUIRE((v >= 0 && v < nv)); + } + } + + // Ensure that each attribute has the correct number of elements + seq_foreach_attribute_read(mesh, [&](auto&& attr) { + REQUIRE(attr.get_num_elements() == nv); + }); + seq_foreach_attribute_read(mesh, [&](auto&& attr) { + REQUIRE(attr.get_num_elements() == nf); + }); + seq_foreach_attribute_read(mesh, [&](auto&& attr) { + REQUIRE(attr.get_num_elements() == nc); + }); + + // Ensure that each element index is in range (or an invalid index) + seq_foreach_attribute_read(mesh, [&](auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + Index n = 0; + auto usage = attr.get_usage(); + if (usage == AttributeUsage::VertexIndex) { + n = nv; + } else if (usage == AttributeUsage::FacetIndex) { + n = nf; + } else if (usage == AttributeUsage::CornerIndex) { + n = nc; + } else if (usage == AttributeUsage::EdgeIndex) { + n = ne; + } else { + return; + } + REQUIRE(std::is_same_v); + if constexpr (std::is_same_v) { + if constexpr (AttributeType::IsIndexed) { + REQUIRE(is_in_range_or_invalid(attr.values().get_all(), 0, n)); + } else { + REQUIRE(is_in_range_or_invalid(attr.get_all(), 0, n)); + } + } else { + LA_IGNORE(n); + } + }); + + // Ensure that only hybrid meshes have c <--> f attributes + if (mesh.is_hybrid()) { + REQUIRE(mesh.attr_id_facet_to_first_corner() != invalid_attribute_id()); + REQUIRE(mesh.attr_id_corner_to_facet() != invalid_attribute_id()); + } else { + REQUIRE(mesh.is_regular()); + REQUIRE(mesh.attr_id_facet_to_first_corner() == invalid_attribute_id()); + REQUIRE(mesh.attr_id_corner_to_facet() == invalid_attribute_id()); + } + + // Ensure that edge and connectivity information is well-formed + if (mesh.has_edges()) { + // Check that all facet edges have a single corresponding edge in the global indexing + auto c2e = mesh.template get_attribute(mesh.attr_id_corner_to_edge()).get_all(); + auto e2c = + mesh.template get_attribute(mesh.attr_id_edge_to_first_corner()).get_all(); + auto v2c = + mesh.template get_attribute(mesh.attr_id_vertex_to_first_corner()).get_all(); + auto next_around_edge = + mesh.template get_attribute(mesh.attr_id_next_corner_around_edge()).get_all(); + auto next_around_vertex = + mesh.template get_attribute(mesh.attr_id_next_corner_around_vertex()).get_all(); + REQUIRE(is_surjective(c2e, 0, ne)); + REQUIRE(is_injective(e2c, 0, nc)); + REQUIRE(is_in_range_or_invalid(v2c, 0, nc)); + REQUIRE(is_restriction_injective( + v2c, + 0, + nc)); // may have isolated vertices that map to invalid<>() + + // Make sure that e2c contains the name number of edges as the mesh + std::set> edges; + for (Index f = 0; f < nf; ++f) { + for (Index lv0 = 0, s = mesh.get_facet_size(f); lv0 < s; ++lv0) { + const Index lv1 = (lv0 + 1) % s; + const Index v0 = mesh.get_facet_vertex(f, lv0); + const Index v1 = mesh.get_facet_vertex(f, lv1); + edges.emplace(std::min(v0, v1), std::max(v0, v1)); + } + } + std::vector> mesh_edges; + for (Index e = 0; e < ne; ++e) { + auto v = mesh.get_edge_vertices(e); + mesh_edges.push_back({v[0], v[1]}); + } + REQUIRE(edges.size() == e2c.size()); + // Make sure we don't have edges that are not in the mesh? + edges.clear(); + for (auto c : e2c) { + const Index f = mesh.get_corner_facet(c); + const Index first_corner = mesh.get_facet_corner_begin(f); + const Index s = mesh.get_facet_size(f); + const Index lv0 = c - first_corner; + const Index lv1 = (lv0 + 1) % s; + const Index v0 = mesh.get_facet_vertex(f, lv0); + const Index v1 = mesh.get_facet_vertex(f, lv1); + edges.emplace(std::min(v0, v1), std::max(v0, v1)); + } + REQUIRE(edges.size() == e2c.size()); + // Make sure that every corner points to an edge and back to the same vertex or the other + // end vertex of the edge. + for (Index f = 0; f < nf; ++f) { + const Index first_corner = mesh.get_facet_corner_begin(f); + for (Index lv0 = 0, s = mesh.get_facet_size(f); lv0 < s; ++lv0) { + const Index v0 = mesh.get_facet_vertex(f, lv0); + const Index c = first_corner + lv0; + const Index e = c2e[c]; + const Index c_other = e2c[e]; + const Index v_other = mesh.get_corner_vertex(c_other); + + // v0 and v_other should be the end points of an edge. + REQUIRE_THAT(mesh_edges[e], Catch::Matchers::Contains(v0)); + REQUIRE_THAT(mesh_edges[e], Catch::Matchers::Contains(v_other)); + } + } + // Check that for every vertex / every edge, the "chain" of corners around it touches + // all the incident corners exactly once (no duplicate). + std::vector> corners_around_vertex(nv); + std::vector> corners_around_edge(ne); + std::vector> facets_around_vertex(nv); + std::vector> facets_around_edge(ne); + for (Index f = 0; f < nf; ++f) { + const Index first_corner = mesh.get_facet_corner_begin(f); + for (Index lv = 0, s = mesh.get_facet_size(f); lv < s; ++lv) { + const Index v = mesh.get_facet_vertex(f, lv); + const Index c = first_corner + lv; + const Index e = c2e[c]; + REQUIRE(mesh.get_edge(f, lv) == e); + REQUIRE(mesh.get_corner_edge(c) == e); + REQUIRE(corners_around_vertex[v].count(c) == 0); + REQUIRE(corners_around_edge[e].count(c) == 0); + corners_around_vertex[v].insert(c); + corners_around_edge[e].insert(c); + facets_around_vertex[v].insert(f); + facets_around_edge[e].insert(f); + } + } + std::unordered_set corners_around; + std::unordered_set facets_around; + for (Index v = 0; v < nv; ++v) { + corners_around.clear(); + facets_around.clear(); + const Index c0 = v2c[v]; + REQUIRE(mesh.get_first_corner_around_vertex(v) == c0); + REQUIRE(mesh.get_one_corner_around_vertex(v) == c0); + for (Index ci = c0; ci != invalid(); ci = next_around_vertex[ci]) { + REQUIRE(mesh.get_next_corner_around_vertex(ci) == next_around_vertex[ci]); + REQUIRE(corners_around_vertex[v].count(ci)); + REQUIRE(corners_around.count(ci) == 0); + corners_around.insert(ci); + } + mesh.foreach_corner_around_vertex(v, [&](Index c) { + REQUIRE(corners_around.count(c)); + }); + mesh.foreach_facet_around_vertex(v, [&](Index f) { + REQUIRE(facets_around_vertex[v].count(f)); + facets_around.insert(f); + }); + REQUIRE(corners_around.size() == corners_around_vertex[v].size()); + REQUIRE(facets_around.size() == facets_around_vertex[v].size()); + REQUIRE( + corners_around.size() == + safe_cast(mesh.count_num_corners_around_vertex(v))); + } + for (Index e = 0; e < ne; ++e) { + corners_around.clear(); + facets_around.clear(); + const Index c0 = e2c[e]; + REQUIRE(mesh.get_first_corner_around_edge(e) == c0); + REQUIRE(mesh.get_one_corner_around_edge(e) == c0); + for (Index ci = c0; ci != invalid(); ci = next_around_edge[ci]) { + REQUIRE(mesh.get_next_corner_around_edge(ci) == next_around_edge[ci]); + REQUIRE(corners_around_edge[e].count(ci)); + REQUIRE(corners_around.count(ci) == 0); + corners_around.insert(ci); + } + mesh.foreach_corner_around_edge(e, [&](Index c) { REQUIRE(corners_around.count(c)); }); + Index first_facet = invalid(); + mesh.foreach_facet_around_edge(e, [&](Index f) { + REQUIRE(facets_around_edge[e].count(f)); + facets_around.insert(f); + if (first_facet == invalid()) { + first_facet = f; + } + }); + REQUIRE(mesh.get_one_facet_around_edge(e) == first_facet); + REQUIRE(corners_around.size() == corners_around_edge[e].size()); + REQUIRE(facets_around.size() == facets_around_edge[e].size()); + REQUIRE( + corners_around.size() == safe_cast(mesh.count_num_corners_around_edge(e))); + if (mesh.is_boundary_edge(e)) { + REQUIRE(corners_around.size() == 1); + } + } + } +} + +} // namespace lagrange::testing diff --git a/modules/testing/include/lagrange/testing/common.h b/modules/testing/include/lagrange/testing/common.h index 36c6c2af..4105cf1f 100644 --- a/modules/testing/include/lagrange/testing/common.h +++ b/modules/testing/include/lagrange/testing/common.h @@ -19,8 +19,8 @@ #include #include +#include #include -#include #include #include @@ -125,10 +125,9 @@ extern template std::unique_ptr load_mesh(const fs::path&); template SurfaceMesh load_surface_mesh(const fs::path& relative_path) { - auto result = - lagrange::io::load_mesh_obj>(get_data_path(relative_path)); - REQUIRE(result.success); - return result.mesh; + auto full_path = get_data_path(relative_path); + REQUIRE(lagrange::fs::exists(full_path)); + return lagrange::io::load_mesh>(full_path); } /// diff --git a/modules/testing/src/common.cpp b/modules/testing/src/common.cpp index c7b6593e..91ecb458 100644 --- a/modules/testing/src/common.cpp +++ b/modules/testing/src/common.cpp @@ -16,7 +16,7 @@ #include -#include +#include #include #include diff --git a/modules/ui/examples/103_MeshFromMemory.cpp b/modules/ui/examples/103_MeshFromMemory.cpp index adf42087..ad9beef2 100644 --- a/modules/ui/examples/103_MeshFromMemory.cpp +++ b/modules/ui/examples/103_MeshFromMemory.cpp @@ -22,8 +22,8 @@ int main(int argc, char** argv) ui::Viewer viewer(argc, argv); // Load mesh via lagrange::io module - auto mesh = lagrange::io::load_mesh(LAGRANGE_DATA_FOLDER - "/open/core/rounded_cube.obj"); + auto mesh = lagrange::io::load_mesh( + LAGRANGE_DATA_FOLDER "/open/core/rounded_cube.obj"); // Create a lagrange mesh and register it with the UI auto mesh_entity = ui::register_mesh(viewer, std::move(mesh)); diff --git a/modules/ui/examples/108_FileDialog.cpp b/modules/ui/examples/108_FileDialog.cpp index b8734a0b..8ddd253d 100644 --- a/modules/ui/examples/108_FileDialog.cpp +++ b/modules/ui/examples/108_FileDialog.cpp @@ -45,7 +45,7 @@ int main(int argc, char** argv) if (!path.empty()) { { - std::ofstream f(path); + lagrange::fs::ofstream f(path.path()); f << "Lorem ipsum dolor sit amet"; } } diff --git a/modules/ui/examples/ui_callbacks/main.cpp b/modules/ui/examples/ui_callbacks/main.cpp index 1eb92bc0..a79caa5c 100644 --- a/modules/ui/examples/ui_callbacks/main.cpp +++ b/modules/ui/examples/ui_callbacks/main.cpp @@ -84,7 +84,7 @@ int main(int argc, char** argv) ui::on(viewer, [&](const ui::TransformChangedEvent& e) { lagrange::logger().info( "Transform of entity {} changed. Position:\n{}", - e.entity, + static_cast(e.entity), ui::get_transform(viewer, e.entity).global.translation()); }); @@ -93,23 +93,23 @@ int main(int argc, char** argv) const auto& cam = ui::get_camera(viewer, e.entity); lagrange::logger().info( "Camera of entity {} changed. Position:\n{}", - e.entity, + static_cast(e.entity), cam.get_position()); }); // Selection and hover events ui::on(viewer, [](const ui::SelectedEvent& e) { - lagrange::logger().info("Entity {} selected", e.entity); + lagrange::logger().info("Entity {} selected", static_cast(e.entity)); }); ui::on(viewer, [](const ui::DeselectedEvent& e) { - lagrange::logger().info("Entity {} deselected", e.entity); + lagrange::logger().info("Entity {} deselected", static_cast(e.entity)); }); ui::on(viewer, [](const ui::HoveredEvent& e) { - lagrange::logger().info("Entity {} hovered", e.entity); + lagrange::logger().info("Entity {} hovered", static_cast(e.entity)); }); ui::on(viewer, [](const ui::DehoveredEvent& e) { - lagrange::logger().info("Entity {} dehovered", e.entity); + lagrange::logger().info("Entity {} dehovered", static_cast(e.entity)); }); diff --git a/modules/ui/include/lagrange/ui/Viewer.h b/modules/ui/include/lagrange/ui/Viewer.h index c3dde7b4..c9cc1a10 100644 --- a/modules/ui/include/lagrange/ui/Viewer.h +++ b/modules/ui/include/lagrange/ui/Viewer.h @@ -220,6 +220,8 @@ class Viewer m_main_thread_max_func_per_frame = limit; } + static std::string get_config_folder(); + protected: virtual void draw_menu(); @@ -236,9 +238,6 @@ class Viewer void update_scale(); void drop(int count, const char** paths); - - static std::string get_config_folder(); - void process_input(); void update_time(); diff --git a/modules/ui/include/lagrange/ui/components/Common.h b/modules/ui/include/lagrange/ui/components/Common.h index cf5f6d2b..609a5658 100644 --- a/modules/ui/include/lagrange/ui/components/Common.h +++ b/modules/ui/include/lagrange/ui/components/Common.h @@ -39,8 +39,10 @@ struct GlobalTime inline std::string get_name(const Registry& r, Entity e) { - if (!r.valid(e)) return lagrange::string_format("Invalid Entity (ID={})", e); - if (!r.all_of(e)) return lagrange::string_format("Unnamed Entity (ID={})", e); + if (!r.valid(e)) + return lagrange::string_format("Invalid Entity (ID={})", static_cast(e)); + if (!r.all_of(e)) + return lagrange::string_format("Unnamed Entity (ID={})", static_cast(e)); return r.get(e); } diff --git a/modules/ui/include/lagrange/ui/default_tools.h b/modules/ui/include/lagrange/ui/default_tools.h index 5050f0f3..7afdec40 100644 --- a/modules/ui/include/lagrange/ui/default_tools.h +++ b/modules/ui/include/lagrange/ui/default_tools.h @@ -13,7 +13,6 @@ #include - namespace lagrange { namespace ui { @@ -59,6 +58,7 @@ struct DefaultTools void register_default_tools(Tools& tools); +void select(Registry& r, const std::function& selection_system); } // namespace ui } // namespace lagrange \ No newline at end of file diff --git a/modules/volume/CMakeLists.txt b/modules/volume/CMakeLists.txt index 3a27ad1b..bd4ab3c2 100644 --- a/modules/volume/CMakeLists.txt +++ b/modules/volume/CMakeLists.txt @@ -10,16 +10,15 @@ # governing permissions and limitations under the License. # # 1. define module -lagrange_add_module(INTERFACE) +lagrange_add_module() # 2. dependencies -lagrange_include_modules(core) +lagrange_include_modules(core winding) include(openvdb) -include(winding_number) -target_link_libraries(lagrange_volume INTERFACE +target_link_libraries(lagrange_volume PUBLIC lagrange::core + lagrange::winding openvdb::openvdb - WindingNumber::WindingNumber ) # 3. installation diff --git a/modules/volume/examples/voxelize_mesh.cpp b/modules/volume/examples/voxelize_mesh.cpp index c978ac59..37550885 100644 --- a/modules/volume/examples/voxelize_mesh.cpp +++ b/modules/volume/examples/voxelize_mesh.cpp @@ -14,15 +14,17 @@ #include #include -// clang-format off -#include -#include -#include -// clang-format on - #include -using MeshType = lagrange::TriangleMesh3D; +const std::map& signing_types() +{ + static std::map _methods = { + {"FloodFill", lagrange::volume::MeshToVolumeOptions::Sign::FloodFill}, + {"WindingNumber", lagrange::volume::MeshToVolumeOptions::Sign::WindingNumber}, + }; + return _methods; +} + int main(int argc, char** argv) { @@ -30,45 +32,38 @@ int main(int argc, char** argv) { std::string input; std::string output = "output.obj"; - double voxel_size = 0.001; - double isovalue = 0.0; - double adaptivity = 0.0; - bool relative = true; } args; + lagrange::volume::MeshToVolumeOptions m2v_opt; + lagrange::volume::VolumeToMeshOptions v2m_opt; + CLI::App app{argv[0]}; + app.option_defaults()->always_capture_default(); app.add_option("input", args.input, "Input mesh.")->required()->check(CLI::ExistingFile); app.add_option("output", args.output, "Output mesh."); - app.add_option("-s,--voxel-size", args.voxel_size, "Voxel size."); - app.add_option("-v,--isovalue", args.isovalue, "Isovalue to mesh."); - app.add_option("-a,--adaptivity", args.adaptivity, "Mesh adaptivity between [0, 1]."); app.add_option( - "-r,--relative", - args.relative, - "Whether to use a voxel size relative to the bbox diagonal."); + "-s,--voxel-size", + m2v_opt.voxel_size, + "Voxel size. Negative means relative to bbox diagonal."); + app.add_option("-m,--method", m2v_opt.signing_method, "Grid signing method.") + ->transform(CLI::Transformer(signing_types(), CLI::ignore_case)); + app.add_option("-v,--isovalue", v2m_opt.isovalue, "Isovalue to mesh."); + app.add_option("-a,--adaptivity", v2m_opt.adaptivity, "Mesh adaptivity between [0, 1]."); CLI11_PARSE(app, argc, argv) - lagrange::logger().info("Loading input mesh: {}", args.input); - auto mesh = lagrange::io::load_mesh(args.input); + spdlog::set_level(spdlog::level::debug); - if (args.relative) { - double diag = igl::bounding_box_diagonal(mesh->get_vertices()); - lagrange::logger().info( - "Using a relative voxel size of {:.3f} x {:.3f} = {:.3f}", - args.voxel_size, - diag, - args.voxel_size * diag); - args.voxel_size *= diag; - } + lagrange::logger().info("Loading input mesh: {}", args.input); + auto mesh = lagrange::io::load_mesh(args.input); lagrange::logger().info("Mesh to volume conversion"); - auto grid = lagrange::volume::mesh_to_volume(*mesh, args.voxel_size); + auto grid = lagrange::volume::mesh_to_volume(mesh, m2v_opt); lagrange::logger().info("Volume to mesh conversion"); - mesh = lagrange::volume::volume_to_mesh(*grid, args.isovalue, args.adaptivity); + mesh = lagrange::volume::volume_to_mesh(*grid, v2m_opt); lagrange::logger().info("Saving result: {}", args.output); - lagrange::io::save_mesh(args.output, *mesh); + lagrange::io::save_mesh(args.output, mesh); return 0; } diff --git a/modules/volume/include/lagrange/volume/GridTypes.h b/modules/volume/include/lagrange/volume/GridTypes.h new file mode 100644 index 00000000..7cd037b6 --- /dev/null +++ b/modules/volume/include/lagrange/volume/GridTypes.h @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/// +/// @file +/// + +#pragma once + +// clang-format off + +/// +/// [X Macro](https://en.wikipedia.org/wiki/X_Macro) arguments for the lagrange::volume::Grid<> +/// class (which is an alias for the common OpenVDB grid). +/// +/// Since other modules might need to explicitly instantiate their own functions, this file is a +/// public header. +/// +/// Use in a .cpp as follows: +/// +/// @code +/// #include +/// #define LA_X_foo(_, Scalar) template void my_function(const Grid &); +/// LA_VOLUME_GRID_X(foo, 0) +/// @endcode +/// +/// The optional `data` argument can forwarded to other macros, in order to implement cartesian +/// products when instantiating nested types. +/// +/// @param mode Suffix to apply to the LA_X_* macro. +/// @param data Data to be passed around as the first argument of the X macro. +/// +/// @return Expansion of LA_X_##mode() for each type to be instantiated. +/// +#define LA_VOLUME_GRID_X(mode, data) \ + LA_X_##mode(data, float) \ + LA_X_##mode(data, double) + +// clang-format on diff --git a/modules/volume/include/lagrange/volume/fill_with_spheres.h b/modules/volume/include/lagrange/volume/fill_with_spheres.h index 8d77099b..559bcc37 100644 --- a/modules/volume/include/lagrange/volume/fill_with_spheres.h +++ b/modules/volume/include/lagrange/volume/fill_with_spheres.h @@ -11,10 +11,12 @@ */ #pragma once -#include +#include -#include #include +#include + +#include namespace lagrange { namespace volume { @@ -32,8 +34,8 @@ namespace volume { /// template void fill_with_spheres( - const GridType &grid, - Eigen::PlainObjectBase &spheres, + const GridType& grid, + Eigen::PlainObjectBase& spheres, int max_spheres, bool overlapping = false) { diff --git a/modules/volume/include/lagrange/volume/legacy/mesh_to_volume.h b/modules/volume/include/lagrange/volume/legacy/mesh_to_volume.h new file mode 100644 index 00000000..74382cf2 --- /dev/null +++ b/modules/volume/include/lagrange/volume/legacy/mesh_to_volume.h @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace lagrange { +namespace volume { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/// +/// Adapter class to interface a Lagrange mesh with OpenVDB functions. +/// +template +class MeshAdapter +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + +public: + /// + /// Constructs a new instance. + /// + /// @param[in] mesh Input mesh. + /// @param[in] transform World to index transform. + /// + MeshAdapter(const MeshType& mesh, const openvdb::math::Transform& transform) + : m_mesh(mesh) + , m_transform(transform) + {} + + /// Number of mesh facets. + size_t polygonCount() const { return static_cast(m_mesh.get_num_facets()); } + + /// Number of mesh vertices. + size_t pointCount() const { return static_cast(m_mesh.get_num_vertices()); } + + /// Number of vertices for a given facet. + size_t vertexCount(size_t /*f*/) const + { + return static_cast(m_mesh.get_vertex_per_facet()); + } + + /// + /// Return a vertex position in the grid index space. + /// + /// @param[in] f Queried facet index. + /// @param[in] lv Queried local vertex index. + /// @param[out] pos Vertex position in grid index space. + /// + void getIndexSpacePoint(size_t f, size_t lv, openvdb::Vec3d& pos) const + { + Eigen::RowVector3d p = + m_mesh.get_vertices().row(m_mesh.get_facets()(f, lv)).template cast(); + pos = openvdb::Vec3d(p.x(), p.y(), p.z()); + pos = m_transform.worldToIndex(pos); + } + +protected: + const MeshType& m_mesh; + const openvdb::math::Transform& m_transform; +}; + +/// +/// Converts a triangle mesh to a OpenVDB sparse voxel grid. +/// +/// @param[in] mesh Input mesh. +/// @param[in] voxel_size Grid voxel size. If the target voxel size is too small, an exception +/// will will be raised. +/// +/// @tparam MeshType Mesh type. +/// @tparam GridType OpenVDB grid type. +/// +/// @return OpenVDB grid. +/// +template +auto mesh_to_volume(const MeshType& mesh, double voxel_size) -> typename GridType::Ptr +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + openvdb::initialize(); + + const openvdb::Vec3d offset(voxel_size / 2.0, voxel_size / 2.0, voxel_size / 2.0); + auto transform = openvdb::math::Transform::createLinearTransform(voxel_size); + transform->postTranslate(offset); + + MeshAdapter adapter(mesh, *transform); + try { + return openvdb::tools::meshToVolume>(adapter, *transform); + } catch (openvdb::ArithmeticError&) { + logger().error("Voxel size too small: {}", voxel_size); + throw; + } +} + +} // namespace legacy +} // namespace volume +} // namespace lagrange diff --git a/modules/volume/include/lagrange/volume/legacy/volume_to_mesh.h b/modules/volume/include/lagrange/volume/legacy/volume_to_mesh.h new file mode 100644 index 00000000..df60487c --- /dev/null +++ b/modules/volume/include/lagrange/volume/legacy/volume_to_mesh.h @@ -0,0 +1,114 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +#include +#include + +namespace lagrange { +namespace volume { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/// +/// Mesh the isosurface of a OpenVDB sparse voxel grid. +/// +/// @param[in] grid Input grid. +/// @param[in] isovalue Determines which isosurface to mesh. +/// @param[in] adaptivity Surface adaptivity threshold [0 to 1]. 0 keeps the +/// original quad mesh, while 1 simplifies the most. +/// @param[in] relax_disoriented_triangles Toggle relaxing disoriented triangles during adaptive +/// meshing. +/// +/// @tparam MeshType Mesh type. +/// @tparam GridType OpenVDB grid type. +/// +/// @return Meshed isosurface. +/// +template +std::unique_ptr volume_to_mesh_legacy( + const GridType& grid, + double isovalue = 0.0, + double adaptivity = 0.0, + bool relax_disoriented_triangles = true) +{ + static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); + + openvdb::initialize(); + + using Scalar = ScalarOf; + using Index = IndexOf; + using VertexArray = VertexArrayOf; + using FacetArray = FacetArrayOf; + using RowVector3s = Eigen::Matrix; + using RowVector3I = Eigen::Matrix; + using RowVector3I = Eigen::Matrix; + using RowVector4I = Eigen::Matrix; + using RowVector4i = Eigen::Matrix; + + if (adaptivity < 0 || adaptivity > 1) { + logger().warn("Adaptivity needs to be between 0 and 1."); + adaptivity = std::max(0.0, std::min(1.0, adaptivity)); + } + + std::vector points; + std::vector triangles; + std::vector quads; + + openvdb::tools::volumeToMesh( + grid, + points, + triangles, + quads, + isovalue, + adaptivity, + relax_disoriented_triangles); + + VertexArray vertices(points.size(), 3); + FacetArray facets(triangles.size() + 2 * quads.size(), 3); + + // Level set grids need their facet flipped + const bool need_flip = (grid.getGridClass() == openvdb::GRID_LEVEL_SET); + + for (size_t v = 0; v < points.size(); ++v) { + const RowVector3s p(points[v].x(), points[v].y(), points[v].z()); + vertices.row(v) << p.template cast(); + } + + for (size_t f = 0; f < triangles.size(); ++f) { + const RowVector3I triangle(triangles[f].x(), triangles[f].y(), triangles[f].z()); + facets.row(f) << triangle.template cast(); + if (need_flip) { + facets.row(f) = facets.row(f).reverse().eval(); + } + } + + for (size_t f = 0, o = triangles.size(); f < quads.size(); ++f) { + const RowVector4I quad_(quads[f].x(), quads[f].y(), quads[f].z(), quads[f].w()); + const RowVector4i quad = quad_.template cast(); + facets.row(o + 2 * f) << quad(0), quad(1), quad(3); + facets.row(o + 2 * f + 1) << quad(3), quad(1), quad(2); + if (need_flip) { + facets.row(o + 2 * f) = facets.row(o + 2 * f).reverse().eval(); + facets.row(o + 2 * f + 1) = facets.row(o + 2 * f + 1).reverse().eval(); + } + } + + return lagrange::create_mesh(std::move(vertices), std::move(facets)); +} + +} // namespace legacy +} // namespace volume +} // namespace lagrange diff --git a/modules/volume/include/lagrange/volume/mesh_to_volume.h b/modules/volume/include/lagrange/volume/mesh_to_volume.h index 7c6f7dbe..ac172240 100644 --- a/modules/volume/include/lagrange/volume/mesh_to_volume.h +++ b/modules/volume/include/lagrange/volume/mesh_to_volume.h @@ -11,93 +11,52 @@ */ #pragma once -#include -#include +#include +#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif -namespace lagrange { -namespace volume { +namespace lagrange::volume { /// -/// Adapter class to interface a Lagrange mesh with OpenVDB functions. +/// Mesh to volume conversion options. /// -template -class MeshAdapter +struct MeshToVolumeOptions { - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - -public: - /// - /// Constructs a new instance. /// - /// @param[in] mesh Input mesh. - /// @param[in] transform World to index transform. + /// Available methods to compute the sign of the distance field (i.e. which voxels are inside or + /// outside of the input volume). /// - MeshAdapter(const MeshType &mesh, const openvdb::math::Transform &transform) - : m_mesh(mesh) - , m_transform(transform) - {} - - /// Number of mesh facets. - size_t polygonCount() const { return static_cast(m_mesh.get_num_facets()); } + enum class Sign { + FloodFill, ///< Default voxel flood-fill method used by OpenVDB. + WindingNumber, ///< Fast winding number approach based on [Barill et al. 2018]. + }; - /// Number of mesh vertices. - size_t pointCount() const { return static_cast(m_mesh.get_num_vertices()); } - - /// Number of vertices for a given facet. - size_t vertexCount(size_t /*f*/) const { return static_cast(m_mesh.get_vertex_per_facet()); } - - /// - /// Return a vertex position in the grid index space. - /// - /// @param[in] f Queried facet index. - /// @param[in] lv Queried local vertex index. - /// @param[out] pos Vertex position in grid index space. - /// - void getIndexSpacePoint(size_t f, size_t lv, openvdb::Vec3d &pos) const { - Eigen::RowVector3d p = m_mesh.get_vertices().row(m_mesh.get_facets()(f, lv)).template cast(); - pos = openvdb::Vec3d(p.x(), p.y(), p.z()); - pos = m_transform.worldToIndex(pos); - } + /// Grid voxel size. If the target voxel size is too small, an exception will will be raised. A + /// negative value is interpreted as being relative to the mesh bbox diagonal. + double voxel_size = -0.01; -protected: - const MeshType & m_mesh; - const openvdb::math::Transform &m_transform; + /// Method used to compute the sign of the distance field that determines interior voxels. + Sign signing_method = Sign::FloodFill; }; /// /// Converts a triangle mesh to a OpenVDB sparse voxel grid. /// -/// @param[in] mesh Input mesh. -/// @param[in] voxel_size Grid voxel size. If the target voxel size is too small, an exception -/// will will be raised. +/// @param[in] mesh Input mesh. Must be a triangle mesh, a quad-mesh, or a quad-dominant +/// mesh. +/// @param[in] options Conversion options. /// -/// @tparam MeshType Mesh type. -/// @tparam GridType OpenVDB grid type. +/// @tparam GridScalar Output OpenVDB Grid scalar type. Only float or double are supported. +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. /// /// @return OpenVDB grid. /// -template -auto mesh_to_volume(const MeshType &mesh, double voxel_size) -> typename GridType::Ptr -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - openvdb::initialize(); - - const openvdb::Vec3d offset(voxel_size / 2.0, voxel_size / 2.0, voxel_size / 2.0); - auto transform = openvdb::math::Transform::createLinearTransform(voxel_size); - transform->postTranslate(offset); - - MeshAdapter adapter(mesh, *transform); - try { - return openvdb::tools::meshToVolume>(adapter, *transform); - } catch (openvdb::ArithmeticError &) { - logger().error("Voxel size too small: {}", voxel_size); - throw; - } -} +template +auto mesh_to_volume(const SurfaceMesh& mesh, const MeshToVolumeOptions& options = {}) + -> typename Grid::Ptr; -} // namespace volume -} // namespace lagrange +} // namespace lagrange::volume diff --git a/modules/volume/include/lagrange/volume/types.h b/modules/volume/include/lagrange/volume/types.h new file mode 100644 index 00000000..8486441d --- /dev/null +++ b/modules/volume/include/lagrange/volume/types.h @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include + +namespace lagrange::volume { + +// clang-format off + +/// Alias for OpenVDB's common grid types. +template +using Grid = openvdb::Grid< + // The following type is the same as + // + // @code + // typename openvdb::tree::Tree4::Type + // @endcode + // + // However, the `::Type` indirection prevents the compiler from deducing the grid scalar type + // automatically, so we use this direct alias. We ensure that the types are the same with a + // static_assert in our .cpp files. + openvdb::tree::Tree< + openvdb::tree::RootNode< + openvdb::tree::InternalNode< + openvdb::tree::InternalNode< + openvdb::tree::LeafNode, + 4>, + 5 + > + > + > +>; + +// clang-format on + +} // namespace lagrange::volume diff --git a/modules/volume/include/lagrange/volume/volume_to_mesh.h b/modules/volume/include/lagrange/volume/volume_to_mesh.h index d45ad149..b5841569 100644 --- a/modules/volume/include/lagrange/volume/volume_to_mesh.h +++ b/modules/volume/include/lagrange/volume/volume_to_mesh.h @@ -11,100 +11,41 @@ */ #pragma once -#include +#include +#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif -namespace lagrange { -namespace volume { +namespace lagrange::volume { + +/// Volume to mesh isosurfacing options. +struct VolumeToMeshOptions +{ + // Value of the isosurface. + double isovalue = 0.0; + + /// Surface adaptivity threshold [0 to 1]. 0 keeps the original quad mesh, while 1 simplifies the most. + double adaptivity = 0.0; + + /// Toggle relaxing disoriented triangles during adaptive meshing. + bool relax_disoriented_triangles = true; +}; /// /// Mesh the isosurface of a OpenVDB sparse voxel grid. /// -/// @param[in] grid Input grid. -/// @param[in] isovalue Determines which isosurface to mesh. -/// @param[in] adaptivity Surface adaptivity threshold [0 to 1]. 0 keeps the -/// original quad mesh, while 1 simplifies the most. -/// @param[in] relax_disoriented_triangles Toggle relaxing disoriented triangles during adaptive -/// meshing. +/// @param[in] grid Input grid. +/// @param[in] options Isosurfacing options. /// -/// @tparam MeshType Mesh type. -/// @tparam GridType OpenVDB grid type. +/// @tparam MeshType Output mesh type. +/// @tparam GridScalar Grid scalar type. Can only be float or double. /// /// @return Meshed isosurface. /// -template -std::unique_ptr volume_to_mesh( - const GridType &grid, - double isovalue = 0.0, - double adaptivity = 0.0, - bool relax_disoriented_triangles = true) -{ - static_assert(MeshTrait::is_mesh(), "Input type is not Mesh"); - - openvdb::initialize(); - - using Scalar = ScalarOf; - using Index = IndexOf; - using VertexArray = VertexArrayOf; - using FacetArray = FacetArrayOf; - using RowVector3s = Eigen::Matrix; - using RowVector3I = Eigen::Matrix; - using RowVector3I = Eigen::Matrix; - using RowVector4I = Eigen::Matrix; - using RowVector4i = Eigen::Matrix; - - if (adaptivity < 0 || adaptivity > 1) { - logger().warn("Adaptivity needs to be between 0 and 1."); - adaptivity = std::max(0.0, std::min(1.0, adaptivity)); - } - - std::vector points; - std::vector triangles; - std::vector quads; - - openvdb::tools::volumeToMesh( - grid, - points, - triangles, - quads, - isovalue, - adaptivity, - relax_disoriented_triangles); - - VertexArray vertices(points.size(), 3); - FacetArray facets(triangles.size() + 2 * quads.size(), 3); - - // Level set grids need their facet flipped - const bool need_flip = (grid.getGridClass() == openvdb::GRID_LEVEL_SET); - - for (size_t v = 0; v < points.size(); ++v) { - const RowVector3s p(points[v].x(), points[v].y(), points[v].z()); - vertices.row(v) << p.template cast(); - } - - for (size_t f = 0; f < triangles.size(); ++f) { - const RowVector3I triangle(triangles[f].x(), triangles[f].y(), triangles[f].z()); - facets.row(f) << triangle.template cast(); - if (need_flip) { - facets.row(f) = facets.row(f).reverse().eval(); - } - } - - for (size_t f = 0, o = triangles.size(); f < quads.size(); ++f) { - const RowVector4I quad_(quads[f].x(), quads[f].y(), quads[f].z(), quads[f].w()); - const RowVector4i quad = quad_.template cast(); - facets.row(o + 2 * f) << quad(0), quad(1), quad(3); - facets.row(o + 2 * f + 1) << quad(3), quad(1), quad(2); - if (need_flip) { - facets.row(o + 2 * f) = facets.row(o + 2 * f).reverse().eval(); - facets.row(o + 2 * f + 1) = facets.row(o + 2 * f + 1).reverse().eval(); - } - } - - return lagrange::create_mesh(std::move(vertices), std::move(facets)); -} +template +auto volume_to_mesh(const Grid& grid, const VolumeToMeshOptions& options = {}) + -> SurfaceMesh; -} // namespace volume -} // namespace lagrange +} // namespace lagrange::volume diff --git a/modules/volume/src/mesh_to_volume.cpp b/modules/volume/src/mesh_to_volume.cpp new file mode 100644 index 00000000..9b3fb071 --- /dev/null +++ b/modules/volume/src/mesh_to_volume.cpp @@ -0,0 +1,196 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace lagrange::volume { + +namespace { + +/// +/// Adapter class to interface a Lagrange mesh with OpenVDB functions. +/// +template +class SurfaceMeshAdapter +{ +public: + /// + /// Constructs a new instance. + /// + /// @param[in] mesh Input mesh. + /// @param[in] transform World to index transform. + /// + SurfaceMeshAdapter( + const SurfaceMesh& mesh, + const openvdb::math::Transform& transform) + : m_mesh(mesh) + , m_transform(transform) + {} + + /// Number of mesh facets. + size_t polygonCount() const { return static_cast(m_mesh.get_num_facets()); } + + /// Number of mesh vertices. + size_t pointCount() const { return static_cast(m_mesh.get_num_vertices()); } + + /// Number of vertices for a given facet. + size_t vertexCount(size_t f) const { return static_cast(m_mesh.get_facet_size(f)); } + + /// + /// Return a vertex position in the grid index space. + /// + /// @param[in] f Queried facet index. + /// @param[in] lv Queried local vertex index. + /// @param[out] pos Vertex position in grid index space. + /// + void getIndexSpacePoint(size_t f, size_t lv, openvdb::Vec3d& pos) const + { + auto p = m_mesh.get_position(m_mesh.get_facet_vertex(f, lv)); + pos = openvdb::Vec3d(p[0], p[1], p[2]); + pos = m_transform.worldToIndex(pos); + } + +protected: + const SurfaceMesh& m_mesh; + const openvdb::math::Transform& m_transform; +}; + +} // namespace + +template +auto mesh_to_volume(const SurfaceMesh& mesh, const MeshToVolumeOptions& options) -> + typename Grid::Ptr +{ + static_assert( + std::is_same_v< + Grid, + openvdb::Grid::Type>>, + "Mismatch between VDB grid types!"); + + la_runtime_assert(mesh.get_dimension() == 3, "Input mesh must be 3D"); + if (mesh.is_hybrid()) { + for (Index f = 0; f < mesh.get_num_facets(); ++f) { + if (auto nv = mesh.get_facet_size(f); nv < 3 || nv > 4) { + throw Error( + fmt::format("Facet size should be 3 or 4, but f{} has #{} vertices", f, nv)); + } + } + } + + openvdb::initialize(); + + auto voxel_size = options.voxel_size; + if (voxel_size < 0) { + // Compute bbox + Eigen::AlignedBox bbox; + for (auto p : vertex_view(mesh).rowwise()) { + bbox.extend(p.transpose()); + } + + const Scalar diag = bbox.diagonal().norm(); + voxel_size = std::abs(voxel_size); + lagrange::logger().debug( + "Using a relative voxel size of {:.3f} x {:.3f} = {:.3f}", + voxel_size, + diag, + voxel_size * diag); + voxel_size *= diag; + } + + const openvdb::Vec3d offset(voxel_size / 2.0, voxel_size / 2.0, voxel_size / 2.0); + auto transform = openvdb::math::Transform::createLinearTransform(voxel_size); + transform->postTranslate(offset); + + using MeshAdapterType = SurfaceMeshAdapter; + + MeshAdapterType adapter(mesh, *transform); + typename Grid::Ptr grid; + try { + if (options.signing_method == MeshToVolumeOptions::Sign::WindingNumber) { + // Two stage grid signing approach + { + // Compute unsigned distance field + logger().debug("Computing unsigned distance field grid"); + const float exterior_bandwidth = 3.0f; + const float interior_bandwidth = 3.0f; + grid = openvdb::tools::meshToVolume, MeshAdapterType>( + adapter, + *transform, + exterior_bandwidth, + interior_bandwidth, + openvdb::tools::UNSIGNED_DISTANCE_FIELD); + } + { + // Initialize fast winding number engine. + // + // TODO: Drop mesh attributes from copy to avoid remapping attributes that may be + // present in the input mesh. + logger().debug("Initializing fast winding number engine"); + auto triangle_mesh = mesh; + if (!triangle_mesh.is_triangle_mesh()) { + // Triangulate quad facets first if needed + triangulate_polygonal_facets(triangle_mesh); + } + // Apply world->index transform (query points for winding number have coords in + // index space) + for (auto p : vertex_ref(triangle_mesh).rowwise()) { + openvdb::Vec3d pos(p[0], p[1], p[2]); + pos = transform->worldToIndex(pos); + p << pos[0], pos[1], pos[2]; + } + winding::FastWindingNumber engine(triangle_mesh); + + // Iterate over all grid values (both voxel and tile, active and inactive) + logger().debug("Applying fast winding number sign to the grid"); + auto sign = [&](auto&& iter) { + const auto pos = iter.getBoundingBox().getCenter(); + const std::array p = { + static_cast(pos[0]), + static_cast(pos[1]), + static_cast(pos[2])}; + if (engine.is_inside(p)) { + iter.setValue(-*iter); + } + }; + openvdb::tools::foreach (grid->beginValueAll(), sign, true /* threaded */); + logger().debug("Done computing grid"); + } + } else { + grid = openvdb::tools::meshToVolume, MeshAdapterType>( + adapter, + *transform); + } + } catch (const openvdb::ArithmeticError&) { + logger().error("Voxel size too small: {}", voxel_size); + throw; + } + + return grid; +} + +#define LA_X_mesh_to_volume(GridScalar, Scalar, Index) \ + template typename Grid::Ptr mesh_to_volume( \ + const SurfaceMesh& mesh, \ + const MeshToVolumeOptions& options); +#define LA_X_mesh_to_volume_aux(_, GridScalar) LA_SURFACE_MESH_X(mesh_to_volume, GridScalar) +LA_VOLUME_GRID_X(mesh_to_volume_aux, 0) + +} // namespace lagrange::volume diff --git a/modules/volume/src/volume_to_mesh.cpp b/modules/volume/src/volume_to_mesh.cpp new file mode 100644 index 00000000..9d64ba2f --- /dev/null +++ b/modules/volume/src/volume_to_mesh.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include + +namespace lagrange::volume { + +template +auto volume_to_mesh(const Grid& grid, const VolumeToMeshOptions& options) + -> SurfaceMesh +{ + openvdb::initialize(); + + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + + auto adaptivity = options.adaptivity; + + if (adaptivity < 0 || adaptivity > 1) { + logger().warn("Adaptivity needs to be between 0 and 1."); + adaptivity = std::clamp(adaptivity, 0.0, 1.0); + } + + std::vector points; + std::vector triangles; + std::vector quads; + + openvdb::tools::volumeToMesh( + grid, + points, + triangles, + quads, + options.isovalue, + adaptivity, + options.relax_disoriented_triangles); + + // Level set grids need their facet flipped + const bool need_flip = (grid.getGridClass() == openvdb::GRID_LEVEL_SET); + + SurfaceMesh mesh; + + mesh.add_vertices(points.size(), [&](Index v, auto p) { + std::copy_n(points[v].asV(), 3, p.data()); + }); + + mesh.add_triangles(triangles.size(), [&](Index f, auto t) { + std::copy_n(triangles[f].asV(), 3, t.data()); + if (need_flip) { + std::reverse(t.begin(), t.end()); + } + }); + + mesh.add_quads(quads.size(), [&](Index f, auto t) { + std::copy_n(quads[f].asV(), 4, t.data()); + if (need_flip) { + std::reverse(t.begin(), t.end()); + } + }); + + return mesh; +} + +#define LA_X_volume_to_mesh(GridScalar, Scalar, Index) \ + template SurfaceMesh volume_to_mesh>( \ + const Grid& grid, \ + const VolumeToMeshOptions& options); +#define LA_X_volume_to_mesh_aux(_, GridScalar) LA_SURFACE_MESH_X(volume_to_mesh, GridScalar) +LA_VOLUME_GRID_X(volume_to_mesh_aux, 0) + +} // namespace lagrange::volume diff --git a/modules/volume/tests/test_voxelization.cpp b/modules/volume/tests/test_voxelization.cpp index c4bb9079..a4194444 100644 --- a/modules/volume/tests/test_voxelization.cpp +++ b/modules/volume/tests/test_voxelization.cpp @@ -10,19 +10,59 @@ * governing permissions and limitations under the License. */ #include +#include +#include +#include #include #include -#include -TEST_CASE("voxelization: reproducibility", "[volume]") +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +TEST_CASE("voxelization: reproducibility (legacy)", "[volume]") { auto mesh = lagrange::create_sphere(); auto grid = lagrange::volume::mesh_to_volume(*mesh, 0.1); auto grid2 = lagrange::volume::mesh_to_volume(*mesh, 0.1); - auto mesh2 = lagrange::volume::volume_to_mesh(*grid); - auto mesh3 = lagrange::volume::volume_to_mesh(*grid2); + auto mesh2 = lagrange::volume::volume_to_mesh_legacy(*grid); + auto mesh3 = lagrange::volume::volume_to_mesh_legacy(*grid2); REQUIRE(mesh2->get_num_vertices() > 0); REQUIRE(mesh2->get_num_facets() > 0); REQUIRE(mesh2->get_vertices() == mesh3->get_vertices()); REQUIRE(mesh2->get_facets() == mesh3->get_facets()); } +#endif + +TEST_CASE("voxelization: reproducibility", "[volume]") +{ + using Scalar = float; + using Index = uint32_t; + using SurfaceMeshType = lagrange::SurfaceMesh; + auto mesh = lagrange::to_surface_mesh_copy(*lagrange::create_sphere()); + lagrange::volume::MeshToVolumeOptions m2v_opt; + m2v_opt.voxel_size = 0.1; + auto grid = lagrange::volume::mesh_to_volume(mesh, m2v_opt); + auto grid2 = lagrange::volume::mesh_to_volume(mesh, m2v_opt); + auto mesh2 = lagrange::volume::volume_to_mesh(*grid); + auto mesh3 = lagrange::volume::volume_to_mesh(*grid2); + REQUIRE(mesh2.get_num_vertices() > 0); + REQUIRE(mesh2.get_num_facets() > 0); + REQUIRE(vertex_view(mesh2) == vertex_view(mesh3)); + REQUIRE(facet_view(mesh2) == facet_view(mesh3)); +} + +TEST_CASE("voxelization: winding number", "[volume]") +{ + using Scalar = float; + using Index = uint32_t; + using SurfaceMeshType = lagrange::SurfaceMesh; + auto mesh = lagrange::testing::load_surface_mesh("open/core/stanford-bunny.obj"); + lagrange::volume::MeshToVolumeOptions m2v_opt; + m2v_opt.signing_method = lagrange::volume::MeshToVolumeOptions::Sign::FloodFill; + auto grid = lagrange::volume::mesh_to_volume(mesh, m2v_opt); + m2v_opt.signing_method = lagrange::volume::MeshToVolumeOptions::Sign::WindingNumber; + auto grid2 = lagrange::volume::mesh_to_volume(mesh, m2v_opt); + auto mesh2 = lagrange::volume::volume_to_mesh(*grid); + auto mesh3 = lagrange::volume::volume_to_mesh(*grid2); + // Winding number result should have more vertices/facets than the flood-fill result + REQUIRE(mesh3.get_num_vertices() > mesh2.get_num_vertices()); + REQUIRE(mesh3.get_num_facets() > mesh2.get_num_facets()); +} diff --git a/modules/winding/CMakeLists.txt b/modules/winding/CMakeLists.txt new file mode 100644 index 00000000..507348a3 --- /dev/null +++ b/modules/winding/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +# 1. define module +lagrange_add_module() + +# 2. dependencies +lagrange_include_modules(core) +include(winding_number) +target_link_libraries(lagrange_winding + PUBLIC + lagrange::core + PRIVATE + WindingNumber::WindingNumber +) + +# 4. unit tests and examples +if(LAGRANGE_UNIT_TESTS) + add_subdirectory(tests) +endif() + +if(LAGRANGE_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/modules/winding/examples/CMakeLists.txt b/modules/winding/examples/CMakeLists.txt new file mode 100644 index 00000000..ea48dbae --- /dev/null +++ b/modules/winding/examples/CMakeLists.txt @@ -0,0 +1,16 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +include(cli11) +lagrange_include_modules(io) + +lagrange_add_example(sample_points_in_mesh sample_points_in_mesh.cpp) +target_link_libraries(sample_points_in_mesh lagrange::winding lagrange::io CLI11::CLI11) diff --git a/modules/winding/examples/sample_points_in_mesh.cpp b/modules/winding/examples/sample_points_in_mesh.cpp new file mode 100644 index 00000000..d4bb9f03 --- /dev/null +++ b/modules/winding/examples/sample_points_in_mesh.cpp @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include +#include + +#include +#include + +#include + +namespace fs = lagrange::fs; + +int main(int argc, char** argv) +{ + using Scalar = float; + using Index = uint32_t; + using SurfaceMeshType = lagrange::SurfaceMesh; + + struct + { + std::string input; + std::string output = "output.xyz"; + size_t num_samples = 10000; + } args; + + CLI::App app{argv[0]}; + app.option_defaults()->always_capture_default(); + app.add_option("input", args.input, "Input mesh.")->required()->check(CLI::ExistingFile); + app.add_option("output", args.output, "Output points."); + app.add_option( + "-n,--num-samples", + args.num_samples, + "Number of points to sample (before filtering)."); + CLI11_PARSE(app, argc, argv) + + if (auto ext = fs::path(args.output).extension(); ext != ".xyz") { + lagrange::logger().error( + "Output file extension should be .xyz. '{}' was given.", + ext.string()); + } + + // Load input mesh + lagrange::logger().info("Loading input mesh: {}", args.input); + auto mesh = lagrange::io::load_mesh(args.input); + + // Compute bbox + Eigen::AlignedBox bbox; + for (auto p : vertex_view(mesh).rowwise()) { + bbox.extend(p.transpose()); + } + std::uniform_real_distribution px(bbox.min().x(), bbox.max().x()); + std::uniform_real_distribution py(bbox.min().y(), bbox.max().y()); + std::uniform_real_distribution pz(bbox.min().z(), bbox.max().z()); + + // Build fast winding number engine + lagrange::winding::FastWindingNumber engine(mesh); + + // Sample points + std::mt19937 gen; + std::vector> points; + for (size_t k = 0; k < args.num_samples; ++k) { + std::array pos = {px(gen), py(gen), pz(gen)}; + if (engine.is_inside(pos)) { + points.push_back(pos); + } + } + + // Save result + lagrange::logger().info("Saving filtered sample points: {}", args.output); + fs::ofstream out(args.output); + for (auto p : points) { + out << fmt::format("{}\n", fmt::join(p, " ")); + } + + return 0; +} diff --git a/modules/winding/include/lagrange/winding/FastWindingNumber.h b/modules/winding/include/lagrange/winding/FastWindingNumber.h new file mode 100644 index 00000000..b1e80fc1 --- /dev/null +++ b/modules/winding/include/lagrange/winding/FastWindingNumber.h @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#pragma once + +#include + +namespace lagrange { + +namespace winding { + +/// +/// Fast winding number computation for triangle soups. +/// +class FastWindingNumber +{ +public: + /// + /// Constructs an acceleration structure on a given mesh to speed up winding number queries. + /// + /// @note Internally, point coordinates are converted to `float` and vertex indices are + /// converted to `int`. + /// + /// @param[in] mesh Triangle mesh used to initialize the fast winding number acceleration + /// structure. + /// + /// @tparam Scalar Mesh scalar type. + /// @tparam Index Mesh index type. + /// + template + FastWindingNumber(const SurfaceMesh& mesh); + + /// + /// Constructs a new instance. + /// + FastWindingNumber(); + + /// + /// Destroys the object. + /// + ~FastWindingNumber(); + + /// + /// Constructs a new instance. + /// + /// @param other Instance to move from. + /// + FastWindingNumber(FastWindingNumber&& other) noexcept; + + /// + /// Assignment operator. + /// + /// @param other Instance to move from. + /// + /// @return The result of the assignment. + /// + FastWindingNumber& operator=(FastWindingNumber&& other) noexcept; + + /// + /// Constructs a new instance. + /// + /// @param[in] other Instance to copy from. + /// + FastWindingNumber(const FastWindingNumber& other) = delete; + + /// + /// Assignment operator. + /// + /// @param[in] other Instance to copy from. + /// + /// @return The result of the assignment. + /// + FastWindingNumber& operator=(const FastWindingNumber& other) = delete; + + /// + /// Determines whether the specified query point is inside the volume. + /// + /// @param[in] pos Query position. + /// + /// @return True if the specified point is inside, False otherwise. + /// + bool is_inside(const std::array& pos) const; + + /// + /// Computes the solid angle at the query point. + /// + /// @param[in] pos Query position. + /// + /// @return Solid angle at the query point. + /// + float solid_angle(const std::array& pos) const; + +protected: + /// Internal implementation. + struct Impl; + + /// PIMPL to hide internal data structures. + value_ptr m_impl; +}; + +} // namespace winding +} // namespace lagrange diff --git a/modules/winding/src/FastWindingNumber.cpp b/modules/winding/src/FastWindingNumber.cpp new file mode 100644 index 00000000..7bee1bd2 --- /dev/null +++ b/modules/winding/src/FastWindingNumber.cpp @@ -0,0 +1,120 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +namespace lagrange { + +namespace winding { + +struct FastWindingNumber::Impl +{ +public: + template + void initialize( + const Eigen::MatrixBase& vertices, + const Eigen::MatrixBase& facets) + { + la_runtime_assert(vertices.cols() == 3); + la_runtime_assert(facets.cols() == 3); + + // TODO: Avoid copy if possible. For now we just copy stuff around. + m_vertices.resize(vertices.rows()); + for (Eigen::Index v = 0; v < vertices.rows(); ++v) { + m_vertices[v][0] = static_cast(vertices(v, 0)); + m_vertices[v][1] = static_cast(vertices(v, 1)); + m_vertices[v][2] = static_cast(vertices(v, 2)); + } + + m_triangles.resize(facets.rows()); + for (Eigen::Index f = 0; f < facets.rows(); ++f) { + m_triangles[f][0] = static_cast(facets(f, 0)); + m_triangles[f][1] = static_cast(facets(f, 1)); + m_triangles[f][2] = static_cast(facets(f, 2)); + } + + const int num_vertices = static_cast(m_vertices.size()); + const int num_triangles = static_cast(m_triangles.size()); + const int* const triangles_ptr = reinterpret_cast(m_triangles.data()); + m_engine.init(num_triangles, triangles_ptr, num_vertices, m_vertices.data()); + } + + bool is_inside(const std::array& pos) const + { + Vector q; + q[0] = pos[0]; + q[1] = pos[1]; + q[2] = pos[2]; + return m_engine.computeSolidAngle(q) / (4.f * M_PI) > 0.5f; + } + + float solid_angle(const std::array& pos) const + { + Vector q; + q[0] = pos[0]; + q[1] = pos[1]; + q[2] = pos[2]; + return m_engine.computeSolidAngle(q); + } + +protected: + using Vector = HDK_Sample::UT_Vector3T; + using Engine = HDK_Sample::UT_SolidAngle; + + std::vector m_vertices; + std::vector> m_triangles; + Engine m_engine; +}; + +template +FastWindingNumber::FastWindingNumber(const SurfaceMesh& mesh) + : m_impl(make_value_ptr()) +{ + la_runtime_assert( + mesh.get_dimension() == 3, + "Fast winding number engine only supports 3D meshes"); + la_runtime_assert( + mesh.is_triangle_mesh(), + "Fast winding number engine only supports triangle meshes"); + m_impl->initialize(vertex_view(mesh), facet_view(mesh)); +} + +FastWindingNumber::FastWindingNumber() = default; +FastWindingNumber::~FastWindingNumber() = default; +FastWindingNumber::FastWindingNumber(FastWindingNumber&& other) noexcept = default; +FastWindingNumber& FastWindingNumber::operator=(FastWindingNumber&& other) noexcept = default; + +bool FastWindingNumber::is_inside(const std::array& pos) const +{ + return m_impl->is_inside(pos); +} + +float FastWindingNumber::solid_angle(const std::array& pos) const +{ + return m_impl->solid_angle(pos); +} + +// Iterate over mesh (scalar, index) types +#define LA_X_fast_winding_number(_, Scalar, Index) \ + template FastWindingNumber::FastWindingNumber(const SurfaceMesh& mesh); +LA_SURFACE_MESH_X(fast_winding_number, 0) + +} // namespace winding +} // namespace lagrange diff --git a/modules/winding/tests/CMakeLists.txt b/modules/winding/tests/CMakeLists.txt new file mode 100644 index 00000000..3e1fa28a --- /dev/null +++ b/modules/winding/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +lagrange_add_test() +target_link_libraries(test_lagrange_winding PUBLIC WindingNumber::WindingNumber) diff --git a/modules/winding/tests/test_fast_winding_number.cpp b/modules/winding/tests/test_fast_winding_number.cpp new file mode 100644 index 00000000..6ff2fb2b --- /dev/null +++ b/modules/winding/tests/test_fast_winding_number.cpp @@ -0,0 +1,126 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include + +#include +#include +#include + +#include + +namespace { + +// Direct wrapper, no PIMPL. We do this to measure the impact of the PIMPL idiom on the query times. +class FastWindingNumberDirect +{ +public: + template + void initialize( + const Eigen::MatrixBase& vertices, + const Eigen::MatrixBase& facets) + { + la_runtime_assert(vertices.cols() == 3); + la_runtime_assert(facets.cols() == 3); + + // TODO: Avoid copy if possible. For now we just copy stuff around. + m_vertices.resize(vertices.rows()); + for (Eigen::Index v = 0; v < vertices.rows(); ++v) { + m_vertices[v][0] = static_cast(vertices(v, 0)); + m_vertices[v][1] = static_cast(vertices(v, 1)); + m_vertices[v][2] = static_cast(vertices(v, 2)); + } + + m_triangles.resize(facets.rows()); + for (Eigen::Index f = 0; f < facets.rows(); ++f) { + m_triangles[f][0] = static_cast(facets(f, 0)); + m_triangles[f][1] = static_cast(facets(f, 1)); + m_triangles[f][2] = static_cast(facets(f, 2)); + } + + const int num_vertices = static_cast(m_vertices.size()); + const int num_triangles = static_cast(m_triangles.size()); + const int* const triangles_ptr = reinterpret_cast(m_triangles.data()); + m_engine.init(num_triangles, triangles_ptr, num_vertices, m_vertices.data()); + } + + bool is_inside(const Eigen::Vector3f& pos) const + { + Vector q; + q[0] = pos.x(); + q[1] = pos.y(); + q[2] = pos.z(); + return m_engine.computeSolidAngle(q) / (4.f * M_PI) > 0.5f; + } + +protected: + using Vector = HDK_Sample::UT_Vector3T; + using Engine = HDK_Sample::UT_SolidAngle; + + std::vector m_vertices; + std::vector> m_triangles; + Engine m_engine; +}; + +} // namespace + +// Make metabuild happy +TEST_CASE("winding number noop", "[winding]") +{ + SUCCEED(); +} + +TEST_CASE("fast winding number", "[winding][!benchmark]") +{ + using Scalar = float; + using Index = uint32_t; + + auto mesh = lagrange::testing::load_surface_mesh("open/core/dragon.obj"); + + Eigen::AlignedBox bbox; + for (auto p : vertex_view(mesh).rowwise()) { + bbox.extend(p.transpose()); + } + std::uniform_real_distribution px(bbox.min().x(), bbox.max().x()); + std::uniform_real_distribution py(bbox.min().y(), bbox.max().y()); + std::uniform_real_distribution pz(bbox.min().z(), bbox.max().z()); + + size_t num_samples = 10000; + + BENCHMARK_ADVANCED("pimpl wrapper")(Catch::Benchmark::Chronometer meter) + { + lagrange::winding::FastWindingNumber engine(mesh); + std::mt19937 gen; + meter.measure([&]() { + size_t num_insides = 0; + for (size_t k = 0; k < num_samples; ++k) { + num_insides += engine.is_inside({px(gen), py(gen), pz(gen)}); + } + return num_insides; + }); + }; + + BENCHMARK_ADVANCED("direct wrapper")(Catch::Benchmark::Chronometer meter) + { + FastWindingNumberDirect engine; + engine.initialize(vertex_view(mesh), facet_view(mesh)); + std::mt19937 gen; + meter.measure([&]() { + size_t num_insides = 0; + for (size_t k = 0; k < num_samples; ++k) { + num_insides += engine.is_inside({px(gen), py(gen), pz(gen)}); + } + return num_insides; + }); + }; +}