├── .clang-format ├── .github └── workflows │ ├── macos.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build_exts.py ├── clang-format.sh ├── cmake ├── FindGoogleTest.cmake └── Macros.cmake ├── data ├── .gitignore ├── get_large_meshes.py └── models │ ├── box.obj │ ├── bunny.ply │ ├── bunny_mini.ply │ ├── fandisk.ply │ ├── plane.obj │ ├── sphere.obj │ ├── torus.obj │ └── torus.ply ├── examples ├── CMakeLists.txt ├── cpp │ ├── CMakeLists.txt │ ├── example_denoise.cpp │ ├── example_hole_fill.cpp │ ├── example_read_write.cpp │ ├── example_remesh.cpp │ ├── example_simplify.cpp │ └── example_smooth.cpp └── python │ ├── construct.ipynb │ ├── curvatures.ipynb │ ├── denoise.ipynb │ ├── feature_line_fields.ipynb │ ├── heat_kernel_signatures.ipynb │ ├── hole_filling.ipynb │ ├── laplace_beltrami.ipynb │ └── viewer.py ├── figures ├── bunny_before.png ├── bunny_hole_fill_adv_front.png ├── bunny_hole_fill_min_dihedral.png ├── bunny_holes.png ├── bunny_remesh_1.png ├── bunny_remesh_2.png ├── dragon_before.png ├── dragon_simplify_10000.png ├── dragon_simplify_50000.png ├── fandisk_before.png ├── fandisk_denoise_l0.png └── fandisk_noise.png ├── pyproject.toml ├── src ├── CMakeLists.txt ├── pybind11.cpp └── tinymesh │ ├── CMakeLists.txt │ ├── core │ ├── api.h │ ├── bounds.h │ ├── bvh.cpp │ ├── bvh.h │ ├── completion.cpp │ ├── debug.h │ ├── eigen.cpp │ ├── eigen.h │ ├── external.cpp │ ├── face.cpp │ ├── face.h │ ├── filesystem.h │ ├── halfedge.cpp │ ├── halfedge.h │ ├── mesh.cpp │ ├── mesh.h │ ├── openmp.h │ ├── progress.h │ ├── triangle.cpp │ ├── triangle.h │ ├── utils.h │ ├── vec.h │ ├── vertex.cpp │ └── vertex.h │ ├── filters │ ├── denoise.cpp │ ├── filters.h │ └── smooth.cpp │ ├── ops │ ├── features.cpp │ ├── metrics.cpp │ ├── operators.cpp │ └── ops.h │ ├── remesh │ ├── remesh.cpp │ ├── remesh.h │ └── simplify.cpp │ ├── restore │ ├── completion.cpp │ └── restore.h │ └── tinymesh.h └── tests ├── CMakeLists.txt ├── gtest.cpp ├── python ├── test_construct.py ├── test_filters.py ├── test_ops.py └── test_remesh.py ├── test_config.h.in ├── test_filters.cpp ├── test_mesh.cpp ├── test_ops.cpp ├── test_remesh.cpp ├── test_utils.h └── test_vec.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | ColumnLimit: 120 4 | IndentWidth: 4 5 | Standard: Cpp11 6 | AccessModifierOffset: -4 7 | IndentCaseLabels: false 8 | 9 | AlignAfterOpenBracket: Align 10 | AlignConsecutiveAssignments: false 11 | AlignTrailingComments: true 12 | AlignOperands: true 13 | AllowAllParametersOfDeclarationOnNextLine: false 14 | AllowAllConstructorInitializersOnNextLine: false 15 | AllowShortBlocksOnASingleLine: false 16 | AllowShortFunctionsOnASingleLine: false 17 | BinPackArguments: true 18 | BreakConstructorInitializers: BeforeColon 19 | BreakConstructorInitializersBeforeComma: true 20 | Cpp11BracedListStyle: false 21 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 22 | DerivePointerAlignment: false 23 | MaxEmptyLinesToKeep: 1 24 | PointerAlignment: Right 25 | ReflowComments: false 26 | SortIncludes: false 27 | 28 | UseTab: Never 29 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - pr 9 | tags: 10 | - v* 11 | 12 | jobs: 13 | macos: 14 | runs-on: macos-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version-major: [3] 19 | python-version-minor: [8, 9, 10, 11, 12] 20 | 21 | env: 22 | BUILD_TYPE: Release 23 | python-version: ${{ format('{0}.{1}', matrix.python-version-major, matrix.python-version-minor) }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: true 29 | 30 | - name: Set up Python version 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ env.python-version }} 34 | 35 | - name: Install Poetry 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install poetry 39 | poetry config virtualenvs.path ~/.local/share/virtualenvs 40 | 41 | - name: Cache Poetry 42 | uses: actions/cache@v3 43 | id: poetry-cache 44 | with: 45 | path: ~/.local/share/virtualenvs 46 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 47 | restore-keys: | 48 | ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 49 | 50 | - name: Install Google Test 51 | env: 52 | C_COMPILER: ${{ matrix.compiler }} 53 | run: | 54 | git clone https://github.com/google/googletest.git --depth 1 55 | cd googletest 56 | mkdir build 57 | cd build 58 | 59 | cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE 60 | cmake --build . --config $BUILD_TYPE --parallel 2 61 | cmake --build . --target install 62 | 63 | - name: CMake build 64 | env: 65 | C_COMPILER: ${{ matrix.compiler }} 66 | run: | 67 | cmake -E make_directory ${{runner.workspace}}/build 68 | cd ${{runner.workspace}}/build 69 | 70 | cmake $GITHUB_WORKSPACE \ 71 | -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ 72 | -DBUILD_EXAMPLES=ON \ 73 | -DBUILD_TESTS=ON 74 | 75 | cmake --build . --config $BUILD_TYPE --parallel 2 76 | 77 | - name: Unit test 78 | working-directory: ${{runner.workspace}}/build 79 | shell: bash 80 | run: ctest -C $BUILD_TYPE 81 | 82 | - name: Install Python modules 83 | if: steps.pipenv-cache.outputs.cache-hit != 'true' 84 | run: | 85 | poetry build -f wheel 86 | poetry install 87 | 88 | - name: Python module test 89 | run: | 90 | poetry run pytest -s tests 91 | 92 | - name: Create release 93 | if: startsWith(github.ref, 'refs/tags/') 94 | uses: softprops/action-gh-release@v1 95 | with: 96 | files: ./dist/*.whl 97 | draft: false 98 | prerelease: false 99 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - pr 9 | tags: 10 | - v* 11 | 12 | jobs: 13 | ubuntu: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version-major: [3] 19 | python-version-minor: [8, 9, 10, 11, 12] 20 | 21 | env: 22 | BUILD_TYPE: Release 23 | python-version: ${{ format('{0}.{1}', matrix.python-version-major, matrix.python-version-minor) }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: true 29 | 30 | - name: Set up Python version 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ env.python-version }} 34 | 35 | - name: Install Poetry 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install poetry 39 | poetry config virtualenvs.path ~/.local/share/virtualenvs 40 | 41 | - name: Cache Poetry 42 | uses: actions/cache@v3 43 | id: poetry-cache 44 | with: 45 | path: ~/.local/share/virtualenvs 46 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 47 | restore-keys: | 48 | ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 49 | 50 | - name: Install GNU C/C++ compiler 51 | run: | 52 | sudo apt-get install -y gcc-9 g++-9 53 | export CC=gcc-9 54 | export CXX=g++-9 55 | 56 | - name: Install Google Test 57 | env: 58 | C_COMPILER: ${{ matrix.compiler }} 59 | run: | 60 | git clone https://github.com/google/googletest.git 61 | cd googletest 62 | mkdir build 63 | cd build 64 | 65 | cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE 66 | cmake --build . --config $BUILD_TYPE --parallel 2 67 | sudo cmake --build . --target install 68 | 69 | - name: CMake build 70 | env: 71 | C_COMPILER: ${{ matrix.compiler }} 72 | run: | 73 | cmake -E make_directory ${{runner.workspace}}/build 74 | cd ${{runner.workspace}}/build 75 | 76 | cmake $GITHUB_WORKSPACE \ 77 | -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ 78 | -DBUILD_EXAMPLES=ON \ 79 | -DBUILD_TESTS=ON 80 | 81 | cmake --build . --config $BUILD_TYPE --parallel 2 82 | 83 | - name: Unit test 84 | working-directory: ${{ runner.workspace }}/build 85 | shell: bash 86 | run: ctest -C $BUILD_TYPE 87 | 88 | - name: Install Python modules 89 | if: steps.pipenv-cache.outputs.cache-hit != 'true' 90 | run: | 91 | poetry build -f wheel 92 | poetry install 93 | 94 | - name: Python module test 95 | run: | 96 | poetry run pytest -s tests 97 | 98 | - name: Create release 99 | if: startsWith(github.ref, 'refs/tags/') 100 | uses: softprops/action-gh-release@v1 101 | with: 102 | files: ./dist/*.whl 103 | draft: false 104 | prerelease: false 105 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - pr 9 | tags: 10 | - v* 11 | 12 | jobs: 13 | windows: 14 | runs-on: windows-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version-major: [3] 19 | python-version-minor: [8, 9, 10, 11, 12] 20 | 21 | env: 22 | BUILD_TYPE: Release 23 | python-version: ${{ format('{0}.{1}', matrix.python-version-major, matrix.python-version-minor) }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: true 29 | 30 | - name: Set up Python version 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ env.python-version }} 34 | 35 | - name: Install Poetry 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install poetry 39 | poetry config virtualenvs.path ~/.local/share/virtualenvs 40 | 41 | - name: Cache Poetry 42 | uses: actions/cache@v3 43 | id: poetry-cache 44 | with: 45 | path: ~/.local/share/virtualenvs 46 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 47 | restore-keys: | 48 | ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 49 | 50 | - name: Install Google Test 51 | run: | 52 | git clone https://github.com/google/googletest.git --depth 1 53 | cd googletest 54 | mkdir build 55 | cd build 56 | cmake .. ` 57 | -G "Visual Studio 17 2022" -A x64 ` 58 | -Dgtest_force_shared_crt=ON ` 59 | -DCMAKE_INSTALL_PREFIX="C:/Program Files/GoogleTest/" 60 | cmake --build . --config "$env:BUILD_TYPE" 61 | cmake --build . --config "$env:BUILD_TYPE" --target INSTALL 62 | 63 | - name: CMake build 64 | run: | 65 | cmake -E make_directory ${{ runner.workspace }}/build 66 | cd ${{ runner.workspace }}/build 67 | cmake "$env:GITHUB_WORKSPACE" ` 68 | -G "Visual Studio 17 2022" -A x64 ` 69 | -DGoogleTest_DIR="C:/Program Files/GoogleTest" ` 70 | -DBUILD_TESTS=ON ` 71 | -DBUILD_EXAMPLES=ON 72 | cmake --build . --config "$env:BUILD_TYPE" 73 | 74 | - name: Unit test 75 | working-directory: ${{ runner.workspace }}/build 76 | shell: bash 77 | run: ctest -C $BUILD_TYPE 78 | 79 | - name: Install Python modules 80 | if: steps.pipenv-cache.outputs.cache-hit != 'true' 81 | run: | 82 | poetry build -f wheel 83 | poetry install 84 | 85 | - name: Python module test 86 | run: | 87 | poetry run pytest -s tests 88 | 89 | - name: Create release 90 | if: startsWith(github.ref, 'refs/tags/') 91 | uses: softprops/action-gh-release@v1 92 | with: 93 | files: ./dist/*.whl 94 | draft: false 95 | prerelease: false 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/* 3 | .vscode/* 4 | build/* 5 | Debug/* 6 | Release/* 7 | cmake-build-debug/* 8 | cmake-build-release/* 9 | Testing/* 10 | 11 | *.TMP 12 | .DS_Store 13 | 14 | test_config.h 15 | 16 | # C/C++ libraries 17 | *.a 18 | *.so 19 | *.lib 20 | *.exp 21 | *.pdb 22 | *.dll 23 | *.dylib 24 | 25 | # Python cache files 26 | setup.py 27 | Pipfile.lock 28 | poetry.lock 29 | *.pyc 30 | *.pyd 31 | dist/* 32 | __pycache__/ 33 | *.egg-info 34 | .ipynb_checkpoints/ 35 | 36 | # Mesh file format 37 | *.ply 38 | *.obj 39 | *.off 40 | *.stl 41 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/tinymesh/ext/tinyobjloader"] 2 | path = src/tinymesh/ext/tinyobjloader 3 | url = https://github.com/syoyo/tinyobjloader 4 | [submodule "src/tinymesh/ext/tinyply"] 5 | path = src/tinymesh/ext/tinyply 6 | url = https://github.com/ddiakopoulos/tinyply 7 | [submodule "src/tinymesh/ext/eigen"] 8 | path = src/tinymesh/ext/eigen 9 | url = https://gitlab.com/libeigen/eigen.git 10 | [submodule "src/tinymesh/ext/spectra"] 11 | path = src/tinymesh/ext/spectra 12 | url = https://github.com/yixuan/spectra 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.2.4 2 | --- 3 | * Update CI scripts to build wheels for 5 latest Python versions. 4 | 5 | 6 | v0.2.3 7 | --- 8 | * Update some method names (e.g., `isStatic` -> `isLocked`) 9 | * Delete `InHalfedgeIterator', and rename `OutHalfedgeIterator` as `HalfedgeIterator`. 10 | * Add const version of iterators. 11 | * Advancing front method [Zhao et al. 2007] is implemented. 12 | * Change dev-env manager to Poetry. 13 | * Add a method to compute principal curvatures (and thier directions) [Rusinkiewicz 2004]. 14 | * Add a method of context-based coherent surface completion [Harary et al. 2016]. 15 | * Add own print function to support both C++/Python standard outputs. 16 | 17 | v0.2.2 18 | --- 19 | * Change Python env manager to Pipenv. 20 | * Add methods computing approximated Laplacian-Beltrami operators (adjacent, cotangent, Belkin+2008). 21 | * Add IPython notebooks as Pythons examples rather than simple scripts. 22 | * Change traversal order of elements around a vertex to counter-clockwise order. 23 | * Add heat kernel signature [Sun et al. 2009]. 24 | 25 | v0.2.1 26 | --- 27 | * Minor bug fix for GCC v8. 28 | 29 | v0.2.0 30 | --- 31 | * Add unit test for each functions (just to check run or not). 32 | * Add feature preservation in remeshing. 33 | * Include Eigen to repo as a submodule. 34 | * Update Python setup to use Poetry. 35 | * Add mesh denoising via bilateral filter [Zheng et al. 2011] and L0 smoothing [He and Schaefer 2013]. 36 | * Update Python module build to use Python-side `pybind11` installation. 37 | * Add mesh denoising via normal Gaussian filter [Ohtake et al. 2001]. 38 | * Implicit fairing `implicit_fair` is updated to its normalized version. 39 | * Hole filling `hole_fill` is updated to take the upper bound of allowed dihedral angle. 40 | * Update `setup.py` to support the operation `install`. 41 | 42 | v0.1.0 43 | --- 44 | * First release 45 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18.0 FATAL_ERROR) 2 | project(TinyMesh) 3 | 4 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") 5 | include(Macros) 6 | 7 | # ---------- 8 | # Options 9 | # ---------- 10 | option(WITH_OPENMP "Use OpenMP or not" ON) 11 | option(BUILD_EXAMPLES "Build examples." OFF) 12 | option(BUILD_TESTS "Build unit tests." OFF) 13 | 14 | set(CMAKE_CXX_STANDARD 17) 15 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 16 | if (UNIX) 17 | set(CMAKE_CXX_FLAGS "-pthread -fPIC -Wall") 18 | set(CMAKE_CXX_FLAGS_RELEASE "-g -O2") 19 | set(CMAKE_CXX_FLAGS_DEBUG "-g -O0") 20 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 21 | set(CXX_FS_LIBRARY "stdc++fs") 22 | endif() 23 | endif() 24 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 25 | 26 | # ---------- 27 | # Common target names 28 | # ---------- 29 | set(TINYMESH_LIBRARY "tinymesh") 30 | set(TINYMESH_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/src") 31 | 32 | # ---------- 33 | # OpenMP 34 | # ---------- 35 | if (WITH_OPENMP) 36 | find_package(OpenMP) 37 | if (OPENMP_FOUND) 38 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") 39 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") 40 | set(OpenMP_INCLUDE_DIRS "${OpenMP_C_INCLUDE_DIRS}") 41 | set(OpenMP_LIBRARIES "${OpenMP_C_LIBRARIES}") 42 | endif() 43 | endif() 44 | 45 | # ---------- 46 | # Output paths 47 | # ---------- 48 | 49 | if (NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY) 50 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 51 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin) 52 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin) 53 | endif() 54 | 55 | if (NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) 56 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 57 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin) 58 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin) 59 | endif() 60 | 61 | if (NOT CMAKE_ARCHIVE_OUTPUT_DIRECTORY) 62 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 63 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/lib) 64 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/lib) 65 | endif() 66 | 67 | set(CMAKE_DEBUG_POSTFIX "-debug") 68 | 69 | # ---------- 70 | # OS specific settings 71 | # ---------- 72 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 73 | 74 | if (MSVC) 75 | add_compile_options("/wd4251" "/wd4819") 76 | endif() 77 | 78 | if (APPLE) 79 | set(CMAKE_MACOSX_RPATH 1) 80 | endif() 81 | 82 | # ---------- 83 | # Traverse subdirectories 84 | # ---------- 85 | add_subdirectory(src) 86 | 87 | if (BUILD_EXAMPLES) 88 | add_subdirectory(examples) 89 | endif() 90 | 91 | if (BUILD_TESTS) 92 | find_package(GoogleTest REQUIRED) 93 | enable_testing() 94 | add_subdirectory(tests) 95 | endif() 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TinyMesh 2 | === 3 | 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2fd9a7f621e44654ad8b81bc38138662)](https://www.codacy.com/manual/tatsy/tinymesh?utm_source=github.com&utm_medium=referral&utm_content=tatsy/tinymesh&utm_campaign=Badge_Grade) 5 | ![Windows CI](https://github.com/tatsy/tinymesh/workflows/Windows%20CI/badge.svg) 6 | ![MacOS CI](https://github.com/tatsy/tinymesh/workflows/MacOS%20CI/badge.svg) 7 | ![Ubuntu CI](https://github.com/tatsy/tinymesh/workflows/Ubuntu%20CI/badge.svg) 8 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) 9 | 10 | > TinyMesh is a light-weight mesh processing library in C/C++. 11 | 12 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/tatsy425) 13 | 14 | 15 | Modules 16 | --- 17 | 18 | Here is the list of modules and reference papers for that. 19 | 20 | * **Geometric properties** 21 | * Principal curvatures [[Rusinkiewicz 2004]](https://ieeexplore.ieee.org/document/1335277) 22 | * Heat kernel signatures [[Sun et al. 2009]](https://onlinelibrary.wiley.com/doi/10.1111/j.1467-8659.2009.01515.x) 23 | * **Smoothing** 24 | * Laplacian smoothing 25 | * Taubin smoothing [[Taubin 1995]](https://dl.acm.org/doi/10.1145/218380.218473) 26 | * Implicit fairing [[Desbrun 1999]](https://dl.acm.org/doi/10.1145/311535.311576) 27 | * **Denoising** 28 | * Normal Gaussian filter [[Ohtake et al. 2001]](https://www.semanticscholar.org/paper/Mesh-Smoothing-by-Adaptive-and-Anisotropic-Gaussian-Ohtake-Belyaev/19b431c843f4b37d2218e7efcd8f64b6ff589c1f) 29 | * Normal bilateral filter [[Zheng et al. 2011]](https://ieeexplore.ieee.org/document/5674028) 30 | * L0 mesh smoothing [[He and Schaefer 2013]](https://dl.acm.org/doi/10.1145/2461912.2461965) 31 | * **Remeshing** 32 | * Uniform triangulation [[Hoppe 1996]](https://dl.acm.org/doi/10.1145/237170.237216) 33 | * **Simplification** 34 | * Quadric error metrics (QEM) [[Garland and Heckbert 1997]](https://dl.acm.org/doi/10.1145/258734.258849) 35 | * **Hole filling** 36 | * Min-area hole filling [[Barequet and Sharir 1995]](https://www.sciencedirect.com/science/article/pii/016783969400011G?via%3Dihub) 37 | * Min-dihedral angle [[Liepa 2003]](http://diglib.eg.org/handle/10.2312/SGP.SGP03.200-206) 38 | * Advancing front [[Zhao et al. 2007]](https://link.springer.com/article/10.1007/s00371-007-0167-y) 39 | 40 | Install 41 | --- 42 | 43 | The module is tested its compilation using the following compilers. 44 | 45 | * **Windows** - Visual Studio 2022 (Microsoft Windows Server 2022) 46 | * **MacOS** - Apple Clang 11.0 (MacOS 11.7) 47 | * **Linux** - LLVM Clang 11.0, GNU C Compiler 9.4.0 (Ubuntu 22.04 LTS) 48 | 49 | ### Library and examples (C++) 50 | 51 | You can build a shared library and all the examples by `CMake` with the following commands. 52 | 53 | ```shell 54 | git clone https://github.com/tatsy/tinymesh.git --depth 1 55 | cd tinymesh 56 | git submodule update --init --recursive 57 | mkdir build && cd build 58 | cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=ON .. 59 | cmake --build . --config Release --parallel 2 60 | ``` 61 | 62 | ### Python module 63 | 64 | You can install the Python module using `Pip`. 65 | 66 | ```shell 67 | pip install git+https://github.com/tatsy/tinymesh.git 68 | ``` 69 | 70 | You can also use `poetry` to install from source codes. 71 | 72 | ```shell 73 | git clone https://github.com/tatsy/tinymesh 74 | cd tinymesh 75 | poetry install 76 | ``` 77 | 78 | Run examples 79 | --- 80 | 81 | #### C++ 82 | 83 | ```shell 84 | ./build/bin/example_simplify data/models/bunny.ply 85 | ``` 86 | 87 | #### Python 88 | 89 | ```shell 90 | # Run "poetry shell" to enable virtualenv if you test with poetry. 91 | python examples/python/fill_and_fair.py data/models/bunny.ply 92 | ``` 93 | 94 | Gallery 95 | --- 96 | 97 | #### Remeshing 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
InputRemeshRemesh (bottom part)
111 | 112 | 113 | #### Simplification 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |
InputSimplify (50000 faces)Simplify (10000 faces)
127 | 128 | #### Denoising (L0 mesh smoothing) 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
OriginalNoisyDenoise
142 | 143 | #### Hole filling 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 |
OriginalHole filled (minimum dihedral angles)Hole filled (advancing front)
157 | 158 | 159 | Notice 160 | --- 161 | 162 | The functions provided by this repo are not perfect and their process will fail for problematic meshes, e.g., with non-manifold faces. In such cases, you can fix the problem by repairing the mesh using [MeshFix](https://github.com/MarcoAttene/MeshFix-V2.1). 163 | 164 | License 165 | --- 166 | 167 | Mozilla Public License v2 (c) Tatsuya Yatagawa 2020-2021 168 | -------------------------------------------------------------------------------- /build_exts.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pathlib 3 | import platform 4 | 5 | from setuptools.errors import CCompilerError, PackageDiscoveryError 6 | from setuptools.command.build_ext import build_ext 7 | 8 | try: 9 | from pybind11.setup_helpers import Pybind11Extension 10 | except ImportError: 11 | from setuptools import Extension as Pybind11Extension 12 | 13 | 14 | class TinyMeshBuildExt(build_ext): 15 | def run(self): 16 | try: 17 | super(TinyMeshBuildExt, self).run() 18 | except FileNotFoundError: 19 | raise Exception("File not found. Could not compile C extension") 20 | 21 | def build_extension(self, ext): 22 | # common settings 23 | for e in self.extensions: 24 | e.define_macros.extend( 25 | [ 26 | ("TINYMESH_PYTHON_MODULE", 1), 27 | ] 28 | ) 29 | 30 | # OS specific settings 31 | if platform.system() == "Darwin": 32 | for e in self.extensions: 33 | e.extra_compile_args.extend( 34 | [ 35 | "-mmacosx-version-min=10.15", 36 | ] 37 | ) 38 | 39 | elif platform.system() == "Linux": 40 | for e in self.extensions: 41 | e.extra_compile_args.extend( 42 | [ 43 | "-fopenmp", 44 | ] 45 | ) 46 | e.extra_link_args.extend( 47 | [ 48 | "-fopenmp", 49 | "-lstdc++fs", 50 | ] 51 | ) 52 | 53 | # compiler specific settings 54 | if self.compiler.compiler_type == "unix": 55 | for e in self.extensions: 56 | e.extra_compile_args.extend( 57 | [ 58 | "-std=c++17", 59 | ] 60 | ) 61 | 62 | elif self.compiler.compiler_type == "msvc": 63 | for e in self.extensions: 64 | e.extra_compile_args.extend(["/utf-8", "/openmp"]) 65 | e.define_macros.extend( 66 | [ 67 | ("_CRT_SECURE_NO_WARNINGS", 1), 68 | ("_SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING", 1), 69 | ] 70 | ) 71 | 72 | # building 73 | try: 74 | super(TinyMeshBuildExt, self).build_extension(ext) 75 | except (CCompilerError, PackageDiscoveryError, ValueError): 76 | raise Exception("Could not compile C extension") 77 | 78 | 79 | exclude = ["src/tinymesh/ext"] 80 | sources = pathlib.Path().glob("src/**/*.cpp") 81 | sources = [str(path).replace("\\", "/") for path in sources] 82 | sources = [path for path in sources if all([not re.search(e, path) for e in exclude])] 83 | 84 | include = [ 85 | "src/tinymesh", 86 | "src/tinymesh/ext/tinyobjloader", 87 | "src/tinymesh/ext/tinyply/source", 88 | "src/tinymesh/ext/eigen", 89 | "src/tinymesh/ext/spectra/include", 90 | ] 91 | 92 | ext_modules = [ 93 | Pybind11Extension( 94 | "tinymesh", 95 | sources, 96 | language="c++", 97 | include_dirs=include, 98 | ) 99 | ] 100 | 101 | 102 | def build(setup_kwargs): 103 | setup_kwargs.update( 104 | { 105 | "ext_modules": ext_modules, 106 | "cmdclass": {"build_ext": TinyMeshBuildExt}, 107 | } 108 | ) 109 | -------------------------------------------------------------------------------- /clang-format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EXTS=".cpp .hpp .c .h" 4 | FILES="" 5 | for e in $EXTS; do 6 | NEW=`git ls-files | grep $e\$` 7 | FILES="${FILES} ${NEW}" 8 | done 9 | 10 | for f in $FILES; do 11 | echo $f 12 | clang-format -i $f 13 | done 14 | 15 | -------------------------------------------------------------------------------- /cmake/FindGoogleTest.cmake: -------------------------------------------------------------------------------- 1 | include(FindPackageHandleStandardArgs) 2 | 3 | set(GoogleTest_DIR "GoogleTest_DIR" CACHE PATH "") 4 | 5 | if (WIN32) 6 | find_path(GTEST_INCLUDE_DIR 7 | NAMES gtest/gtest.h 8 | PATHS 9 | ${GoogleTest_DIR} 10 | ${GoogleTest_DIR}/include) 11 | 12 | find_library(GTEST_LIBRARY 13 | NAMES gtest 14 | PATHS 15 | ${GoogleTest_DIR} 16 | ${GoogleTest_DIR}/lib 17 | ${GoogleTest_DIR}/lib/Release) 18 | 19 | find_library(GTEST_MAIN_LIBRARY 20 | NAMES gtest_main 21 | PATHS 22 | ${GoogleTest_DIR} 23 | ${GoogleTest_DIR}/lib 24 | ${GoogleTest_DIR}/lib/Release) 25 | else() 26 | find_path(GTEST_INCLUDE_DIR 27 | NAMES gtest/gtest.h 28 | PATHS 29 | /usr/include 30 | /usr/local/include 31 | ${GoogleTest_DIR} 32 | ${GoogleTest_DIR}/include) 33 | 34 | find_library(GTEST_LIBRARY 35 | NAMES gtest 36 | PATHS 37 | /usr/lib 38 | /usr/local/lib 39 | /usr/lib/x86_64-linux-gnu 40 | ${GoogleTest_DIR} 41 | ${GoogleTest_DIR}/lib) 42 | 43 | find_library(GTEST_MAIN_LIBRARY 44 | NAMES gtest_main 45 | PATHS 46 | /usr/lib 47 | /usr/local/lib 48 | /usr/lib/x86_64-linux-gnu 49 | ${GoogleTest_DIR} 50 | ${GoogleTest_DIR}/lib 51 | ${GoogleTest_DIR}/lib/Release) 52 | endif() 53 | 54 | find_package_handle_standard_args( 55 | GoogleTest DEFAULT_MSG 56 | GTEST_INCLUDE_DIR 57 | GTEST_LIBRARY 58 | GTEST_MAIN_LIBRARY 59 | ) 60 | 61 | if (GoogleTest_FOUND) 62 | message(STATUS "GoogleTest include: ${GTEST_INCLUDE_DIR}") 63 | message(STATUS "GoogleTest library: ${GTEST_LIBRARY}") 64 | message(STATUS "GoogleTest main library: ${GTEST_MAIN_LIBRARY}") 65 | set(GTEST_INCLUDE_DIRS ${GTEST_INCLUDE_DIR}) 66 | set(GTEST_LIBRARIES ${GTEST_LIBRARY} ${GTEST_MAIN_LIBRARY}) 67 | endif() 68 | 69 | mark_as_advanced(GTEST_INCLUDE_DIR GTEST_LIBRARY GTEST_MAIN_LIBRARY) 70 | -------------------------------------------------------------------------------- /cmake/Macros.cmake: -------------------------------------------------------------------------------- 1 | # ---------- 2 | # Add source codes in the folder 3 | # ---------- 4 | function(add_folder SOURCES FOLDER_NAME) 5 | file(GLOB LOCAL_FILES 6 | "${FOLDER_NAME}/*.cpp" "${FOLDER_NAME}/*.hpp" 7 | "${FOLDER_NAME}/*.c" "${FOLDER_NAME}/*.h") 8 | set(${SOURCES} ${${SOURCES}} ${LOCAL_FILES} PARENT_SCOPE) 9 | 10 | source_group(${FOLDER_NAME} FILES ${LOCAL_FILES}) 11 | endfunction() 12 | 13 | # ---------- 14 | # Add example program 15 | # ---------- 16 | function(ADD_EXAMPLE) 17 | set(options) 18 | set(oneValueArgs NAME) 19 | set(multiValueArgs SOURCES) 20 | cmake_parse_arguments(ADD_EXAMPLE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 21 | 22 | message(STATUS "Example: ${ADD_EXAMPLE_NAME}") 23 | set(EXP_EXE_NAME "example_${ADD_EXAMPLE_NAME}") 24 | add_executable(${EXP_EXE_NAME} ${ADD_EXAMPLE_SOURCES}) 25 | add_dependencies(${EXP_EXE_NAME} ${TINYMESH_LIBRARY}) 26 | 27 | target_include_directories(${EXP_EXE_NAME} PRIVATE ${TINYMESH_INCLUDE_DIR}) 28 | target_link_libraries(${EXP_EXE_NAME} PRIVATE ${TINYMESH_LIBRARY}) 29 | 30 | set_target_properties(${EXP_EXE_NAME} PROPERTIES FOLDER "Examples") 31 | set_target_properties(${EXP_EXE_NAME} PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) 32 | source_group("Source Files" FILES ${SOURCE_FILES}) 33 | 34 | if (MSVC) 35 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi") 36 | set_property(TARGET ${EXP_EXE_NAME} APPEND PROPERTY LINK_FLAGS "/DEBUG /PROFILE") 37 | endif() 38 | endfunction(ADD_EXAMPLE) 39 | 40 | # ---------- 41 | # Add test program 42 | # ---------- 43 | function(ADD_UNIT_TEST) 44 | set(options) 45 | set(oneValueArgs NAME DEPS) 46 | set(multiValueArgs SOURCES) 47 | cmake_parse_arguments(ADD_UNIT_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 48 | 49 | message(STATUS "Test: ${ADD_UNIT_TEST_NAME}") 50 | add_executable(${ADD_UNIT_TEST_NAME}) 51 | add_dependencies(${ADD_UNIT_TEST_NAME} ${TINYMESH_LIBRARY}) 52 | 53 | target_sources(${ADD_UNIT_TEST_NAME} PRIVATE ${ADD_UNIT_TEST_SOURCES}) 54 | target_include_directories(${ADD_UNIT_TEST_NAME} PRIVATE ${TINYMESH_INCLUDE_DIR} ${GTEST_INCLUDE_DIRS}) 55 | target_link_libraries(${ADD_UNIT_TEST_NAME} PRIVATE ${TINYMESH_LIBRARY} ${GTEST_LIBRARIES}) 56 | 57 | set_target_properties(${ADD_UNIT_TEST_NAME} PROPERTIES FOLDER "Tests") 58 | set_target_properties(${ADD_UNIT_TEST_NAME} PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) 59 | source_group("Source Files" FILES ${SOURCE_FILES}) 60 | 61 | set(${ADD_UNIT_TEST_DEPS} ${${ADD_UNIT_TEST_DEPS}} ${ADD_UNIT_TEST_NAME} PARENT_SCOPE) 62 | 63 | add_test(NAME ${ADD_UNIT_TEST_NAME} COMMAND ${ADD_UNIT_TEST_NAME}) 64 | set_tests_properties(${ADD_UNIT_TEST_NAME} PROPERTIES TIMEOUT 120) 65 | endfunction(ADD_UNIT_TEST) 66 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | !models/bunny.ply 2 | !models/bunny_mini.ply 3 | !models/fandisk.ply 4 | !models/box.obj 5 | !models/plane.obj 6 | !models/sphere.obj 7 | !models/torus.ply 8 | -------------------------------------------------------------------------------- /data/get_large_meshes.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import requests 5 | import zipfile 6 | import tarfile 7 | from collections import OrderedDict 8 | 9 | class Item: 10 | def __init__(self, url, name): 11 | self.url = url 12 | self.name = name 13 | 14 | def __repr__(self): 15 | return self.url 16 | 17 | target_folder = 'large_meshes' 18 | 19 | url_list = OrderedDict([ 20 | ('happy_budda', Item('http://graphics.stanford.edu/pub/3Dscanrep/happy/happy_recon.tar.gz', 'happy_vrip.ply')), 21 | ('dragon', Item('http://graphics.stanford.edu/pub/3Dscanrep/dragon/dragon_recon.tar.gz', 'dragon_vrip.ply')), 22 | ('lucy', Item('http://graphics.stanford.edu/data/3Dscanrep/lucy.tar.gz', 'lucy.ply')) 23 | ]) 24 | 25 | def get_file_from_url(url): 26 | temp = url.split('/')[-1] 27 | length = temp.rfind('?') 28 | if length == -1: 29 | return temp 30 | else: 31 | return temp[:length] 32 | 33 | def bar_string(rate): 34 | length = 40 35 | if rate == 100.0: 36 | return '=' * length 37 | else: 38 | num = int(rate / (100.0 / length)) 39 | return ('=' * num) + '>' + (' ' * (length - num - 1)) 40 | 41 | def download(url): 42 | print('Download: {:s}'.format(url)) 43 | 44 | filename = get_file_from_url(url) 45 | r = requests.get(url, stream=True) 46 | 47 | size_total = int(r.headers['content-length']) 48 | size_get = 0 49 | 50 | chunk_size = 1024 51 | with open(filename, 'wb') as f: 52 | for chunk in r.iter_content(chunk_size=chunk_size): 53 | if chunk: 54 | f.write(chunk) 55 | f.flush() 56 | size_get += chunk_size 57 | 58 | rate = min(100.0, 100.0 * size_get / size_total) 59 | print('[ %6.2f %% ] [ %s ]' % (rate, bar_string(rate)), end='\r') 60 | print('\nDownload finished!!') 61 | return filename 62 | 63 | return None 64 | 65 | def unarchive(filename, target, extract_to): 66 | _, ext = os.path.splitext(filename) 67 | try: 68 | if ext == '.gz' or ext == '.tar': 69 | tar = tarfile.open(filename) 70 | members = list(filter(lambda mem: target in mem.name, tar.getmembers())) 71 | if len(members) != 1: 72 | raise Exception('Multiple files with name "{:s}" are detected!'.format(target)) 73 | 74 | with open(extract_to, 'wb') as f: 75 | f.write(tar.extractfile(members[0]).read()) 76 | tar.close() 77 | 78 | elif ext == '.zip': 79 | zip_file = zipfile.ZipFile(filename) 80 | members = list(filter(lambda mem: target in mem.filename, zip_file.infolist())) 81 | if len(members) != 1: 82 | raise Exception('Multiple files with name "{:s}" are detected!'.format(target)) 83 | 84 | with open(extract_to, 'wb') as f: 85 | f.write(zip_file.open(members[0].filename).read()) 86 | zip_file.close() 87 | 88 | else: 89 | raise Exception('Unknown extension: %s' % ext) 90 | 91 | except Exception as e: 92 | raise e 93 | 94 | print('File is unarchived: {:s}'.format(extract_to)) 95 | 96 | 97 | def process(name, item): 98 | # Download 99 | filename = download(item.url) 100 | # Extract target file 101 | _, ext = os.path.splitext(item.name) 102 | extract_to = os.path.join(target_folder, name) + ext 103 | unarchive(filename, item.name, os.path.join(target_folder, name) + ext) 104 | # Remove urchive file 105 | os.remove(filename) 106 | 107 | def main(): 108 | if not os.path.exists(target_folder): 109 | os.makedirs(target_folder) 110 | 111 | for name, item in url_list.items(): 112 | process(name, item) 113 | 114 | if __name__ == '__main__': 115 | main() -------------------------------------------------------------------------------- /data/models/box.obj: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # OBJ File Generated by Meshlab 4 | # 5 | #### 6 | # Object box.obj 7 | # 8 | # Vertices: 8 9 | # Faces: 6 10 | # 11 | #### 12 | vn -0.577350 -0.577350 -0.577350 13 | v -0.500000 -0.500000 -0.500000 14 | vn 0.333333 -0.666667 -0.666667 15 | v 0.500000 -0.500000 -0.500000 16 | vn -0.666667 0.333333 -0.666667 17 | v -0.500000 0.500000 -0.500000 18 | vn 0.666667 0.666667 -0.333333 19 | v 0.500000 0.500000 -0.500000 20 | vn -0.666667 -0.666667 0.333333 21 | v -0.500000 -0.500000 0.500000 22 | vn 0.666667 -0.333333 0.666667 23 | v 0.500000 -0.500000 0.500000 24 | vn -0.333333 0.666667 0.666667 25 | v -0.500000 0.500000 0.500000 26 | vn 0.577350 0.577350 0.577350 27 | v 0.500000 0.500000 0.500000 28 | # 8 vertices, 0 vertices normals 29 | 30 | f 1//1 3//3 4//4 2//2 31 | f 1//1 5//5 7//7 3//3 32 | f 1//1 2//2 6//6 5//5 33 | f 8//8 7//7 5//5 6//6 34 | f 8//8 4//4 3//3 7//7 35 | f 8//8 6//6 2//2 4//4 36 | # 6 faces, 0 coords texture 37 | 38 | # End of File 39 | -------------------------------------------------------------------------------- /data/models/bunny.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/data/models/bunny.ply -------------------------------------------------------------------------------- /data/models/bunny_mini.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/data/models/bunny_mini.ply -------------------------------------------------------------------------------- /data/models/fandisk.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/data/models/fandisk.ply -------------------------------------------------------------------------------- /data/models/torus.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/data/models/torus.ply -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(cpp) 2 | -------------------------------------------------------------------------------- /examples/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # ---------- 2 | # Add example directories 3 | # ---------- 4 | add_example(NAME read_write SOURCES example_read_write.cpp) 5 | add_example(NAME remesh SOURCES example_remesh.cpp) 6 | add_example(NAME simplify SOURCES example_simplify.cpp) 7 | add_example(NAME smooth SOURCES example_smooth.cpp) 8 | add_example(NAME denoise SOURCES example_denoise.cpp) 9 | add_example(NAME hole_fill SOURCES example_hole_fill.cpp) 10 | -------------------------------------------------------------------------------- /examples/cpp/example_denoise.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "tinymesh/tinymesh.h" 5 | 6 | namespace fs = std::filesystem; 7 | namespace tms = tinymesh; 8 | 9 | int main(int argc, char **argv) { 10 | try { 11 | if (argc <= 1) { 12 | printf("usage: %s [input mesh] [sigma]\n", argv[0]); 13 | return 1; 14 | } 15 | 16 | const double sigma = argc <= 2 ? 0.2 : atof(argv[2]); 17 | 18 | // Basename of input file 19 | const fs::path filepath = fs::canonical(fs::path(argv[1])); 20 | const fs::path dirpath = filepath.parent_path(); 21 | const std::string extension = filepath.extension().string(); 22 | const std::string basename = filepath.stem().string(); 23 | 24 | // Random number generator 25 | std::random_device randev; 26 | std::mt19937 mt(randev()); 27 | std::uniform_real_distribution dist(-0.5, 0.5); 28 | 29 | // Save noise mesh 30 | std::string noiseMeshFile; 31 | { 32 | tms::Mesh mesh(argv[1]); 33 | mesh.fillHoles(); 34 | const double Lavg = mesh.getMeanEdgeLength(); 35 | 36 | // Add noise 37 | for (int i = 0; i < (int)mesh.numVertices(); i++) { 38 | const Vec3 pos = mesh.vertex(i)->pos(); 39 | const Vec3 noise = Vec3(dist(mt), dist(mt), dist(mt)); 40 | mesh.vertex(i)->setPos(pos + Lavg * noise); 41 | } 42 | noiseMeshFile = (dirpath / fs::path((basename + "_noise" + extension).c_str())).string(); 43 | mesh.save(noiseMeshFile); 44 | } 45 | 46 | // Denoise (Normal Gaussian filter) 47 | { 48 | tms::Mesh mesh(noiseMeshFile); 49 | mesh.fillHoles(); 50 | tms::denoiseNormalGaussian(mesh, sigma, 10); 51 | 52 | const std::string outfile = 53 | (dirpath / fs::path((basename + "_denoise_Gaussian" + extension).c_str())).string(); 54 | mesh.save(outfile); 55 | printf("Save: %s\n", outfile.c_str()); 56 | } 57 | 58 | // Denoise (Normal bilateral filter) 59 | { 60 | tms::Mesh mesh(noiseMeshFile); 61 | mesh.fillHoles(); 62 | tms::denoiseNormalBilateral(mesh, sigma, 0.1, 10); 63 | 64 | const std::string outfile = 65 | (dirpath / fs::path((basename + "_denoise_bilateral" + extension).c_str())).string(); 66 | mesh.save(outfile); 67 | printf("Save: %s\n", outfile.c_str()); 68 | } 69 | 70 | // Denoise (L0 smoothing) 71 | { 72 | tms::Mesh mesh(noiseMeshFile); 73 | mesh.fillHoles(); 74 | tms::denoiseL0Smooth(mesh); 75 | 76 | const std::string outfile = (dirpath / fs::path((basename + "_denoise_l0" + extension).c_str())).string(); 77 | mesh.save(outfile); 78 | printf("Save: %s\n", outfile.c_str()); 79 | } 80 | } catch (std::runtime_error &e) { 81 | std::cerr << e.what() << std::endl; 82 | return 1; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/cpp/example_hole_fill.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tinymesh/tinymesh.h" 4 | 5 | namespace fs = std::filesystem; 6 | namespace tms = tinymesh; 7 | 8 | int main(int argc, char **argv) { 9 | if (argc <= 1) { 10 | printf("usage: %s [input mesh]\n", argv[0]); 11 | return 1; 12 | } 13 | 14 | // Basename of input file 15 | const fs::path filepath = fs::canonical(fs::path(argv[1])); 16 | const fs::path dirpath = filepath.parent_path(); 17 | const std::string extension = filepath.extension().string(); 18 | const std::string basename = filepath.stem().string(); 19 | 20 | // Hole filling with minimum dihedral angle [Liepa 2003] 21 | { 22 | tms::Mesh mesh(argv[1]); 23 | for (int i = 0; i < mesh.numFaces(); i++) { 24 | tms::Face *f = mesh.face(i); 25 | if (f->isHole()) { 26 | tms::holeFillMinDihedral(mesh, f, Pi / 6.0); 27 | } 28 | } 29 | const std::string outfile = (dirpath / fs::path((basename + "_fill_minDA" + extension).c_str())).string(); 30 | mesh.save(outfile); 31 | printf("Save: %s\n", outfile.c_str()); 32 | } 33 | 34 | // Hole filling with advancing front algorithm [Zhao et al. 2007] 35 | { 36 | tms::Mesh mesh(argv[1]); 37 | std::vector holeFaces; 38 | for (int i = 0; i < mesh.numFaces(); i++) { 39 | tms::Face *f = mesh.face(i); 40 | if (f->isHole()) { 41 | tms::holeFillAdvancingFront(mesh, f); 42 | } 43 | } 44 | const std::string outfile = (dirpath / fs::path((basename + "_fill_adv_front" + extension).c_str())).string(); 45 | mesh.save(outfile); 46 | printf("Save: %s\n", outfile.c_str()); 47 | } 48 | 49 | // Context-based coherent surface completion [Harary and Tal 2016] 50 | { 51 | tms::Mesh mesh(argv[1]); 52 | tms::holeFillContextCoherent(mesh, 300); 53 | 54 | const std::string outfile = 55 | (dirpath / fs::path((basename + "_fill_context_coherent" + extension).c_str())).string(); 56 | mesh.save(outfile); 57 | printf("Save: %s\n", outfile.c_str()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/cpp/example_read_write.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tinymesh/tinymesh.h" 4 | 5 | namespace fs = std::filesystem; 6 | namespace tms = tinymesh; 7 | 8 | int main(int argc, char **argv) { 9 | if (argc <= 1) { 10 | printf("usage: %s [input mesh]\n", argv[0]); 11 | return 1; 12 | } 13 | 14 | // Load 15 | tms::Mesh mesh(argv[1]); 16 | 17 | // Save 18 | const fs::path filepath = fs::canonical(fs::path(argv[1])); 19 | const fs::path dirpath = filepath.parent_path(); 20 | const std::string extension = filepath.extension().string(); 21 | const std::string basename = filepath.stem().string(); 22 | const std::string outfile = (dirpath / fs::path((basename + "_copy" + extension).c_str())).string(); 23 | mesh.save(outfile); 24 | printf("Save: %s\n", outfile.c_str()); 25 | } 26 | -------------------------------------------------------------------------------- /examples/cpp/example_remesh.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tinymesh/tinymesh.h" 4 | 5 | namespace fs = std::filesystem; 6 | namespace tms = tinymesh; 7 | 8 | int main(int argc, char **argv) { 9 | if (argc <= 1) { 10 | printf("usage: %s [input mesh] [keep angle]\n", argv[0]); 11 | return 1; 12 | } 13 | 14 | // Load 15 | tms::Mesh mesh(argv[1]); 16 | 17 | // Fill holes & remesh 18 | const double keepAngle = argc > 2 ? std::atof(argv[2]) * Pi / 180.0 : 0.0; 19 | mesh.fillHoles(); 20 | tms::remeshTriangular(mesh, 0.8, 1.333, keepAngle); 21 | 22 | // Save 23 | const fs::path filepath = fs::canonical(fs::path(argv[1])); 24 | const fs::path dirpath = filepath.parent_path(); 25 | const std::string extension = filepath.extension().string(); 26 | const std::string basename = filepath.stem().string(); 27 | const std::string outfile = (dirpath / fs::path((basename + "_remesh" + extension).c_str())).string(); 28 | mesh.save(outfile); 29 | printf("Save: %s\n", outfile.c_str()); 30 | } 31 | -------------------------------------------------------------------------------- /examples/cpp/example_simplify.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tinymesh/tinymesh.h" 4 | 5 | namespace fs = std::filesystem; 6 | namespace tms = tinymesh; 7 | 8 | int main(int argc, char **argv) { 9 | if (argc <= 1) { 10 | printf("usage: %s [input mesh] [ratio]\n", argv[0]); 11 | return 1; 12 | } 13 | 14 | printf("Hello\n"); 15 | 16 | const double ratio = argc <= 2 ? 0.1 : atof(argv[2]); 17 | 18 | // Load 19 | tms::Mesh mesh(argv[1]); 20 | 21 | // Simplify 22 | const int target = (int)(ratio * mesh.numFaces()); 23 | mesh.fillHoles(); 24 | tms::simplifyQEM(mesh, target, 10, true); 25 | tms::remeshTriangular(mesh, 0.0, 1e6); // remesh by keeping #faces 26 | 27 | // Save 28 | const fs::path filepath = fs::canonical(fs::path(argv[1])); 29 | const fs::path dirpath = filepath.parent_path(); 30 | const std::string extension = filepath.extension().string(); 31 | const std::string basename = filepath.stem().string(); 32 | const std::string outfile = (dirpath / fs::path((basename + "_simplify" + extension).c_str())).string(); 33 | mesh.save(outfile); 34 | printf("Save: %s\n", outfile.c_str()); 35 | } 36 | -------------------------------------------------------------------------------- /examples/cpp/example_smooth.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tinymesh/tinymesh.h" 4 | 5 | namespace fs = std::filesystem; 6 | namespace tms = tinymesh; 7 | 8 | int main(int argc, char **argv) { 9 | if (argc <= 1) { 10 | printf("usage: %s [input mesh]\n", argv[0]); 11 | return 1; 12 | } 13 | 14 | // Basename of input file 15 | const fs::path filepath = fs::canonical(fs::path(argv[1])); 16 | const fs::path dirpath = filepath.parent_path(); 17 | const std::string extension = filepath.extension().string(); 18 | const std::string basename = filepath.stem().string(); 19 | 20 | // Smooth (Laplacian) 21 | { 22 | tms::Mesh mesh(argv[1]); 23 | mesh.fillHoles(); 24 | tms::smoothLaplacian(mesh, 0.5, true, 100); 25 | const std::string outfile = (dirpath / fs::path((basename + "_laplace_smooth" + extension).c_str())).string(); 26 | mesh.save(outfile); 27 | printf("Save: %s\n", outfile.c_str()); 28 | } 29 | 30 | // Smooth (Taubin) 31 | { 32 | tms::Mesh mesh(argv[1]); 33 | mesh.fillHoles(); 34 | tms::smoothTaubin(mesh, 0.5, 0.53, 100); 35 | const std::string outfile = (dirpath / fs::path((basename + "_taubin_smooth" + extension).c_str())).string(); 36 | mesh.save(outfile); 37 | printf("Save: %s\n", outfile.c_str()); 38 | } 39 | 40 | // Smooth (implicit fairing) 41 | { 42 | tms::Mesh mesh(argv[1]); 43 | mesh.fillHoles(); 44 | tms::implicitFairing(mesh, 1.0e-2, 1); 45 | const std::string outfile = (dirpath / fs::path((basename + "_implicit_fair" + extension).c_str())).string(); 46 | mesh.save(outfile); 47 | printf("Save: %s\n", outfile.c_str()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/python/viewer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.collections import PolyCollection 4 | 5 | 6 | def normalize(x): 7 | return x / np.linalg.norm(x) 8 | 9 | 10 | def frustum(left, right, bottom, top, znear, zfar): 11 | M = np.zeros((4, 4)) 12 | M[0, 0] = +2.0 * znear / (right - left) 13 | M[1, 1] = +2.0 * znear / (top - bottom) 14 | M[2, 2] = -(zfar + znear) / (zfar - znear) 15 | M[0, 2] = (right + left) / (right - left) 16 | M[2, 1] = (top + bottom) / (top - bottom) 17 | M[2, 3] = -2.0 * znear * zfar / (zfar - znear) 18 | M[3, 2] = -1.0 19 | return M 20 | 21 | 22 | class Viewer(object): 23 | def __init__(self, width=500, height=500, dpi=100): 24 | self.width = width 25 | self.height = height 26 | self.aspect = width / height 27 | self.dpi = 100 28 | self.set_identity() 29 | 30 | def set_identity(self): 31 | self.M = np.eye(4) 32 | self.V = np.eye(4) 33 | self.P = np.eye(4) 34 | 35 | def perspective(self, fovy, aspect, znear, zfar): 36 | h = np.tan(0.5 * np.radians(fovy)) * znear 37 | w = h * aspect 38 | P = frustum(-w, w, -h, h, znear, zfar) 39 | self.P = self.P @ P 40 | 41 | def lookat(self, ex, ey, ez, cx, cy, cz, ux, uy, uz): 42 | e = np.array((ex, ey, ez)) 43 | c = np.array((cx, cy, cz)) 44 | u = np.array((ux, uy, uz)) 45 | z = normalize(e - c) 46 | x = normalize(np.cross(u, z)) 47 | y = normalize(np.cross(z, x)) 48 | rot = np.eye(4) 49 | rot[0, :3] = x 50 | rot[1, :3] = y 51 | rot[2, :3] = z 52 | tran = np.eye(4) 53 | tran[:3, 3] = -e 54 | V = rot @ tran 55 | self.V = self.V @ V 56 | 57 | def translate(self, x, y, z): 58 | M = np.array([ 59 | [1.0, 0.0, 0.0, x], 60 | [0.0, 1.0, 0.0, y], 61 | [0.0, 0.0, 1.0, z], 62 | [0.0, 0.0, 0.0, 1.0], 63 | ]) 64 | self.M = self.M @ M 65 | 66 | def rotate(self, theta, wx, wy, wz): 67 | rad = np.radians(theta) 68 | st = np.sin(rad) 69 | ct = np.cos(rad) 70 | K = np.array([[0.0, -wz, wy], [wz, 0.0, -wx], [-wy, wx, 0.0]]) 71 | R = np.eye(3) + st * K + (1.0 - ct) * (K @ K) 72 | M = np.eye(4) 73 | M[:3, :3] = R 74 | self.M = self.M @ M 75 | 76 | def scale(self, sx, sy, sz): 77 | M = np.eye(4) 78 | M[0, 0] = sx 79 | M[1, 1] = sy 80 | M[2, 2] = sz 81 | self.M = self.M @ M 82 | 83 | def visualization(self, 84 | verts, 85 | faces, 86 | colors=None, 87 | wireframe=False, 88 | shade=True, 89 | title="", 90 | save=False, 91 | filename="output.png"): 92 | V = verts.copy() 93 | F = faces.copy() 94 | if colors is not None: 95 | C = colors[:, :3].copy() 96 | else: 97 | C = 0.7 * np.ones_like(verts) 98 | 99 | ## coordinate transformation 100 | MV = self.V @ self.M 101 | MVP = self.P @ MV 102 | V = V[F] 103 | C = C[F] 104 | 105 | N = np.cross(V[:, 1, :] - V[:, 0, :], V[:, 2, :] - V[:, 0, :]) 106 | N /= np.linalg.norm(N, axis=-1, keepdims=True) 107 | N = N @ MV[:3, :3].T 108 | N /= np.linalg.norm(N, axis=-1, keepdims=True) 109 | L = np.c_[np.zeros(N.shape), np.ones((*N.shape[:-1], 1))] @ MV.T 110 | L = -L[:, :3] / L[:, 3:4] 111 | L /= np.linalg.norm(L, axis=-1, keepdims=True) 112 | NdotL = np.maximum(0.0, np.sum(N * L, axis=-1, keepdims=True)) 113 | 114 | V = np.c_[V, np.ones((*V.shape[:-1], 1))] @ MVP.T 115 | V /= V[:, :, 3:4] 116 | T = V[:, :, :2] 117 | 118 | ## z-sort 119 | Z = -V[:, :, 2].mean(axis=1) 120 | zmin, zmax = Z.min(), Z.max() 121 | Z = (Z - zmin) / (zmax - zmin) 122 | l = np.argsort(Z) 123 | 124 | C = C.mean(axis=1) 125 | if shade: 126 | C *= NdotL 127 | T, C = T[l, :], C[l, :] 128 | 129 | if wireframe: 130 | linewidth = 0.1 131 | edgecolor = "black" 132 | else: 133 | linewidth = 1.0 134 | edgecolor = C 135 | 136 | xsiz = int(self.width / self.dpi) 137 | ysiz = int(self.height / self.dpi) 138 | fig = plt.figure(figsize=(xsiz, ysiz), dpi=self.dpi) 139 | ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, 1], ylim=[-1, 1], aspect=1.0 / self.aspect, frameon=False) 140 | ax.axis("off") 141 | collection = PolyCollection(T, 142 | closed=True, 143 | linewidth=linewidth, 144 | facecolor=C, 145 | edgecolor=edgecolor, 146 | antialiaseds=True) 147 | ax.add_collection(collection) 148 | 149 | if len(title) != 0: 150 | plt.title(title) 151 | 152 | if save: 153 | plt.savefig(filename, bbox_inches="tight") 154 | 155 | plt.show() 156 | 157 | def mesh_visualization(self, mesh, *args, **kwargs): 158 | verts = mesh.get_vertices() 159 | faces = mesh.get_vertex_indices() 160 | verts = np.asarray(verts, dtype="float32") 161 | faces = np.asarray(faces, dtype="uint32").reshape((-1, 3)) 162 | self.visualization(verts, faces, *args, **kwargs) 163 | -------------------------------------------------------------------------------- /figures/bunny_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/bunny_before.png -------------------------------------------------------------------------------- /figures/bunny_hole_fill_adv_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/bunny_hole_fill_adv_front.png -------------------------------------------------------------------------------- /figures/bunny_hole_fill_min_dihedral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/bunny_hole_fill_min_dihedral.png -------------------------------------------------------------------------------- /figures/bunny_holes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/bunny_holes.png -------------------------------------------------------------------------------- /figures/bunny_remesh_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/bunny_remesh_1.png -------------------------------------------------------------------------------- /figures/bunny_remesh_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/bunny_remesh_2.png -------------------------------------------------------------------------------- /figures/dragon_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/dragon_before.png -------------------------------------------------------------------------------- /figures/dragon_simplify_10000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/dragon_simplify_10000.png -------------------------------------------------------------------------------- /figures/dragon_simplify_50000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/dragon_simplify_50000.png -------------------------------------------------------------------------------- /figures/fandisk_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/fandisk_before.png -------------------------------------------------------------------------------- /figures/fandisk_denoise_l0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/fandisk_denoise_l0.png -------------------------------------------------------------------------------- /figures/fandisk_noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatsy/tinymesh/c3f46d036a3a04b84baf236be7b08358ac6124bf/figures/fandisk_noise.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "poetry_core>=1.5.0", "pybind11>=2.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "tinymesh" 7 | version = "0.2.4" 8 | description = "TinyMesh: a light-weight mesh processing library" 9 | 10 | license = "MPL-v2" 11 | 12 | authors = ["Tatsuya Yatagawa "] 13 | readme = 'README.md' 14 | repository = "https://github.com/tatsy/tinymesh" 15 | homepage = "https://github.com/tatsy/tinymesh" 16 | 17 | keywords = ['graphics', 'geometry'] 18 | exclude = ["src/*", "tests/*", "cmake/*", "data/*", "figures/*"] 19 | 20 | [tool.poetry.build] 21 | script = "build_exts.py" 22 | generate-setup-file = true 23 | 24 | [tool.poetry.dependencies] 25 | python = ">=3.8,<=3.12" 26 | setuptools = "^57" 27 | pybind11 = "^2.0" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | black = "*" 31 | flake8 = "*" 32 | mypy = "*" 33 | isort = "*" 34 | pytest = "*" 35 | jupyterlab = "*" 36 | matplotlib = "*" 37 | 38 | numpy = [ 39 | { version = "~1.26.0", python = "^3.12" }, 40 | { version = "*", python = ">=3.8,<3.12" }, 41 | ] 42 | 43 | scipy = [ 44 | { version = "~1.11.2", python = "^3.12" }, 45 | { version = "*", python = ">=3.8,<3.12" }, 46 | ] 47 | 48 | [tool.isort] 49 | profile = "black" 50 | length_sort = "True" 51 | line_length = 120 52 | 53 | [tool.mypy] 54 | python_version = 3.9 55 | follow_imports = "silent" 56 | disallow_any_generics = "True" 57 | strict_optional = "True" 58 | check_untyped_defs = "True" 59 | allow_redefinition = "True" 60 | 61 | [tool.flake8] 62 | max-line-length = 120 63 | ignore = "Q000,E127,E203,E402,W503,W504" 64 | 65 | [tool.black] 66 | target-version = ["py39", "py310", "py311"] 67 | line-length = 120 68 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(tinymesh) 2 | -------------------------------------------------------------------------------- /src/pybind11.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | namespace py = pybind11; 6 | 7 | #include "tinymesh/tinymesh.h" 8 | using namespace tinymesh; 9 | 10 | // type caster: Matrix <-> NumPy-array 11 | namespace pybind11 { 12 | namespace detail { 13 | 14 | template 15 | struct type_caster> { 16 | public: 17 | using Type = Vec; 18 | PYBIND11_TYPE_CASTER(Type, _("Vec")); 19 | 20 | // Conversion part 1 (Python -> C++) 21 | bool load(py::handle src, bool convert) { 22 | if (!convert && !py::array_t::check_(src)) return false; 23 | 24 | auto buf = py::array_t::ensure(src); 25 | if (!buf) return false; 26 | 27 | auto dims = buf.ndim(); 28 | if (dims != 1) return false; 29 | auto elems = buf.shape()[0]; 30 | if (elems != Dims) return false; 31 | 32 | value = Vec(); 33 | const Float *ptr = buf.data(); 34 | for (int d = 0; d < Dims; d++) { 35 | value[d] = ptr[d]; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | // Conversion part 2 (C++ -> Python) 42 | static py::handle cast(const Vec &src, py::return_value_policy policy, py::handle parent) { 43 | std::vector shape(1, Dims); 44 | std::vector strides(1, sizeof(Float)); 45 | py::array a(std::move(shape), std::move(strides), (Float *)&src); 46 | return a.release(); 47 | } 48 | }; 49 | 50 | } // namespace detail 51 | } // namespace pybind11 52 | 53 | PYBIND11_MODULE(tinymesh, m) { 54 | py::class_(m, "Mesh") 55 | .def(py::init<>()) 56 | .def(py::init()) 57 | .def(py::init &, const std::vector &>()) 58 | .def("clone", &Mesh::clone, py::return_value_policy::move) 59 | .def("load", &Mesh::load) 60 | .def("save", &Mesh::save) 61 | .def("vertex", &Mesh::vertex, py::return_value_policy::reference) 62 | .def("face", &Mesh::face, py::return_value_policy::reference) 63 | .def("get_vertices", &Mesh::getVertices) 64 | .def("get_vertex_indices", &Mesh::getVertexIndices) 65 | .def("num_vertices", &Mesh::numVertices) 66 | .def("num_edges", &Mesh::numEdges) 67 | .def("num_halfedges", &Mesh::numHalfedges) 68 | .def("num_faces", &Mesh::numFaces) 69 | .def("fill_holes", &Mesh::fillHoles, "Fill all holes") 70 | .def("verify", &Mesh::verify); 71 | 72 | py::class_>(m, "Vertex") 73 | .def(py::init<>()) 74 | .def("pos", &Vertex::pos) 75 | .def("set_pos", &Vertex::setPos) 76 | .def("normal", &Vertex::normal) 77 | .def("K", &Vertex::K) 78 | .def("H", &Vertex::H) 79 | .def("is_boundary", &Vertex::isBoundary) 80 | .def("is_locked", &Vertex::isLocked) 81 | .def("lock", &Vertex::lock) 82 | .def("unlock", &Vertex::unlock); 83 | 84 | py::class_>(m, "Face") 85 | .def(py::init<>()) 86 | .def("area", &Face::area) 87 | .def("is_boundary", &Face::isBoundary) 88 | .def("is_hole", &Face::isHole) 89 | .def("is_locked", &Face::isLocked); 90 | 91 | /*** Metrics ***/ 92 | m.def("get_hausdorff_distance", &getHausdorffDistance, "Hausdorff distance", py::arg("mesh0"), py::arg("mesh1")); 93 | m.def("get_per_vertex_shortest_distances", &getPerVertexShortestDistances, "Per-vertex shortest distances", 94 | py::arg("src"), py::arg("dst")); 95 | 96 | /*** Utilities ***/ 97 | py::enum_(m, "MeshLaplace") 98 | .value("ADJACENT", MeshLaplace::Adjacent) 99 | .value("COTANGENT", MeshLaplace::Cotangent) 100 | .value("BELKIN08", MeshLaplace::Belkin08); 101 | 102 | m.def("get_mesh_laplacian", &getMeshLaplacian, "Laplacian-Beltrami opeartor", py::arg("mesh"), py::arg("type")); 103 | m.def("get_heat_kernel_signatures", &getHeatKernelSignatures, "Heat kernel signatures", py::arg("mesh"), 104 | py::arg("K") = 300, py::arg("n_times") = 100); 105 | m.def("get_principal_curvatures", &getPrincipalCurvatures, "Principal curvatures", py::arg("mesh"), 106 | py::arg("smooth_tensors") = false); 107 | m.def("get_principal_curvatures_with_derivatives", &getPrincipalCurvaturesWithDerivatives, 108 | "Principal curvatures with derivatives", py::arg("mesh"), py::arg("smooth_tensors") = false); 109 | m.def("get_feature_line_field", &getFeatureLineField, "Feature line field", py::arg("mesh"), 110 | py::arg("smooth_tensors") = false); 111 | m.def("get_feature_line_field_with_flags", getFeatureLineFieldWithFlags, 112 | "Feature line field with ridge/valley flags", py::arg("mesh"), py::arg("smooth_tensors") = false); 113 | 114 | /*** Smoothing ***/ 115 | m.def("smooth_laplacian", &smoothLaplacian, "Laplacian smoothing", py::arg("mesh"), py::arg("epsilon") = 1.0, 116 | py::arg("cotangent_weight") = false, py::arg("iterations") = 3); 117 | m.def("smooth_taubin", &smoothTaubin, "Taubin smoothing", py::arg("mesh"), py::arg("shrink") = 0.5, 118 | py::arg("inflate") = 0.53, py::arg("iterations") = 3); 119 | m.def("implicit_fairing", &implicitFairing, "Implicit fairing", py::arg("mesh"), py::arg("epsilon") = 1.0, 120 | py::arg("iterations") = 1); 121 | 122 | /*** Denoising ***/ 123 | m.def("denoise_normal_gaussian", &denoiseNormalGaussian, "Denoising by normal Gaussian filter", py::arg("mesh"), 124 | py::arg("sigma") = 0.2, py::arg("iterations") = 3); 125 | m.def("denoise_normal_bilateral", &denoiseNormalBilateral, "Denoising by normal bilateral filter", py::arg("mesh"), 126 | py::arg("sigma_c") = 0.2, py::arg("sigma_s") = 0.1, py::arg("iterations") = 3); 127 | m.def("denoise_l0_smooth", &denoiseL0Smooth, "Denoising by L0 smoothing", py::arg("mesh"), py::arg("alpha") = 0.1, 128 | py::arg("beta") = 0.001); 129 | 130 | /*** Remesh ***/ 131 | m.def("remesh_triangular", &remeshTriangular, "Triangle remeshing", py::arg("mesh"), py::arg("short_length") = 0.8, 132 | py::arg("long_length") = 1.333, py::arg("angle_keep_less_than") = 0.0, py::arg("iterations") = 5, 133 | py::arg("verbose") = false); 134 | 135 | /*** Simplification ***/ 136 | m.def("simplify_qem", &simplifyQEM, "QEM-based simplification", py::arg("mesh"), py::arg("n_triangles"), 137 | py::arg("n_trials") = 10, py::arg("verbose") = false); 138 | 139 | /*** Hole filling ***/ 140 | m.def("hole_fill_min_dihedral", &holeFillMinDihedral, "Max-area hole filling", py::arg("mesh"), py::arg("face"), 141 | py::arg("dihedral_bound") = Pi); 142 | m.def("hole_fill_advancing_front", &holeFillAdvancingFront, "Advancing front hole filling", py::arg("mesh"), 143 | py::arg("face")); 144 | m.def("hole_fill_context_coherent", &holeFillContextCoherent, "Context-based coherent surface completion", 145 | py::arg("mesh"), py::arg("maxiters") = 200); 146 | } 147 | -------------------------------------------------------------------------------- /src/tinymesh/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # ---------- 2 | # Common target names 3 | # ---------- 4 | set(EIGEN_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/ext/eigen) 5 | set(SPECTRA_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/ext/spectra/include) 6 | set(TINYOBJ_LOADER_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/ext/tinyobjloader) 7 | set(SOURCE_FILES ${SOURCE_FILES};${TINYOBJ_LOADER_INCLUDE_DIR}/tiny_obj_loader.h) 8 | set(TINYPLY_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/ext/tinyply/source) 9 | set(SOURCE_FILES ${SOURCE_FILES};${TINYPLY_INCLUDE_DIR}/tinyply.h) 10 | 11 | # ---------- 12 | # Components 13 | # ---------- 14 | set(SOURCE_FILES ${SOURCE_FILES} "tinymesh.h") 15 | add_folder(SOURCE_FILES "core") 16 | add_folder(SOURCE_FILES "filters") 17 | add_folder(SOURCE_FILES "ops") 18 | add_folder(SOURCE_FILES "remesh") 19 | add_folder(SOURCE_FILES "restore") 20 | 21 | add_library(${TINYMESH_LIBRARY} STATIC ${SOURCE_FILES}) 22 | 23 | source_group("Source Files" FILES ${SOURCE_FILES}) 24 | 25 | target_include_directories( 26 | ${TINYMESH_LIBRARY} PUBLIC 27 | ${CMAKE_CURRENT_LIST_DIR} 28 | ${OpenMP_INCLUDE_DIRS} 29 | ${EIGEN_INCLUDE_DIR} 30 | ${SPECTRA_INCLUDE_DIR} 31 | ${TINYOBJ_LOADER_INCLUDE_DIR} 32 | ${TINYPLY_INCLUDE_DIR}) 33 | 34 | target_link_libraries( 35 | ${TINYMESH_LIBRARY} PUBLIC 36 | ${CXX_FS_LIBRARY} 37 | ${OpenMP_LIBRARIES}) 38 | 39 | # Disable warning for external files 40 | set(WARNING_DISABLED_CXX_FLAGS) 41 | if (MSVC) 42 | set(WARNING_DISABLED_CXX_FLAGS "${CMAKE_CXX_FLAGS} /w") 43 | else() 44 | set(WARNING_DISABLED_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") 45 | endif() 46 | 47 | set_source_files_properties( 48 | ${CMAKE_CURRENT_LIST_DIR}/core/external.cpp 49 | PROPERTIES 50 | COMPILE_FLAGS ${WARNING_DISABLED_CXX_FLAGS}) 51 | 52 | # Enable profiler options for MSVC 53 | if (MSVC) 54 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi") 55 | set_property(TARGET ${TINYMESH_LIBRARY} APPEND PROPERTY LINK_FLAGS "/DEBUG /PROFILE") 56 | endif() 57 | -------------------------------------------------------------------------------- /src/tinymesh/core/api.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_API_H 6 | #define TINYMESH_API_H 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | static const double Pi = 4.0 * std::atan(1.0); 13 | 14 | // ----------------------------------------------------------------------------- 15 | // Forward declaration 16 | // ----------------------------------------------------------------------------- 17 | namespace tinymesh { 18 | 19 | class Vertex; 20 | class Halfedge; 21 | class Face; 22 | class Mesh; 23 | 24 | } // namespace tinymesh 25 | 26 | // ----------------------------------------------------------------------------- 27 | // API export macro 28 | // ----------------------------------------------------------------------------- 29 | 30 | #if (defined(WIN32) || defined(_WIN32) || defined(WINCE) || defined(__CYGWIN__)) 31 | #if defined(TINYMESH_API_EXPORT) 32 | #define TINYMESH_API __declspec(dllexport) 33 | #define TINYMESH_IMPORTS 34 | #else 35 | #define TINYMESH_API 36 | #define TINYMESH_IMPORTS __declspec(dllimport) 37 | #endif 38 | #elif defined(__GNUC__) && __GNUC__ >= 4 39 | #define TINYMESH_API __attribute__((visibility("default"))) 40 | #define TINYMESH_IMPORTS 41 | #else 42 | #define TINYMESH_API 43 | #define TINYMESH_IMPORTS 44 | #endif 45 | 46 | #endif // TINYMESH_API_H 47 | -------------------------------------------------------------------------------- /src/tinymesh/core/bounds.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_BOUNDS_H 6 | #define TINYMESH_BOUNDS_H 7 | 8 | #include "debug.h" 9 | #include "vec.h" 10 | 11 | namespace tinymesh { 12 | 13 | template 14 | class Bounds { 15 | typedef Vec VecD; 16 | 17 | public: 18 | Bounds() = default; 19 | 20 | Bounds(const VecD &posMin, const VecD &posMax) 21 | : posMin_(posMin) 22 | , posMax_(posMax) { 23 | } 24 | 25 | void merge(const VecD &p) { 26 | posMin_ = std::min(posMin_, p); 27 | posMax_ = std::max(posMax_, p); 28 | } 29 | 30 | double area() const { 31 | static_assert(Dims == 3, "Bounds::area is defined only for 3D bounds!"); 32 | const VecD d = std::abs(posMax_ - posMin_); 33 | return (2.0 * (d[0] * d[1] + d[1] * d[2] + d[2] * d[0])); 34 | } 35 | 36 | bool inside(const VecD &p) const { 37 | for (int d = 0; d < Dims; d++) { 38 | if (p[d] < posMin_[d] || posMax_[d] < p[d]) return false; 39 | } 40 | return true; 41 | } 42 | 43 | int maxExtent() const { 44 | const VecD v = std::abs(posMax_ - posMin_); 45 | int dim = -1; 46 | Float vmax = -1.0; 47 | for (int d = 0; d < Dims; d++) { 48 | if (vmax < v[d]) { 49 | vmax = v[d]; 50 | dim = d; 51 | } 52 | } 53 | 54 | Assertion(dim >= 0, "Something is wrong!"); 55 | return dim; 56 | } 57 | 58 | double distance(const VecD &p) { 59 | const Vec3 vmin = std::abs(posMin_ - p); 60 | const Vec3 vmax = std::abs(posMax_ - p); 61 | double dist = 0.0; 62 | for (int d = 0; d < Dims; d++) { 63 | if (p[d] < posMin_[d] || posMax_[d] < p[d]) { 64 | const double gap = std::min(vmin[d], vmax[d]); 65 | dist += gap * gap; 66 | } 67 | } 68 | return std::sqrt(dist); 69 | } 70 | 71 | static Bounds merge(const Bounds &b0, const Bounds &b1) { 72 | Bounds ret; 73 | ret.posMin_ = std::min(b0.posMin_, b1.posMin_); 74 | ret.posMax_ = std::max(b0.posMax_, b1.posMax_); 75 | return ret; 76 | } 77 | 78 | VecD posMin() const { 79 | return posMin_; 80 | } 81 | VecD posMax() const { 82 | return posMax_; 83 | } 84 | 85 | private: 86 | VecD posMin_ = VecD(1.0e20); 87 | VecD posMax_ = VecD(-1.0e20); 88 | }; 89 | 90 | using Bounds2 = Bounds; 91 | using Bounds3 = Bounds; 92 | 93 | } // namespace tinymesh 94 | 95 | #endif // TINYMESH_BOUNDS_H 96 | -------------------------------------------------------------------------------- /src/tinymesh/core/bvh.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "bvh.h" 3 | 4 | #include 5 | 6 | #include "mesh.h" 7 | 8 | namespace tinymesh { 9 | 10 | struct BucketInfo { 11 | int count; 12 | Bounds3 bounds; 13 | BucketInfo() 14 | : count(0) 15 | , bounds() { 16 | } 17 | }; 18 | 19 | struct ComparePoint { 20 | int dim; 21 | explicit ComparePoint(int d) 22 | : dim(d) { 23 | } 24 | bool operator()(const BVHPrimitiveInfo &a, const BVHPrimitiveInfo &b) const { 25 | return a.centroid[dim] < b.centroid[dim]; 26 | } 27 | }; 28 | 29 | struct CompareToBucket { 30 | int splitBucket, nBuckets, dim; 31 | const Bounds3 ¢roidBounds; 32 | 33 | CompareToBucket(int split, int num, int d, const Bounds3 &b) 34 | : splitBucket(split) 35 | , nBuckets(num) 36 | , dim(d) 37 | , centroidBounds(b) { 38 | } 39 | 40 | bool operator()(const BVHPrimitiveInfo &p) const { 41 | const double cmin = centroidBounds.posMin()[dim]; 42 | const double cmax = centroidBounds.posMax()[dim]; 43 | const double inv = (1.0) / (std::abs(cmax - cmin) + 1.0e-12); 44 | const double diff = std::abs(p.centroid[dim] - cmin); 45 | int b = static_cast(nBuckets * diff * inv); 46 | if (b >= nBuckets) { 47 | b = nBuckets - 1; 48 | } 49 | return b <= splitBucket; 50 | } 51 | }; 52 | 53 | BVH::BVH(const Mesh &mesh) 54 | : BVH{ mesh.getVertices(), mesh.getVertexIndices() } { 55 | } 56 | 57 | BVH::BVH(const std::vector &vertices, const std::vector &indices) { 58 | construct(vertices, indices); 59 | } 60 | 61 | void BVH::construct(const std::vector &vertices, const std::vector &indices) { 62 | tris.clear(); 63 | std::vector buildData; 64 | for (size_t i = 0; i < indices.size(); i += 3) { 65 | const uint32_t i0 = indices[i + 0]; 66 | const uint32_t i1 = indices[i + 1]; 67 | const uint32_t i2 = indices[i + 2]; 68 | const size_t idx = tris.size(); 69 | tris.emplace_back(vertices[i0], vertices[i1], vertices[i2]); 70 | buildData.emplace_back(idx, tris[idx].bounds()); 71 | } 72 | 73 | root = constructRec(buildData, 0, tris.size()); 74 | } 75 | 76 | double BVH::distance(const Vec3 &p) const { 77 | const Vec3 pc = closestPoint(p); 78 | return length(pc - p); 79 | } 80 | 81 | Vec3 BVH::closestPoint(const Vec3 &query) const { 82 | std::stack stk; 83 | stk.push(root); 84 | 85 | double dist = 1.0e20; 86 | Vec3 ret; 87 | while (!stk.empty()) { 88 | BVHNode *node = stk.top(); 89 | stk.pop(); 90 | 91 | if (node->isLeaf()) { 92 | const Vec3 pc = tris[node->primIdx].closestPoint(query); 93 | const double dc = length(query - pc); 94 | if (dc < dist) { 95 | dist = dc; 96 | ret = pc; 97 | } 98 | } else { 99 | // distance to furthest corner 100 | const bool isInside = node->bounds.inside(query); 101 | const double dc = node->bounds.distance(query); 102 | if (isInside || dc <= dist) { 103 | if (node->left) stk.push(node->left); 104 | if (node->right) stk.push(node->right); 105 | } 106 | } 107 | } 108 | return ret; 109 | } 110 | 111 | BVHNode *BVH::constructRec(std::vector &buildData, int start, int end) { 112 | if (start == end) return nullptr; 113 | 114 | BVHNode *node = new BVHNode(); 115 | nodes.emplace_back(node); 116 | 117 | Bounds3 bounds; 118 | for (int i = start; i < end; i++) { 119 | bounds = Bounds3::merge(bounds, buildData[i].bounds); 120 | } 121 | 122 | int nprims = end - start; 123 | if (nprims == 1) { 124 | // Leaf node 125 | node->initLeaf(bounds, buildData[start].primIdx); 126 | } else { 127 | // Fork node 128 | Bounds3 centroidBounds; 129 | for (int i = start; i < end; i++) { 130 | centroidBounds.merge(buildData[i].centroid); 131 | } 132 | 133 | int splitAxis = centroidBounds.maxExtent(); 134 | int mid = (start + end) / 2; 135 | if (nprims <= 8) { 136 | std::nth_element(buildData.begin() + start, buildData.begin() + mid, buildData.begin() + end, 137 | ComparePoint(splitAxis)); 138 | } else { 139 | // Seperate with SAH (surface area heuristics) 140 | const int nBuckets = 16; 141 | BucketInfo buckets[nBuckets]; 142 | 143 | const double cmin = centroidBounds.posMin()[splitAxis]; 144 | const double cmax = centroidBounds.posMax()[splitAxis]; 145 | const double idenom = 1.0 / (std::abs(cmax - cmin) + 1.0e-12); 146 | for (int i = start; i < end; i++) { 147 | const double numer = buildData[i].centroid[splitAxis] - centroidBounds.posMin()[splitAxis]; 148 | int b = static_cast(nBuckets * std::abs(numer) * idenom); 149 | if (b == nBuckets) { 150 | b = nBuckets - 1; 151 | } 152 | 153 | buckets[b].count++; 154 | buckets[b].bounds = Bounds3::merge(buckets[b].bounds, buildData[i].bounds); 155 | } 156 | 157 | double bucketCost[nBuckets - 1] = { 0 }; 158 | for (int i = 0; i < nBuckets - 1; i++) { 159 | Bounds3 b0, b1; 160 | int cnt0 = 0, cnt1 = 0; 161 | for (int j = 0; j <= i; j++) { 162 | b0 = Bounds3::merge(b0, buckets[j].bounds); 163 | cnt0 += buckets[j].count; 164 | } 165 | for (int j = i + 1; j < nBuckets; j++) { 166 | b1 = Bounds3::merge(b1, buckets[j].bounds); 167 | cnt1 += buckets[j].count; 168 | } 169 | bucketCost[i] += 0.125 + (cnt0 * b0.area() + cnt1 * b1.area()) / bounds.area(); 170 | } 171 | 172 | double minCost = bucketCost[0]; 173 | int minCostSplit = 0; 174 | for (int i = 1; i < nBuckets - 1; i++) { 175 | if (minCost > bucketCost[i]) { 176 | minCost = bucketCost[i]; 177 | minCostSplit = i; 178 | } 179 | } 180 | 181 | if (minCost < nprims) { 182 | auto it = std::partition(buildData.begin() + start, buildData.begin() + end, 183 | CompareToBucket(minCostSplit, nBuckets, splitAxis, centroidBounds)); 184 | mid = it - buildData.begin(); 185 | } 186 | } 187 | 188 | BVHNode *left = constructRec(buildData, start, mid); 189 | BVHNode *right = constructRec(buildData, mid, end); 190 | node->initFork(bounds, left, right, splitAxis); 191 | } 192 | return node; 193 | } 194 | 195 | } // namespace tinymesh 196 | -------------------------------------------------------------------------------- /src/tinymesh/core/bvh.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif // _MSC_VER 4 | 5 | #ifndef TINYMESH_BVH_H 6 | #define TINYMESH_BVH_H 7 | 8 | #include 9 | #include 10 | 11 | #include "api.h" 12 | #include "vec.h" 13 | #include "bounds.h" 14 | #include "triangle.h" 15 | 16 | namespace tinymesh { 17 | 18 | struct BVHPrimitiveInfo { 19 | BVHPrimitiveInfo() { 20 | } 21 | BVHPrimitiveInfo(int pid, const Bounds3 &b) 22 | : primIdx(pid) 23 | , centroid() 24 | , bounds(b) { 25 | centroid = (b.posMax() + b.posMin()) * 0.5; 26 | } 27 | 28 | int primIdx; 29 | Vec3 centroid; 30 | Bounds3 bounds; 31 | }; 32 | 33 | struct BVHNode { 34 | void initLeaf(const Bounds3 &b, int pid) { 35 | this->bounds = b; 36 | this->splitAxis = 0; 37 | this->primIdx = pid; 38 | } 39 | 40 | void initFork(const Bounds3 &b, BVHNode *l, BVHNode *r, int axis) { 41 | this->bounds = b; 42 | this->left = l; 43 | this->right = r; 44 | this->splitAxis = axis; 45 | this->primIdx = -1; 46 | } 47 | 48 | bool isLeaf() const { 49 | return primIdx >= 0; 50 | } 51 | 52 | Bounds3 bounds; 53 | BVHNode *left = nullptr; 54 | BVHNode *right = nullptr; 55 | int splitAxis; 56 | int primIdx; 57 | }; 58 | 59 | class TINYMESH_API BVH { 60 | public: 61 | BVH() = default; 62 | BVH(const Mesh &mesh); 63 | BVH(const std::vector &vertices, const std::vector &indices); 64 | void construct(const std::vector &vertices, const std::vector &indices); 65 | 66 | double distance(const Vec3 &p) const; 67 | Vec3 closestPoint(const Vec3 &p) const; 68 | 69 | private: 70 | BVHNode *constructRec(std::vector &buildData, int start, int end); 71 | 72 | BVHNode *root = nullptr; 73 | std::vector tris; 74 | std::vector> nodes; 75 | }; 76 | 77 | } // namespace tinymesh 78 | 79 | #endif // TINYMESH_BVH_H 80 | -------------------------------------------------------------------------------- /src/tinymesh/core/debug.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_DEBUG_H 6 | #define TINYMESH_DEBUG_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if defined(TINYMESH_PYTHON_MODULE) 14 | #include 15 | namespace py = pybind11; 16 | #endif // TINYMESH_PYTHON_MODULE 17 | 18 | // ----------------------------------------------------------------------------- 19 | // ANSI color 20 | // ----------------------------------------------------------------------------- 21 | #define FG_RED "\033[31m" 22 | #define FG_GRN "\033[32m" 23 | #define FG_YEL "\033[33m" 24 | #define FG_BLU "\033[34m" 25 | #define FG_NC "\033[0m" 26 | 27 | // ----------------------------------------------------------------------------- 28 | // Debug output stream 29 | // ----------------------------------------------------------------------------- 30 | class PrintStream { 31 | public: 32 | PrintStream() { 33 | } 34 | 35 | virtual ~PrintStream() { 36 | flush(); 37 | } 38 | 39 | template 40 | PrintStream &operator<<(const Type &s) { 41 | ss << s; 42 | if (ss.str().size() >= bufSize) { 43 | flush(); 44 | } 45 | return *this; 46 | } 47 | 48 | PrintStream &operator<<(std::ostream &(*f)(std::ostream &)) { 49 | ss << f; 50 | flush(); 51 | return *this; 52 | } 53 | 54 | void flush() { 55 | #if !defined(TINYMESH_PYTHON_MODULE) 56 | std::cout << ss.str(); 57 | std::flush(std::cout); 58 | #else 59 | py::print(ss.str(), py::arg("end") = "", py::arg("flush") = true); 60 | #endif 61 | ss.str(""); 62 | } 63 | 64 | private: 65 | std::ostringstream ss; 66 | static const int bufSize = 128; 67 | }; 68 | 69 | // ----------------------------------------------------------------------------- 70 | // Message handlers 71 | // ----------------------------------------------------------------------------- 72 | 73 | template 74 | std::string STR_FMT(const char *format, Args... args) { 75 | #ifdef __clang__ 76 | #pragma clang diagnostic push 77 | #pragma clang diagnostic ignored "-Wformat" 78 | #endif // __clang__ 79 | int size_s = std::snprintf(nullptr, 0, format, args...) + 1; 80 | #ifdef __clang__ 81 | #pragma clang diagnostic pop 82 | #endif // __clang__ 83 | if (size_s <= 0) { 84 | throw std::runtime_error("Error during formatting."); 85 | } 86 | auto size = static_cast(size_s); 87 | auto buf = std::make_unique(size); 88 | #ifdef __clang__ 89 | #pragma clang diagnostic push 90 | #pragma clang diagnostic ignored "-Wformat" 91 | #endif // __clang__ 92 | std::snprintf(buf.get(), size, format, args...); 93 | #ifdef __clang__ 94 | #pragma clang diagnostic pop 95 | #endif // __clang__ 96 | return std::string(buf.get(), buf.get() + size - 1); 97 | } 98 | 99 | template 100 | void Print(const char *format, Args... args) { 101 | PrintStream() << STR_FMT(format, args...); 102 | } 103 | 104 | template 105 | void Info(const char *format, Args... args) { 106 | PrintStream() << FG_BLU << "[INFO] " << FG_NC << STR_FMT(format, args...) << std::endl; 107 | } 108 | 109 | template 110 | void Debug(const char *format, Args... args) { 111 | PrintStream() << FG_GRN << "[DEBUG] " << FG_NC << STR_FMT(format, args...) << std::endl; 112 | } 113 | 114 | template 115 | void Warn(const char *format, Args... args) { 116 | PrintStream() << FG_YEL << "[WARNING] " << FG_NC << STR_FMT(format, args...) << std::endl; 117 | } 118 | 119 | #if !defined(TINYMESH_PYTHON_MODULE) 120 | template 121 | void Error(const char *format, Args... args) { 122 | PrintStream() << FG_RED << "[ERROR] " << FG_NC << STR_FMT(format, args...) << std::endl; 123 | std::terminate(); 124 | } 125 | #else // TINYMESH_PYTHON_MODULE 126 | template 127 | void Error(const char *format, Args... args) { 128 | throw std::runtime_error(STR_FMT(format, args...)); 129 | } 130 | #endif // TINYMESH_PYTHON_MODULE 131 | 132 | // ----------------------------------------------------------------------------- 133 | // Assertion with message 134 | // ----------------------------------------------------------------------------- 135 | 136 | #ifndef __FUNCTION_NAME__ 137 | #if defined(_WIN32) || defined(__WIN32__) 138 | #define __FUNCTION_NAME__ __FUNCTION__ 139 | #else 140 | #define __FUNCTION_NAME__ __func__ 141 | #endif 142 | #endif 143 | 144 | inline std::string ASRT_LABEL(const char *predicate, const char *file, int line, const char *funcname) { 145 | return STR_FMT("Assertion \"%s\" failed in %s L-%d (%s)", predicate, file, line, funcname); 146 | } 147 | 148 | #if defined(TINYMESH_PYTHON_MODULE) 149 | #define Assertion(PREDICATE, ...) \ 150 | do { \ 151 | if (!(PREDICATE)) { \ 152 | throw std::runtime_error(ASRT_LABEL(#PREDICATE, __FILE__, __LINE__, __FUNCTION_NAME__)); \ 153 | } \ 154 | } while (false) 155 | #elif !defined(TINYMESH_DISABLE_ASSERT) 156 | #define Assertion(PREDICATE, ...) \ 157 | do { \ 158 | if (!(PREDICATE)) { \ 159 | std::cerr << ASRT_LABEL(#PREDICATE, __FILE__, __LINE__, __FUNCTION_NAME__) << std::endl; \ 160 | std::abort(); \ 161 | } \ 162 | } while (false) 163 | #else // TINYMESH_DISABLE_ASSERT 164 | #define Assertion(PREDICATE, ...) \ 165 | do { \ 166 | } while (false) 167 | #endif // TINYMESH_DISABLE_ASSERT 168 | 169 | #endif // TINYMESH_DEBUG_H 170 | -------------------------------------------------------------------------------- /src/tinymesh/core/eigen.cpp: -------------------------------------------------------------------------------- 1 | #include "eigen.h" 2 | 3 | #include 4 | 5 | void eigenSVD(const Eigen::MatrixXd &A, Eigen::MatrixXd &U, Eigen::VectorXd &sigma, Eigen::MatrixXd &V) { 6 | Eigen::JacobiSVD svd(A, Eigen::ComputeFullU | Eigen::ComputeFullV); 7 | U = svd.matrixU(); 8 | sigma = svd.singularValues(); 9 | V = svd.matrixV(); 10 | } 11 | -------------------------------------------------------------------------------- /src/tinymesh/core/eigen.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_EIGEN_H 6 | #define TINYMESH_EIGEN_H 7 | 8 | #include 9 | 10 | using FloatType = double; 11 | using IndexType = int64_t; 12 | 13 | #include 14 | using EigenVector2 = Eigen::Matrix; 15 | using EigenVector3 = Eigen::Matrix; 16 | using EigenVector4 = Eigen::Matrix; 17 | using EigenVector = Eigen::Matrix; 18 | using EigenMatrix2 = Eigen::Matrix; 19 | using EigenMatrix3 = Eigen::Matrix; 20 | using EigenMatrix4 = Eigen::Matrix; 21 | using EigenMatrix = Eigen::Matrix; 22 | 23 | //! SVD using Eigen 24 | void eigenSVD(const EigenMatrix &A, EigenMatrix &U, EigenVector &Sigma, EigenMatrix &V); 25 | 26 | // #ifdef EIGEN_ENABLE_SPARSE 27 | #include 28 | using EigenTriplet = Eigen::Triplet; 29 | using EigenSparseVector = Eigen::SparseVector; 30 | using EigenSparseMatrix = Eigen::SparseMatrix; 31 | // #endif // EIGEN_ENABLE_SPARSE 32 | 33 | #endif // TINYMESH_EIGEN_H 34 | -------------------------------------------------------------------------------- /src/tinymesh/core/external.cpp: -------------------------------------------------------------------------------- 1 | #define TINYOBJLOADER_IMPLEMENTATION 2 | #include "tiny_obj_loader.h" 3 | #define TINYPLY_IMPLEMENTATION 4 | #include "tinyply.h" 5 | -------------------------------------------------------------------------------- /src/tinymesh/core/face.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "face.h" 3 | 4 | #include 5 | 6 | #include "debug.h" 7 | #include "vertex.h" 8 | #include "halfedge.h" 9 | #include "triangle.h" 10 | 11 | namespace tinymesh { 12 | 13 | Face::Face() { 14 | } 15 | 16 | bool Face::operator==(const Face &other) const { 17 | bool ret = true; 18 | ret &= (halfedge_ == other.halfedge_); 19 | ret &= (index_ == other.index_); 20 | return ret; 21 | } 22 | 23 | Triangle Face::toTriangle() const { 24 | std::vector vs; 25 | for (auto it = v_begin(); it != v_end(); ++it) { 26 | vs.push_back(it->pos()); 27 | } 28 | 29 | Assertion(vs.size() == 3, "Non-triangle face cannot be converted to Triangle!"); 30 | 31 | return { vs[0], vs[1], vs[2] }; 32 | } 33 | 34 | Vec3 Face::normal() const { 35 | std::vector vs; 36 | for (auto it = v_begin(); it != v_end(); ++it) { 37 | vs.push_back(it->pos()); 38 | } 39 | 40 | const int N = static_cast(vs.size()); 41 | Vec3 norm(0.0); 42 | for (int i = 0; i < N; i++) { 43 | const int prev = (i - 1 + N) % N; 44 | const int next = (i + 1) % N; 45 | const Vec3 &p0 = vs[prev]; 46 | const Vec3 &p1 = vs[i]; 47 | const Vec3 &p2 = vs[next]; 48 | norm += cross(p2 - p1, p0 - p1); 49 | } 50 | return normalize(norm); 51 | } 52 | 53 | double Face::area() const { 54 | std::vector vs; 55 | for (auto it = v_begin(); it != v_end(); ++it) { 56 | vs.push_back(it->pos()); 57 | } 58 | 59 | const int N = static_cast(vs.size()); 60 | double area = 0.0; 61 | for (int i = 1; i < N - 1; i++) { 62 | const Vec3 &p0 = vs[i]; 63 | const Vec3 &p1 = vs[i - 1]; 64 | const Vec3 &p2 = vs[i + 1]; 65 | area += 0.5 * length(cross(p1 - p0, p2 - p0)); 66 | } 67 | return area; 68 | } 69 | 70 | int Face::numCorners() const { 71 | int count = 0; 72 | for (auto it = v_begin(); it != v_end(); ++it) { 73 | count++; 74 | } 75 | return count; 76 | } 77 | 78 | bool Face::isHole() const { 79 | // Face is hole if all the vertices are at the boundary. 80 | for (auto it = this->he_begin(); it != this->he_end(); ++it) { 81 | if (!it->isBoundary()) { 82 | return false; 83 | } 84 | } 85 | return true; 86 | } 87 | 88 | bool Face::isBoundary() const { 89 | for (auto it = this->he_begin(); it != this->he_end(); ++it) { 90 | if (it->isBoundary()) { 91 | return true; 92 | } 93 | } 94 | return false; 95 | } 96 | 97 | bool Face::isLocked() const { 98 | for (auto it = this->v_begin(); it != this->v_end(); ++it) { 99 | if (it->isLocked()) { 100 | return true; 101 | } 102 | } 103 | return false; 104 | } 105 | 106 | Face::VertexIterator Face::v_begin() { 107 | return Face::VertexIterator(halfedge_); 108 | } 109 | 110 | Face::VertexIterator Face::v_end() { 111 | return Face::VertexIterator(nullptr); 112 | } 113 | 114 | Face::ConstVertexIterator Face::v_begin() const { 115 | return Face::ConstVertexIterator(halfedge_); 116 | } 117 | 118 | Face::ConstVertexIterator Face::v_end() const { 119 | return Face::ConstVertexIterator(nullptr); 120 | } 121 | 122 | Face::HalfedgeIterator Face::he_begin() { 123 | return Face::HalfedgeIterator(halfedge_); 124 | } 125 | 126 | Face::HalfedgeIterator Face::he_end() { 127 | return Face::HalfedgeIterator(nullptr); 128 | } 129 | 130 | Face::ConstHalfedgeIterator Face::he_begin() const { 131 | return Face::ConstHalfedgeIterator(halfedge_); 132 | } 133 | 134 | Face::ConstHalfedgeIterator Face::he_end() const { 135 | return Face::ConstHalfedgeIterator(nullptr); 136 | } 137 | 138 | Face::FaceIterator Face::f_begin() { 139 | return Face::FaceIterator(halfedge_); 140 | } 141 | 142 | Face::FaceIterator Face::f_end() { 143 | return Face::FaceIterator(nullptr); 144 | } 145 | 146 | Face::ConstFaceIterator Face::f_begin() const { 147 | return Face::ConstFaceIterator(halfedge_); 148 | } 149 | 150 | Face::ConstFaceIterator Face::f_end() const { 151 | return Face::ConstFaceIterator(nullptr); 152 | } 153 | 154 | // ---------- 155 | // VertexIterator 156 | // ---------- 157 | 158 | Face::VertexIterator::VertexIterator(Halfedge *he) 159 | : halfedge_{ he } 160 | , iter_{ he } { 161 | } 162 | 163 | bool Face::VertexIterator::operator!=(const Face::VertexIterator &it) const { 164 | return iter_ != it.iter_; 165 | } 166 | 167 | Vertex &Face::VertexIterator::operator*() { 168 | return *iter_->src(); 169 | } 170 | 171 | Vertex *Face::VertexIterator::ptr() const { 172 | return iter_->src(); 173 | } 174 | 175 | Vertex *Face::VertexIterator::operator->() const { 176 | return iter_->src(); 177 | } 178 | 179 | Face::VertexIterator &Face::VertexIterator::operator++() { 180 | iter_ = iter_->next(); 181 | if (iter_ == halfedge_) { 182 | iter_ = nullptr; 183 | } 184 | return *this; 185 | } 186 | 187 | Face::VertexIterator Face::VertexIterator::operator++(int) { 188 | Halfedge *tmp = iter_; 189 | iter_ = iter_->next(); 190 | if (iter_ == halfedge_) { 191 | iter_ = nullptr; 192 | } 193 | return Face::VertexIterator(tmp); 194 | } 195 | 196 | // ---------- 197 | // ConstVertexIterator 198 | // ---------- 199 | 200 | Face::ConstVertexIterator::ConstVertexIterator(Halfedge *he) 201 | : halfedge_{ he } 202 | , iter_{ he } { 203 | } 204 | 205 | bool Face::ConstVertexIterator::operator!=(const Face::ConstVertexIterator &it) const { 206 | return iter_ != it.iter_; 207 | } 208 | 209 | const Vertex &Face::ConstVertexIterator::operator*() const { 210 | return *iter_->src(); 211 | } 212 | 213 | const Vertex *Face::ConstVertexIterator::ptr() const { 214 | return iter_->src(); 215 | } 216 | 217 | const Vertex *Face::ConstVertexIterator::operator->() const { 218 | return iter_->src(); 219 | } 220 | 221 | Face::ConstVertexIterator &Face::ConstVertexIterator::operator++() { 222 | iter_ = iter_->next(); 223 | if (iter_ == halfedge_) { 224 | iter_ = nullptr; 225 | } 226 | return *this; 227 | } 228 | 229 | Face::ConstVertexIterator Face::ConstVertexIterator::operator++(int) { 230 | Halfedge *tmp = iter_; 231 | iter_ = iter_->next(); 232 | if (iter_ == halfedge_) { 233 | iter_ = nullptr; 234 | } 235 | return Face::ConstVertexIterator(tmp); 236 | } 237 | 238 | // ---------- 239 | // HalfedgeIterator 240 | // ---------- 241 | 242 | Face::HalfedgeIterator::HalfedgeIterator(Halfedge *he) 243 | : halfedge_{ he } 244 | , iter_{ he } { 245 | } 246 | 247 | bool Face::HalfedgeIterator::operator!=(const Face::HalfedgeIterator &it) const { 248 | return iter_ != it.iter_; 249 | } 250 | 251 | Halfedge &Face::HalfedgeIterator::operator*() { 252 | return *iter_; 253 | } 254 | 255 | Halfedge *Face::HalfedgeIterator::ptr() const { 256 | return iter_; 257 | } 258 | 259 | Halfedge *Face::HalfedgeIterator::operator->() const { 260 | return iter_; 261 | } 262 | 263 | Face::HalfedgeIterator &Face::HalfedgeIterator::operator++() { 264 | iter_ = iter_->next(); 265 | if (iter_ == halfedge_) { 266 | iter_ = nullptr; 267 | } 268 | return *this; 269 | } 270 | 271 | Face::HalfedgeIterator Face::HalfedgeIterator::operator++(int) { 272 | Halfedge *tmp = iter_; 273 | iter_ = iter_->next(); 274 | if (iter_ == halfedge_) { 275 | iter_ = nullptr; 276 | } 277 | return Face::HalfedgeIterator(tmp); 278 | } 279 | 280 | // ---------- 281 | // ConstHalfedgeIterator 282 | // ---------- 283 | 284 | Face::ConstHalfedgeIterator::ConstHalfedgeIterator(Halfedge *he) 285 | : halfedge_{ he } 286 | , iter_{ he } { 287 | } 288 | 289 | bool Face::ConstHalfedgeIterator::operator!=(const Face::ConstHalfedgeIterator &it) const { 290 | return iter_ != it.iter_; 291 | } 292 | 293 | const Halfedge &Face::ConstHalfedgeIterator::operator*() const { 294 | return *iter_; 295 | } 296 | 297 | const Halfedge *Face::ConstHalfedgeIterator::ptr() const { 298 | return iter_; 299 | } 300 | 301 | const Halfedge *Face::ConstHalfedgeIterator::operator->() const { 302 | return iter_; 303 | } 304 | 305 | Face::ConstHalfedgeIterator &Face::ConstHalfedgeIterator::operator++() { 306 | iter_ = iter_->next(); 307 | if (iter_ == halfedge_) { 308 | iter_ = nullptr; 309 | } 310 | return *this; 311 | } 312 | 313 | Face::ConstHalfedgeIterator Face::ConstHalfedgeIterator::operator++(int) { 314 | Halfedge *tmp = iter_; 315 | iter_ = iter_->next(); 316 | if (iter_ == halfedge_) { 317 | iter_ = nullptr; 318 | } 319 | return Face::ConstHalfedgeIterator(tmp); 320 | } 321 | 322 | // ---------- 323 | // FaceIterator 324 | // ---------- 325 | 326 | Face::FaceIterator::FaceIterator(Halfedge *he) 327 | : halfedge_{ he } 328 | , iter_{ he } { 329 | } 330 | 331 | bool Face::FaceIterator::operator!=(const Face::FaceIterator &it) const { 332 | return iter_ != it.iter_; 333 | } 334 | 335 | Face &Face::FaceIterator::operator*() { 336 | return *iter_->rev()->face(); 337 | } 338 | 339 | Face *Face::FaceIterator::ptr() const { 340 | return iter_->rev()->face(); 341 | } 342 | 343 | Face *Face::FaceIterator::operator->() const { 344 | return iter_->rev()->face(); 345 | } 346 | 347 | Face::FaceIterator &Face::FaceIterator::operator++() { 348 | iter_ = iter_->next(); 349 | if (iter_ == halfedge_) { 350 | iter_ = nullptr; 351 | } 352 | return *this; 353 | } 354 | 355 | Face::FaceIterator Face::FaceIterator::operator++(int) { 356 | Halfedge *tmp = iter_; 357 | iter_ = iter_->next(); 358 | if (iter_ == halfedge_) { 359 | iter_ = nullptr; 360 | } 361 | return Face::FaceIterator(tmp); 362 | } 363 | 364 | // ---------- 365 | // ConstFaceIterator 366 | // ---------- 367 | 368 | Face::ConstFaceIterator::ConstFaceIterator(Halfedge *he) 369 | : halfedge_{ he } 370 | , iter_{ he } { 371 | } 372 | 373 | bool Face::ConstFaceIterator::operator!=(const Face::ConstFaceIterator &it) const { 374 | return iter_ != it.iter_; 375 | } 376 | 377 | const Face &Face::ConstFaceIterator::operator*() const { 378 | return *iter_->rev()->face(); 379 | } 380 | 381 | const Face *Face::ConstFaceIterator::ptr() const { 382 | return iter_->rev()->face(); 383 | } 384 | 385 | const Face *Face::ConstFaceIterator::operator->() const { 386 | return iter_->rev()->face(); 387 | } 388 | 389 | Face::ConstFaceIterator &Face::ConstFaceIterator::operator++() { 390 | iter_ = iter_->next(); 391 | if (iter_ == halfedge_) { 392 | iter_ = nullptr; 393 | } 394 | return *this; 395 | } 396 | 397 | Face::ConstFaceIterator Face::ConstFaceIterator::operator++(int) { 398 | Halfedge *tmp = iter_; 399 | iter_ = iter_->next(); 400 | if (iter_ == halfedge_) { 401 | iter_ = nullptr; 402 | } 403 | return Face::ConstFaceIterator(tmp); 404 | } 405 | 406 | } // namespace tinymesh 407 | -------------------------------------------------------------------------------- /src/tinymesh/core/face.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_FACE_H 6 | #define TINYMESH_FACE_H 7 | 8 | #include 9 | 10 | #include "core/api.h" 11 | #include "core/vec.h" 12 | 13 | namespace tinymesh { 14 | 15 | class Triangle; 16 | 17 | class TINYMESH_API Face { 18 | public: 19 | // Forward declaration 20 | class VertexIterator; 21 | class ConstVertexIterator; 22 | class HalfedgeIterator; 23 | class ConstHalfedgeIterator; 24 | class FaceIterator; 25 | class ConstFaceIterator; 26 | 27 | public: 28 | Face(); 29 | Face(const Face &face) = default; 30 | Face(Face &&face) noexcept = default; 31 | virtual ~Face() = default; 32 | 33 | Face &operator=(const Face &face) = default; 34 | Face &operator=(Face &&face) noexcept = default; 35 | 36 | bool operator==(const Face &other) const; 37 | 38 | Triangle toTriangle() const; 39 | 40 | VertexIterator v_begin(); 41 | VertexIterator v_end(); 42 | ConstVertexIterator v_begin() const; 43 | ConstVertexIterator v_end() const; 44 | 45 | HalfedgeIterator he_begin(); 46 | HalfedgeIterator he_end(); 47 | ConstHalfedgeIterator he_begin() const; 48 | ConstHalfedgeIterator he_end() const; 49 | 50 | FaceIterator f_begin(); 51 | FaceIterator f_end(); 52 | ConstFaceIterator f_begin() const; 53 | ConstFaceIterator f_end() const; 54 | 55 | int index() const { 56 | return index_; 57 | } 58 | 59 | Vec3 normal() const; 60 | double area() const; 61 | int numCorners() const; 62 | 63 | bool isHole() const; 64 | bool isBoundary() const; 65 | bool isLocked() const; 66 | 67 | private: 68 | Halfedge *halfedge_ = nullptr; 69 | int index_ = -1; 70 | 71 | friend class Mesh; 72 | }; 73 | 74 | class TINYMESH_API Face::VertexIterator { 75 | public: 76 | explicit VertexIterator(Halfedge *he); 77 | bool operator!=(const VertexIterator &it) const; 78 | Vertex &operator*(); 79 | Vertex *operator->() const; 80 | Vertex *ptr() const; 81 | VertexIterator &operator++(); 82 | VertexIterator operator++(int); 83 | 84 | private: 85 | Halfedge *halfedge_, *iter_; 86 | }; 87 | 88 | class TINYMESH_API Face::ConstVertexIterator { 89 | public: 90 | explicit ConstVertexIterator(Halfedge *he); 91 | bool operator!=(const ConstVertexIterator &it) const; 92 | const Vertex &operator*() const; 93 | const Vertex *operator->() const; 94 | const Vertex *ptr() const; 95 | ConstVertexIterator &operator++(); 96 | ConstVertexIterator operator++(int); 97 | 98 | private: 99 | Halfedge *halfedge_, *iter_; 100 | }; 101 | 102 | class TINYMESH_API Face::HalfedgeIterator { 103 | public: 104 | explicit HalfedgeIterator(Halfedge *he); 105 | bool operator!=(const HalfedgeIterator &it) const; 106 | Halfedge &operator*(); 107 | Halfedge *operator->() const; 108 | Halfedge *ptr() const; 109 | HalfedgeIterator &operator++(); 110 | HalfedgeIterator operator++(int); 111 | 112 | private: 113 | Halfedge *halfedge_, *iter_; 114 | }; 115 | 116 | class TINYMESH_API Face::ConstHalfedgeIterator { 117 | public: 118 | explicit ConstHalfedgeIterator(Halfedge *he); 119 | bool operator!=(const ConstHalfedgeIterator &it) const; 120 | const Halfedge &operator*() const; 121 | const Halfedge *operator->() const; 122 | const Halfedge *ptr() const; 123 | ConstHalfedgeIterator &operator++(); 124 | ConstHalfedgeIterator operator++(int); 125 | 126 | private: 127 | Halfedge *halfedge_, *iter_; 128 | }; 129 | 130 | class TINYMESH_API Face::FaceIterator { 131 | public: 132 | explicit FaceIterator(Halfedge *he); 133 | bool operator!=(const FaceIterator &it) const; 134 | Face &operator*(); 135 | Face *operator->() const; 136 | Face *ptr() const; 137 | FaceIterator &operator++(); 138 | FaceIterator operator++(int); 139 | 140 | private: 141 | Halfedge *halfedge_, *iter_; 142 | }; 143 | 144 | class TINYMESH_API Face::ConstFaceIterator { 145 | public: 146 | explicit ConstFaceIterator(Halfedge *he); 147 | bool operator!=(const ConstFaceIterator &it) const; 148 | const Face &operator*() const; 149 | const Face *operator->() const; 150 | const Face *ptr() const; 151 | ConstFaceIterator &operator++(); 152 | ConstFaceIterator operator++(int); 153 | 154 | private: 155 | Halfedge *halfedge_, *iter_; 156 | }; 157 | 158 | } // namespace tinymesh 159 | 160 | #endif // TINYMESH_FACE_H 161 | -------------------------------------------------------------------------------- /src/tinymesh/core/filesystem.h: -------------------------------------------------------------------------------- 1 | // We haven't checked which filesystem to include yet 2 | #ifndef INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 3 | 4 | // Check for feature test macro for 5 | #if defined(__cpp_lib_filesystem) 6 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 0 7 | 8 | // Check for feature test macro for 9 | #elif defined(__cpp_lib_experimental_filesystem) 10 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1 11 | 12 | // We can't check if headers exist... 13 | // Let's assume experimental to be safe 14 | #elif !defined(__has_include) 15 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1 16 | 17 | // Check if the header "" exists 18 | #elif __has_include() 19 | 20 | // If we're compiling on Visual Studio and are not compiling with C++17, we need to use experimental 21 | #ifdef _MSC_VER 22 | 23 | // Check and include header that defines "_HAS_CXX17" 24 | #if __has_include() 25 | #include 26 | 27 | // Check for enabled C++17 support 28 | #if defined(_HAS_CXX17) && _HAS_CXX17 29 | // We're using C++17, so let's use the normal version 30 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 0 31 | #endif 32 | #endif 33 | 34 | // If the marco isn't defined yet, that means any of the other VS specific checks failed, so we need to use experimental 35 | #ifndef INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 36 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1 37 | #endif 38 | 39 | // Not on Visual Studio. Let's use the normal version 40 | #else // #ifdef _MSC_VER 41 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 0 42 | #endif 43 | 44 | // Check if the header "" exists 45 | #elif __has_include() 46 | #define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1 47 | 48 | // Fail if neither header is available with a nice error message 49 | #else 50 | #error Could not find system header "" or "" 51 | #endif 52 | 53 | // We priously determined that we need the exprimental version 54 | #if INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 55 | // Include it 56 | #include 57 | 58 | // We need the alias from std::experimental::filesystem to std::filesystem 59 | namespace std { 60 | namespace filesystem = experimental::filesystem; 61 | } 62 | 63 | // We have a decent compiler and can use the normal version 64 | #else 65 | // Include it 66 | #include 67 | #endif 68 | 69 | #endif // #ifndef INCLUDE_STD_FILESYSTEM_EXPERIMENTAL -------------------------------------------------------------------------------- /src/tinymesh/core/halfedge.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "halfedge.h" 3 | 4 | #include "core/vertex.h" 5 | #include "core/face.h" 6 | 7 | namespace tinymesh { 8 | 9 | Halfedge::Halfedge() { 10 | } 11 | 12 | bool Halfedge::operator==(const Halfedge &other) const { 13 | bool ret = true; 14 | ret &= (src_ == other.src_); 15 | ret &= (next_ == other.next_); 16 | ret &= (rev_ == other.rev_); 17 | ret &= (face_ == other.face_); 18 | ret &= (index_ == other.index_); 19 | return ret; 20 | } 21 | 22 | double Halfedge::length() const { 23 | return ::length(src()->pos() - dst()->pos()); 24 | } 25 | 26 | double Halfedge::cotWeight() const { 27 | Vertex *v0 = this->src(); 28 | Vertex *v1 = this->dst(); 29 | Vertex *v2 = this->next()->dst(); 30 | Vertex *v3 = this->rev()->next()->dst(); 31 | const Vec3 &p0 = v0->pos(); 32 | const Vec3 &p1 = v1->pos(); 33 | const Vec3 &p2 = v2->pos(); 34 | const Vec3 &p3 = v3->pos(); 35 | const double sin_a = ::length(cross(p0 - p2, p1 - p2)); 36 | const double cos_a = dot(p0 - p2, p1 - p2); 37 | const double cot_a = cos_a / std::max(sin_a, 1.0e-6); 38 | const double sin_b = ::length(cross(p0 - p3, p1 - p3)); 39 | const double cos_b = dot(p0 - p3, p1 - p3); 40 | const double cot_b = cos_b / std::max(sin_b, 1.0e-6); 41 | return 0.5 * (cot_a + cot_b); 42 | } 43 | 44 | bool Halfedge::isLocked() const { 45 | return this->src()->isLocked() && this->dst()->isLocked(); 46 | } 47 | 48 | } // namespace tinymesh 49 | -------------------------------------------------------------------------------- /src/tinymesh/core/halfedge.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_HALFEDGE_H 6 | #define TINYMESH_HALFEDGE_H 7 | 8 | #include 9 | 10 | #include "core/api.h" 11 | 12 | namespace tinymesh { 13 | 14 | class TINYMESH_API Halfedge { 15 | public: 16 | Halfedge(); 17 | Halfedge(const Halfedge &he) = default; 18 | Halfedge(Halfedge &&he) noexcept = default; 19 | virtual ~Halfedge() = default; 20 | 21 | Halfedge &operator=(const Halfedge &he) = default; 22 | Halfedge &operator=(Halfedge &&he) noexcept = default; 23 | 24 | bool operator==(const Halfedge &other) const; 25 | 26 | double length() const; 27 | double cotWeight() const; 28 | 29 | Vertex *src() const { 30 | return src_; 31 | } 32 | Vertex *dst() const { 33 | return next_->src_; 34 | } 35 | Halfedge *next() const { 36 | return next_; 37 | } 38 | Halfedge *prev() const { 39 | Halfedge *iter = next_; 40 | while (iter->next_ != this) { 41 | iter = iter->next_; 42 | } 43 | return iter; 44 | } 45 | 46 | Halfedge *rev() const { 47 | return rev_; 48 | } 49 | Face *face() const { 50 | return face_; 51 | } 52 | int index() const { 53 | return index_; 54 | } 55 | bool isLocked() const; 56 | 57 | bool isBoundary() const { 58 | return isBoundary_; 59 | } 60 | 61 | void setIsBoundary(bool flag) { 62 | isBoundary_ = flag; 63 | } 64 | 65 | private: 66 | Vertex *src_ = nullptr; 67 | Halfedge *next_ = nullptr; 68 | Halfedge *rev_ = nullptr; 69 | Face *face_ = nullptr; 70 | int index_ = -1; 71 | bool isBoundary_ = false; 72 | 73 | friend class Mesh; 74 | }; 75 | 76 | } // namespace tinymesh 77 | 78 | #endif // TINYMESH_HALFEDGE_H 79 | -------------------------------------------------------------------------------- /src/tinymesh/core/mesh.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_MESH_H 6 | #define TINYMESH_MESH_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "core/api.h" 16 | #include "core/vec.h" 17 | #include "core/utils.h" 18 | #include "core/debug.h" 19 | 20 | namespace tinymesh { 21 | 22 | /** 23 | * "Mesh" is a class for halfedge data structure for polygonal mesh. 24 | */ 25 | class TINYMESH_API Mesh { 26 | public: 27 | // Public methods 28 | Mesh(); 29 | explicit Mesh(const std::string &filename); 30 | Mesh(const std::vector &vertices, const std::vector &indices); 31 | 32 | void load(const std::string &filename); 33 | void save(const std::string &filename) const; 34 | void construct(const std::vector &vertices, const std::vector &indices); 35 | Mesh clone(); 36 | 37 | std::vector getVertices() const; 38 | std::vector getVertexIndices() const; 39 | 40 | bool flipHE(Halfedge *he); 41 | bool splitHE(Halfedge *he); 42 | bool collapseHE(Halfedge *he); 43 | bool collapseFace(Face *f); 44 | void fillHoles(); 45 | 46 | double getMeanEdgeLength() const; 47 | double getMeanDihedralAngle() const; 48 | double getMeanFaceArea() const; 49 | 50 | bool verify() const; 51 | 52 | Vertex *vertex(int index) const { 53 | Assertion(index >= 0 && index < (int)vertices_.size(), "Vertex index out of bounds!"); 54 | return vertices_[index].get(); 55 | } 56 | 57 | Halfedge *halfedge(int index) const { 58 | Assertion(index >= 0 && index < (int)halfedges_.size(), "Halfedge index out of bounds!"); 59 | return halfedges_[index].get(); 60 | } 61 | 62 | Face *face(int index) const { 63 | Assertion(index >= 0 && index < (int)faces_.size(), "Face index out of bounds!"); 64 | return faces_[index].get(); 65 | } 66 | 67 | size_t numVertices() const; 68 | size_t numEdges() const; 69 | size_t numHalfedges() const; 70 | size_t numFaces() const; 71 | 72 | private: 73 | // Private methods 74 | void loadOBJ(const std::string &filename); 75 | void loadPLY(const std::string &filename); 76 | void saveOBJ(const std::string &filename) const; 77 | void savePLY(const std::string &filename) const; 78 | 79 | void construct(); 80 | 81 | void addVertex(Vertex *v); 82 | void addHalfedge(Halfedge *he); 83 | void addFace(Face *f); 84 | void removeVertex(Vertex *v); 85 | void removeHalfedge(Halfedge *he); 86 | void removeFace(Face *f); 87 | 88 | void triangulate(Face *f); 89 | bool verifyVertex(Vertex *v) const; 90 | 91 | // ----- Mesh completion methods (in completion.cpp) ----- 92 | 93 | //! This method adds a new triangle using vertices specified with "boundary" and indices in the list. 94 | Face *addNewTriangle(const std::vector &boundary, const std::tuple &tri, 95 | std::unordered_map &pair2he, 96 | std::unordered_map &he2pair); 97 | void holeFillMinDihedral_(Face *face, double dihedralBound = Pi); 98 | void holeFillAdvancingFront_(Face *face); 99 | 100 | // Private parameters 101 | std::vector> vertices_; 102 | //std::vector> edges_; 103 | std::vector> halfedges_; 104 | std::vector> faces_; 105 | std::vector indices_; 106 | 107 | // Friend methods 108 | friend TINYMESH_API void holeFillMinDihedral(Mesh &mesh, Face *face, double dihedralBound); 109 | friend TINYMESH_API void holeFillAdvancingFront(Mesh &mesh, Face *face); 110 | }; 111 | 112 | } // namespace tinymesh 113 | 114 | #endif // TINYMESH_MESH_H 115 | -------------------------------------------------------------------------------- /src/tinymesh/core/openmp.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef COMMON_OPENMP_H 6 | #define COMMON_OPENMP_H 7 | 8 | #if defined(_OPENMP) 9 | #include 10 | #if defined(_MSC_VER) 11 | #define omp_pragma __pragma(omp parallel for) 12 | #define omp_critical __pragma(omp critical) 13 | #else 14 | #define omp_pragma _Pragma("omp parallel for") 15 | #define omp_critical _Pragma("omp critical") 16 | #define omp_atomic _Pragma("omp atomic") 17 | #endif 18 | #define omp_parallel_for omp_pragma for 19 | #define omp_lock_t omp_lock_t 20 | #define omp_init_lock(lock) omp_init_lock(lock) 21 | #define omp_destroy_lock(lock) omp_destroy_lock(lock) 22 | #define omp_lock(lock) omp_set_lock(lock) 23 | #define omp_unlock(lock) omp_unset_lock(lock) 24 | #else 25 | #define omp_set_num_threads(n) 26 | #define omp_get_thread_num() 0 27 | #define omp_get_max_threads() 1 28 | #define omp_get_num_threads() 1 29 | #define omp_parallel_for for 30 | #define omp_critical 31 | #define omp_lock_t int 32 | #define omp_init_lock(lock) 33 | #define omp_destroy_lock(lock) 34 | #define omp_lock(lock) 35 | #define omp_unlock(lock) 36 | #endif 37 | 38 | #endif // COMMON_OPENMP_H 39 | -------------------------------------------------------------------------------- /src/tinymesh/core/progress.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef PROGRESS_H 6 | #define PROGRESS_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "debug.h" 15 | 16 | class ProgressBar { 17 | public: 18 | ProgressBar() { 19 | } 20 | 21 | ProgressBar(int total) { 22 | m_step = 0; 23 | m_total = total; 24 | m_description = ""; 25 | start = std::chrono::system_clock::now(); 26 | } 27 | 28 | virtual ~ProgressBar() { 29 | } 30 | 31 | template 32 | void set_description(const char *format, Args... args) { 33 | m_description = STR_FMT(format, args...); 34 | } 35 | 36 | void step(int n = 1) { 37 | m_step += n; 38 | auto now = std::chrono::system_clock::now(); 39 | 40 | const double percent = 100.0 * m_step / m_total; 41 | const int64_t elapsed = std::chrono::duration_cast(now - start).count(); 42 | const double time_msec_per_step = (double)elapsed / (double)m_step; 43 | const double rest_time = time_msec_per_step * (m_total - m_step); 44 | 45 | const int n_min = (int)(elapsed / 1000.0) / 60; 46 | const int n_sec = (int)(elapsed / 1000.0) % 60; 47 | const int r_min = (int)(rest_time / 1000.0) / 60; 48 | const int r_sec = (int)(rest_time / 1000.0) % 60; 49 | 50 | std::string it_text = ""; 51 | const int steps_per_sec = (int)(1000.0 / time_msec_per_step); 52 | if (steps_per_sec > 0) { 53 | std::ostringstream oss; 54 | if (steps_per_sec < 1000) { 55 | oss << steps_per_sec; 56 | } else { 57 | oss << "1000+"; 58 | } 59 | oss << "it/s"; 60 | it_text = oss.str(); 61 | } else { 62 | std::ostringstream oss; 63 | oss << std::setprecision(2); 64 | oss << time_msec_per_step / 1000.0 << "s/it"; 65 | it_text = oss.str(); 66 | } 67 | 68 | const int tick = (int)(m_width * m_step / m_total); 69 | std::string pbar = std::string(tick, '='); 70 | if (tick != m_width) { 71 | pbar += ">"; 72 | pbar += std::string(m_width - tick - 1, ' '); 73 | } 74 | 75 | if (m_step == m_total || m_step % std::max(1, (m_total / 1000)) == 0) { 76 | Print("\r"); 77 | if (!m_description.empty()) { 78 | Print("%s ", m_description.c_str()); 79 | } 80 | Print("[%3d%%]|%s| %d/%d [%02d:%02d<%02d:%02d, %s]", (int)percent, pbar.c_str(), m_step, m_total, n_min, 81 | n_sec, r_min, r_sec, it_text.c_str()); 82 | } 83 | 84 | if (m_step == m_total) { 85 | Print("\n"); 86 | } 87 | } 88 | 89 | void finish() { 90 | if (m_step != m_total) { 91 | step(m_total - m_step); 92 | } 93 | } 94 | 95 | private: 96 | const int m_width = 40; 97 | int m_step, m_total; 98 | std::string m_description = ""; 99 | std::chrono::system_clock::time_point start; 100 | }; 101 | 102 | #endif // PROGRESS_H 103 | -------------------------------------------------------------------------------- /src/tinymesh/core/triangle.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "triangle.h" 3 | 4 | namespace tinymesh { 5 | 6 | double Triangle::distance(const Vec3 &p) const { 7 | return length(p - closestPoint(p)); 8 | } 9 | 10 | Vec3 Triangle::closestPoint(const Vec3 &p) const { 11 | const Vec3 ab = p1 - p0; 12 | const Vec3 ac = p2 - p0; 13 | const Vec3 ap = p - p0; 14 | 15 | const double d1 = dot(ab, ap); 16 | const double d2 = dot(ac, ap); 17 | if (d1 <= 0.0 && d2 <= 0.0) return p0; //#1 18 | 19 | const Vec3 bp = p - p2; 20 | const double d3 = dot(ab, bp); 21 | const double d4 = dot(ac, bp); 22 | if (d3 >= 0.0 && d4 <= d3) return p1; //#2 23 | 24 | const Vec3 cp = p - p2; 25 | const double d5 = dot(ab, cp); 26 | const double d6 = dot(ac, cp); 27 | if (d6 >= 0.0 && d5 <= d6) return p2; //#3 28 | 29 | const double vc = d1 * d4 - d3 * d2; 30 | if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) { 31 | const double v = d1 / (d1 - d3); 32 | return p0 + v * ab; //#4 33 | } 34 | 35 | const double vb = d5 * d2 - d1 * d6; 36 | if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) { 37 | const double v = d2 / (d2 - d6); 38 | return p0 + v * ac; //#5 39 | } 40 | 41 | const double va = d3 * d6 - d5 * d4; 42 | if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) { 43 | const double v = (d4 - d3) / ((d4 - d3) + (d5 - d6)); 44 | return p1 + v * (p2 - p1); //#6 45 | } 46 | 47 | const double denom = 1.0 / (va + vb + vc); 48 | const double v = vb * denom; 49 | const double w = vc * denom; 50 | return p0 + v * ab + w * ac; //#0 51 | } 52 | 53 | Bounds3 Triangle::bounds() const { 54 | Bounds3 ret; 55 | ret.merge(p0); 56 | ret.merge(p1); 57 | ret.merge(p2); 58 | return ret; 59 | } 60 | 61 | } // namespace tinymesh 62 | -------------------------------------------------------------------------------- /src/tinymesh/core/triangle.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_TRIANGLE_H 6 | #define TINYMESH_TRIANGLE_H 7 | 8 | #include "api.h" 9 | #include "vec.h" 10 | #include "bounds.h" 11 | 12 | namespace tinymesh { 13 | 14 | class TINYMESH_API Triangle { 15 | public: 16 | Triangle() = default; 17 | Triangle(const Vec3 &p0, const Vec3 &p1, const Vec3 &p2) 18 | : p0(p0) 19 | , p1(p1) 20 | , p2(p2) { 21 | } 22 | 23 | double distance(const Vec3 &p) const; 24 | Vec3 closestPoint(const Vec3 &p) const; 25 | Bounds3 bounds() const; 26 | 27 | private: 28 | Vec3 p0, p1, p2; 29 | }; 30 | 31 | } // namespace tinymesh 32 | 33 | #endif // TINYMESH_TRIANGLE_H 34 | -------------------------------------------------------------------------------- /src/tinymesh/core/utils.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_UTILS_H 6 | #define TINYMESH_UTILS_H 7 | 8 | #include 9 | #include 10 | 11 | #include "vec.h" 12 | #include "eigen.h" 13 | 14 | //! Pair of indices (i.e., unsigned int) 15 | using IndexPair = std::pair; 16 | 17 | namespace std { 18 | 19 | //! Hash type for IndexPair 20 | template <> 21 | struct hash { 22 | std::size_t operator()(const IndexPair &k) const { 23 | return std::get<0>(k) ^ std::get<1>(k); 24 | } 25 | }; 26 | 27 | } // namespace std 28 | 29 | //! Local coordinate frame 30 | using LocalFrame = std::tuple; 31 | 32 | //! Rodrigues rotation formula 33 | inline EigenMatrix3 matrixCrossProd(const Vec3 &w) { 34 | EigenMatrix3 K; 35 | K << 0.0, -w.z(), w.y(), // 1st row 36 | w.z(), 0.0, -w.x(), // 2nd row 37 | -w.y(), w.x(), 0.0; // 3rd row 38 | return K; 39 | } 40 | 41 | inline EigenMatrix3 rotationAxisAngle(double theta, const Vec3 &axis) { 42 | EigenMatrix3 I = EigenMatrix3::Identity(); 43 | EigenMatrix3 K = matrixCrossProd(axis); 44 | return I + std::sin(theta) * K + (1.0 - std::cos(theta)) * K * K; 45 | } 46 | 47 | //! Rotate vector v0 to vector v1 48 | inline EigenMatrix3 rotationFromTwoVectors(const Vec3 &v0, const Vec3 &v1) { 49 | const Vec3 outer = cross(v0, v1); 50 | const double theta = std::atan2(length(outer), dot(v0, v1)); 51 | if (std::abs(theta) < 1.0e-12) { 52 | return EigenMatrix3::Identity(); 53 | } 54 | const Vec3 axis = normalize(outer); 55 | return rotationAxisAngle(theta, axis); 56 | } 57 | 58 | #endif // TINYMESH_UTILS_H 59 | -------------------------------------------------------------------------------- /src/tinymesh/core/vec.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_VEC_H 6 | #define TINYMESH_VEC_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | template 16 | class Vec { 17 | static_assert(std::is_floating_point::value, "Vector base type must be floating point number!"); 18 | static_assert(Dims > 1, "Vector dimension must be more than 1!"); 19 | 20 | using EigenVectorType = Eigen::Matrix; 21 | 22 | public: 23 | Vec() { 24 | std::fill(elems.begin(), elems.end(), Float(0.0)); 25 | } 26 | 27 | explicit Vec(Float x) { 28 | std::fill(elems.begin(), elems.end(), x); 29 | } 30 | 31 | explicit Vec(Float x, Float y, Float z = 0.0, Float w = 0.0) { 32 | std::fill(elems.begin(), elems.end(), w); 33 | 34 | elems[0] = x; 35 | if (Dims >= 1) elems[1] = y; 36 | if (Dims >= 2) elems[2] = z; 37 | } 38 | 39 | explicit Vec(const Vec &other) { 40 | std::copy(other.elems.begin(), other.elems.end(), elems.begin()); 41 | } 42 | 43 | Vec &operator=(const Vec other) { 44 | std::copy(other.elems.begin(), other.elems.end(), elems.begin()); 45 | return *this; 46 | } 47 | 48 | Vec(const EigenVectorType &v) { 49 | for (int d = 0; d < Dims; d++) { 50 | elems[d] = v(d); 51 | } 52 | } 53 | 54 | Vec &operator=(const EigenVectorType &v) { 55 | for (int d = 0; d < Dims; d++) { 56 | elems[d] = v(d); 57 | } 58 | return *this; 59 | } 60 | 61 | operator EigenVectorType() const { 62 | EigenVectorType v; 63 | for (int d = 0; d < Dims; d++) { 64 | v(d) = elems[d]; 65 | } 66 | return v; 67 | } 68 | 69 | bool operator==(const Vec &other) const { 70 | if (elems.size() != other.elems.size()) { 71 | return false; 72 | } 73 | 74 | for (int i = 0; i < Dims; i++) { 75 | if (elems[i] != other.elems[i]) { 76 | return false; 77 | } 78 | } 79 | 80 | return true; 81 | } 82 | 83 | bool operator!=(const Vec &other) const { 84 | return !this->operator==(other); 85 | } 86 | 87 | Vec &operator+=(const Vec &other) { 88 | if (elems.size() != other.elems.size()) { 89 | throw std::runtime_error("Dimensions do not match!"); 90 | } 91 | 92 | for (size_t i = 0; i < Dims; i++) { 93 | elems[i] += other.elems[i]; 94 | } 95 | 96 | return *this; 97 | } 98 | 99 | Vec operator-() const { 100 | Vec ret; 101 | for (size_t i = 0; i < elems.size(); i++) { 102 | ret.elems[i] = -elems[i]; 103 | } 104 | return ret; 105 | } 106 | 107 | Vec &operator-=(const Vec &other) { 108 | return this->operator+=(-other); 109 | } 110 | 111 | Vec &operator*=(const Vec &other) { 112 | if (elems.size() != other.elems.size()) { 113 | throw std::runtime_error("Dimensions do not match!"); 114 | } 115 | 116 | for (int i = 0; i < Dims; i++) { 117 | elems[i] *= other.elems[i]; 118 | } 119 | 120 | return *this; 121 | } 122 | 123 | Vec &operator*=(Float s) { 124 | for (int i = 0; i < Dims; i++) { 125 | elems[i] *= s; 126 | } 127 | 128 | return *this; 129 | } 130 | 131 | Vec &operator/=(const Vec &other) { 132 | if (elems.size() != other.elems.size()) { 133 | throw std::runtime_error("Dimensions do not match!"); 134 | } 135 | 136 | for (int i = 0; i < Dims; i++) { 137 | if (other.elems[i] == 0.0) { 138 | throw std::runtime_error("Zero division!"); 139 | } 140 | elems[i] /= other.elems[i]; 141 | } 142 | 143 | return *this; 144 | } 145 | 146 | Vec &operator/=(double s) { 147 | if (s == 0.0) { 148 | throw std::runtime_error("Zero division"); 149 | } 150 | 151 | for (int i = 0; i < Dims; i++) { 152 | elems[i] /= s; 153 | } 154 | 155 | return *this; 156 | } 157 | 158 | Float &operator[](int i) { 159 | return elems[i]; 160 | } 161 | 162 | Float operator[](int i) const { 163 | return elems[i]; 164 | } 165 | 166 | template 167 | typename std::enable_if= 1, Dummy>::type x() const { 168 | return elems[0]; 169 | } 170 | 171 | template 172 | typename std::enable_if= 2, Dummy>::type y() const { 173 | return elems[1]; 174 | } 175 | 176 | template 177 | typename std::enable_if= 3, Dummy>::type z() const { 178 | return elems[2]; 179 | } 180 | 181 | template 182 | typename std::enable_if= 4, Dummy>::type w() const { 183 | return elems[3]; 184 | } 185 | 186 | private: 187 | std::array elems; 188 | }; 189 | 190 | // Type definition 191 | using Vec2 = Vec; 192 | using Vec3 = Vec; 193 | 194 | // standard output 195 | template 196 | std::ostream &operator<<(std::ostream &os, const Vec &v) { 197 | os << "[ "; 198 | for (int d = 0; d < Dims; d++) { 199 | os << v[d]; 200 | if (d != Dims - 1) os << ", "; 201 | } 202 | os << " ]"; 203 | return os; 204 | } 205 | 206 | // Basic arithmetics 207 | template 208 | Vec operator+(const Vec &v1, const Vec &v2) { 209 | auto ret = v1; 210 | ret += v2; 211 | return ret; 212 | } 213 | 214 | template 215 | Vec operator-(const Vec &v1, const Vec &v2) { 216 | auto ret = v1; 217 | ret -= v2; 218 | return ret; 219 | } 220 | 221 | template 222 | Vec operator-(const Vec &v, Float s) { 223 | auto ret = v; 224 | ret *= s; 225 | return ret; 226 | } 227 | 228 | template 229 | Vec operator-(Float s, const Vec &v) { 230 | auto ret = v; 231 | ret *= s; 232 | return ret; 233 | } 234 | template 235 | Vec operator*(const Vec &v1, const Vec &v2) { 236 | auto ret = v1; 237 | ret *= v2; 238 | return ret; 239 | } 240 | 241 | template 242 | Vec operator*(const Vec &v, Float s) { 243 | auto ret = v; 244 | ret *= s; 245 | return ret; 246 | } 247 | 248 | template 249 | Vec operator*(Float s, const Vec &v) { 250 | auto ret = v; 251 | ret *= s; 252 | return ret; 253 | } 254 | 255 | template 256 | Vec operator/(const Vec &v1, const Vec &v2) { 257 | auto ret = v1; 258 | ret /= v2; 259 | return ret; 260 | } 261 | 262 | template 263 | Vec operator/(const Vec &v, Float s) { 264 | auto ret = v; 265 | ret /= s; 266 | return ret; 267 | } 268 | 269 | template 270 | Vec operator*(const Eigen::Matrix &m, const Vec &v) { 271 | return Vec(m * Eigen::Matrix(v)); 272 | } 273 | 274 | // GLSL like vector arithmetics 275 | template 276 | Float dot(const Vec &v1, const Vec &v2) { 277 | Float ret = 0.0; 278 | for (int i = 0; i < Dims; i++) { 279 | ret += v1[i] * v2[i]; 280 | } 281 | return ret; 282 | } 283 | 284 | template 285 | Vec cross(const Vec &v1, const Vec &v2, 286 | typename std::enable_if::type * = 0) { 287 | const Float x = v1[1] * v2[2] - v1[2] * v2[1]; 288 | const Float y = v1[2] * v2[0] - v1[0] * v2[2]; 289 | const Float z = v1[0] * v2[1] - v1[1] * v2[0]; 290 | return Vec{ x, y, z }; 291 | } 292 | 293 | template 294 | Float length(const Vec &v) { 295 | return std::sqrt(dot(v, v)); 296 | } 297 | 298 | template 299 | Vec normalize(const Vec &v) { 300 | return v / length(v); 301 | } 302 | 303 | // Arithmetics for 3D vectors 304 | 305 | //! Angle a-b-c in radian (smaller side) 306 | template 307 | double angle(const Vec &a, const Vec &b, const Vec &c, 308 | typename std::enable_if::type * = 0) { 309 | const Vec3 e0 = a - b; 310 | const Vec3 e1 = c - b; 311 | return std::atan2(length(cross(e0, e1)), dot(e0, e1)); 312 | } 313 | 314 | //! check if the triangle is obtuse 315 | template 316 | bool obtuse(const Vec &a, const Vec &b, const Vec &c, 317 | typename std::enable_if::type * = 0) { 318 | const Float l0 = length(b - a); 319 | const Float l1 = length(c - b); 320 | const Float l2 = length(a - c); 321 | std::array ls = { l0, l1, l2 }; 322 | std::sort(ls.begin(), ls.end(), std::less()); 323 | return ls[0] * ls[0] + ls[1] * ls[1] < ls[2] * ls[2]; 324 | } 325 | 326 | //! cotangent for angle a-b-c 327 | template 328 | Float cot(const Vec &a, const Vec &b, const Vec &c, 329 | typename std::enable_if::type * = 0) { 330 | const Vec3 e0 = a - b; 331 | const Vec3 e1 = c - b; 332 | return dot(e0, e1) / (length(cross(e0, e1)) + (Float)1.0e-20); 333 | } 334 | 335 | template 336 | Float dihedral(const Vec &v1, const Vec &v2, const Vec &v3, 337 | const Vec &v1rev, typename std::enable_if::type * = 0) { 338 | using V = Vec; 339 | const V e12 = v2 - v1; 340 | const V e13 = v3 - v1; 341 | const V n1 = cross(e12, e13); 342 | const Float l1 = length(n1); 343 | const V e42 = v2 - v1rev; 344 | const V e43 = v3 - v1rev; 345 | const V n4 = cross(e42, e43); 346 | const Float l4 = length(n4); 347 | if (l1 == 0.0 || l4 == 0.0) { 348 | return 0.0; 349 | } 350 | 351 | const double cosTheta = dot(n1, n4) / (l1 * l4); 352 | return std::acos(std::max(-1.0, std::min(cosTheta, 1.0))); 353 | } 354 | 355 | namespace std { 356 | 357 | // Hash 358 | template 359 | struct hash> { 360 | std::size_t operator()(const Vec &v) const { 361 | std::size_t h = 0; 362 | for (int i = 0; i < Dims; i++) { 363 | h = std::hash()(v[i]) ^ (h << 1); 364 | } 365 | return h; 366 | } 367 | }; 368 | 369 | template 370 | Vec abs(const Vec &v) { 371 | Vec ret; 372 | for (int d = 0; d < Dims; d++) { 373 | ret[d] = std::abs(v[d]); 374 | } 375 | return ret; 376 | } 377 | 378 | template 379 | Vec min(const Vec &v0, const Vec &v1) { 380 | Vec v; 381 | for (int d = 0; d < Dims; d++) { 382 | v[d] = std::min(v0[d], v1[d]); 383 | } 384 | return v; 385 | } 386 | 387 | template 388 | Vec max(const Vec &v0, const Vec &v1) { 389 | Vec v; 390 | for (int d = 0; d < Dims; d++) { 391 | v[d] = std::max(v0[d], v1[d]); 392 | } 393 | return v; 394 | } 395 | 396 | } // namespace std 397 | 398 | #endif // TINYMESH_VEC_H 399 | -------------------------------------------------------------------------------- /src/tinymesh/core/vertex.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_VERTEX_H 6 | #define TINYMESH_VERTEX_H 7 | 8 | #include 9 | 10 | #include "core/api.h" 11 | #include "core/vec.h" 12 | 13 | namespace tinymesh { 14 | 15 | class TINYMESH_API Vertex { 16 | public: 17 | // Forward declaration 18 | class VertexIterator; 19 | class ConstVertexIterator; 20 | class HalfedgeIterator; 21 | class ConstHalfedgeIterator; 22 | class FaceIterator; 23 | class ConstFaceIterator; 24 | 25 | public: 26 | Vertex(); 27 | Vertex(const Vertex &v) = default; 28 | Vertex(Vertex &&p) noexcept = default; 29 | explicit Vertex(const Vec3 &v); 30 | virtual ~Vertex() = default; 31 | 32 | Vertex &operator=(const Vertex &p) = default; 33 | Vertex &operator=(Vertex &&p) noexcept = default; 34 | 35 | bool operator==(const Vertex &other) const; 36 | 37 | //! Number of connected vertices 38 | int degree() const; 39 | 40 | //! Compute vertex normal with those of surrounding faces 41 | Vec3 normal() const; 42 | 43 | //! Obtuse-safe Volonoi area calculation [Meyer et al. 2003] 44 | double volonoiArea() const; 45 | 46 | //! Volonoi area in specified face [Meyer et al. 2003] 47 | double volonoiArea(const Face *const f) const; 48 | 49 | /** 50 | * Gaussian curvature for vertex [Meyer et al. 2003] 51 | */ 52 | double K() const; 53 | /** 54 | * Mean curvature for vertex [Meyer et al. 2003] 55 | */ 56 | double H() const; 57 | 58 | VertexIterator v_begin(); 59 | VertexIterator v_end(); 60 | ConstVertexIterator v_begin() const; 61 | ConstVertexIterator v_end() const; 62 | 63 | HalfedgeIterator he_begin(); 64 | HalfedgeIterator he_end(); 65 | ConstHalfedgeIterator he_begin() const; 66 | ConstHalfedgeIterator he_end() const; 67 | 68 | FaceIterator f_begin(); 69 | FaceIterator f_end(); 70 | ConstFaceIterator f_begin() const; 71 | ConstFaceIterator f_end() const; 72 | 73 | Vec3 pos() const { 74 | return pos_; 75 | } 76 | void setPos(const Vec3 &pos) { 77 | pos_ = pos; 78 | } 79 | 80 | int index() const { 81 | return index_; 82 | } 83 | 84 | bool isBoundary() const; 85 | 86 | bool isLocked() const { 87 | return isLocked_; 88 | } 89 | 90 | void lock() { 91 | isLocked_ = true; 92 | } 93 | 94 | void unlock() { 95 | isLocked_ = false; 96 | } 97 | 98 | private: 99 | Vec3 pos_; 100 | Halfedge *halfedge_ = nullptr; 101 | int index_ = -1; 102 | bool isLocked_ = false; 103 | 104 | friend class Mesh; 105 | }; 106 | 107 | /** 108 | * VertexIterator 109 | * @detail Traverse neighboring vertices in the clockwise order. 110 | */ 111 | class TINYMESH_API Vertex::VertexIterator { 112 | public: 113 | explicit VertexIterator(Halfedge *he); 114 | bool operator!=(const VertexIterator &it) const; 115 | Vertex &operator*(); 116 | Vertex *ptr() const; 117 | Vertex *operator->() const; 118 | VertexIterator &operator++(); 119 | VertexIterator operator++(int); 120 | 121 | private: 122 | Halfedge *halfedge_, *iter_; 123 | }; 124 | 125 | /** 126 | * ConstVertexIterator 127 | * @detail Traverse neighboring vertices in the clockwise order. 128 | */ 129 | class TINYMESH_API Vertex::ConstVertexIterator { 130 | public: 131 | explicit ConstVertexIterator(Halfedge *he); 132 | bool operator!=(const ConstVertexIterator &it) const; 133 | const Vertex &operator*() const; 134 | const Vertex *ptr() const; 135 | const Vertex *operator->() const; 136 | ConstVertexIterator &operator++(); 137 | ConstVertexIterator operator++(int); 138 | 139 | private: 140 | Halfedge *halfedge_, *iter_; 141 | }; 142 | 143 | /** 144 | * HalfedgeIterator 145 | * @detail Traverse outward halfedges in the clockwise order. 146 | */ 147 | class TINYMESH_API Vertex::HalfedgeIterator { 148 | public: 149 | HalfedgeIterator(Halfedge *he); 150 | bool operator!=(const HalfedgeIterator &it) const; 151 | Halfedge &operator*(); 152 | Halfedge *ptr() const; 153 | Halfedge *operator->() const; 154 | HalfedgeIterator &operator++(); 155 | HalfedgeIterator operator++(int); 156 | 157 | private: 158 | Halfedge *halfedge_, *iter_; 159 | }; 160 | 161 | /** 162 | * ConstHalfedgeIterator 163 | * @detail Traverse outward halfedges in the clockwise order. 164 | */ 165 | 166 | class TINYMESH_API Vertex::ConstHalfedgeIterator { 167 | public: 168 | ConstHalfedgeIterator(Halfedge *he); 169 | bool operator!=(const ConstHalfedgeIterator &it) const; 170 | const Halfedge &operator*() const; 171 | const Halfedge *ptr() const; 172 | const Halfedge *operator->() const; 173 | ConstHalfedgeIterator &operator++(); 174 | ConstHalfedgeIterator operator++(int); 175 | 176 | private: 177 | Halfedge *halfedge_, *iter_; 178 | }; 179 | 180 | /** 181 | * FaceIterator 182 | * @detail Traverse neighboring faces in clockwise order. 183 | */ 184 | class TINYMESH_API Vertex::FaceIterator { 185 | public: 186 | FaceIterator(Halfedge *he); 187 | bool operator!=(const FaceIterator &it) const; 188 | Face &operator*(); 189 | Face *ptr() const; 190 | Face *operator->() const; 191 | FaceIterator &operator++(); 192 | FaceIterator operator++(int); 193 | 194 | private: 195 | Halfedge *halfedge_, *iter_; 196 | }; 197 | 198 | /** 199 | * ConstFaceIterator 200 | * @detail Traverse neighboring faces in clockwise order. 201 | */ 202 | class TINYMESH_API Vertex::ConstFaceIterator { 203 | public: 204 | ConstFaceIterator(Halfedge *he); 205 | bool operator!=(const ConstFaceIterator &it) const; 206 | const Face &operator*() const; 207 | const Face *ptr() const; 208 | const Face *operator->() const; 209 | ConstFaceIterator &operator++(); 210 | ConstFaceIterator operator++(int); 211 | 212 | private: 213 | Halfedge *halfedge_, *iter_; 214 | }; 215 | 216 | } // namespace tinymesh 217 | 218 | #endif // TINYMESH_VERTEX_H 219 | -------------------------------------------------------------------------------- /src/tinymesh/filters/denoise.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "filters.h" 3 | 4 | #include 5 | 6 | #include "core/debug.h" 7 | #include "core/openmp.h" 8 | #include "core/mesh.h" 9 | #include "core/vertex.h" 10 | #include "core/halfedge.h" 11 | #include "core/face.h" 12 | 13 | #define EIGEN_ENABLE_SPARSE 14 | #include "core/eigen.h" 15 | #include 16 | 17 | namespace { 18 | 19 | inline double K(double x, double sigma) { 20 | const double sigma2 = sigma * sigma; 21 | const double coef = 1.0 / (std::sqrt(2.0 * Pi) * sigma); 22 | if (x < 2.0 * sigma) { 23 | return coef * std::exp(-x * x / sigma2); 24 | } 25 | 26 | if (x < 4.0 * sigma) { 27 | const double a = (4.0 - std::abs(x) / sigma); 28 | return coef * (1.0 / (16.0 * std::exp(2.0))) * (a * a * a * a); 29 | } 30 | 31 | return 0.0; 32 | } 33 | 34 | } // anonymous namespace 35 | 36 | namespace tinymesh { 37 | 38 | void denoiseNormalGaussian(Mesh &mesh, double sigma, int iterations) { 39 | // Average edge length 40 | double avgEdge = mesh.getMeanEdgeLength(); 41 | 42 | for (int it = 0; it < iterations; it++) { 43 | // Smooth vertex positions 44 | smoothTaubin(mesh); 45 | 46 | // Compute normals, centroids, and areas 47 | const int nf = (int)mesh.numFaces(); 48 | std::vector normals(nf); 49 | std::vector centroids(nf); 50 | std::vector areas(nf); 51 | omp_parallel_for(int i = 0; i < nf; i++) { 52 | Face *f = mesh.face(i); 53 | std::vector vs; 54 | for (auto vit = f->v_begin(); vit != f->v_end(); ++vit) { 55 | vs.push_back(vit->pos()); 56 | } 57 | 58 | if (vs.size() != 3) { 59 | Error("Mesh is not triangular! Call \"mesh.triangulate()\" first!"); 60 | } 61 | 62 | const Vec3 outer = cross(vs[1] - vs[0], vs[2] - vs[0]); 63 | normals[i] = normalize(outer); 64 | centroids[i] = (vs[0] + vs[1] + vs[2]) / 3.0; 65 | areas[i] = 0.5 * length(outer); 66 | } 67 | 68 | // Filter 69 | std::vector newNormals(nf); 70 | omp_parallel_for(int i = 0; i < nf; i++) { 71 | Face *f = mesh.face(i); 72 | Vec3 norm(0.0); 73 | for (auto fit = f->f_begin(); fit != f->f_end(); ++fit) { 74 | const int j = fit->index(); 75 | const double d = length(centroids[i] - centroids[j]); 76 | const double weight = K(d, sigma * avgEdge) * areas[j]; 77 | norm += normals[j] * weight; 78 | } 79 | 80 | const double l = length(norm); 81 | if (l != 0.0) { 82 | newNormals[i] = norm / l; 83 | } 84 | } 85 | 86 | // Update vertex positions 87 | const int nv = (int)mesh.numVertices(); 88 | omp_parallel_for(int i = 0; i < nv; i++) { 89 | Vertex *v = mesh.vertex(i); 90 | 91 | double sumArea = 0.0; 92 | Vec3 incr(0.0); 93 | for (auto fit = v->f_begin(); fit != v->f_end(); ++fit) { 94 | const int j = fit->index(); 95 | sumArea += areas[j]; 96 | incr += (areas[j] * dot(newNormals[j], centroids[j] - v->pos())) * newNormals[j]; 97 | } 98 | incr /= sumArea; 99 | v->setPos(v->pos() + incr); 100 | } 101 | } 102 | } 103 | 104 | void denoiseNormalBilateral(Mesh &mesh, double sigmaCenter, double sigmaNormal, int iterations) { 105 | // Average edge length 106 | double avgEdge = mesh.getMeanEdgeLength(); 107 | 108 | for (int it = 0; it < iterations; it++) { 109 | // Smooth vertex positions 110 | smoothTaubin(mesh); 111 | 112 | // Compute normals, centroids, and areas 113 | const int nf = (int)mesh.numFaces(); 114 | std::vector normals(nf); 115 | std::vector centroids(nf); 116 | std::vector areas(nf); 117 | omp_parallel_for(int i = 0; i < nf; i++) { 118 | Face *f = mesh.face(i); 119 | std::vector vs; 120 | for (auto vit = f->v_begin(); vit != f->v_end(); ++vit) { 121 | vs.push_back(vit->pos()); 122 | } 123 | 124 | if (vs.size() != 3) { 125 | Error("Mesh is not triangular! Call \"mesh.triangulate()\" first!"); 126 | } 127 | 128 | const Vec3 outer = cross(vs[1] - vs[0], vs[2] - vs[0]); 129 | normals[i] = normalize(outer); 130 | centroids[i] = (vs[0] + vs[1] + vs[2]) / 3.0; 131 | areas[i] = 0.5 * length(outer); 132 | } 133 | 134 | // Filter 135 | std::vector newNormals(nf); 136 | omp_parallel_for(int i = 0; i < nf; i++) { 137 | Face *f = mesh.face(i); 138 | Vec3 norm(0.0); 139 | for (auto fit = f->f_begin(); fit != f->f_end(); ++fit) { 140 | const int j = fit->index(); 141 | const double d = length(centroids[i] - centroids[j]); 142 | const double eta = areas[j]; 143 | const double Wc = K(d, sigmaCenter * avgEdge); 144 | const double nd = length(normals[i] - normals[j]); 145 | const double Ws = K(nd, sigmaNormal); 146 | const double weight = eta * Wc * Ws; 147 | norm += normals[j] * weight; 148 | } 149 | 150 | const double l = length(norm); 151 | if (l != 0.0) { 152 | newNormals[i] = norm / l; 153 | } 154 | } 155 | 156 | // Update vertex positions 157 | const int nv = (int)mesh.numVertices(); 158 | omp_parallel_for(int i = 0; i < nv; i++) { 159 | Vertex *v = mesh.vertex(i); 160 | 161 | double sumArea = 0.0; 162 | Vec3 incr(0.0); 163 | for (auto fit = v->f_begin(); fit != v->f_end(); ++fit) { 164 | const int j = fit->index(); 165 | sumArea += areas[j]; 166 | incr += (areas[j] * dot(newNormals[j], centroids[j] - v->pos())) * newNormals[j]; 167 | } 168 | incr /= sumArea; 169 | v->setPos(v->pos() + incr); 170 | } 171 | } 172 | } 173 | 174 | void denoiseL0Smooth(Mesh &mesh, double alpha, double beta) { 175 | const int ne = (int)mesh.numEdges(); 176 | const int nv = (int)mesh.numVertices(); 177 | const double avgEdge = mesh.getMeanEdgeLength(); 178 | const double avgDihed = mesh.getMeanDihedralAngle(); 179 | 180 | // List unique halfedges 181 | std::unordered_set uniqueHEs; 182 | for (int i = 0; i < mesh.numHalfedges(); i++) { 183 | Halfedge *he = mesh.halfedge(i); 184 | if (uniqueHEs.count(he) != 0) continue; 185 | if (uniqueHEs.count(he->rev()) != 0) continue; 186 | uniqueHEs.insert(he); 187 | } 188 | std::vector edges(uniqueHEs.begin(), uniqueHEs.end()); 189 | Assertion(edges.size() == ne, "Collecting unique edges inconsistent!"); 190 | 191 | std::vector tripR; 192 | for (size_t e = 0; e < edges.size(); e++) { 193 | Halfedge *he = edges[e]; 194 | Halfedge *rev = he->rev(); 195 | 196 | Vertex *vh1 = he->src(); 197 | Vertex *vh2 = he->next()->dst(); 198 | Vertex *vh3 = rev->src(); 199 | Vertex *vh4 = rev->next()->dst(); 200 | 201 | const int i1 = vh1->index(); 202 | const int i2 = vh2->index(); 203 | const int i3 = vh3->index(); 204 | const int i4 = vh4->index(); 205 | tripR.emplace_back(e, i1, 1.0); 206 | tripR.emplace_back(e, i2, -1.0); 207 | tripR.emplace_back(e, i3, 1.0); 208 | tripR.emplace_back(e, i4, -1.0); 209 | } 210 | 211 | EigenSparseMatrix R(ne, nv); 212 | R.setFromTriplets(tripR.begin(), tripR.end()); 213 | 214 | std::vector tripI; 215 | for (int i = 0; i < nv; i++) { 216 | tripI.emplace_back(i, i, 1.0); 217 | } 218 | EigenSparseMatrix I(nv, nv); 219 | I.setFromTriplets(tripI.begin(), tripI.end()); 220 | 221 | // Store vertex positions to Eigen Matrix 222 | EigenMatrix pVec, pVecInit; 223 | pVec.resize(nv, 3); 224 | pVecInit.resize(nv, 3); 225 | for (int i = 0; i < nv; i++) { 226 | const Vec3 p = mesh.vertex(i)->pos(); 227 | pVec(i, 0) = p[0]; 228 | pVec(i, 1) = p[1]; 229 | pVec(i, 2) = p[2]; 230 | pVecInit(i, 0) = p[0]; 231 | pVecInit(i, 1) = p[1]; 232 | pVecInit(i, 2) = p[2]; 233 | } 234 | 235 | // Solve 236 | const double bmax = 1.0e3 * avgEdge; 237 | const double lambda = 0.02 * avgEdge * avgEdge * avgDihed; 238 | const double mu = std::sqrt(2.0); 239 | alpha = alpha * avgEdge; 240 | beta = beta * avgEdge; 241 | 242 | // Smooth vertex positions 243 | smoothTaubin(mesh); 244 | 245 | while (beta < bmax) { 246 | // Construct sparse matrix D 247 | std::vector tripD; 248 | for (int e = 0; e < edges.size(); e++) { 249 | Halfedge *he = edges[e]; 250 | Halfedge *rev = he->rev(); 251 | 252 | Vertex *vh1 = he->src(); 253 | Vertex *vh2 = he->next()->dst(); 254 | Vertex *vh3 = rev->src(); 255 | Vertex *vh4 = rev->next()->dst(); 256 | 257 | const int i1 = vh1->index(); 258 | const int i2 = vh2->index(); 259 | const int i3 = vh3->index(); 260 | const int i4 = vh4->index(); 261 | 262 | const Vec3 p1 = vh1->pos(); 263 | const Vec3 p2 = vh2->pos(); 264 | const Vec3 p3 = vh3->pos(); 265 | const Vec3 p4 = vh4->pos(); 266 | 267 | const double S123 = 0.5 * length(cross(p2 - p1, p2 - p3)); 268 | const double S134 = 0.5 * length(cross(p4 - p1, p4 - p3)); 269 | double l13 = length(p1 - p3); 270 | 271 | const double coef1 = 272 | (S123 * dot(p4 - p3, p3 - p1) + S134 * dot(p1 - p3, p3 - p2)) / (l13 * l13 * (S123 + S134)); 273 | const double coef2 = S134 / (S123 + S134); 274 | const double coef3 = 275 | (S123 * dot(p3 - p1, p1 - p4) + S134 * dot(p2 - p1, p1 - p3)) / (l13 * l13 * (S123 + S134)); 276 | const double coef4 = S123 / (S123 + S134); 277 | 278 | tripD.emplace_back(e, i1, coef1); 279 | tripD.emplace_back(e, i2, coef2); 280 | tripD.emplace_back(e, i3, coef3); 281 | tripD.emplace_back(e, i4, coef4); 282 | } 283 | 284 | EigenSparseMatrix D(ne, nv); 285 | D.setFromTriplets(tripD.begin(), tripD.end()); 286 | 287 | // Solve sub-problem for delta 288 | EigenMatrix y = D * pVec; 289 | EigenMatrix yNorm2 = y.array().square().rowwise().sum().replicate(1, 3); 290 | EigenMatrix yNorm = yNorm2.array().sqrt().matrix(); 291 | EigenMatrix delta = (yNorm2.array() < (lambda / beta)).select(0.0, y); 292 | 293 | // Solve sub-problem for positions 294 | EigenSparseMatrix A = I + alpha * R.transpose() * R + beta * D.transpose() * D; 295 | EigenMatrix bb = pVecInit + beta * D.transpose() * delta; 296 | 297 | Eigen::BiCGSTAB cg; 298 | cg.compute(A); 299 | pVec = cg.solve(bb); 300 | if (cg.info() != Eigen::Success) { 301 | Warn("Solve linear system failed\n"); 302 | } 303 | 304 | // Update 305 | beta *= mu; 306 | alpha *= 0.5; 307 | } 308 | 309 | // Update vertex in a mesh 310 | for (int i = 0; i < nv; i++) { 311 | const Vec3 newPos(pVec(i, 0), pVec(i, 1), pVec(i, 2)); 312 | mesh.vertex(i)->setPos(newPos); 313 | } 314 | } 315 | 316 | } // namespace tinymesh 317 | -------------------------------------------------------------------------------- /src/tinymesh/filters/filters.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_FILTERS_H 6 | #define TINYMESH_FILTERS_H 7 | 8 | #include "core/api.h" 9 | #include "core/mesh.h" 10 | 11 | namespace tinymesh { 12 | 13 | /** 14 | * Laplacian smoothing 15 | */ 16 | TINYMESH_API void smoothLaplacian(Mesh &mesh, double strength = 1.0, bool cotangentWeight = false, int iterations = 3); 17 | 18 | /** 19 | * Taubin smoothing [Taubin et al. 1995] 20 | */ 21 | TINYMESH_API void smoothTaubin(Mesh &mesh, double shrink = 0.5, double inflate = 0.53, int iterations = 3); 22 | 23 | /** 24 | * Implicit fairing [Gesbrun et al. 1999] 25 | */ 26 | TINYMESH_API void implicitFairing(Mesh &mesh, double lambda = 1.0, int iterations = 1); 27 | 28 | /** 29 | * Denoising by normal Gaussian filter [Ohtake et al. 2001] 30 | */ 31 | TINYMESH_API void denoiseNormalGaussian(Mesh &mesh, double sigma = 0.2, int iterations = 5); 32 | 33 | /** 34 | * Denoising by normal bilateral filter [Zhen et al. 2011] 35 | */ 36 | TINYMESH_API void denoiseNormalBilateral(Mesh &mesh, double sigmaCenter = 0.2, double sigmaNormal = 0.1, 37 | int iterations = 5); 38 | 39 | /** 40 | * Denoising by L0 smoothing [He and Schaefer 2013] 41 | */ 42 | TINYMESH_API void denoiseL0Smooth(Mesh &mesh, double alpha = 0.1, double beta = 1.0e-3); 43 | 44 | } // namespace tinymesh 45 | 46 | #endif // TINYMESH_FILTERS_H 47 | -------------------------------------------------------------------------------- /src/tinymesh/filters/smooth.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "filters.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | using ScalarType = double; 11 | using IndexType = int64_t; 12 | using Triplet = Eigen::Triplet; 13 | using SparseMatrix = Eigen::SparseMatrix; 14 | 15 | #include "core/debug.h" 16 | #include "core/openmp.h" 17 | #include "core/face.h" 18 | #include "core/vertex.h" 19 | #include "core/halfedge.h" 20 | 21 | namespace tinymesh { 22 | 23 | void smoothLaplacian(Mesh &mesh, double epsilon, bool cotangentWeight, int iterations) { 24 | for (int it = 0; it < iterations; it++) { 25 | // Volonoi tessellation 26 | const int nv = (int)mesh.numVertices(); 27 | std::vector centroids(nv); 28 | 29 | // Compute centroids and tangent planes 30 | omp_parallel_for(int i = 0; i < nv; i++) { 31 | Vertex *v = mesh.vertex(i); 32 | std::vector pts; 33 | Vec3 cent(0.0); 34 | double sumWgt = 0.0; 35 | for (auto it = v->he_begin(); it != v->he_end(); ++it) { 36 | double weight = 1.0; 37 | if (cotangentWeight) { 38 | weight = it->cotWeight(); 39 | } 40 | cent += weight * it->dst()->pos(); 41 | sumWgt += weight; 42 | } 43 | centroids[i] = cent / sumWgt; 44 | } 45 | 46 | // Update vertex positions 47 | omp_parallel_for(int i = 0; i < (int)mesh.numVertices(); i++) { 48 | Vertex *v = mesh.vertex(i); 49 | if (v->isBoundary() || v->isLocked()) { 50 | continue; 51 | } 52 | 53 | const Vec3 pt = v->pos(); 54 | const Vec3 e = centroids[i] - pt; 55 | const Vec3 newPos = (1.0 - epsilon) * pt + epsilon * (pt + e); 56 | v->setPos(newPos); 57 | } 58 | } 59 | } 60 | 61 | void smoothTaubin(Mesh &mesh, double shrink, double inflate, int iterations) { 62 | for (int it = 0; it < iterations * 2; it++) { 63 | // Volonoi tessellation 64 | const int nv = (int)mesh.numVertices(); 65 | std::vector centroids(nv); 66 | 67 | // Compute centroids and tangent planes 68 | omp_parallel_for(int i = 0; i < nv; i++) { 69 | Vertex *v = mesh.vertex(i); 70 | 71 | // Collect surrounding vertices 72 | std::vector pts; 73 | for (auto vit = v->v_begin(); vit != v->v_end(); ++vit) { 74 | pts.push_back(vit->pos()); 75 | } 76 | 77 | // Compute centroids 78 | Vec3 cent(0.0); 79 | for (int i = 0; i < (int)pts.size(); i++) { 80 | cent += pts[i]; 81 | } 82 | centroids[i] = cent / (double)pts.size(); 83 | } 84 | 85 | // Update vertex positions 86 | const double epsilon = it % 2 == 0 ? shrink : -inflate; 87 | omp_parallel_for(int i = 0; i < (int)mesh.numVertices(); i++) { 88 | Vertex *v = mesh.vertex(i); 89 | if (v->isBoundary() || v->isLocked()) { 90 | continue; 91 | } 92 | 93 | const Vec3 pt = v->pos(); 94 | const Vec3 e = centroids[i] - pt; 95 | const Vec3 newPos = (1.0 - epsilon) * v->pos() + epsilon * (pt + e); 96 | v->setPos(newPos); 97 | } 98 | } 99 | } 100 | 101 | void implicitFairing(Mesh &mesh, double epsilon, int iterations) { 102 | /* 103 | * This method is based on the following paper. The normalized version in Sec.5.5 is used. 104 | * Desbrun et al. "Implicit Fairing of Irregular Meshes using Diffusion and Curvature Flow", 1999. 105 | */ 106 | 107 | for (int it = 0; it < iterations; it++) { 108 | // Indexing vertices 109 | const int n_verts = (int)mesh.numVertices(); 110 | std::unordered_map v2i; 111 | for (int i = 0; i < n_verts; i++) { 112 | v2i.insert(std::make_pair(mesh.vertex(i), i)); 113 | } 114 | 115 | // Compute diffusion matrix 116 | Eigen::MatrixXd X(n_verts, 3); 117 | std::vector triplets; 118 | 119 | for (int i = 0; i < n_verts; i++) { 120 | Vertex *v = mesh.vertex(i); 121 | 122 | // Compute weights 123 | double sumW = 0.0; 124 | std::vector tripletsInColumn; 125 | for (auto he_it = v->he_begin(); he_it != v->he_end(); ++he_it) { 126 | const double W = he_it->cotWeight(); 127 | tripletsInColumn.emplace_back(i, he_it->dst()->index(), W); 128 | if (std::isnan(W) || std::isinf(W)) { 129 | Warn("NaN of inf matrix entry is detedted!"); 130 | } 131 | sumW += W; 132 | } 133 | 134 | for (const auto &t : tripletsInColumn) { 135 | triplets.emplace_back(t.row(), t.col(), t.value() / sumW); 136 | } 137 | triplets.emplace_back(i, i, -1.0); 138 | 139 | X(i, 0) = v->pos()[0]; 140 | X(i, 1) = v->pos()[1]; 141 | X(i, 2) = v->pos()[2]; 142 | } 143 | 144 | // Solve sparse linear system 145 | SparseMatrix K(n_verts, n_verts); 146 | K.setFromTriplets(triplets.begin(), triplets.end()); 147 | 148 | SparseMatrix I(n_verts, n_verts); 149 | I.setIdentity(); 150 | 151 | SparseMatrix A = I - epsilon * K; 152 | 153 | Eigen::BiCGSTAB cg; 154 | cg.setTolerance(1.0e-6); 155 | cg.setMaxIterations(50); 156 | cg.compute(A); 157 | 158 | Eigen::MatrixXd Xnext(n_verts, 3); 159 | Xnext = cg.solve(X); 160 | 161 | for (int i = 0; i < n_verts; i++) { 162 | const Vec3 newpos = Vec3(Xnext(i, 0), Xnext(i, 1), Xnext(i, 2)); 163 | mesh.vertex(i)->setPos(newpos); 164 | } 165 | } 166 | } 167 | 168 | } // namespace tinymesh 169 | -------------------------------------------------------------------------------- /src/tinymesh/ops/metrics.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "ops.h" 3 | 4 | #include 5 | 6 | #include "core/bvh.h" 7 | #include "core/vertex.h" 8 | #include "core/mesh.h" 9 | 10 | namespace tinymesh { 11 | 12 | double getHausdorffDistance(const Mesh &m0, const Mesh &m1) { 13 | EigenVector dist01 = getPerVertexShortestDistances(m0, m1); 14 | EigenVector dist10 = getPerVertexShortestDistances(m1, m0); 15 | 16 | const double dist01max = dist01.maxCoeff(); 17 | const double dist10max = dist10.maxCoeff(); 18 | return std::max(dist01max, dist10max); 19 | } 20 | 21 | EigenVector getPerVertexShortestDistances(const Mesh &src, const Mesh &dst) { 22 | const int N = (int)src.numVertices(); 23 | EigenVector distances(N); 24 | 25 | BVH bvh(dst); 26 | for (size_t i = 0; i < N; i++) { 27 | const Vec3 query = src.vertex(i)->pos(); 28 | distances(i) = bvh.distance(query); 29 | } 30 | 31 | return distances; 32 | } 33 | 34 | } // namespace tinymesh 35 | -------------------------------------------------------------------------------- /src/tinymesh/ops/operators.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "ops.h" 3 | 4 | #include 5 | 6 | #include "core/debug.h" 7 | #include "core/vertex.h" 8 | #include "core/halfedge.h" 9 | 10 | namespace tinymesh { 11 | 12 | void getMeshLaplacianAdjacent(const Mesh &mesh, EigenSparseMatrix &L) { 13 | const int N = mesh.numVertices(); 14 | std::vector triplets; 15 | for (int i = 0; i < N; i++) { 16 | Vertex *v = mesh.vertex(i); 17 | int valence = 0; 18 | for (auto it = v->v_begin(); it != v->v_end(); ++it) { 19 | const int j = it->index(); 20 | triplets.emplace_back(i, j, -1.0); 21 | valence++; 22 | } 23 | triplets.emplace_back(i, i, valence); 24 | } 25 | L.resize(N, N); 26 | L.setFromTriplets(triplets.begin(), triplets.end()); 27 | } 28 | 29 | void getMeshLaplacianCotangent(const Mesh &mesh, EigenSparseMatrix &L) { 30 | const int N = mesh.numVertices(); 31 | std::vector triplets; 32 | for (int i = 0; i < N; i++) { 33 | Vertex *v = mesh.vertex(i); 34 | double sumWgt = 0.0; 35 | for (auto it = v->he_begin(); it != v->he_end(); ++it) { 36 | const int j = it->dst()->index(); 37 | const double weight = it->cotWeight(); 38 | triplets.emplace_back(i, j, -weight); 39 | sumWgt += weight; 40 | } 41 | triplets.emplace_back(i, i, sumWgt); 42 | } 43 | L.resize(N, N); 44 | L.setFromTriplets(triplets.begin(), triplets.end()); 45 | } 46 | 47 | /** 48 | * Reference: 49 | * Belkin et al., "Discrete Laplacian Operator on Meshed Surfaces," 2008. 50 | */ 51 | void getMeshLaplacianBelkin08(const Mesh &mesh, EigenSparseMatrix &L) { 52 | // Compute average edge length 53 | double avgEdgeLength = mesh.getMeanEdgeLength(); 54 | 55 | // Construct sparse matrix 56 | const int N = mesh.numVertices(); 57 | std::vector triplets; 58 | std::vector areas; 59 | for (int i = 0; i < N; i++) { 60 | Vertex *v = mesh.vertex(i); 61 | double sumWgt = 0.0; 62 | double area = 0.0; 63 | for (auto it = v->he_begin(); it != v->he_end(); ++it) { 64 | // Gaussian weight 65 | Vertex *u = it->dst(); 66 | const double h = avgEdgeLength; 67 | const double norm = length(v->pos() - u->pos()); 68 | const double weight = std::exp(-norm * norm / (4 * h)) / (4.0 * Pi * h); 69 | const int j = u->index(); 70 | triplets.emplace_back(i, j, -weight); 71 | sumWgt += weight; 72 | // Area 73 | Vertex *w = it->next()->dst(); 74 | area += length(cross(u->pos() - v->pos(), w->pos() - v->pos())) / 6.0; 75 | } 76 | triplets.emplace_back(i, i, sumWgt); 77 | areas.emplace_back(i, i, area); 78 | } 79 | EigenSparseMatrix W(N, N); 80 | W.setFromTriplets(triplets.begin(), triplets.end()); 81 | L = W; 82 | 83 | // EigenSparseMatrix A(N, N); 84 | // A.setFromTriplets(areas.begin(), areas.end()); 85 | // L = A * W; 86 | } 87 | 88 | EigenSparseMatrix getMeshLaplacian(const Mesh &mesh, MeshLaplace type) { 89 | EigenSparseMatrix L; 90 | if (type == MeshLaplace::Adjacent) { 91 | getMeshLaplacianAdjacent(mesh, L); 92 | } else if (type == MeshLaplace::Cotangent) { 93 | getMeshLaplacianCotangent(mesh, L); 94 | } else if (type == MeshLaplace::Belkin08) { 95 | getMeshLaplacianBelkin08(mesh, L); 96 | } else { 97 | Error("Unknown mesh Laplacian type specified: %d", (int)type); 98 | } 99 | return L; 100 | } 101 | 102 | } // namespace tinymesh 103 | -------------------------------------------------------------------------------- /src/tinymesh/ops/ops.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_OPS_H 6 | #define TINYMESH_OPS_H 7 | 8 | #include "core/api.h" 9 | #include "core/mesh.h" 10 | #include "core/utils.h" 11 | 12 | #define EIGEN_ENABLE_SPARSE 13 | #include "core/eigen.h" 14 | 15 | namespace tinymesh { 16 | 17 | /** 18 | * Hausdorff distance of two meshes 19 | */ 20 | TINYMESH_API double getHausdorffDistance(const Mesh &m0, const Mesh &m1); 21 | 22 | /** 23 | * Compute per-vertex shortest distances 24 | */ 25 | TINYMESH_API EigenVector getPerVertexShortestDistances(const Mesh &src, const Mesh &dst); 26 | 27 | //! Enumerator for mesh Laplacian types 28 | enum MeshLaplace : int { 29 | Adjacent, 30 | Cotangent, 31 | Belkin08, 32 | }; 33 | 34 | /** 35 | * 36 | */ 37 | TINYMESH_API EigenSparseMatrix getMeshLaplacian(const Mesh &mesh, MeshLaplace type); 38 | 39 | /** 40 | * 41 | */ 42 | TINYMESH_API EigenMatrix getHeatKernelSignatures(const EigenSparseMatrix &L, int K = 300, int nTimes = 100); 43 | 44 | /** 45 | * Compute per-vertex curvature tensor (i.e., the 2nd fundamental form) 46 | */ 47 | TINYMESH_API void getCurvatureTensors(const Mesh &mesh, std::vector &tensors, 48 | const std::vector &frames); 49 | 50 | /** 51 | * Compute principal curvatures and their directions [Rusinkiewicz 2004] 52 | */ 53 | TINYMESH_API std::tuple getPrincipalCurvatures( 54 | const Mesh &mesh, bool smoothTensors = false); 55 | 56 | /** 57 | * Compute principal curvatures with derivatives [Rusinkiewicz 2004] 58 | */ 59 | TINYMESH_API std::tuple 60 | getPrincipalCurvaturesWithDerivatives(const Mesh &mesh, bool smoothTensors = false); 61 | 62 | /** 63 | * Compute feature line field [Leifman and Tal 2012] 64 | */ 65 | TINYMESH_API EigenMatrix getFeatureLineField(const Mesh &mesh, bool smoothTensors = false); 66 | TINYMESH_API std::tuple getFeatureLineFieldWithFlags(const Mesh &mesh, 67 | bool smoothTensors = false); 68 | 69 | } // namespace tinymesh 70 | 71 | #endif // TINYMESH_OPS_H 72 | -------------------------------------------------------------------------------- /src/tinymesh/remesh/remesh.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "remesh.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "core/vec.h" 9 | #include "core/debug.h" 10 | #include "core/openmp.h" 11 | #include "core/mesh.h" 12 | #include "core/vertex.h" 13 | #include "core/halfedge.h" 14 | #include "core/face.h" 15 | #include "filters/filters.h" 16 | 17 | namespace tinymesh { 18 | 19 | void remeshTriangular(Mesh &mesh, double shortLength, double longLength, double keepAngleLessThan, int iterations, 20 | bool verbose) { 21 | Assertion(mesh.verify(), "Invalid mesh!"); 22 | 23 | // Compute average edge length 24 | int count; 25 | double Lavg, Lvar; 26 | Lavg = 0.0; 27 | Lvar = 0.0; 28 | 29 | count = 0; 30 | for (int i = 0; i < (int)mesh.numHalfedges(); i++) { 31 | Halfedge *he = mesh.halfedge(i); 32 | const double l = he->length(); 33 | Lavg += l; 34 | Lvar += l * l; 35 | count += 1; 36 | } 37 | 38 | Lavg = Lavg / count; 39 | Lvar = Lvar / count - Lavg * Lavg; 40 | 41 | // Check whether each vertex is on feature line 42 | for (int i = 0; i < (int)mesh.numVertices(); i++) { 43 | Vertex *v = mesh.vertex(i); 44 | std::vector neighbors; 45 | for (auto vit = v->v_begin(); vit != v->v_end(); ++vit) { 46 | neighbors.push_back(vit->pos()); 47 | } 48 | 49 | const auto nn = static_cast(neighbors.size()); 50 | double minDihed = Pi; 51 | for (int j = 0; j < nn; j++) { 52 | const int k = (j + 1) % nn; 53 | const int l = (j - 1 + nn) % nn; 54 | 55 | const Vec3 p0 = v->pos(); 56 | const Vec3 p1 = neighbors[j]; 57 | const Vec3 p2 = neighbors[k]; 58 | const Vec3 p3 = neighbors[l]; 59 | const double dihed = dihedral(p2, p0, p1, p3); 60 | minDihed = std::min(dihed, minDihed); 61 | } 62 | 63 | if (minDihed < keepAngleLessThan) { 64 | v->lock(); 65 | } 66 | } 67 | 68 | // Initialize random number generator 69 | std::vector indices; 70 | std::random_device randev; 71 | std::mt19937 rnd(randev()); 72 | 73 | // Remesh loop 74 | for (int k = 0; k < iterations; k++) { 75 | if (verbose) { 76 | Info("*** Original #%d ***\n", k + 1); 77 | Info("#vert: %d\n", (int)mesh.numVertices()); 78 | Info("#face: %d\n", (int)mesh.numFaces()); 79 | } 80 | 81 | // Split long edges 82 | indices.clear(); 83 | for (int i = 0; i < (int)mesh.numHalfedges(); i++) { 84 | indices.push_back(i); 85 | } 86 | 87 | std::shuffle(indices.begin(), indices.end(), rnd); 88 | 89 | for (int i : indices) { 90 | if (i >= 0 && i < (int)mesh.numHalfedges()) { 91 | Halfedge *he = mesh.halfedge(i); 92 | const Vec3 p1 = he->src()->pos(); 93 | const Vec3 p2 = he->dst()->pos(); 94 | const double l = length(p1 - p2); 95 | 96 | if (l >= Lavg * longLength) { 97 | mesh.splitHE(he); 98 | } 99 | } 100 | } 101 | 102 | if (verbose) { 103 | Info("*** After split ***\n"); 104 | Info("#vert: %d\n", (int)mesh.numVertices()); 105 | Info("#face: %d\n", (int)mesh.numFaces()); 106 | } 107 | 108 | mesh.verify(); 109 | 110 | // Collapse short edges 111 | indices.clear(); 112 | for (int i = 0; i < (int)mesh.numHalfedges(); i++) { 113 | indices.push_back(i); 114 | } 115 | 116 | std::shuffle(indices.begin(), indices.end(), rnd); 117 | 118 | for (int i : indices) { 119 | if (i >= 0 && i < (int)mesh.numHalfedges()) { 120 | Halfedge *he = mesh.halfedge(i); 121 | if (he->face()->isLocked() || he->rev()->face()->isLocked()) { 122 | continue; 123 | } 124 | 125 | if (he->src()->isLocked() || he->dst()->isLocked()) { 126 | continue; 127 | } 128 | 129 | const Vec3 p1 = he->src()->pos(); 130 | const Vec3 p2 = he->dst()->pos(); 131 | const double l = length(p1 - p2); 132 | 133 | if (l <= Lavg * shortLength) { 134 | // Check if collapse does not generate long edge 135 | Vertex *a = he->src(); 136 | Vertex *b = he->dst(); 137 | bool collapseOK = true; 138 | for (auto vit = b->v_begin(); vit != b->v_end(); ++vit) { 139 | if (length(a->pos() - vit->pos()) >= Lavg * longLength) { 140 | collapseOK = false; 141 | break; 142 | } 143 | } 144 | 145 | // Collapse 146 | if (collapseOK) { 147 | mesh.collapseHE(he); 148 | } 149 | } 150 | } 151 | } 152 | 153 | if (verbose) { 154 | Info("*** After collapse ***\n"); 155 | Info("#vert: %d\n", (int)mesh.numVertices()); 156 | Info("#face: %d\n", (int)mesh.numFaces()); 157 | } 158 | 159 | // Flip edges 160 | for (int i = 0; i < (int)mesh.numHalfedges(); i++) { 161 | Halfedge *he = mesh.halfedge(i); 162 | if (he->face()->isBoundary() || he->rev()->face()->isBoundary()) { 163 | continue; 164 | } 165 | 166 | if (he->face()->isLocked() || he->rev()->face()->isLocked()) { 167 | continue; 168 | } 169 | 170 | Vertex *v0 = he->src(); 171 | Vertex *v1 = he->dst(); 172 | Vertex *v2 = he->next()->dst(); 173 | Vertex *v3 = he->rev()->next()->dst(); 174 | 175 | if (v0->isLocked() || v1->isLocked()) { 176 | continue; 177 | } 178 | 179 | const int d0 = v0->degree(); 180 | const int d1 = v1->degree(); 181 | const int d2 = v2->degree(); 182 | const int d3 = v3->degree(); 183 | const int t0 = v0->isBoundary() ? 4 : 6; 184 | const int t1 = v1->isBoundary() ? 4 : 6; 185 | const int t2 = v2->isBoundary() ? 4 : 6; 186 | const int t3 = v3->isBoundary() ? 4 : 6; 187 | 188 | const int score = std::abs(d0 - t0) + std::abs(d1 - t1) + std::abs(d2 - t2) + std::abs(d3 - t3); 189 | const int after = 190 | std::abs(d0 - 1 - t0) + std::abs(d1 - 1 - t1) + std::abs(d2 + 1 - t2) + std::abs(d3 + 1 - t3); 191 | if (score > after) { 192 | mesh.flipHE(he); 193 | } 194 | } 195 | 196 | // Smoothing 197 | smoothTaubin(mesh); 198 | } 199 | } 200 | 201 | } // namespace tinymesh 202 | -------------------------------------------------------------------------------- /src/tinymesh/remesh/remesh.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_REMESH_H 6 | #define TINYMESH_REMESH_H 7 | 8 | #include "core/api.h" 9 | 10 | namespace tinymesh { 11 | 12 | /** 13 | * Triangular remeshing [Hoppe 1996] 14 | */ 15 | TINYMESH_API void remeshTriangular(Mesh &mesh, double shortLength = 0.8, double longLength = 1.333, 16 | double keepAngleLessThan = 0.0, int maxiter = 5, bool verbose = false); 17 | 18 | /** 19 | * QEM-based simplification [Garland and Heckbert 1997] 20 | */ 21 | TINYMESH_API void simplifyQEM(Mesh &mesh, int numTarget, int maxTrials = 10, bool verbose = false); 22 | 23 | } // namespace tinymesh 24 | 25 | #endif // TINYMESH_REMESH_H 26 | -------------------------------------------------------------------------------- /src/tinymesh/remesh/simplify.cpp: -------------------------------------------------------------------------------- 1 | #define TINYMESH_API_EXPORT 2 | #include "remesh.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | using Matrix4 = Eigen::Matrix4d; 13 | using Vector4 = Eigen::Vector4d; 14 | 15 | #include "core/debug.h" 16 | #include "core/vec.h" 17 | #include "core/progress.h" 18 | #include "core/openmp.h" 19 | #include "core/mesh.h" 20 | #include "core/face.h" 21 | #include "core/halfedge.h" 22 | #include "core/vertex.h" 23 | #include "filters/filters.h" 24 | 25 | namespace { 26 | 27 | struct UnionFindTree { 28 | UnionFindTree() = default; 29 | 30 | explicit UnionFindTree(size_t n) 31 | : n{ n } 32 | , values{} { 33 | values.assign(n, -1); 34 | } 35 | 36 | int root(int x) { 37 | if (values[x] < 0) { 38 | return x; 39 | } 40 | 41 | values[x] = root(values[x]); 42 | return values[x]; 43 | } 44 | 45 | void merge(int x, int y) { 46 | x = root(x); 47 | y = root(y); 48 | if (x == y) { 49 | return; 50 | } 51 | 52 | values[x] = values[y]; 53 | values[y] = x; 54 | } 55 | 56 | bool same(int x, int y) { 57 | return values[x] == values[y]; 58 | } 59 | 60 | size_t n; 61 | std::vector values; 62 | }; 63 | 64 | } // anonymous namespace 65 | 66 | namespace tinymesh { 67 | 68 | struct QEMNode { 69 | QEMNode(double value, Halfedge *he, const Vec3 &v) 70 | : value{ value } 71 | , he{ he } 72 | , v{ v } { 73 | } 74 | 75 | bool operator<(const QEMNode &n) const { 76 | return value < n.value; 77 | } 78 | 79 | bool operator>(const QEMNode &n) const { 80 | return value > n.value; 81 | } 82 | 83 | double value; 84 | Halfedge *he; 85 | Vec3 v; 86 | }; 87 | 88 | double computeQEM(const Matrix4 &m1, const Matrix4 &m2, const Vertex &v1, const Vertex &v2, Vec3 *v) { 89 | Matrix4 Q = Matrix4::Identity(); 90 | for (int i = 0; i < 3; i++) { 91 | for (int j = 0; j < 4; j++) { 92 | Q(i, j) = m1(i, j) + m2(i, j); 93 | } 94 | } 95 | 96 | Vector4 v_bar; 97 | const double D = Q.determinant(); 98 | if (D < 1.0e-8) { 99 | Vec3 m = 0.5 * (v1.pos() + v2.pos()); 100 | v_bar << m[0], m[1], m[2], 1.0; 101 | } else { 102 | Vector4 bb; 103 | bb << 0.0, 0.0, 0.0, 1.0; 104 | //v_bar = Q.colPivHouseholderQr().solve(bb); 105 | v_bar = Q.partialPivLu().solve(bb); 106 | } 107 | 108 | const double qem = (v_bar.transpose() * ((m1 + m2) * v_bar))(0, 0); 109 | if (v) { 110 | *v = Vec3(v_bar(0, 0), v_bar(1, 0), v_bar(2, 0)); 111 | } 112 | return qem; 113 | } 114 | 115 | void simplifyQEM(Mesh &mesh, int numTarget, int maxTrials, bool verbose) { 116 | static const double Eps = 1.0e-12; 117 | const int numTargetRemove = (int)mesh.numFaces() - numTarget; 118 | if (numTarget == 0) { 119 | Assertion(numTarget > 0, "Target face number is equal or less than 0!"); 120 | } 121 | 122 | if (numTargetRemove <= 0) { 123 | Warn("#vertices is already less than #target: %d < %d", (int)mesh.numFaces(), numTarget); 124 | return; 125 | } 126 | 127 | ProgressBar pbar(numTargetRemove); 128 | 129 | // Pre-smoothing 130 | smoothLaplacian(mesh); 131 | 132 | // Simplification 133 | int numRemoved = 0; 134 | for (int trial = 0; trial < maxTrials; trial++) { 135 | // Current mesh status 136 | const int numVertices = (int)mesh.numVertices(); 137 | const int numHalfedges = (int)mesh.numHalfedges(); 138 | const int numFaces = (int)mesh.numFaces(); 139 | std::vector checked(numVertices, 0); 140 | 141 | // Compute quadric metric tensor for current vertices. 142 | std::vector Qs(numVertices, Matrix4::Zero()); 143 | for (int i = 0; i < numFaces; i++) { 144 | Face *f = mesh.face(i); 145 | if (f->isBoundary()) { 146 | continue; 147 | } 148 | 149 | std::vector vs; 150 | for (auto vit = f->v_begin(); vit != f->v_end(); ++vit) { 151 | vs.push_back(vit.ptr()); 152 | } 153 | 154 | if (vs.size() != 3) { 155 | Warn("Non trianglar mesh is detected: #vertex = %d", (int)vs.size()); 156 | return; 157 | } 158 | 159 | Vec3 norm = cross(vs[1]->pos() - vs[0]->pos(), vs[2]->pos() - vs[0]->pos()); 160 | const double w = length(norm); 161 | norm /= (w + Eps); 162 | 163 | const double nx = norm.x(); 164 | const double ny = norm.y(); 165 | const double nz = norm.z(); 166 | const double d = -dot(norm, vs[0]->pos()); 167 | 168 | Matrix4 Kp; 169 | Kp << nx * nx, nx * ny, nx * nz, nx * d, ny * nx, ny * ny, ny * nz, ny * d, nz * nx, nz * ny, nz * nz, 170 | nz * d, d * nx, d * ny, d * nz, d * d; 171 | 172 | Qs[vs[0]->index()] += Kp; 173 | Qs[vs[1]->index()] += Kp; 174 | Qs[vs[2]->index()] += Kp; 175 | } 176 | 177 | // Push QEMs 178 | std::priority_queue, std::greater> que; 179 | for (int i = 0; i < numHalfedges; i++) { 180 | Halfedge *he = mesh.halfedge(i); 181 | 182 | Vertex *v1 = he->src(); 183 | Vertex *v2 = he->dst(); 184 | 185 | int i1 = v1->index(); 186 | int i2 = v2->index(); 187 | Matrix4 &q1 = Qs[i1]; 188 | Matrix4 &q2 = Qs[i2]; 189 | Vec3 v; 190 | const double qem = computeQEM(q1, q2, *v1, *v2, &v); 191 | que.push(QEMNode(qem, he, v)); 192 | } 193 | 194 | // Remove halfedges 195 | std::set removedHalfedges; 196 | while (!que.empty() && numRemoved < numTargetRemove) { 197 | QEMNode qn = que.top(); 198 | que.pop(); 199 | 200 | if (removedHalfedges.find(qn.he) != removedHalfedges.end()) { 201 | continue; 202 | } 203 | 204 | const int ii = qn.he->src()->index(); 205 | const int jj = qn.he->dst()->index(); 206 | const Vec3 v_bar = qn.v; 207 | 208 | Vertex *v_i = mesh.vertex(ii); 209 | Vertex *v_j = mesh.vertex(jj); 210 | 211 | // Skip checked vertices 212 | if (checked[v_i->index()] || checked[v_j->index()]) { 213 | continue; 214 | } 215 | 216 | // Skip boundary vertices 217 | if (v_i->isBoundary() || v_j->isBoundary()) { 218 | continue; 219 | } 220 | 221 | // Vertices with degree < 4 cannot be contracted 222 | if (v_i->degree() <= 3 || v_j->degree() <= 3) { 223 | continue; 224 | } 225 | 226 | // Check face flip 227 | bool isFlip = false; 228 | std::vector faces; 229 | for (auto it = v_i->f_begin(); it != v_i->f_end(); ++it) { 230 | faces.push_back(it.ptr()); 231 | } 232 | 233 | for (auto it = v_j->f_begin(); it != v_j->f_end(); ++it) { 234 | faces.push_back(it.ptr()); 235 | } 236 | 237 | for (Face *f : faces) { 238 | bool has_i = false; 239 | bool has_j = false; 240 | std::vector vs; 241 | std::vector ps; 242 | for (auto it = f->v_begin(); it != f->v_end(); ++it) { 243 | vs.push_back(it.ptr()); 244 | ps.push_back(it->pos()); 245 | if (it.ptr() == v_i) { 246 | has_i = true; 247 | } 248 | if (it.ptr() == v_j) { 249 | has_j = true; 250 | } 251 | } 252 | 253 | if (!has_i || !has_j) { 254 | // This is contracted face 255 | continue; 256 | } 257 | 258 | const Vec3 n0 = cross(ps[2] - ps[0], ps[1] - ps[0]); 259 | bool isFound = false; 260 | for (size_t i = 0; i < vs.size(); i++) { 261 | if (vs[i] == v_i || vs[i] == v_j) { 262 | ps[i] = v_bar; 263 | isFound = true; 264 | break; 265 | } 266 | } 267 | 268 | if (!isFound) { 269 | Warn("Contractible vertex not found!"); 270 | break; 271 | } 272 | 273 | const Vec3 n1 = cross(ps[2] - ps[0], ps[1] - ps[0]); 274 | 275 | const double cos = dot(n0, n1) / (length(n0) * length(n1)); 276 | if (cos <= 1.0e-12) { 277 | isFlip = true; 278 | } 279 | } 280 | 281 | // Face flips if target vertex is contracted 282 | if (isFlip) { 283 | continue; 284 | } 285 | 286 | // Check face degeneration 287 | std::set s_i; 288 | for (auto it = v_i->v_begin(); it != v_i->v_end(); ++it) { 289 | s_i.insert(it.ptr()); 290 | } 291 | std::vector neighbors; 292 | for (auto it = v_j->v_begin(); it != v_j->v_end(); ++it) { 293 | if (s_i.count(it.ptr()) != 0) { 294 | neighbors.push_back(it.ptr()); 295 | } 296 | } 297 | 298 | bool isDegenerate = false; 299 | for (Vertex *v : neighbors) { 300 | if (v->degree() < 4) { 301 | isDegenerate = true; 302 | break; 303 | } 304 | } 305 | 306 | if (isDegenerate) { 307 | continue; 308 | } 309 | 310 | // Update removed halfedges 311 | removedHalfedges.insert(qn.he); 312 | removedHalfedges.insert(qn.he->next()); 313 | removedHalfedges.insert(qn.he->next()->next()); 314 | removedHalfedges.insert(qn.he->rev()); 315 | removedHalfedges.insert(qn.he->rev()->next()); 316 | removedHalfedges.insert(qn.he->rev()->next()->next()); 317 | 318 | // Collapse halfedge 319 | qn.he->src()->setPos(qn.v); 320 | checked[qn.he->src()->index()] = 1; 321 | if (mesh.collapseHE(qn.he)) { 322 | numRemoved += 2; 323 | if (verbose) pbar.step(2); 324 | } 325 | 326 | if (numRemoved >= numTargetRemove) { 327 | break; 328 | } 329 | } 330 | 331 | // Flip edges 332 | for (int i = 0; i < (int)mesh.numHalfedges(); i++) { 333 | Halfedge *he = mesh.halfedge(i); 334 | if (he->face()->isBoundary() || he->rev()->face()->isBoundary()) { 335 | continue; 336 | } 337 | 338 | Vertex *v0 = he->src(); 339 | Vertex *v1 = he->dst(); 340 | Vertex *v2 = he->next()->dst(); 341 | Vertex *v3 = he->rev()->next()->dst(); 342 | const int d0 = v0->degree(); 343 | const int d1 = v1->degree(); 344 | const int d2 = v2->degree(); 345 | const int d3 = v3->degree(); 346 | 347 | const int score = std::abs(d0 - 6) + std::abs(d1 - 6) + std::abs(d2 - 6) + std::abs(d3 - 6); 348 | const int after = std::abs(d0 - 1 - 6) + std::abs(d1 - 1 - 6) + std::abs(d2 + 1 - 6) + std::abs(d3 + 1 - 6); 349 | if (score > after) { 350 | mesh.flipHE(he); 351 | } 352 | } 353 | 354 | // Smoothing 355 | smoothTaubin(mesh); 356 | 357 | if (numRemoved >= numTargetRemove) { 358 | break; 359 | } 360 | 361 | if (numFaces > mesh.numFaces()) { 362 | trial = 0; 363 | } 364 | } 365 | 366 | if (verbose) pbar.finish(); 367 | 368 | if (verbose) { 369 | Info("%d faces removed (%6.2f%% achievement)", numRemoved, 100.0 * numRemoved / numTargetRemove); 370 | } 371 | } 372 | 373 | } // namespace tinymesh 374 | -------------------------------------------------------------------------------- /src/tinymesh/restore/restore.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_RESTORE_H 6 | #define TINYMESH_RESTORE_H 7 | 8 | #include "core/api.h" 9 | #include "core/mesh.h" 10 | #include "core/face.h" 11 | 12 | namespace tinymesh { 13 | 14 | /** 15 | * Minimum direhdral hole filling [Leipa 2003] 16 | * @details The triangulation algorithm is based on the following papers. 17 | * Barequet and Sharir, "Filling Gaps in the Boundary of a Polyhedron", 1995. 18 | * Liepa, "Filling Holes in Meshes", 2003. If "dihedralBound" is specified as "Pi", 19 | * then this method works as Barequet's method; otherwise, it works as Liepa's method. 20 | */ 21 | TINYMESH_API void holeFillMinDihedral(Mesh &mesh, Face *face, double dihedralBound = Pi); 22 | 23 | /** 24 | * Advancing front hole filling [Zhao et al. 2007] 25 | */ 26 | TINYMESH_API void holeFillAdvancingFront(Mesh &mesh, Face *face); 27 | 28 | /** 29 | * Context-based Coherent Surface Completion [Harary et al. 2016] 30 | */ 31 | TINYMESH_API void holeFillContextCoherent(Mesh &mesh, int maxiters = 200); 32 | 33 | } // namespace tinymesh 34 | 35 | #endif // TINYMESH_RESTORE_H 36 | -------------------------------------------------------------------------------- /src/tinymesh/tinymesh.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_H 6 | #define TINYMESH_H 7 | 8 | #include "core/api.h" 9 | #include "core/debug.h" 10 | #include "core/vec.h" 11 | #include "core/bounds.h" 12 | #include "core/openmp.h" 13 | #include "core/filesystem.h" 14 | 15 | #include "core/mesh.h" 16 | #include "core/vertex.h" 17 | #include "core/halfedge.h" 18 | #include "core/face.h" 19 | 20 | #include "core/triangle.h" 21 | #include "core/bvh.h" 22 | 23 | #include "ops/ops.h" 24 | #include "remesh/remesh.h" 25 | #include "filters/filters.h" 26 | #include "restore/restore.h" 27 | 28 | #endif // TINYMESH_H 29 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(BUILD_TARGET "run_all_tests") 2 | 3 | configure_file("${CMAKE_CURRENT_LIST_DIR}/test_config.h.in" 4 | "${CMAKE_CURRENT_LIST_DIR}/test_config.h" @ONLY) 5 | 6 | set(TEST_HEADERS test_config.h test_utils.h) 7 | 8 | set(SOURCE_FILES 9 | test_config.h 10 | test_utils.h 11 | gtest.cpp 12 | test_filters.cpp 13 | test_mesh.cpp 14 | test_ops.cpp 15 | test_remesh.cpp 16 | test_vec.cpp) 17 | 18 | set(TEST_DEPS) 19 | add_unit_test(DEPS TEST_DEPS NAME "test_filters" SOURCES test_filters.cpp ${TEST_HEADERS}) 20 | add_unit_test(DEPS TEST_DEPS NAME "test_mesh" SOURCES test_mesh.cpp ${TEST_HEADERS}) 21 | add_unit_test(DEPS TEST_DEPS NAME "test_ops" SOURCES test_ops.cpp ${TEST_HEADERS}) 22 | add_unit_test(DEPS TEST_DEPS NAME "test_remesh" SOURCES test_remesh.cpp ${TEST_HEADERS}) 23 | add_unit_test(DEPS TEST_DEPS NAME "test_vec" SOURCES test_vec.cpp ${TEST_HEADERS}) 24 | add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --verbose --gtest_shuffle DEPENDS ${TEST_DEPS}) 25 | 26 | 27 | # include_directories(${TINYMESH_INCLUDE_DIR} ${GTEST_INCLUDE_DIRS}) 28 | # add_executable(${BUILD_TARGET}) 29 | 30 | # target_sources(${BUILD_TARGET} PRIVATE ${SOURCE_FILES}) 31 | # target_include_directories(${BUILD_TARGET} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) 32 | # target_link_libraries(${BUILD_TARGET} PRIVATE ${TINYMESH_LIBRARY} ${GTEST_LIBRARIES}) 33 | 34 | # set_target_properties(${BUILD_TARGET} PROPERTIES FOLDER "Tests") 35 | # set_target_properties(${BUILD_TARGET} PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) 36 | # source_group("Source Files" FILES ${SOURCE_FILES}) 37 | 38 | # add_test(NAME ${BUILD_TARGET} COMMAND ${BUILD_TARGET}) 39 | # add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --verbose --gtest_shuffle DEPENDS ${BUILD_TARGET}) 40 | -------------------------------------------------------------------------------- /tests/gtest.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | int main(int argc, char **argv) { 4 | ::testing::InitGoogleTest(&argc, argv); 5 | return RUN_ALL_TESTS(); 6 | } -------------------------------------------------------------------------------- /tests/python/test_construct.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from tinymesh import Mesh 7 | 8 | CWD = os.getcwd() 9 | 10 | model_dir = 'data/models' 11 | filenames = [ 12 | 'torus.obj', 13 | 'fandisk.ply', 14 | 'bunny.ply', 15 | ] 16 | 17 | 18 | @pytest.mark.parametrize("filename", filenames) 19 | def test_mesh_io(filename): 20 | filename = os.path.join(CWD, model_dir, filename) 21 | base, ext = os.path.splitext(filename) 22 | 23 | try: 24 | mesh = Mesh(filename) 25 | except Exception: 26 | pytest.fail('Failed to load mesh!') 27 | 28 | assert mesh.num_vertices() > 0 29 | assert mesh.num_faces() > 0 30 | assert mesh.num_edges() > 0 31 | assert mesh.num_halfedges() > 0 32 | 33 | try: 34 | mesh.save(base + '_test' + ext) 35 | except Exception: 36 | pytest.fail('Failed to save mesh!') 37 | 38 | 39 | @pytest.mark.parametrize("filename", ["not_exist.ply", "invalid_ext.obj"]) 40 | def test_mesh_invalid_io(filename): 41 | with pytest.raises(RuntimeError): 42 | _ = Mesh(filename) 43 | 44 | 45 | def test_tetrahedron(): 46 | # Construct a simple tetrahedron 47 | a = np.asarray([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype='double') 48 | b = np.asarray([0, 1, 2, 0, 2, 3, 0, 3, 1, 3, 2, 1], dtype='uint32') 49 | try: 50 | _ = Mesh(a, b) 51 | except Exception: 52 | pytest.fail('Mesh construction from vertices/indices was failed!') 53 | -------------------------------------------------------------------------------- /tests/python/test_filters.py: -------------------------------------------------------------------------------- 1 | import os 2 | from itertools import product 3 | 4 | import pytest 5 | 6 | import tinymesh as tms 7 | 8 | CWD = os.getcwd() 9 | 10 | model_dir = 'data/models' 11 | filenames = [ 12 | 'torus.obj', 13 | 'fandisk.ply', 14 | 'bunny_mini.ply', 15 | ] 16 | 17 | methods = [ 18 | tms.smooth_laplacian, 19 | tms.smooth_taubin, 20 | tms.implicit_fairing, 21 | tms.denoise_normal_gaussian, 22 | tms.denoise_normal_bilateral, 23 | tms.denoise_l0_smooth, 24 | ] 25 | 26 | 27 | @pytest.mark.parametrize("method, filename", product(methods, filenames)) 28 | def test_smooth_method(method, filename): 29 | filename = os.path.join(CWD, model_dir, filename) 30 | mesh = tms.Mesh(filename) 31 | mesh.fill_holes() 32 | 33 | try: 34 | method(mesh) 35 | except Exception: 36 | pytest.fail('Failed!') 37 | -------------------------------------------------------------------------------- /tests/python/test_ops.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import partial 3 | from itertools import product 4 | 5 | import pytest 6 | 7 | import tinymesh as tms 8 | 9 | CWD = os.getcwd() 10 | 11 | model_dir = 'data/models' 12 | filenames = [ 13 | 'torus.obj', 14 | 'fandisk.ply', 15 | 'bunny_mini.ply', 16 | ] 17 | 18 | methods = [ 19 | partial(tms.get_mesh_laplacian, type=tms.MeshLaplace.ADJACENT), 20 | partial(tms.get_mesh_laplacian, type=tms.MeshLaplace.COTANGENT), 21 | partial(tms.get_mesh_laplacian, type=tms.MeshLaplace.BELKIN08), 22 | tms.get_principal_curvatures, 23 | tms.get_principal_curvatures_with_derivatives, 24 | tms.get_feature_line_field, 25 | ] 26 | 27 | 28 | @pytest.mark.parametrize("filename", filenames) 29 | def test_hks(filename): 30 | filename = os.path.join(CWD, model_dir, filename) 31 | mesh = tms.Mesh(filename) 32 | mesh.fill_holes() 33 | 34 | L = tms.get_mesh_laplacian(mesh, tms.MeshLaplace.COTANGENT) 35 | K = min(L.shape[0], 200) 36 | 37 | try: 38 | tms.get_heat_kernel_signatures(L, K) 39 | except Exception: 40 | pytest.fail('Failed!') 41 | 42 | assert mesh.verify(), "Mesh verification failed!" 43 | 44 | 45 | @pytest.mark.parametrize("method, filename", product(methods, filenames)) 46 | def test_ops_method(method, filename): 47 | filename = os.path.join(CWD, model_dir, filename) 48 | mesh = tms.Mesh(filename) 49 | mesh.fill_holes() 50 | 51 | try: 52 | method(mesh) 53 | except Exception: 54 | pytest.fail('Failed!') 55 | 56 | assert mesh.verify(), "Mesh verification failed!" 57 | -------------------------------------------------------------------------------- /tests/python/test_remesh.py: -------------------------------------------------------------------------------- 1 | import os 2 | from itertools import product 3 | 4 | import pytest 5 | 6 | import tinymesh as tms 7 | 8 | CWD = os.getcwd() 9 | 10 | model_dir = 'data/models' 11 | filenames = [ 12 | 'torus.obj', 13 | 'fandisk.ply', 14 | 'bunny_mini.ply', 15 | ] 16 | 17 | methods = [ 18 | tms.remesh_triangular, 19 | lambda m: tms.simplify_qem(m, 20 | m.num_faces() // 2), 21 | lambda m: tms.simplify_qem(m, 22 | m.num_faces() // 10), 23 | ] 24 | 25 | 26 | @pytest.mark.parametrize("filename", filenames) 27 | def test_hole_fill(filename): 28 | filename = os.path.join(CWD, model_dir, filename) 29 | mesh = tms.Mesh(filename) 30 | 31 | try: 32 | mesh.fill_holes() 33 | except Exception: 34 | pytest.fail('Failed!') 35 | 36 | assert mesh.verify(), "Mesh verification failed!" 37 | 38 | 39 | @pytest.mark.parametrize("method, filename", product(methods, filenames)) 40 | def test_remesh_method(method, filename): 41 | filename = os.path.join(CWD, model_dir, filename) 42 | mesh = tms.Mesh(filename) 43 | mesh.fill_holes() 44 | 45 | try: 46 | method(mesh) 47 | except Exception: 48 | pytest.fail('Failed!') 49 | 50 | assert mesh.verify(), "Mesh verification failed!" 51 | -------------------------------------------------------------------------------- /tests/test_config.h.in: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TEST_CONFIG_H 6 | #define TEST_CONFIG_H 7 | 8 | static const char *MODEL_DIRECTORY = "@CMAKE_SOURCE_DIR@/data/models/"; 9 | 10 | #endif // TEST_CONFIG_H 11 | -------------------------------------------------------------------------------- /tests/test_filters.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "tinymesh/tinymesh.h" 10 | using namespace tinymesh; 11 | 12 | #include "test_utils.h" 13 | 14 | class MeshFilterTest : public TinyMeshBaseTestWithParam { 15 | public: 16 | MeshFilterTest() = default; 17 | virtual ~MeshFilterTest() = default; 18 | 19 | void SetUp() override { 20 | mesh.load(filename); 21 | mesh.fillHoles(); 22 | } 23 | }; 24 | 25 | // Smoothing 26 | 27 | TEST_P(MeshFilterTest, SmoothLaplacian) { 28 | EXPECT_NO_FATAL_FAILURE(smoothLaplacian(mesh)); 29 | } 30 | 31 | TEST_P(MeshFilterTest, SmoothTaubin) { 32 | EXPECT_NO_FATAL_FAILURE(smoothTaubin(mesh)); 33 | } 34 | 35 | TEST_P(MeshFilterTest, ImplicitFairing) { 36 | EXPECT_NO_FATAL_FAILURE(implicitFairing(mesh)); 37 | } 38 | 39 | // Denoising 40 | 41 | TEST_P(MeshFilterTest, DenoiseNormalGaussian) { 42 | EXPECT_NO_FATAL_FAILURE(denoiseNormalGaussian(mesh)); 43 | } 44 | 45 | TEST_P(MeshFilterTest, DenoiseNormalBilateral) { 46 | EXPECT_NO_FATAL_FAILURE(denoiseNormalBilateral(mesh)); 47 | } 48 | 49 | TEST_P(MeshFilterTest, DenoiseL0Smoothing) { 50 | EXPECT_NO_FATAL_FAILURE(denoiseL0Smooth(mesh)); 51 | } 52 | 53 | static std::vector filenames = { 54 | "torus.obj", 55 | "fandisk.ply", 56 | "bunny_mini.ply", 57 | }; 58 | 59 | INSTANTIATE_TEST_SUITE_P(, MeshFilterTest, ::testing::ValuesIn(filenames)); 60 | -------------------------------------------------------------------------------- /tests/test_mesh.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "tinymesh/tinymesh.h" 11 | using namespace tinymesh; 12 | 13 | #include "test_utils.h" 14 | 15 | class MeshBasicTest : public TinyMeshBaseTestWithParam { 16 | public: 17 | MeshBasicTest() = default; 18 | virtual ~MeshBasicTest() = default; 19 | }; 20 | 21 | TEST(MeshBaseTest, MeshInvaidLoad) { 22 | Mesh mesh; 23 | ASSERT_DEATH(mesh.load("mesh_not_found.ply"), ""); 24 | } 25 | 26 | TEST_P(MeshBasicTest, MeshLoad) { 27 | std::set_terminate([] { ADD_FAILURE(); }); 28 | ASSERT_NO_FATAL_FAILURE(mesh.load(filename)); 29 | ASSERT_TRUE(mesh.verify()); 30 | 31 | EXPECT_GT(mesh.numVertices(), 0); 32 | EXPECT_GT(mesh.numFaces(), 0); 33 | EXPECT_GT(mesh.numEdges(), 0); 34 | EXPECT_GT(mesh.numHalfedges(), 0); 35 | 36 | const int nVerts = (int)mesh.numVertices(); 37 | for (int i = 0; i < nVerts; i++) { 38 | EXPECT_GT(mesh.vertex(i)->degree(), 0); 39 | } 40 | } 41 | 42 | TEST_P(MeshBasicTest, MeshClosetPoint) { 43 | std::random_device randev; 44 | std::mt19937 mt(randev()); 45 | std::uniform_real_distribution dist(-1.0, 1.0); 46 | const Vec3 query(dist(mt), dist(mt), dist(mt)); 47 | 48 | std::set_terminate([] { ADD_FAILURE(); }); 49 | ASSERT_NO_FATAL_FAILURE(mesh.load(filename)); 50 | mesh.fillHoles(); 51 | 52 | double answer = 1.0e20; 53 | for (int i = 0; i < mesh.numFaces(); i++) { 54 | const Triangle t = mesh.face(i)->toTriangle(); 55 | const double dist = t.distance(query); 56 | if (dist < answer) { 57 | answer = dist; 58 | } 59 | } 60 | 61 | BVH bvh(mesh); 62 | 63 | EXPECT_NEAR(bvh.distance(query), answer, 1.0e-5); 64 | } 65 | 66 | static std::vector filenames = { 67 | "box.obj", "sphere.obj", "torus.obj", "plane.obj", "fandisk.ply", "bunny.ply", 68 | }; 69 | 70 | INSTANTIATE_TEST_SUITE_P(, MeshBasicTest, ::testing::ValuesIn(filenames)); 71 | -------------------------------------------------------------------------------- /tests/test_ops.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "tinymesh/tinymesh.h" 10 | using namespace tinymesh; 11 | 12 | #include "test_utils.h" 13 | 14 | class MeshOpsTest : public TinyMeshBaseTestWithParam { 15 | public: 16 | MeshOpsTest() = default; 17 | virtual ~MeshOpsTest() = default; 18 | 19 | void SetUp() override { 20 | mesh.load(filename); 21 | mesh.fillHoles(); 22 | } 23 | }; 24 | 25 | // Operators 26 | 27 | TEST_P(MeshOpsTest, GetLaplacian) { 28 | ASSERT_NO_FATAL_FAILURE(getMeshLaplacian(mesh, MeshLaplace::Adjacent)); 29 | ASSERT_NO_FATAL_FAILURE(getMeshLaplacian(mesh, MeshLaplace::Cotangent)); 30 | ASSERT_NO_FATAL_FAILURE(getMeshLaplacian(mesh, MeshLaplace::Belkin08)); 31 | } 32 | 33 | // Features 34 | 35 | TEST_P(MeshOpsTest, GetHeatKernelSignatures) { 36 | auto L = getMeshLaplacian(mesh, MeshLaplace::Cotangent); 37 | const int K = std::min((int)L.rows(), 200); 38 | ASSERT_NO_FATAL_FAILURE(getHeatKernelSignatures(L, K)); 39 | } 40 | 41 | TEST_P(MeshOpsTest, GetPrincipalCurvatures) { 42 | ASSERT_NO_FATAL_FAILURE(getPrincipalCurvatures(mesh)); 43 | } 44 | 45 | TEST_P(MeshOpsTest, GetPrincipalCurvaturesWithDerivatives) { 46 | ASSERT_NO_FATAL_FAILURE(getPrincipalCurvaturesWithDerivatives(mesh)); 47 | } 48 | 49 | TEST_P(MeshOpsTest, GetFeatureLineField) { 50 | ASSERT_NO_FATAL_FAILURE(getFeatureLineField(mesh)); 51 | } 52 | 53 | static std::vector filenames = { 54 | "torus.obj", 55 | "fandisk.ply", 56 | "bunny_mini.ply", 57 | }; 58 | 59 | INSTANTIATE_TEST_SUITE_P(, MeshOpsTest, ::testing::ValuesIn(filenames)); 60 | -------------------------------------------------------------------------------- /tests/test_remesh.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "tinymesh/tinymesh.h" 10 | using namespace tinymesh; 11 | 12 | #include "test_utils.h" 13 | 14 | class MeshRemeshTest : public TinyMeshBaseTestWithParam { 15 | public: 16 | MeshRemeshTest() = default; 17 | virtual ~MeshRemeshTest() = default; 18 | 19 | void SetUp() override { 20 | mesh.load(filename); 21 | } 22 | }; 23 | 24 | TEST_P(MeshRemeshTest, HoleFill) { 25 | EXPECT_NO_FATAL_FAILURE(mesh.fillHoles()); 26 | EXPECT_TRUE(mesh.verify()); 27 | } 28 | 29 | TEST_P(MeshRemeshTest, RemeshTriangular) { 30 | EXPECT_NO_FATAL_FAILURE(mesh.fillHoles()); 31 | EXPECT_NO_FATAL_FAILURE(remeshTriangular(mesh)); 32 | } 33 | 34 | TEST_P(MeshRemeshTest, SimplifyQEMOneHalf) { 35 | EXPECT_NO_FATAL_FAILURE(mesh.fillHoles()); 36 | EXPECT_NO_FATAL_FAILURE(simplifyQEM(mesh, (int)mesh.numFaces() / 2)); 37 | } 38 | 39 | TEST_P(MeshRemeshTest, SimplifyQEMOneTenth) { 40 | EXPECT_NO_FATAL_FAILURE(mesh.fillHoles()); 41 | EXPECT_NO_FATAL_FAILURE(simplifyQEM(mesh, (int)mesh.numFaces() / 10)); 42 | } 43 | 44 | static std::vector filenames = { 45 | "box.obj", 46 | "torus.obj", 47 | "fandisk.ply", 48 | "bunny_mini.ply", 49 | }; 50 | 51 | INSTANTIATE_TEST_SUITE_P(, MeshRemeshTest, ::testing::ValuesIn(filenames)); 52 | -------------------------------------------------------------------------------- /tests/test_utils.h: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #pragma once 3 | #endif 4 | 5 | #ifndef TINYMESH_TEST_UTILS_H 6 | #define TINYMESH_TEST_UTILS_H 7 | 8 | #include "gtest/gtest.h" 9 | #include "test_config.h" 10 | 11 | namespace fs = std::filesystem; 12 | 13 | class TinyMeshBaseTest : public ::testing::Test { 14 | protected: 15 | TinyMeshBaseTest() { 16 | } 17 | virtual ~TinyMeshBaseTest() { 18 | } 19 | }; 20 | 21 | class TinyMeshBaseTestWithParam : public TinyMeshBaseTest, public ::testing::WithParamInterface { 22 | public: 23 | TinyMeshBaseTestWithParam() { 24 | fs::path modelDir(MODEL_DIRECTORY); 25 | fs::path filePath(GetParam().c_str()); 26 | filename = (modelDir / filePath).string(); 27 | } 28 | 29 | virtual ~TinyMeshBaseTestWithParam() { 30 | } 31 | 32 | protected: 33 | std::string filename; 34 | Mesh mesh; 35 | }; 36 | 37 | #endif // TINYMESH_TEST_UTILS_H 38 | -------------------------------------------------------------------------------- /tests/test_vec.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "tinymesh/tinymesh.h" 9 | using namespace tinymesh; 10 | 11 | using VecPair = std::tuple; 12 | 13 | class VecTest : public ::testing::Test { 14 | protected: 15 | VecTest() { 16 | } 17 | virtual ~VecTest() { 18 | } 19 | }; 20 | 21 | class VecUnaryTest : public VecTest, public ::testing::WithParamInterface { 22 | protected: 23 | Vec3 v1; 24 | 25 | protected: 26 | VecUnaryTest() { 27 | } 28 | virtual ~VecUnaryTest() { 29 | } 30 | 31 | void SetUp() { 32 | v1 = GetParam(); 33 | } 34 | }; 35 | 36 | class VecPairwiseTest : public VecTest, public ::testing::WithParamInterface { 37 | protected: 38 | Vec3 v1, v2; 39 | 40 | protected: 41 | VecPairwiseTest() { 42 | } 43 | virtual ~VecPairwiseTest() { 44 | } 45 | 46 | void SetUp() { 47 | v1 = std::get<0>(GetParam()); 48 | v2 = std::get<1>(GetParam()); 49 | } 50 | }; 51 | 52 | TEST_F(VecTest, DefaultInstance) { 53 | Vec3 v; 54 | EXPECT_EQ(0.0, v.x()); 55 | EXPECT_EQ(0.0, v.y()); 56 | EXPECT_EQ(0.0, v.z()); 57 | } 58 | 59 | TEST_P(VecUnaryTest, Instance) { 60 | Vec3 u(v1.x(), v1.y(), v1.z()); 61 | EXPECT_EQ(v1.x(), u.x()); 62 | EXPECT_EQ(v1.y(), u.y()); 63 | EXPECT_EQ(v1.z(), u.z()); 64 | 65 | Vec3 v = Vec3{ v1.x(), v1.y(), v1.z() }; 66 | EXPECT_EQ(v1.x(), v.x()); 67 | EXPECT_EQ(v1.y(), v.y()); 68 | EXPECT_EQ(v1.z(), v.z()); 69 | } 70 | 71 | TEST_P(VecUnaryTest, Copy) { 72 | Vec3 v = Vec(v1); 73 | EXPECT_EQ(v1.x(), v.x()); 74 | EXPECT_EQ(v1.y(), v.y()); 75 | EXPECT_EQ(v1.z(), v.z()); 76 | } 77 | 78 | TEST_P(VecUnaryTest, Assignment) { 79 | Vec3 v; 80 | v = v1; 81 | EXPECT_EQ(v1.x(), v.x()); 82 | EXPECT_EQ(v1.y(), v.y()); 83 | EXPECT_EQ(v1.z(), v.z()); 84 | } 85 | 86 | TEST_P(VecPairwiseTest, PlusOperator) { 87 | Vec w = v1 + v2; 88 | EXPECT_EQ(v1.x() + v2.x(), w.x()); 89 | EXPECT_EQ(v1.y() + v2.y(), w.y()); 90 | EXPECT_EQ(v1.z() + v2.z(), w.z()); 91 | } 92 | 93 | TEST_P(VecPairwiseTest, MinusOperator) { 94 | Vec w = v1 - v2; 95 | EXPECT_EQ(v1.x() - v2.x(), w.x()); 96 | EXPECT_EQ(v1.y() - v2.y(), w.y()); 97 | EXPECT_EQ(v1.z() - v2.z(), w.z()); 98 | } 99 | 100 | TEST_P(VecPairwiseTest, Multiplication) { 101 | const Vec v = v1 * v2; 102 | EXPECT_EQ(v1.x() * v2.x(), v.x()); 103 | EXPECT_EQ(v1.y() * v2.y(), v.y()); 104 | EXPECT_EQ(v1.z() * v2.z(), v.z()); 105 | 106 | const double a = v2.x(); 107 | const Vec u = v1 * a; 108 | EXPECT_EQ(v1.x() * a, u.x()); 109 | EXPECT_EQ(v1.y() * a, u.y()); 110 | EXPECT_EQ(v1.z() * a, u.z()); 111 | 112 | const double b = v2.y(); 113 | const Vec w = b * v1; 114 | EXPECT_EQ(b * v1.x(), w.x()); 115 | EXPECT_EQ(b * v1.y(), w.y()); 116 | EXPECT_EQ(b * v1.z(), w.z()); 117 | } 118 | 119 | TEST_P(VecPairwiseTest, Division) { 120 | if (v2.x() != 0.0 && v2.y() != 0.0 && v2.z() != 0.0) { 121 | const Vec v = v1 / v2; 122 | EXPECT_EQ(v1.x() / v2.x(), v.x()); 123 | EXPECT_EQ(v1.y() / v2.y(), v.y()); 124 | EXPECT_EQ(v1.z() / v2.z(), v.z()); 125 | } else { 126 | ASSERT_ANY_THROW(v1 / v2); 127 | } 128 | 129 | const double a = v2.x(); 130 | if (a != 0.0) { 131 | const Vec u = v1 / a; 132 | EXPECT_DOUBLE_EQ(v1.x() / a, u.x()); 133 | EXPECT_DOUBLE_EQ(v1.y() / a, u.y()); 134 | EXPECT_DOUBLE_EQ(v1.z() / a, u.z()); 135 | } else { 136 | ASSERT_ANY_THROW(v1 / a); 137 | } 138 | } 139 | 140 | TEST_P(VecUnaryTest, NormAndNormalize) { 141 | const double sqnrm = v1.x() * v1.x() + v1.y() * v1.y() + v1.z() * v1.z(); 142 | const double nrm = sqrt(sqnrm); 143 | EXPECT_EQ(sqrt(sqnrm), length(v1)); 144 | 145 | if (nrm != 0.0) { 146 | const Vec w = normalize(v1); 147 | EXPECT_EQ(v1.x() / nrm, w.x()); 148 | EXPECT_EQ(v1.y() / nrm, w.y()); 149 | EXPECT_EQ(v1.z() / nrm, w.z()); 150 | EXPECT_DOUBLE_EQ(length(w), 1.0); 151 | } else { 152 | ASSERT_DEATH(normalize(v1), ""); 153 | } 154 | } 155 | 156 | TEST_P(VecUnaryTest, Negation) { 157 | Vec v = -v1; 158 | EXPECT_EQ(-v1.x(), v.x()); 159 | EXPECT_EQ(-v1.y(), v.y()); 160 | EXPECT_EQ(-v1.z(), v.z()); 161 | } 162 | 163 | TEST_P(VecPairwiseTest, Equal) { 164 | EXPECT_EQ(v1.x() == v2.x() && v1.y() == v2.y() && v1.z() == v2.z(), v1 == v2); 165 | } 166 | 167 | TEST_P(VecPairwiseTest, DotAndCross) { 168 | double dt = dot(v1, v2); 169 | EXPECT_EQ(v1.x() * v2.x() + v1.y() * v2.y() + v1.z() * v2.z(), dt); 170 | EXPECT_EQ(dt, dot(v2, v1)); 171 | 172 | const Vec w = cross(v1, v2); 173 | EXPECT_EQ(v1.y() * v2.z() - v1.z() * v2.y(), w.x()); 174 | EXPECT_EQ(v1.z() * v2.x() - v1.x() * v2.z(), w.y()); 175 | EXPECT_EQ(v1.x() * v2.y() - v1.y() * v2.x(), w.z()); 176 | } 177 | 178 | std::vector vectors = { Vec3(0.0, 1.0, 2.0), Vec3(-2.0, -1.0, 0.0), Vec3(3.14, 1.59, 2.65), 179 | Vec3(1.0e8, 1.0e8, 1.0e8), Vec3(1.0e-8, 1.0e-8, 1.0e-8) }; 180 | 181 | INSTANTIATE_TEST_SUITE_P(, VecUnaryTest, ::testing::ValuesIn(vectors)); 182 | 183 | INSTANTIATE_TEST_SUITE_P(, VecPairwiseTest, 184 | ::testing::Combine(::testing::ValuesIn(vectors), ::testing::ValuesIn(vectors))); --------------------------------------------------------------------------------