├── .clang-format ├── .github └── workflows │ ├── clang-format.sh │ ├── clang-format.yml │ ├── macos.yml │ └── ubuntu.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets ├── torus.mesh └── torus.obj ├── docs ├── cloth.gif ├── fem-2d.gif ├── fem-2d │ └── fem-2d.mp4 ├── fem-3d.gif ├── fem-3d │ ├── fem-3d.jpg │ ├── fem-3d.mp4 │ └── fem-3d.png ├── pbd-xpbd-comparison.jpg ├── wind.jpg └── wind │ ├── wind-0.jpg │ ├── wind-1.jpg │ ├── wind-2.jpg │ └── wind-3.jpg ├── examples ├── aerodynamics │ └── main.cpp ├── cloth-alembic │ └── main.cpp ├── explicit-2d │ └── main.cpp ├── interactive-app │ └── main.cpp ├── pbd-xpbd-comparison │ └── main.cpp ├── variational-implicit-2d │ └── main.cpp └── variational-implicit-3d │ ├── main.cpp │ └── mesh.hpp ├── include └── elasty │ ├── alembic-manager.hpp │ ├── algorithm-type.hpp │ ├── cloth-sim-object.hpp │ ├── constraint.hpp │ ├── engine.hpp │ ├── fem.hpp │ ├── particle.hpp │ ├── sim-object.hpp │ └── utils.hpp ├── src ├── alembic-manager.cpp ├── cloth-sim-object.cpp ├── constraint.cpp ├── engine.cpp └── utils.cpp └── tests ├── constraint-test.cpp └── fem-test.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveAssignments: true 5 | AlignConsecutiveDeclarations: true 6 | AlignEscapedNewlines: Right 7 | AlignOperands: true 8 | AlignTrailingComments: true 9 | AllowAllParametersOfDeclarationOnNextLine: false 10 | AllowShortBlocksOnASingleLine: false 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: Inline 13 | AllowShortIfStatementsOnASingleLine: true 14 | AllowShortLambdasOnASingleLine: Inline 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: MultiLine 20 | BinPackArguments: false 21 | BinPackParameters: false 22 | BraceWrapping: 23 | AfterClass: false 24 | AfterControlStatement: false 25 | AfterEnum: false 26 | AfterFunction: false 27 | AfterNamespace: false 28 | AfterObjCDeclaration: false 29 | AfterStruct: false 30 | AfterUnion: false 31 | AfterExternBlock: false 32 | BeforeCatch: false 33 | BeforeElse: false 34 | IndentBraces: false 35 | SplitEmptyFunction: true 36 | SplitEmptyRecord: true 37 | SplitEmptyNamespace: true 38 | BreakBeforeBinaryOperators: None 39 | BreakBeforeBraces: Allman 40 | BreakBeforeInheritanceComma: false 41 | BreakInheritanceList: BeforeColon 42 | BreakBeforeTernaryOperators: true 43 | BreakConstructorInitializersBeforeComma: false 44 | BreakConstructorInitializers: BeforeColon 45 | BreakAfterJavaFieldAnnotations: false 46 | BreakStringLiterals: true 47 | ColumnLimit: 120 48 | CommentPragmas: '^ IWYU pragma:' 49 | CompactNamespaces: false 50 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 51 | ConstructorInitializerIndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | Cpp11BracedListStyle: true 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: false 57 | FixNamespaceComments: true 58 | ForEachMacros: 59 | - foreach 60 | - Q_FOREACH 61 | - BOOST_FOREACH 62 | IncludeBlocks: Merge 63 | IncludeCategories: 64 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 65 | Priority: 2 66 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 67 | Priority: 3 68 | - Regex: '.*' 69 | Priority: 1 70 | IncludeIsMainRegex: '(Test)?$' 71 | IndentCaseLabels: true 72 | IndentPPDirectives: None 73 | IndentWidth: 4 74 | IndentWrappedFunctionNames: false 75 | JavaScriptQuotes: Leave 76 | JavaScriptWrapImports: true 77 | KeepEmptyLinesAtTheStartOfBlocks: true 78 | MacroBlockBegin: '' 79 | MacroBlockEnd: '' 80 | MaxEmptyLinesToKeep: 1 81 | NamespaceIndentation: All 82 | ObjCBinPackProtocolList: Auto 83 | ObjCBlockIndentWidth: 2 84 | ObjCSpaceAfterProperty: false 85 | ObjCSpaceBeforeProtocolList: true 86 | PenaltyBreakAssignment: 2 87 | PenaltyBreakBeforeFirstCallParameter: 19 88 | PenaltyBreakComment: 300 89 | PenaltyBreakFirstLessLess: 120 90 | PenaltyBreakString: 1000 91 | PenaltyBreakTemplateDeclaration: 10 92 | PenaltyExcessCharacter: 1000000 93 | PenaltyReturnTypeOnItsOwnLine: 60 94 | PointerAlignment: Left 95 | ReflowComments: true 96 | SortIncludes: true 97 | SortUsingDeclarations: true 98 | SpaceAfterCStyleCast: true 99 | SpaceAfterTemplateKeyword: true 100 | SpaceBeforeAssignmentOperators: true 101 | SpaceBeforeCpp11BracedList: false 102 | SpaceBeforeCtorInitializerColon: true 103 | SpaceBeforeInheritanceColon: true 104 | SpaceBeforeParens: ControlStatements 105 | SpaceBeforeRangeBasedForLoopColon: true 106 | SpaceInEmptyParentheses: false 107 | SpacesBeforeTrailingComments: 1 108 | SpacesInAngles: false 109 | SpacesInContainerLiterals: true 110 | SpacesInCStyleCastParentheses: false 111 | SpacesInParentheses: false 112 | SpacesInSquareBrackets: false 113 | Standard: Cpp11 114 | StatementMacros: 115 | - Q_UNUSED 116 | - QT_REQUIRE_VERSION 117 | TabWidth: 4 118 | UseTab: Never 119 | -------------------------------------------------------------------------------- /.github/workflows/clang-format.sh: -------------------------------------------------------------------------------- 1 | clang-format src/*.cpp include/elasty/*.hpp tests/*.*pp examples/**/*.*pp --output-replacements-xml | grep -c "/dev/null 2 | if [ $? -ne 1 ]; then echo "Ill-styled code detected" && exit 1; fi 3 | -------------------------------------------------------------------------------- /.github/workflows/clang-format.yml: -------------------------------------------------------------------------------- 1 | name: Format Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | format-test: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: install-clang-format 13 | run: brew install clang-format 14 | - name: clang-format 15 | run: | 16 | sh ./.github/workflows/clang-format.sh 17 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: install-build-dependencies 13 | run: brew install eigen imath 14 | - name: submodule 15 | run: git submodule update --init --recursive 16 | - name: cmake 17 | run: cmake . -DELASTY_EXAMPLES=ON -DELASTY_TESTS=ON 18 | - name: make 19 | run: make 20 | - name: ctest 21 | run: ctest --verbose 22 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-20.04] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: install-openexr-v2.3.0 17 | run: | 18 | curl -L https://github.com/AcademySoftwareFoundation/Imath/archive/refs/tags/v3.0.2.tar.gz | tar -xvz 19 | mkdir build-imath 20 | cd build-imath 21 | cmake ../Imath-3.0.2 -DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=OFF 22 | make 23 | sudo make install 24 | - name: install-other-build-dependencies 25 | run: sudo apt-get -y install libeigen3-dev libglu1-mesa-dev xorg-dev 26 | - name: submodule 27 | run: git submodule update --init --recursive 28 | - name: cmake 29 | run: | 30 | cmake . -DELASTY_EXAMPLES=ON -DELASTY_TESTS=ON 31 | - name: make 32 | run: make 33 | - name: ctest 34 | run: ctest --verbose 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,xcode,macos,windows 3 | # Edit at https://www.gitignore.io/?templates=linux,xcode,macos,windows 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Windows ### 49 | # Windows thumbnail cache files 50 | Thumbs.db 51 | ehthumbs.db 52 | ehthumbs_vista.db 53 | 54 | # Dump file 55 | *.stackdump 56 | 57 | # Folder config file 58 | [Dd]esktop.ini 59 | 60 | # Recycle Bin used on file shares 61 | $RECYCLE.BIN/ 62 | 63 | # Windows Installer files 64 | *.cab 65 | *.msi 66 | *.msix 67 | *.msm 68 | *.msp 69 | 70 | # Windows shortcuts 71 | *.lnk 72 | 73 | ### Xcode ### 74 | # Xcode 75 | # 76 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 77 | 78 | ## User settings 79 | xcuserdata/ 80 | 81 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 82 | *.xcscmblueprint 83 | *.xccheckout 84 | 85 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 86 | build/ 87 | DerivedData/ 88 | *.moved-aside 89 | *.pbxuser 90 | !default.pbxuser 91 | *.mode1v3 92 | !default.mode1v3 93 | *.mode2v3 94 | !default.mode2v3 95 | *.perspectivev3 96 | !default.perspectivev3 97 | 98 | ### Xcode Patch ### 99 | *.xcodeproj/* 100 | !*.xcodeproj/project.pbxproj 101 | !*.xcodeproj/xcshareddata/ 102 | !*.xcworkspace/contents.xcworkspacedata 103 | /*.gcno 104 | **/xcshareddata/WorkspaceSettings.xcsettings 105 | 106 | # End of https://www.gitignore.io/api/linux,xcode,macos,windows -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/bigger"] 2 | path = external/bigger 3 | url = https://github.com/yuki-koyama/bigger.git 4 | [submodule "external/alembic"] 5 | path = external/alembic 6 | url = https://github.com/alembic/alembic.git 7 | [submodule "external/timer"] 8 | path = external/timer 9 | url = https://github.com/yuki-koyama/timer.git 10 | [submodule "external/googletest"] 11 | path = external/googletest 12 | url = https://github.com/google/googletest.git 13 | [submodule "external/mathtoolbox"] 14 | path = external/mathtoolbox 15 | url = https://github.com/yuki-koyama/mathtoolbox.git 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(elasty) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | set(CMAKE_CXX_EXTENSIONS OFF) 7 | 8 | # ------------------------------------------------------------------------------ 9 | # Find prerequisites 10 | # ------------------------------------------------------------------------------ 11 | 12 | find_package(Eigen3 REQUIRED) 13 | if((NOT TARGET Eigen3::Eigen) AND (DEFINED EIGEN3_INCLUDE_DIR)) 14 | # Eigen 3.0--3.2 do not provide the target named Eigen3::Eigen 15 | add_library(AliasEigen3 INTERFACE) 16 | target_include_directories(AliasEigen3 INTERFACE ${EIGEN3_INCLUDE_DIR}) 17 | add_library(Eigen3::Eigen ALIAS AliasEigen3) 18 | endif() 19 | 20 | # ------------------------------------------------------------------------------ 21 | # Build external libraries 22 | # ------------------------------------------------------------------------------ 23 | 24 | set(ALEMBIC_BUILD_LIBS ON CACHE INTERNAL "" FORCE) 25 | set(ALEMBIC_LIB_USES_BOOST OFF CACHE INTERNAL "" FORCE) 26 | set(ALEMBIC_LIB_USES_TR1 OFF CACHE INTERNAL "" FORCE) 27 | set(ALEMBIC_SHARED_LIBS OFF CACHE INTERNAL "" FORCE) 28 | set(USE_ARNOLD OFF CACHE INTERNAL "" FORCE) 29 | set(USE_BINARIES ON CACHE INTERNAL "" FORCE) 30 | set(USE_EXAMPLES OFF CACHE INTERNAL "" FORCE) 31 | set(USE_HDF5 OFF CACHE INTERNAL "" FORCE) 32 | set(USE_MAYA OFF CACHE INTERNAL "" FORCE) 33 | set(USE_PRMAN OFF CACHE INTERNAL "" FORCE) 34 | set(USE_PYALEMBIC OFF CACHE INTERNAL "" FORCE) 35 | set(USE_STATIC_BOOST OFF CACHE INTERNAL "" FORCE) 36 | set(USE_STATIC_HDF5 OFF CACHE INTERNAL "" FORCE) 37 | set(USE_TESTS OFF CACHE INTERNAL "" FORCE) 38 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/alembic) 39 | target_include_directories(Alembic PUBLIC $) 40 | 41 | set(TIMER_BUILD_TEST OFF CACHE INTERNAL "" FORCE) 42 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/timer) 43 | 44 | # ------------------------------------------------------------------------------ 45 | # Build the core library 46 | # ------------------------------------------------------------------------------ 47 | 48 | file(GLOB sources ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) 49 | file(GLOB headers ${CMAKE_CURRENT_SOURCE_DIR}/include/elasty/*.hpp) 50 | 51 | add_library(elasty STATIC ${sources} ${headers}) 52 | target_include_directories(elasty PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) 53 | target_link_libraries(elasty PUBLIC Eigen3::Eigen) 54 | target_link_libraries(elasty PRIVATE Alembic tinyobjloader) 55 | 56 | # ------------------------------------------------------------------------------ 57 | # Build examples 58 | # ------------------------------------------------------------------------------ 59 | 60 | option(ELASTY_EXAMPLES ON "") 61 | if(ELASTY_EXAMPLES) 62 | set(BIGGER_EXAMPLES OFF CACHE INTERNAL "" FORCE) 63 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/bigger) 64 | 65 | set(MATHTOOLBOX_BUILD_EXAMPLES OFF CACHE INTERNAL "" FORCE) 66 | set(MATHTOOLBOX_PYTHON_BINDINGS OFF CACHE INTERNAL "" FORCE) 67 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/mathtoolbox) 68 | 69 | add_executable(cloth-alembic ${CMAKE_CURRENT_SOURCE_DIR}/examples/cloth-alembic/main.cpp) 70 | target_link_libraries(cloth-alembic elasty timer) 71 | 72 | add_executable(explicit-2d ${CMAKE_CURRENT_SOURCE_DIR}/examples/explicit-2d/main.cpp) 73 | target_link_libraries(explicit-2d elasty timer Alembic) 74 | 75 | add_executable(variational-implicit-2d ${CMAKE_CURRENT_SOURCE_DIR}/examples/variational-implicit-2d/main.cpp) 76 | target_link_libraries(variational-implicit-2d elasty timer Alembic mathtoolbox) 77 | 78 | add_executable(variational-implicit-3d ${CMAKE_CURRENT_SOURCE_DIR}/examples/variational-implicit-3d/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/examples/variational-implicit-3d/mesh.hpp) 79 | target_link_libraries(variational-implicit-3d elasty timer Alembic mathtoolbox) 80 | add_custom_command(TARGET variational-implicit-3d POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/assets $/assets) 81 | 82 | add_executable(interactive-app ${CMAKE_CURRENT_SOURCE_DIR}/examples/interactive-app/main.cpp) 83 | target_link_libraries(interactive-app bigger elasty) 84 | add_custom_command(TARGET interactive-app POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${BIGGER_SHADER_OUTPUT_DIR} $/shaders) 85 | 86 | add_executable(pbd-xpbd-comparison ${CMAKE_CURRENT_SOURCE_DIR}/examples/pbd-xpbd-comparison/main.cpp) 87 | target_link_libraries(pbd-xpbd-comparison elasty timer) 88 | 89 | add_executable(aerodynamics ${CMAKE_CURRENT_SOURCE_DIR}/examples/aerodynamics/main.cpp) 90 | target_link_libraries(aerodynamics elasty timer) 91 | else() 92 | # Both bigger and elasty depend on tinyobjloader; when bigger is not added, it is necessary to add tinyobjloader explicitly 93 | set(TINYOBJLOADER_BUILD_OBJ_STICHER OFF CACHE INTERNAL "" FORCE) 94 | set(TINYOBJLOADER_BUILD_TEST_LOADER OFF CACHE INTERNAL "" FORCE) 95 | set(TINYOBJLOADER_USE_DOUBLE OFF CACHE INTERNAL "" FORCE) 96 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/bigger/external/tinyobjloader) 97 | endif() 98 | 99 | # ------------------------------------------------------------------------------ 100 | # Build tests 101 | # ------------------------------------------------------------------------------ 102 | 103 | option(ELASTY_TESTS ON "") 104 | if(ELASTY_TESTS) 105 | enable_testing() 106 | 107 | set(BUILD_GMOCK OFF CACHE INTERNAL "" FORCE) 108 | set(INSTALL_GTEST OFF CACHE INTERNAL "" FORCE) 109 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/googletest) 110 | 111 | add_executable(constraint-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/constraint-test.cpp) 112 | target_link_libraries(constraint-test elasty gtest_main) 113 | 114 | add_executable(fem-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/fem-test.cpp) 115 | target_link_libraries(fem-test elasty gtest_main) 116 | 117 | add_test(NAME constraint-test COMMAND $) 118 | add_test(NAME fem-test COMMAND $) 119 | endif() 120 | 121 | # ------------------------------------------------------------------------------ 122 | # Format codes 123 | # ------------------------------------------------------------------------------ 124 | 125 | find_program(CLANG_FORMAT clang-format) 126 | if(CLANG_FORMAT) 127 | set(format_target_sources 128 | ${headers} 129 | ${sources} 130 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/cloth-alembic/main.cpp 131 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/explicit-2d/main.cpp 132 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/variational-implicit-2d/main.cpp 133 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/variational-implicit-3d/main.cpp 134 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/variational-implicit-3d/mesh.hpp 135 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/interactive-app/main.cpp 136 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/pbd-xpbd-comparison/main.cpp 137 | ${CMAKE_CURRENT_SOURCE_DIR}/examples/aerodynamics/main.cpp 138 | ${CMAKE_CURRENT_SOURCE_DIR}/tests/constraint-test.cpp 139 | ${CMAKE_CURRENT_SOURCE_DIR}/tests/fem-test.cpp 140 | ) 141 | add_custom_target(elasty-format COMMAND ${CLANG_FORMAT} -i ${format_target_sources} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 142 | endif() 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yuki Koyama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elasty 2 | 3 | ![macOS](https://github.com/yuki-koyama/elasty/workflows/macOS/badge.svg) 4 | ![Ubuntu](https://github.com/yuki-koyama/elasty/workflows/Ubuntu/badge.svg) 5 | ![License](https://img.shields.io/github/license/yuki-koyama/elasty) 6 | 7 | A research-oriented elastic body simulator 8 | 9 | ![](./docs/cloth.gif) 10 | 11 | ## Algorithms 12 | 13 | ### Frameworks 14 | 15 | - [x] Position-based dynamics (PBD) [Müller+07] 16 | - [x] Extended position-based dynamics (XPBD) [Macklin+16] 17 | - [x] Small-steps XPBD [Macklin+19] 18 | - [ ] Projective dynamics [Bouaziz+14] 19 | - [ ] Quasi-Newton dynamics [Liu+17] 20 | - [ ] ... 21 | 22 | ### Update Schemes for PBD/XPBD 23 | 24 | - [x] Gauss-Seidel update 25 | - [ ] Jacobi update 26 | 27 | ### Constraints for PBD/XPBD 28 | 29 | - [ ] Area conservation constraint [Müller+14] 30 | - [x] Bending constraint [Müller+07] 31 | - [ ] Continuum-tetrahedron constraint [Bender+14] 32 | - [x] Continuum-triangle constraint [Bender+14] 33 | - [x] Distance constraint [Müller+07] 34 | - [x] Environmental collision constraint 35 | - [ ] Example-based shape matching constraint [Koyama+12] 36 | - [x] Fixed point constraint 37 | - [x] Isometric bending constraint [Bergou+06; Bender+14] 38 | - [ ] Long range attachments constraint [Kim+12] 39 | - [x] Shape matching constraint [Müller+05] 40 | - [ ] Stable Neo-Hookean constraint [Macklin+21] 41 | - [ ] Tetrahedron strain constraint [Müller+14] 42 | - [ ] Triangle strain constraint [Müller+14] 43 | - [ ] Volume conservation constraint [Müller+14] 44 | - [ ] ... 45 | 46 | ### Continuum Materials for FEM Simulation 47 | 48 | - [x] St. Venant Kirchhoff model 49 | - [x] Co-rotational model 50 | - [ ] Stable Neo-Hookean model [Smith+18] 51 | - [ ] ... 52 | 53 | ## Additional Features 54 | 55 | - Alembic export of triangle meshes 56 | - Simple aerodynamics for clothes [Wilson+14] 57 | - 2D FEM simulation with explicit Euler integration 58 | - 2D FEM simulation with variational implicit Euler integration [Martin+11] 59 | - 3D FEM simulation with variational implicit Euler integration [Martin+11] 60 | 61 | ## Dependencies 62 | 63 | ### Core Library 64 | 65 | - Alembic [BSD 3-Clause] 66 | - Imath (3.0.2+) [BSD 3-Clause] 67 | - Eigen [MPL2] 68 | - tinyobjloader [MIT] 69 | 70 | ### Demos 71 | 72 | - bigger [MIT] 73 | - bigg [Unlicense] 74 | - bgfx.cmake [CC0] 75 | - bgfx [BSD 2-Clause] 76 | - bimg [BSD 2-Clause] 77 | - bx [BSD 2-Clause] 78 | - Dear ImGui [MIT] 79 | - GLFW [Zlib] 80 | - GLM [MIT] 81 | - random-util [MIT] 82 | - string-util [MIT] 83 | - tinyobjloader [MIT] 84 | - timer [MIT] 85 | - mathtoolbox [MIT] 86 | 87 | ### Tests 88 | 89 | - googletest [BSD 3-Clause] 90 | 91 | ## Prerequisites 92 | 93 | ### macOS 94 | 95 | ```bash 96 | brew install eigen imath 97 | ``` 98 | 99 | ### Ubuntu 20.04 100 | 101 | ```bash 102 | apt install libeigen3-dev 103 | ``` 104 | and manually install Imath 3.0.2+. 105 | 106 | ### Windows 107 | 108 | (not tested yet) 109 | 110 | ## Build 111 | 112 | ```bash 113 | git clone https://github.com/yuki-koyama/elasty.git --recursive 114 | mkdir build 115 | cd build 116 | cmake ../elasty 117 | make 118 | ``` 119 | 120 | ## Gallery 121 | 122 | ### PBD vs. XPBD 123 | 124 | The constraint stiffnesses in PBD [Müller+07] are dependent on the number of iterations for constraint solving. As the number of iterations increases, the constraints become infinitely stiff regardless of the `stiffness` parameters. This issue makes the parameter tuning difficult. 125 | 126 | XPBD [Macklin+16] resolves this issue. As the number of iterations increases, the constraint stiffnesses converge to some (non-infinitely-stiff) states in accordance with the `compliance` parameters. This property makes the parameter tuning easier and more consistent. 127 | 128 | ![](./docs/pbd-xpbd-comparison.jpg) 129 | 130 | These simulated results were generated by `examples/pbd-xpbd-comparison/main.cpp`. 131 | 132 | ### Wind Effects for Cloth Simulation 133 | 134 | This library supports wind effects for cloth simulation. This library calculates aerodynamic "drag" and "lift" forces based on the model used in Disney's Frozen [Wilson+14]. 135 | 136 | ![](./docs/wind.jpg) 137 | 138 | This simulated result was generated by `examples/aerodynamics/main.cpp`. 139 | 140 | ### Finite Element Methods 141 | 142 | This library offers some utility functions to implement the finite element method (FEM) and simple examples of such implementations. 143 | 144 | ![](./docs/fem-2d.gif) 145 | ![](./docs/fem-3d.gif) 146 | 147 | These simulated results were generated by `examples/variational-implicit-2d/main.cpp` and `examples/variational-implicit-3d/main.cpp`, respectively, which use the variational implicit Euler method [Martin+11] and the co-rotational model. 148 | 149 | ## License 150 | 151 | MIT License 152 | 153 | ## Contributing 154 | 155 | Issue reports and pull requests are highly welcomed. 156 | 157 | ## References 158 | 159 | - __[Bender+14]__ Jan Bender, Dan Koschier, Patrick Charrier, and Daniel Weber. 2014. Position-based simulation of continuous materials. Comput. Graph. 44 (2014), 1-10. DOI: http://doi.org/10.1016/j.cag.2014.07.004 160 | - __[Bender+17]__ Jan Bender, Matthias Müller, and Miles Macklin. 2017. A survey on position based dynamics, 2017. In Proc. Eurographics '17 Tutorials, Article 6, 31 pages. DOI: https://doi.org/10.2312/egt.20171034 161 | - __[Bergou+06]__ Miklos Bergou, Max Wardetzky, David Harmon, Denis Zorin, and Eitan Grinspun. 2006. A quadratic bending model for inextensible surfaces. In Proc. SGP '06, 227--230. DOI: https://doi.org/10.2312/SGP/SGP06/227-230 162 | - __[Bouaziz+14]__ Sofien Bouaziz, Sebastian Martin, Tiantian Liu, Ladislav Kavan, and Mark Pauly. 2014. Projective dynamics: fusing constraint projections for fast simulation. ACM Trans. Graph. 33, 4 (2014), 154:1--154:11. DOI: https://doi.org/10.1145/2601097.2601116 163 | - __[Kim+12]__ Tae-Yong Kim, Nuttapong Chentanez, and Matthias Müller-Fischer. 2012. Long range attachments: a method to simulate inextensible clothing in computer games. In Proc. SCA '12, 305--310. DOI: https://doi.org/10.2312/SCA/SCA12/305-310 164 | - __[Koyama+12]__ Yuki Koyama, Kenshi Takayama, Nobuyuki Umetani, and Takeo Igarashi. 2012. Real-time example-based elastic deformation. In Proc. SCA '12, 19-24. DOI: https://doi.org/10.2312/SCA/SCA12/019-024 165 | - __[Liu+17]__ Tiantian Liu, Sofien Bouaziz, and Ladislav Kavan. 2017. Quasi-Newton methods for real-time simulation of hyperelastic materials. ACM Trans. Graph. 36, 3 (2017), 23:1--23:16. DOI: https://doi.org/10.1145/2990496 166 | - __[Macklin+16]__ Miles Macklin, Matthias Müller, and Nuttapong Chentanez. 2016. XPBD: position-based simulation of compliant constrained dynamics. In Proc. MIG '16, 49-54. DOI: https://doi.org/10.1145/2994258.2994272 167 | - __[Macklin+19]__ Miles Macklin, Kier Storey, Michelle Lu, Pierre Terdiman, Nuttapong Chentanez, Stefan Jeschke, and Matthias Müller. 2019. Small steps in physics simulation. In Proc. SCA '19, 2:1–2:7. DOI: https://doi.org/10.1145/3309486.3340247 168 | - __[Macklin+21]__ Miles Macklin and Matthias Muller. 2021. A constraint-based formulation of stable Neo-Hookean materials. In Proc. MIG '21, 12:1–12:7. DOI: https://doi.org/10.1145/3487983.3488289 169 | - __[Martin+11]__ Sebastian Martin, Bernhard Thomaszewski, Eitan Grinspun, and Markus Gross. 2011. Example-based elastic materials. ACM Trans. Graph. 30, 4, 72:1--72:8 (July 2011). DOI: https://doi.org/10.1145/2010324.1964967 170 | - __[Müller+05]__ Matthias Müller, Bruno Heidelberger, Matthias Teschner, and Markus Gross. 2005. Meshless deformations based on shape matching. ACM Trans. Graph. 24, 3 (2005), 471-478. DOI: https://doi.org/10.1145/1073204.1073216 171 | - __[Müller+07]__ Matthias Müller, Bruno Heidelberger, Marcus Hennix, and John Ratcliff. 2007. Position based dynamics. J. Vis. Comun. Image Represent. 18, 2 (2007), 109-118. DOI: https://doi.org/10.1016/j.jvcir.2007.01.005 172 | - __[Smith+18]__ Breannan Smith, Fernando De Goes, and Theodore Kim. 2018. Stable Neo-Hookean Flesh Simulation. ACM Trans. Graph. 37, 2, 12:1-12:15 (July 2018). DOI: https://doi.org/10.1145/3180491 173 | - __[Umetani+14]__ Nobuyuki Umetani, Ryan Schmidt, and Jos Stam. 2014. Position-based elastic rods. In Proc. SCA '14, 21-30. DOI: https://doi.org/10.2312/sca.20141119 174 | - __[Wilson+14]__ Keith Wilson, Aleka McAdams, Hubert Leo, and Maryann Simmons. 2014. Simulating wind effects on cloth and hair in Disney’s Frozen. In ACM SIGGRAPH 2014 Talks, 48:1. DOI: https://doi.org/10.1145/2614106.2614120 175 | - (TODO) 176 | -------------------------------------------------------------------------------- /docs/cloth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/cloth.gif -------------------------------------------------------------------------------- /docs/fem-2d.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/fem-2d.gif -------------------------------------------------------------------------------- /docs/fem-2d/fem-2d.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/fem-2d/fem-2d.mp4 -------------------------------------------------------------------------------- /docs/fem-3d.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/fem-3d.gif -------------------------------------------------------------------------------- /docs/fem-3d/fem-3d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/fem-3d/fem-3d.jpg -------------------------------------------------------------------------------- /docs/fem-3d/fem-3d.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/fem-3d/fem-3d.mp4 -------------------------------------------------------------------------------- /docs/fem-3d/fem-3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/fem-3d/fem-3d.png -------------------------------------------------------------------------------- /docs/pbd-xpbd-comparison.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/pbd-xpbd-comparison.jpg -------------------------------------------------------------------------------- /docs/wind.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/wind.jpg -------------------------------------------------------------------------------- /docs/wind/wind-0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/wind/wind-0.jpg -------------------------------------------------------------------------------- /docs/wind/wind-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/wind/wind-1.jpg -------------------------------------------------------------------------------- /docs/wind/wind-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/wind/wind-2.jpg -------------------------------------------------------------------------------- /docs/wind/wind-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuki-koyama/elasty/b959790659968d3793f78c882bb39619c81886ce/docs/wind/wind-3.jpg -------------------------------------------------------------------------------- /examples/aerodynamics/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class SimpleEngine final : public elasty::AbstractEngine 9 | { 10 | public: 11 | SimpleEngine(const double draf_coeff, const double lift_coeff, const double wind_velocity) 12 | : elasty::AbstractEngine(1.0 / 60.0, 16, 4, elasty::AlgorithmType::Xpbd), 13 | m_wind_velocity(0.0, 0.0, wind_velocity), 14 | m_drag_coeff(draf_coeff), 15 | m_lift_coeff(lift_coeff) 16 | { 17 | } 18 | 19 | void initializeScene() override 20 | { 21 | // Instantiate a cloth object 22 | constexpr double cloth_in_plane_stiffness = 1.000; ///< PBD 23 | constexpr double cloth_in_plane_compliance = 5e-02; ///< XPBD 24 | constexpr double cloth_out_of_plane_stiffness = 0.100; ///< PBD 25 | constexpr double cloth_out_of_plane_compliance = 5e+04; ///< XPBD 26 | constexpr unsigned cloth_resolution = 60; 27 | 28 | const Eigen::Affine3d cloth_import_transform = Eigen::Affine3d(Eigen::Translation3d(0.0, 2.0, 1.0)); 29 | 30 | m_cloth_sim_object = 31 | std::make_shared(cloth_resolution, 32 | cloth_in_plane_stiffness, 33 | cloth_in_plane_compliance, 34 | cloth_out_of_plane_stiffness, 35 | cloth_out_of_plane_compliance, 36 | getDeltaPhysicsTime(), 37 | cloth_import_transform, 38 | elasty::ClothSimObject::InPlaneStrategy::EdgeDistance, 39 | elasty::ClothSimObject::OutOfPlaneStrategy::IsometricBending); 40 | 41 | // Register the cloth object 42 | std::copy(m_cloth_sim_object->m_particles.begin(), 43 | m_cloth_sim_object->m_particles.end(), 44 | std::back_inserter(m_particles)); 45 | std::copy(m_cloth_sim_object->m_constraints.begin(), 46 | m_cloth_sim_object->m_constraints.end(), 47 | std::back_inserter(m_constraints)); 48 | 49 | // Pin two of the corners of the cloth 50 | constexpr double range_radius = 0.1; 51 | for (const auto& particle : m_particles) 52 | { 53 | if ((particle->x - Eigen::Vector3d(+1.0, 2.0, 0.0)).norm() < range_radius) 54 | { 55 | m_constraints.push_back(std::make_shared( 56 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x)); 57 | } 58 | if ((particle->x - Eigen::Vector3d(-1.0, 2.0, 0.0)).norm() < range_radius) 59 | { 60 | m_constraints.push_back(std::make_shared( 61 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x)); 62 | } 63 | } 64 | } 65 | 66 | void setExternalForces() override 67 | { 68 | const Eigen::Vector3d gravity = Eigen::Vector3d(0.0, -9.8, 0.0); 69 | 70 | for (auto particle : m_particles) 71 | { 72 | particle->f = particle->m * gravity; 73 | } 74 | 75 | m_cloth_sim_object->applyAerodynamicForces(m_wind_velocity, m_drag_coeff, m_lift_coeff); 76 | } 77 | 78 | void generateCollisionConstraints() override 79 | { 80 | // Collision with a sphere 81 | const Eigen::Vector3d center(0.0, 1.0, 0.0); 82 | constexpr double tolerance = 0.05; 83 | constexpr double radius = 0.50 + 0.02; 84 | constexpr double stiffness = 1.00; 85 | constexpr double compliance = 0.00; 86 | for (auto particle : m_particles) 87 | { 88 | const Eigen::Vector3d direction = particle->x - center; 89 | if (direction.norm() < radius + tolerance) 90 | { 91 | const Eigen::Vector3d normal = direction.normalized(); 92 | const double distance = center.transpose() * normal + radius; 93 | m_instant_constraints.push_back(std::make_shared( 94 | particle, stiffness, compliance, getDeltaPhysicsTime(), normal, distance)); 95 | } 96 | } 97 | } 98 | 99 | void updateVelocities() override 100 | { 101 | const double decay_rate = std::exp(std::log(0.95) * getDeltaPhysicsTime()); 102 | 103 | for (auto particle : m_particles) 104 | { 105 | particle->v *= decay_rate; 106 | } 107 | } 108 | 109 | std::shared_ptr m_cloth_sim_object; 110 | 111 | private: 112 | const Eigen::Vector3d m_wind_velocity; 113 | const double m_drag_coeff; 114 | const double m_lift_coeff; 115 | }; 116 | 117 | int main(int argc, char** argv) 118 | { 119 | constexpr std::tuple conditions[] = { 120 | {0.000, 0.000, 0.0, "without-aerodynamics"}, 121 | {0.060, 0.030, 0.0, "with-aerodynamics"}, 122 | {0.080, 0.030, 8.0, "wind"}, 123 | {0.080, 0.000, 8.0, "wind-high-drag"}, 124 | {0.080, 0.080, 8.0, "wind-high-lift"}, 125 | }; 126 | 127 | for (const auto& condition : conditions) 128 | { 129 | const auto& drag_coeff = std::get<0>(condition); 130 | const auto& lift_coeff = std::get<1>(condition); 131 | const auto& wind_velocity = std::get<2>(condition); 132 | const auto& condition_name = std::get<3>(condition); 133 | 134 | const std::string name{condition_name}; 135 | 136 | const timer::Timer t(name); 137 | 138 | SimpleEngine engine(drag_coeff, lift_coeff, wind_velocity); 139 | engine.initializeScene(); 140 | 141 | auto alembic_manager = 142 | elasty::createClothAlembicManager(name + ".abc", engine.m_cloth_sim_object, engine.getDeltaFrameTime()); 143 | 144 | for (unsigned int frame = 0; frame < 300; ++frame) 145 | { 146 | alembic_manager->submitCurrentStatus(); 147 | engine.proceedFrame(); 148 | } 149 | } 150 | 151 | return 0; 152 | } 153 | -------------------------------------------------------------------------------- /examples/cloth-alembic/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // #define SPHERE_COLLISION 9 | #define MOVING_SPHERE_COLLISION 10 | 11 | #define CLOTH_FALL 12 | 13 | class SimpleEngine final : public elasty::AbstractEngine 14 | { 15 | public: 16 | SimpleEngine() : elasty::AbstractEngine(1.0 / 60.0, 10, 5, elasty::AlgorithmType::Xpbd) {} 17 | 18 | void initializeScene() override 19 | { 20 | // Instantiate a cloth object 21 | constexpr double cloth_in_plane_stiffness = 1.000; ///< PBD 22 | constexpr double cloth_in_plane_compliance = 5e-02; ///< XPBD 23 | constexpr double cloth_out_of_plane_stiffness = 0.100; ///< PBD 24 | constexpr double cloth_out_of_plane_compliance = 5e+04; ///< XPBD 25 | constexpr unsigned cloth_resolution = 50; 26 | 27 | const Eigen::Affine3d cloth_import_transform = 28 | #if defined(CLOTH_FALL) 29 | Eigen::Affine3d(Eigen::Translation3d(0.0, 2.0, 1.0)); 30 | #else 31 | Eigen::Translation3d(0.0, 1.0, 0.0) * Eigen::AngleAxisd(0.5 * elasty::pi(), Eigen::Vector3d::UnitX()); 32 | #endif 33 | 34 | m_cloth_sim_object = 35 | std::make_shared(cloth_resolution, 36 | cloth_in_plane_stiffness, 37 | cloth_in_plane_compliance, 38 | cloth_out_of_plane_stiffness, 39 | cloth_out_of_plane_compliance, 40 | getDeltaPhysicsTime(), 41 | cloth_import_transform, 42 | elasty::ClothSimObject::InPlaneStrategy::EdgeDistance, 43 | elasty::ClothSimObject::OutOfPlaneStrategy::IsometricBending); 44 | 45 | // Register the cloth object 46 | std::copy(m_cloth_sim_object->m_particles.begin(), 47 | m_cloth_sim_object->m_particles.end(), 48 | std::back_inserter(m_particles)); 49 | std::copy(m_cloth_sim_object->m_constraints.begin(), 50 | m_cloth_sim_object->m_constraints.end(), 51 | std::back_inserter(m_constraints)); 52 | 53 | // Add small perturb 54 | for (const auto& particle : m_particles) 55 | { 56 | particle->v = 1e-03 * Eigen::Vector3d::Random(); 57 | } 58 | 59 | // Pin two of the corners of the cloth 60 | constexpr double range_radius = 0.1; 61 | for (const auto& particle : m_particles) 62 | { 63 | if ((particle->x - Eigen::Vector3d(+1.0, 2.0, 0.0)).norm() < range_radius) 64 | { 65 | m_constraints.push_back(std::make_shared( 66 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x)); 67 | } 68 | if ((particle->x - Eigen::Vector3d(-1.0, 2.0, 0.0)).norm() < range_radius) 69 | { 70 | m_constraints.push_back(std::make_shared( 71 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x)); 72 | } 73 | } 74 | } 75 | 76 | void setExternalForces() override 77 | { 78 | const auto gravity = Eigen::Vector3d{0.0, -9.8, 0.0}; 79 | 80 | for (auto particle : m_particles) 81 | { 82 | particle->f = particle->m * gravity; 83 | } 84 | 85 | // Aerodynamic force for cloth 86 | m_cloth_sim_object->applyAerodynamicForces(); 87 | } 88 | 89 | void generateCollisionConstraints() override 90 | { 91 | #if defined(SPHERE_COLLISION) 92 | // Collision with a sphere 93 | const Eigen::Vector3d center(0.0, 1.0, 0.0); 94 | constexpr double tolerance = 0.05; 95 | constexpr double radius = 0.50 + 0.02; 96 | constexpr double stiffness = 1.00; 97 | constexpr double compliance = 0.00; 98 | for (auto particle : m_particles) 99 | { 100 | const Eigen::Vector3d direction = particle->x - center; 101 | if (direction.norm() < radius + tolerance) 102 | { 103 | const Eigen::Vector3d normal = direction.normalized(); 104 | const double distance = center.transpose() * normal + radius; 105 | m_instant_constraints.push_back(std::make_shared( 106 | particle, stiffness, compliance, m_dt, normal, distance)); 107 | } 108 | } 109 | #elif defined(MOVING_SPHERE_COLLISION) 110 | // Collision with a moving sphere 111 | const Eigen::Vector3d center(0.0, 1.0, std::max(0.0, (getCurrentPhysicsTime() - 1.80))); 112 | constexpr double tolerance = 0.05; 113 | constexpr double radius = 0.50 + 0.02; 114 | constexpr double stiffness = 1.00; 115 | constexpr double compliance = 0.00; 116 | for (auto particle : m_particles) 117 | { 118 | const Eigen::Vector3d direction = particle->x - center; 119 | if (direction.norm() < radius + tolerance) 120 | { 121 | const Eigen::Vector3d normal = direction.normalized(); 122 | const double distance = center.transpose() * normal + radius; 123 | m_instant_constraints.push_back(std::make_shared( 124 | particle, stiffness, compliance, getDeltaPhysicsTime(), normal, distance)); 125 | } 126 | } 127 | #endif 128 | } 129 | 130 | void updateVelocities() override 131 | { 132 | const double decay_rate = std::exp(std::log(0.95) * getDeltaPhysicsTime()); 133 | 134 | for (auto particle : m_particles) 135 | { 136 | particle->v *= decay_rate; 137 | } 138 | } 139 | 140 | std::shared_ptr m_cloth_sim_object; 141 | }; 142 | 143 | int main(int argc, char** argv) 144 | { 145 | SimpleEngine engine; 146 | engine.initializeScene(); 147 | 148 | auto alembic_manager = 149 | elasty::createClothAlembicManager("./cloth.abc", engine.m_cloth_sim_object, engine.getDeltaFrameTime()); 150 | 151 | for (unsigned int frame = 0; frame < 300; ++frame) 152 | { 153 | timer::Timer t(std::to_string(frame)); 154 | alembic_manager->submitCurrentStatus(); 155 | 156 | engine.proceedFrame(); 157 | } 158 | 159 | return 0; 160 | } 161 | -------------------------------------------------------------------------------- /examples/explicit-2d/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace 9 | { 10 | constexpr size_t k_num_dims = 2; 11 | 12 | constexpr double k_youngs_modulus = 800.0; 13 | constexpr double k_poisson_ratio = 0.40; 14 | 15 | constexpr double k_first_lame = elasty::fem::calcFirstLame(k_youngs_modulus, k_poisson_ratio); 16 | constexpr double k_second_lame = elasty::fem::calcSecondLame(k_youngs_modulus, k_poisson_ratio); 17 | 18 | constexpr unsigned k_num_substeps = 10; 19 | constexpr double k_delta_time = 1.0 / 60.0; 20 | 21 | constexpr double k_damping_factor = 0.1; 22 | 23 | enum class Model 24 | { 25 | CoRotational, 26 | StVenantKirchhoff 27 | }; 28 | 29 | constexpr Model k_model = Model::CoRotational; 30 | } // namespace 31 | 32 | template struct Mesh 33 | { 34 | using ElemList = Eigen::Matrix; 35 | 36 | ElemList elems; 37 | 38 | Eigen::VectorXd x_rest; 39 | Eigen::VectorXd x; 40 | Eigen::VectorXd v; 41 | Eigen::VectorXd f; 42 | 43 | double mass = 1.0; 44 | 45 | /// \details Should be precomputed 46 | Eigen::VectorXd lumped_mass; 47 | 48 | /// \details Should be precomputed 49 | std::vector volume_array; 50 | 51 | /// \details Should be precomputed 52 | std::vector> rest_shape_mat_inv_array; 53 | 54 | /// \details Should be precomputed 55 | std::vector> vec_PFPx_array; 56 | }; 57 | 58 | using TriangleMesh = Mesh<2>; 59 | 60 | template typename Derived::Scalar calcEnergyDensity(const Eigen::MatrixBase& deform_grad) 61 | { 62 | switch (k_model) 63 | { 64 | case Model::StVenantKirchhoff: 65 | return elasty::fem::calcStVenantKirchhoffEnergyDensity(deform_grad, k_first_lame, k_second_lame); 66 | case Model::CoRotational: 67 | return elasty::fem::calcCoRotationalEnergyDensity(deform_grad, k_first_lame, k_second_lame); 68 | } 69 | } 70 | 71 | template 72 | Eigen::Matrix calcPiolaStress(const Eigen::MatrixBase& deform_grad) 73 | { 74 | switch (k_model) 75 | { 76 | case Model::StVenantKirchhoff: 77 | return elasty::fem::calcStVenantKirchhoffPiolaStress(deform_grad, k_first_lame, k_second_lame); 78 | case Model::CoRotational: 79 | return elasty::fem::calcCoRotationalPiolaStress(deform_grad, k_first_lame, k_second_lame); 80 | } 81 | } 82 | 83 | class Explicit2dEngine 84 | { 85 | public: 86 | Explicit2dEngine() {} 87 | 88 | void proceedFrame() 89 | { 90 | const std::size_t num_verts = m_mesh.x_rest.size() / k_num_dims; 91 | const std::vector constrained_verts = {0, 1, 2, 3, 4}; 92 | 93 | // Reset forces 94 | m_mesh.f = Eigen::VectorXd::Zero(2 * num_verts); 95 | 96 | // Calculate forces 97 | for (std::size_t i = 0; i < m_mesh.elems.cols(); ++i) 98 | { 99 | const auto& indices = m_mesh.elems.col(i); 100 | 101 | // Retrieve precomputed values 102 | const auto& D_m_inv = m_mesh.rest_shape_mat_inv_array[i]; 103 | const auto& area = m_mesh.volume_array[i]; 104 | const auto& vec_PFPx = m_mesh.vec_PFPx_array[i]; 105 | 106 | // Calculate the deformation gradient $\mathbf{F}$ 107 | const auto F = elasty::fem::calc2dTriangleDeformGrad(m_mesh.x.segment<2>(2 * indices[0]), 108 | m_mesh.x.segment<2>(2 * indices[1]), 109 | m_mesh.x.segment<2>(2 * indices[2]), 110 | D_m_inv); 111 | 112 | // Calculate $\frac{\partial \Phi}{\partial \mathbf{x}}$ and related values 113 | const auto P = calcPiolaStress(F); 114 | const auto vec_P = Eigen::Map(P.data(), P.size()); 115 | const auto PPsiPx = vec_PFPx.transpose() * vec_P; 116 | 117 | // Calculate the internal force 118 | const auto force = -area * PPsiPx; 119 | 120 | assert(indices.size() == 3); 121 | assert(F.rows() == 2 && F.cols() == 2); 122 | assert(vec_PFPx.rows() == 4 && vec_PFPx.cols() == 6); 123 | assert(PPsiPx.rows() == 6 && PPsiPx.cols() == 1); 124 | 125 | // Accumulate forces 126 | m_mesh.f.segment(2 * indices[0], 2) += force.segment(2 * 0, 2); 127 | m_mesh.f.segment(2 * indices[1], 2) += force.segment(2 * 1, 2); 128 | m_mesh.f.segment(2 * indices[2], 2) += force.segment(2 * 2, 2); 129 | } 130 | 131 | // Apply gravity force 132 | for (size_t i = 0; i < num_verts; ++i) 133 | { 134 | m_mesh.f[i * 2 + 1] += m_mesh.lumped_mass(i * 2 + 1) * (-9.80665); 135 | } 136 | 137 | // Calculate the "modified" inverse lumped mass matrix 138 | // Note: This "mass modification" is described in "Large Steps in Cloth Simulation" (SIGGRAPH '98) 139 | Eigen::VectorXd W_diags = m_mesh.lumped_mass.cwiseInverse(); 140 | for (size_t i : constrained_verts) 141 | { 142 | W_diags.segment(i * 2, 2) = Eigen::Vector2d::Zero(); 143 | } 144 | const auto W = W_diags.asDiagonal(); 145 | 146 | // Note: Explicit Euler integration (for velocities) 147 | m_mesh.v = m_mesh.v + m_delta_physics_time * W * m_mesh.f; 148 | 149 | // Note: Explicit Euler integration (for positions) 150 | m_mesh.x = m_mesh.x + m_delta_physics_time * m_mesh.v; 151 | 152 | // Apply naive damping 153 | m_mesh.v *= std::exp(-k_damping_factor * m_delta_physics_time); 154 | } 155 | 156 | void initializeScene() 157 | { 158 | // A simple cantilever 159 | 160 | constexpr std::size_t num_cols = 20; 161 | constexpr std::size_t num_rows = 4; 162 | constexpr std::size_t num_verts = (num_cols + 1) * (num_rows + 1); 163 | constexpr std::size_t num_elems = (num_cols * num_rows) * 2; 164 | constexpr double size = 1.0; 165 | 166 | m_mesh.elems.resize(3, num_elems); 167 | m_mesh.x_rest.resize(num_verts * k_num_dims); 168 | 169 | // Generate a triangle mesh 170 | for (std::size_t col = 0; col < num_cols; ++col) 171 | { 172 | for (std::size_t row = 0; row < num_rows; ++row) 173 | { 174 | const auto base = col * (num_rows + 1) + row; 175 | 176 | m_mesh.elems.col(2 * num_rows * col + 2 * row + 0) << 0 + base, 1 + base, (num_rows + 1) + 1 + base; 177 | m_mesh.elems.col(2 * num_rows * col + 2 * row + 1) << 0 + base, (num_rows + 1) + 1 + base, 178 | (num_rows + 1) + base; 179 | 180 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * col + row), k_num_dims) = 181 | Eigen::Vector2d{col * 1.0, -1.0 * row}; 182 | } 183 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * col + num_rows), k_num_dims) = 184 | Eigen::Vector2d{col * 1.0, -1.0 * num_rows}; 185 | } 186 | for (std::size_t row = 0; row < num_rows; ++row) 187 | { 188 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * num_cols + row), k_num_dims) = 189 | Eigen::Vector2d{num_cols * 1.0, -1.0 * row}; 190 | } 191 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * num_cols + num_rows), k_num_dims) = 192 | Eigen::Vector2d{num_cols * 1.0, -1.0 * num_rows}; 193 | 194 | // Set transform 195 | m_mesh.x_rest *= 1.0 / static_cast(num_rows); 196 | for (std::size_t vert = 0; vert < num_verts; ++vert) 197 | { 198 | m_mesh.x_rest[2 * vert + 1] += 0.5; 199 | } 200 | m_mesh.x_rest *= size; 201 | 202 | // Initialize other values 203 | m_mesh.x = m_mesh.x_rest; 204 | m_mesh.v = Eigen::VectorXd::Zero(k_num_dims * num_verts); 205 | m_mesh.f = Eigen::VectorXd::Zero(k_num_dims * num_verts); 206 | 207 | // Perform precomputation 208 | m_mesh.volume_array.resize(num_elems); 209 | m_mesh.rest_shape_mat_inv_array.resize(num_elems); 210 | m_mesh.vec_PFPx_array.resize(num_elems); 211 | for (std::size_t elem_index = 0; elem_index < num_elems; ++elem_index) 212 | { 213 | const auto& indices = m_mesh.elems.col(elem_index); 214 | 215 | m_mesh.volume_array[elem_index] = elasty::fem::calc2dTriangleArea(m_mesh.x_rest.segment<2>(2 * indices[0]), 216 | m_mesh.x_rest.segment<2>(2 * indices[1]), 217 | m_mesh.x_rest.segment<2>(2 * indices[2])); 218 | m_mesh.rest_shape_mat_inv_array[elem_index] = 219 | elasty::fem::calc2dShapeMatrix(m_mesh.x_rest.segment<2>(2 * indices[0]), 220 | m_mesh.x_rest.segment<2>(2 * indices[1]), 221 | m_mesh.x_rest.segment<2>(2 * indices[2])) 222 | .inverse(); 223 | m_mesh.vec_PFPx_array[elem_index] = 224 | elasty::fem::calcVecTrianglePartDeformGradPartPos(m_mesh.rest_shape_mat_inv_array[elem_index]); 225 | } 226 | m_mesh.lumped_mass = elasty::fem::calcTriangleMeshLumpedMass(m_mesh.x_rest, m_mesh.elems, m_mesh.mass); 227 | } 228 | 229 | /// \brief Getter of the delta physics time. 230 | /// 231 | /// \details The value equals to the delta frame time devided by the number of substeps. 232 | double getDeltaPhysicsTime() const { return m_delta_physics_time; } 233 | 234 | void setDeltaPhysicsTime(const double delta_physics_time) { m_delta_physics_time = delta_physics_time; } 235 | 236 | const TriangleMesh* getMesh() const { return &m_mesh; } 237 | 238 | private: 239 | double m_delta_physics_time = 1.0 / 60.0; 240 | 241 | TriangleMesh m_mesh; 242 | }; 243 | 244 | int main(int argc, char** argv) 245 | { 246 | Explicit2dEngine engine; 247 | 248 | engine.initializeScene(); 249 | engine.setDeltaPhysicsTime(k_delta_time / static_cast(k_num_substeps)); 250 | 251 | const auto mesh = engine.getMesh(); 252 | const std::size_t num_verts = mesh->x_rest.size() / 2; 253 | const std::size_t num_elems = mesh->elems.cols(); 254 | 255 | auto alembic_manager = elasty::createTriangleMesh2dAlembicManager( 256 | "./out.abc", k_delta_time, num_verts, num_elems, mesh->x.data(), mesh->elems.data()); 257 | 258 | for (unsigned int frame = 0; frame < 240; ++frame) 259 | { 260 | timer::Timer t(std::to_string(frame)); 261 | 262 | alembic_manager->submitCurrentStatus(); 263 | 264 | for (int i = 0; i < k_num_substeps; ++i) 265 | { 266 | engine.proceedFrame(); 267 | } 268 | } 269 | 270 | return 0; 271 | } 272 | -------------------------------------------------------------------------------- /examples/interactive-app/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | // #define CAPTURE_SCREEN 19 | // #define EXPORT_ALEMBIC 20 | 21 | #ifdef EXPORT_ALEMBIC 22 | #include 23 | #endif 24 | 25 | namespace 26 | { 27 | inline glm::vec3 eigen2glm(const Eigen::Vector3d& eigen) 28 | { 29 | return glm::vec3(eigen.x(), eigen.y(), eigen.z()); 30 | } 31 | } // namespace 32 | 33 | class SimpleEngine final : public elasty::AbstractEngine 34 | { 35 | public: 36 | void initializeScene() override 37 | { 38 | // Rod 39 | constexpr unsigned int num_particles = 30; 40 | constexpr double total_length = 2.0; 41 | constexpr double segment_length = total_length / double(num_particles - 1); 42 | 43 | std::shared_ptr last_particle = nullptr; 44 | 45 | for (unsigned int i = 0; i < num_particles; ++i) 46 | { 47 | const Eigen::Vector3d x = Eigen::Vector3d(-1.0, 1.0 + segment_length * double(i), 0.0); 48 | const Eigen::Vector3d v = 50.0 * Eigen::Vector3d::Random(); 49 | const double m = 1.0; 50 | 51 | auto particle = std::make_shared(x, v, m); 52 | 53 | m_particles.push_back(particle); 54 | 55 | if (last_particle != nullptr) 56 | { 57 | addConstraint(std::make_shared( 58 | last_particle, particle, 0.5, 0.0, getDeltaPhysicsTime(), segment_length)); 59 | } 60 | 61 | last_particle = particle; 62 | } 63 | 64 | // Pin the tip of the rod 65 | addConstraint(std::make_shared( 66 | last_particle, 1.0, 0.0, getDeltaPhysicsTime(), last_particle->x)); 67 | 68 | // Register the cloth object 69 | std::copy(m_cloth_sim_object->m_particles.begin(), 70 | m_cloth_sim_object->m_particles.end(), 71 | std::back_inserter(m_particles)); 72 | std::copy(m_cloth_sim_object->m_constraints.begin(), 73 | m_cloth_sim_object->m_constraints.end(), 74 | std::back_inserter(m_constraints)); 75 | 76 | // Pin two of the corners of the cloth 77 | constexpr double range_radius = 0.1; 78 | for (const auto& particle : m_particles) 79 | { 80 | const Eigen::Vector3d translation{0.0, 0.0, 0.0}; 81 | 82 | if ((particle->x - Eigen::Vector3d(+1.0 + 1.0, 3.0, 0.0)).norm() < range_radius) 83 | { 84 | m_constraints.push_back(std::make_shared( 85 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x + translation)); 86 | } 87 | if ((particle->x - Eigen::Vector3d(-1.0 + 1.0, 3.0, 0.0)).norm() < range_radius) 88 | { 89 | m_constraints.push_back(std::make_shared( 90 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x + translation)); 91 | } 92 | } 93 | 94 | // Create a solid blob object 95 | constexpr double shape_matching_stiffness = 0.2; 96 | constexpr double shape_matching_total_mass = 5.0; 97 | constexpr int shape_matching_num_particles = 1000; 98 | 99 | std::vector> shape_matching_particles; 100 | for (int i = 0; i < shape_matching_num_particles; ++i) 101 | { 102 | const double size = 0.4; 103 | const Eigen::Vector3d rand_vec = Eigen::Vector3d::Random(); 104 | const Eigen::Vector3d offset = Eigen::Vector3d(0.0, 5.0, -2.0); 105 | 106 | const Eigen::Vector3d x = size * (rand_vec / rand_vec.lpNorm()) + offset; 107 | const Eigen::Vector3d v = Eigen::Vector3d::Random(); 108 | const double m = shape_matching_total_mass / shape_matching_num_particles; 109 | 110 | auto particle = std::make_shared(x, v, m); 111 | 112 | shape_matching_particles.push_back(particle); 113 | m_particles.push_back(particle); 114 | } 115 | auto shape_matching_constaint = std::make_shared( 116 | shape_matching_particles, shape_matching_stiffness, 0.0, getDeltaPhysicsTime()); 117 | m_constraints.push_back(shape_matching_constaint); 118 | } 119 | 120 | void setExternalForces() override 121 | { 122 | const Eigen::Vector3d gravity = Eigen::Vector3d(0.0, -9.8, 0.0); 123 | 124 | for (auto particle : m_particles) 125 | { 126 | particle->f = particle->m * gravity; 127 | } 128 | } 129 | 130 | void generateCollisionConstraints() override 131 | { 132 | for (const auto particle : m_particles) 133 | { 134 | if (particle->p.y() < 0.0) 135 | { 136 | addInstantConstraint(std::make_shared( 137 | particle, 1.0, 0.0, getDeltaPhysicsTime(), Eigen::Vector3d(0.0, 1.0, 0.0), 0.0)); 138 | } 139 | } 140 | } 141 | 142 | void updateVelocities() override 143 | { 144 | const double decay_rate = std::exp(std::log(0.9) * getDeltaPhysicsTime()); 145 | 146 | for (auto particle : m_particles) 147 | { 148 | particle->v *= decay_rate; 149 | } 150 | } 151 | 152 | std::shared_ptr m_cloth_sim_object; 153 | }; 154 | 155 | class SimpleApp final : public bigger::App 156 | { 157 | public: 158 | SimpleApp(); 159 | 160 | void initialize(int argc, char** argv) override; 161 | void onReset() override; 162 | 163 | void updateApp() override; 164 | void releaseSharedResources() override; 165 | 166 | std::shared_ptr m_engine; 167 | 168 | bool m_capture_screen; 169 | 170 | private: 171 | const double m_cloth_in_plane_stiffness = 0.95; 172 | const double m_cloth_in_plane_compliance = 5e-2; 173 | const double m_cloth_out_of_plane_stiffness = 0.05; 174 | const double m_cloth_out_of_plane_compliance = 1e+4; 175 | const unsigned m_cloth_resolution = 20; 176 | const Eigen::Affine3d m_cloth_import_transform = Eigen::Affine3d(Eigen::Translation3d(1.0, 3.0, 1.0)); 177 | 178 | // Shared resources 179 | std::shared_ptr m_default_material; 180 | std::shared_ptr m_checker_white_material; 181 | std::shared_ptr m_checker_black_material; 182 | std::shared_ptr m_sphere_primitive; 183 | std::shared_ptr m_plane_primitive; 184 | }; 185 | 186 | SimpleApp::SimpleApp() 187 | { 188 | getCamera().m_position = glm::vec3(1.0f, 2.0f, -8.0f); 189 | getCamera().m_target = glm::vec3(0.0f, 1.5f, 0.0f); 190 | getCamera().m_fov = 45.0f; 191 | 192 | m_capture_screen = false; 193 | } 194 | 195 | class ClothObject final : public bigger::SceneObject 196 | { 197 | public: 198 | ClothObject(std::shared_ptr cloth_sim_object, 199 | std::shared_ptr material) 200 | : bigger::SceneObject(material), m_cloth_sim_object(cloth_sim_object) 201 | { 202 | std::vector vertex_data = generateVertexData(); 203 | 204 | std::vector triangle_list; 205 | for (unsigned int i = 0; i < cloth_sim_object->m_triangle_list.rows(); ++i) 206 | { 207 | triangle_list.push_back(cloth_sim_object->m_triangle_list(i, 0)); 208 | triangle_list.push_back(cloth_sim_object->m_triangle_list(i, 1)); 209 | triangle_list.push_back(cloth_sim_object->m_triangle_list(i, 2)); 210 | } 211 | 212 | m_dynamic_mesh_primitive = std::make_unique(vertex_data, triangle_list); 213 | 214 | #ifdef EXPORT_ALEMBIC 215 | m_alembic_manager = elasty::createClothAlembicManager("./cloth.abc", m_cloth_sim_object, 1.0 / 60.0); 216 | m_alembic_manager->submitCurrentStatus(); 217 | #endif 218 | } 219 | 220 | void update() override 221 | { 222 | #ifdef EXPORT_ALEMBIC 223 | assert(m_alembic_manager != nullptr); 224 | m_alembic_manager->submitCurrentStatus(); 225 | #endif 226 | } 227 | 228 | void draw(const glm::mat4& parent_transform_matrix = glm::mat4(1.0f)) override 229 | { 230 | m_material->submitUniforms(); 231 | 232 | const std::vector vertex_data = generateVertexData(); 233 | m_dynamic_mesh_primitive->updateVertexData(vertex_data); 234 | 235 | // Do not cull back-facing triangles 236 | bgfx::setState(BGFX_STATE_DEFAULT & (~BGFX_STATE_CULL_CW)); 237 | 238 | m_dynamic_mesh_primitive->submitPrimitive(m_material->m_program); 239 | } 240 | 241 | std::shared_ptr m_cloth_sim_object; 242 | 243 | private: 244 | #ifdef EXPORT_ALEMBIC 245 | std::shared_ptr m_alembic_manager; 246 | #endif 247 | 248 | std::unique_ptr m_dynamic_mesh_primitive; 249 | 250 | std::vector generateVertexData() const 251 | { 252 | std::vector vertex_data(m_cloth_sim_object->m_particles.size()); 253 | for (unsigned int i = 0; i < m_cloth_sim_object->m_particles.size(); ++i) 254 | { 255 | vertex_data[i] = {{m_cloth_sim_object->m_particles[i]->x(0), 256 | m_cloth_sim_object->m_particles[i]->x(1), 257 | m_cloth_sim_object->m_particles[i]->x(2)}, 258 | glm::vec3(0.0f)}; 259 | } 260 | 261 | for (unsigned int i = 0; i < m_cloth_sim_object->m_triangle_list.rows(); ++i) 262 | { 263 | const auto& i_0 = m_cloth_sim_object->m_triangle_list(i, 0); 264 | const auto& i_1 = m_cloth_sim_object->m_triangle_list(i, 1); 265 | const auto& i_2 = m_cloth_sim_object->m_triangle_list(i, 2); 266 | 267 | const glm::vec3& x_0 = vertex_data[i_0].position; 268 | const glm::vec3& x_1 = vertex_data[i_1].position; 269 | const glm::vec3& x_2 = vertex_data[i_2].position; 270 | 271 | const glm::vec3 area_scaled_face_normal = glm::cross(x_1 - x_0, x_2 - x_0); 272 | 273 | vertex_data[i_0].normal += area_scaled_face_normal; 274 | vertex_data[i_1].normal += area_scaled_face_normal; 275 | vertex_data[i_2].normal += area_scaled_face_normal; 276 | } 277 | 278 | for (unsigned int i = 0; i < m_cloth_sim_object->m_particles.size(); ++i) 279 | { 280 | vertex_data[i].normal = glm::normalize(vertex_data[i].normal); 281 | } 282 | 283 | return vertex_data; 284 | } 285 | }; 286 | 287 | class ParticlesObject final : public bigger::SceneObject 288 | { 289 | public: 290 | ParticlesObject(std::shared_ptr engine, 291 | std::shared_ptr sphere_primitive, 292 | std::shared_ptr material) 293 | : bigger::SceneObject(material), m_engine(engine), m_sphere_primitive(sphere_primitive) 294 | { 295 | } 296 | 297 | void draw(const glm::mat4& parent_transform_matrix = glm::mat4(1.0f)) override 298 | { 299 | m_material->submitUniforms(); 300 | 301 | for (const auto& particle : m_engine->getParticles()) 302 | { 303 | const glm::mat4 translate_matrix = glm::translate(glm::mat4(1.0f), eigen2glm(particle->x)); 304 | const glm::mat4 scale_matrix = glm::scale(glm::mat4(1.0f), glm::vec3(scale)); 305 | 306 | const glm::mat4 transform = parent_transform_matrix * translate_matrix * scale_matrix; 307 | 308 | bgfx::setTransform(glm::value_ptr(transform)); 309 | 310 | m_sphere_primitive->submitPrimitive(m_material->m_program, true); 311 | } 312 | } 313 | 314 | private: 315 | const float scale = 0.02f; 316 | 317 | std::shared_ptr m_engine; 318 | std::shared_ptr m_sphere_primitive; 319 | }; 320 | 321 | class CheckerBoardObject final : public bigger::SceneObject 322 | { 323 | public: 324 | CheckerBoardObject(std::shared_ptr plane_primitive, 325 | std::shared_ptr checker_white_material, 326 | std::shared_ptr checker_black_material) 327 | : bigger::SceneObject(nullptr), 328 | m_plane_primitive(plane_primitive), 329 | m_checker_white_material(checker_white_material), 330 | m_checker_black_material(checker_black_material) 331 | { 332 | } 333 | 334 | void draw(const glm::mat4& parent_transform_matrix = glm::mat4(1.0f)) 335 | { 336 | const glm::mat4 transform = parent_transform_matrix * getTransform(); 337 | 338 | for (int i = -m_resolution; i <= m_resolution; ++i) 339 | { 340 | for (int j = -m_resolution; j <= m_resolution; ++j) 341 | { 342 | const glm::mat4 local_transform = glm::translate(transform, glm::vec3(float(i), 0.0f, float(j))); 343 | 344 | bgfx::setTransform(glm::value_ptr(local_transform)); 345 | 346 | auto target_material = ((i + j) % 2 == 0) ? m_checker_white_material : m_checker_black_material; 347 | target_material->submitUniforms(); 348 | m_plane_primitive->submitPrimitive(target_material->m_program); 349 | } 350 | } 351 | } 352 | 353 | private: 354 | const int m_resolution = 4; 355 | 356 | std::shared_ptr m_plane_primitive; 357 | std::shared_ptr m_checker_white_material; 358 | std::shared_ptr m_checker_black_material; 359 | }; 360 | 361 | void SimpleApp::initialize(int argc, char** argv) 362 | { 363 | // Register and apply BGFX configuration settings 364 | reset(BGFX_RESET_VSYNC | BGFX_RESET_MSAA_X8); 365 | 366 | m_default_material = std::make_shared(); 367 | m_checker_white_material = std::make_shared(); 368 | m_checker_white_material->m_diffuse = glm::vec3(0.9); 369 | m_checker_white_material->m_specular = glm::vec3(0.0); 370 | m_checker_black_material = std::make_shared(); 371 | m_checker_black_material->m_diffuse = glm::vec3(0.6); 372 | m_checker_black_material->m_specular = glm::vec3(0.0); 373 | m_sphere_primitive = std::make_shared(); 374 | m_plane_primitive = std::make_shared(); 375 | 376 | m_engine = std::make_unique(); 377 | m_engine->m_cloth_sim_object = std::make_shared(m_cloth_resolution, 378 | m_cloth_in_plane_stiffness, 379 | m_cloth_in_plane_compliance, 380 | m_cloth_out_of_plane_stiffness, 381 | m_cloth_out_of_plane_compliance, 382 | m_engine->getDeltaPhysicsTime(), 383 | m_cloth_import_transform); 384 | m_engine->initializeScene(); 385 | 386 | addSceneObject(std::make_shared(m_engine, m_sphere_primitive, m_default_material)); 387 | addSceneObject( 388 | std::make_shared(m_plane_primitive, m_checker_white_material, m_checker_black_material)); 389 | addSceneObject(std::make_shared(m_engine->m_cloth_sim_object, m_default_material), "cloth"); 390 | } 391 | 392 | void SimpleApp::onReset() 393 | { 394 | constexpr uint32_t bg_color = 0x303030ff; 395 | 396 | bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, bg_color, 1.0f, 0); 397 | } 398 | 399 | void SimpleApp::updateApp() 400 | { 401 | // Display ImGui components 402 | ImGui::Begin("Config"); 403 | { 404 | ImGui::Text("frame: %d", m_frame); 405 | ImGui::Text("time: %.2f", m_time); 406 | ImGui::Text("fps: %.2f", 1.0f / m_last_dt); 407 | ImGui::Separator(); 408 | getCamera().drawImgui(); 409 | ImGui::Separator(); 410 | m_default_material->drawImgui(); 411 | } 412 | ImGui::End(); 413 | 414 | ImGui::Begin("Control"); 415 | { 416 | if (ImGui::Button("Reset")) 417 | { 418 | // Clear 419 | m_engine->m_cloth_sim_object = nullptr; 420 | m_engine->clearScene(); 421 | 422 | // Init 423 | m_engine->m_cloth_sim_object = std::make_shared(m_cloth_resolution, 424 | m_cloth_in_plane_stiffness, 425 | m_cloth_in_plane_compliance, 426 | m_cloth_out_of_plane_stiffness, 427 | m_cloth_out_of_plane_compliance, 428 | m_engine->getDeltaPhysicsTime(), 429 | m_cloth_import_transform); 430 | m_engine->initializeScene(); 431 | 432 | // Re-register 433 | m_scene_objects["cloth"] = std::make_shared(m_engine->m_cloth_sim_object, m_default_material); 434 | } 435 | } 436 | ImGui::End(); 437 | 438 | // Request screen capture 439 | if (m_capture_screen) 440 | { 441 | const std::string dir_path = "."; 442 | const std::string file_name = stringutil::ConvertWithZeroPadding(m_frame, 5); 443 | const std::string file_path = dir_path + "/" + file_name; 444 | bgfx::requestScreenShot(BGFX_INVALID_HANDLE, file_path.c_str()); 445 | } 446 | 447 | // Physics 448 | m_engine->proceedFrame(); 449 | } 450 | 451 | void SimpleApp::releaseSharedResources() 452 | { 453 | m_plane_primitive = nullptr; 454 | m_sphere_primitive = nullptr; 455 | m_default_material = nullptr; 456 | m_checker_white_material = nullptr; 457 | m_checker_black_material = nullptr; 458 | } 459 | 460 | int main(int argc, char** argv) 461 | { 462 | SimpleApp app; 463 | #ifdef CAPTURE_SCREEN 464 | app.m_capture_screen = true; 465 | return app.runApp(argc, argv, bgfx::RendererType::OpenGL); 466 | #else 467 | return app.runApp(argc, argv); 468 | #endif 469 | } 470 | -------------------------------------------------------------------------------- /examples/pbd-xpbd-comparison/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class SimpleEngine final : public elasty::AbstractEngine 10 | { 11 | public: 12 | SimpleEngine(const unsigned num_constraint_iters, const elasty::AlgorithmType type) 13 | : elasty::AbstractEngine(1.0 / 60.0, num_constraint_iters, 1, type) 14 | { 15 | } 16 | 17 | void initializeScene() override 18 | { 19 | // Instantiate a cloth object 20 | constexpr double cloth_in_plane_stiffness = 1.000; ///< PBD 21 | constexpr double cloth_in_plane_compliance = 5e-02; ///< XPBD 22 | constexpr double cloth_out_of_plane_stiffness = 0.100; ///< PBD 23 | constexpr double cloth_out_of_plane_compliance = 5e+04; ///< XPBD 24 | constexpr unsigned cloth_resolution = 40; 25 | 26 | const Eigen::Affine3d cloth_import_transform = 27 | Eigen::Translation3d(0.0, 1.0, 0.0) * Eigen::AngleAxisd(0.5 * elasty::pi(), Eigen::Vector3d::UnitX()); 28 | 29 | m_cloth_sim_object = 30 | std::make_shared(cloth_resolution, 31 | cloth_in_plane_stiffness, 32 | cloth_in_plane_compliance, 33 | cloth_out_of_plane_stiffness, 34 | cloth_out_of_plane_compliance, 35 | getDeltaPhysicsTime(), 36 | cloth_import_transform, 37 | elasty::ClothSimObject::InPlaneStrategy::EdgeDistance, 38 | elasty::ClothSimObject::OutOfPlaneStrategy::IsometricBending); 39 | 40 | // Register the cloth object 41 | std::copy(m_cloth_sim_object->m_particles.begin(), 42 | m_cloth_sim_object->m_particles.end(), 43 | std::back_inserter(m_particles)); 44 | std::copy(m_cloth_sim_object->m_constraints.begin(), 45 | m_cloth_sim_object->m_constraints.end(), 46 | std::back_inserter(m_constraints)); 47 | 48 | // Pin two of the corners of the cloth 49 | constexpr double range_radius = 0.1; 50 | for (const auto& particle : m_particles) 51 | { 52 | if ((particle->x - Eigen::Vector3d(+1.0, 2.0, 0.0)).norm() < range_radius) 53 | { 54 | m_constraints.push_back(std::make_shared( 55 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x)); 56 | } 57 | if ((particle->x - Eigen::Vector3d(-1.0, 2.0, 0.0)).norm() < range_radius) 58 | { 59 | m_constraints.push_back(std::make_shared( 60 | particle, 1.0, 0.0, getDeltaPhysicsTime(), particle->x)); 61 | } 62 | } 63 | } 64 | 65 | void setExternalForces() override 66 | { 67 | const Eigen::Vector3d gravity = Eigen::Vector3d(0.0, -9.8, 0.0); 68 | 69 | for (auto particle : m_particles) 70 | { 71 | particle->f = particle->m * gravity; 72 | } 73 | } 74 | 75 | void generateCollisionConstraints() override {} 76 | 77 | void updateVelocities() override 78 | { 79 | for (auto particle : m_particles) 80 | { 81 | particle->v *= 0.95; 82 | } 83 | } 84 | 85 | std::shared_ptr m_cloth_sim_object; 86 | 87 | int m_count = 0; 88 | }; 89 | 90 | int main(int argc, char** argv) 91 | { 92 | constexpr unsigned num_iters_conditions[] = {2, 4, 8, 16, 32, 64, 128, 256, 512}; 93 | constexpr elasty::AlgorithmType type_conditions[] = {elasty::AlgorithmType::Pbd, elasty::AlgorithmType::Xpbd}; 94 | 95 | for (const auto type : type_conditions) 96 | { 97 | const std::string type_name = (type == elasty::AlgorithmType::Pbd) ? "pbd" : "xpbd"; 98 | 99 | for (const auto num_iters : num_iters_conditions) 100 | { 101 | const std::string name = type_name + "-" + std::to_string(num_iters); 102 | 103 | const timer::Timer t(name); 104 | 105 | SimpleEngine engine(num_iters, type); 106 | engine.initializeScene(); 107 | 108 | auto alembic_manager = elasty::createClothAlembicManager( 109 | name + ".abc", engine.m_cloth_sim_object, engine.getDeltaPhysicsTime()); 110 | 111 | for (unsigned int frame = 0; frame < 300; ++frame) 112 | { 113 | alembic_manager->submitCurrentStatus(); 114 | 115 | engine.m_count = frame; 116 | engine.proceedFrame(); 117 | } 118 | 119 | elasty::exportCurrentClothStateAsObj(name + ".obj", engine.m_cloth_sim_object); 120 | } 121 | } 122 | 123 | return 0; 124 | } 125 | -------------------------------------------------------------------------------- /examples/variational-implicit-2d/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | constexpr std::size_t k_num_dims = 2; 12 | 13 | constexpr double k_youngs_modulus = 200.0; 14 | constexpr double k_poisson_ratio = 0.45; 15 | 16 | constexpr double k_first_lame = elasty::fem::calcFirstLame(k_youngs_modulus, k_poisson_ratio); 17 | constexpr double k_second_lame = elasty::fem::calcSecondLame(k_youngs_modulus, k_poisson_ratio); 18 | 19 | constexpr unsigned k_num_substeps = 5; 20 | constexpr double k_delta_time = 1.0 / 60.0; 21 | 22 | constexpr double k_damping_factor = 0.0; 23 | 24 | constexpr double k_spring_stiffness = 10000.0; 25 | 26 | enum class Model 27 | { 28 | CoRotational, 29 | StVenantKirchhoff 30 | }; 31 | 32 | constexpr Model k_model = Model::CoRotational; 33 | } // namespace 34 | 35 | template struct Mesh 36 | { 37 | using ElemList = Eigen::Matrix; 38 | 39 | ElemList elems; 40 | 41 | Eigen::VectorXd x_rest; 42 | Eigen::VectorXd x; 43 | Eigen::VectorXd v; 44 | Eigen::VectorXd f; 45 | 46 | double mass = 1.0; 47 | 48 | /// \details Should be precomputed 49 | Eigen::VectorXd lumped_mass; 50 | 51 | /// \details Should be precomputed 52 | std::vector volume_array; 53 | 54 | /// \details Should be precomputed 55 | std::vector> rest_shape_mat_inv_array; 56 | 57 | /// \details Should be precomputed 58 | std::vector> vec_PFPx_array; 59 | }; 60 | 61 | using TriangleMesh = Mesh<2>; 62 | 63 | struct Constraint 64 | { 65 | std::size_t vert_index; 66 | std::function motion; 67 | double stiffness; 68 | }; 69 | 70 | template typename Derived::Scalar calcEnergyDensity(const Eigen::MatrixBase& deform_grad) 71 | { 72 | switch (k_model) 73 | { 74 | case Model::StVenantKirchhoff: 75 | return elasty::fem::calcStVenantKirchhoffEnergyDensity(deform_grad, k_first_lame, k_second_lame); 76 | case Model::CoRotational: 77 | return elasty::fem::calcCoRotationalEnergyDensity(deform_grad, k_first_lame, k_second_lame); 78 | } 79 | } 80 | 81 | template 82 | Eigen::Matrix calcPiolaStress(const Eigen::MatrixBase& deform_grad) 83 | { 84 | switch (k_model) 85 | { 86 | case Model::StVenantKirchhoff: 87 | return elasty::fem::calcStVenantKirchhoffPiolaStress(deform_grad, k_first_lame, k_second_lame); 88 | case Model::CoRotational: 89 | return elasty::fem::calcCoRotationalPiolaStress(deform_grad, k_first_lame, k_second_lame); 90 | } 91 | } 92 | 93 | class VariationalImplicit2dEngine 94 | { 95 | public: 96 | VariationalImplicit2dEngine() {} 97 | 98 | void proceedFrame() 99 | { 100 | const std::size_t num_verts = m_mesh.x_rest.size() / k_num_dims; 101 | 102 | // Reset forces 103 | m_mesh.f = Eigen::VectorXd::Zero(2 * num_verts); 104 | 105 | // Apply gravity force 106 | for (std::size_t i = 0; i < num_verts; ++i) 107 | { 108 | m_mesh.f[i * 2 + 1] += m_mesh.lumped_mass(i * 2 + 1) * (-9.80665); 109 | } 110 | 111 | // Calculate the inverse lumped mass matrix 112 | const auto W = m_mesh.lumped_mass.cwiseInverse().asDiagonal(); 113 | 114 | // Calculate the "inertia" position 115 | const double& h = m_delta_physics_time; 116 | const Eigen::VectorXd y = m_mesh.x + h * m_mesh.v + h * h * W * m_mesh.f; 117 | 118 | const auto calcInternalPotential = [&](const Eigen::VectorXd& x) 119 | { 120 | double sum = 0.0; 121 | 122 | // Elastic potential 123 | for (std::size_t i = 0; i < m_mesh.elems.cols(); ++i) 124 | { 125 | const auto& indices = m_mesh.elems.col(i); 126 | 127 | // Retrieve precomputed values 128 | const auto& D_m_inv = m_mesh.rest_shape_mat_inv_array[i]; 129 | const auto& area = m_mesh.volume_array[i]; 130 | 131 | // Calculate the deformation gradient $\mathbf{F}$ 132 | const auto F = elasty::fem::calc2dTriangleDeformGrad( 133 | x.segment<2>(2 * indices[0]), x.segment<2>(2 * indices[1]), x.segment<2>(2 * indices[2]), D_m_inv); 134 | 135 | sum += area * calcEnergyDensity(F); 136 | } 137 | 138 | // Attach-spring potential 139 | for (const auto& constraint : m_constraints) 140 | { 141 | const std::size_t vert_index = constraint.vert_index; 142 | const double& k = constraint.stiffness; 143 | const auto p = x.segment<2>(vert_index * 2); 144 | const auto q = constraint.motion(m_physics_time + m_delta_physics_time); 145 | const double squared_dist = (p - q).squaredNorm(); 146 | 147 | sum += 0.5 * k * squared_dist; 148 | } 149 | 150 | return sum; 151 | }; 152 | 153 | const auto calcInternalPotentialGrad = [&](const Eigen::VectorXd& x) -> Eigen::VectorXd 154 | { 155 | Eigen::VectorXd sum = Eigen::VectorXd::Zero(x.size()); 156 | for (std::size_t i = 0; i < m_mesh.elems.cols(); ++i) 157 | { 158 | const auto& indices = m_mesh.elems.col(i); 159 | 160 | // Retrieve precomputed values 161 | const auto& D_m_inv = m_mesh.rest_shape_mat_inv_array[i]; 162 | const auto& area = m_mesh.volume_array[i]; 163 | const auto& vec_PFPx = m_mesh.vec_PFPx_array[i]; 164 | 165 | // Calculate the deformation gradient $\mathbf{F}$ 166 | const auto F = elasty::fem::calc2dTriangleDeformGrad( 167 | x.segment<2>(2 * indices[0]), x.segment<2>(2 * indices[1]), x.segment<2>(2 * indices[2]), D_m_inv); 168 | 169 | // Calculate $\frac{\partial \Phi}{\partial \mathbf{x}}$ and related values 170 | const auto P = calcPiolaStress(F); 171 | const auto vec_P = Eigen::Map(P.data(), P.size()); 172 | const auto PPsiPx = vec_PFPx.transpose() * vec_P; 173 | 174 | // Calculate $\frac{\partial E}{\partial \mathbf{x}}$ 175 | const auto PEPx = area * PPsiPx; 176 | 177 | sum.segment<2>(2 * indices[0]) += PEPx.segment<2>(0 * 2); 178 | sum.segment<2>(2 * indices[1]) += PEPx.segment<2>(1 * 2); 179 | sum.segment<2>(2 * indices[2]) += PEPx.segment<2>(2 * 2); 180 | } 181 | 182 | for (const auto& constraint : m_constraints) 183 | { 184 | const std::size_t vert_index = constraint.vert_index; 185 | const double& k = constraint.stiffness; 186 | const auto p = x.segment<2>(vert_index * 2); 187 | const auto q = constraint.motion(m_physics_time + m_delta_physics_time); 188 | const auto r = p - q; 189 | 190 | sum.segment<2>(2 * vert_index) += k * r; 191 | } 192 | 193 | return sum; 194 | }; 195 | 196 | const auto calcMomentumPotential = [&](const Eigen::VectorXd& x) 197 | { 198 | return (0.5 / (h * h)) * (x - y).transpose() * m_mesh.lumped_mass.asDiagonal() * (x - y); 199 | }; 200 | 201 | const auto calcMomentumPotentialGrad = [&](const Eigen::VectorXd& x) -> Eigen::VectorXd 202 | { 203 | return (1.0 / (h * h)) * m_mesh.lumped_mass.asDiagonal() * (x - y); 204 | }; 205 | 206 | const auto calcObjective = [&](const Eigen::VectorXd& x) 207 | { 208 | const double momentum_potential = calcMomentumPotential(x); 209 | const double internal_potential = calcInternalPotential(x); 210 | 211 | assert(momentum_potential >= 0.0); 212 | assert(internal_potential >= 0.0); 213 | 214 | return momentum_potential + internal_potential; 215 | }; 216 | 217 | const auto calcObjectiveGrad = [&](const Eigen::VectorXd& x) -> Eigen::VectorXd 218 | { 219 | return calcMomentumPotentialGrad(x) + calcInternalPotentialGrad(x); 220 | }; 221 | 222 | // Solve the minimization problem 223 | unsigned num_iters; 224 | Eigen::VectorXd x_opt; 225 | mathtoolbox::optimization::RunLBfgs(y, calcObjective, calcObjectiveGrad, 1e-06, 100, x_opt, num_iters); 226 | 227 | // Update the internal state 228 | m_mesh.v = (1.0 / h) * (x_opt - m_mesh.x); 229 | m_mesh.x = x_opt; 230 | 231 | // Apply naive damping 232 | m_mesh.v *= std::exp(-k_damping_factor * m_delta_physics_time); 233 | 234 | // Update time counter 235 | m_physics_time += m_delta_physics_time; 236 | } 237 | 238 | void initializeScene() 239 | { 240 | constexpr std::size_t num_cols = 20; 241 | constexpr std::size_t num_rows = 5; 242 | constexpr std::size_t num_verts = (num_cols + 1) * (num_rows + 1); 243 | constexpr std::size_t num_elems = (num_cols * num_rows) * 2; 244 | constexpr double size = 1.0; 245 | 246 | m_mesh.elems.resize(3, num_elems); 247 | m_mesh.x_rest.resize(num_verts * k_num_dims); 248 | 249 | // Generate a triangle mesh 250 | for (std::size_t col = 0; col < num_cols; ++col) 251 | { 252 | for (std::size_t row = 0; row < num_rows; ++row) 253 | { 254 | const auto base = col * (num_rows + 1) + row; 255 | 256 | m_mesh.elems.col(2 * num_rows * col + 2 * row + 0) << 0 + base, 1 + base, (num_rows + 1) + 1 + base; 257 | m_mesh.elems.col(2 * num_rows * col + 2 * row + 1) << 0 + base, (num_rows + 1) + 1 + base, 258 | (num_rows + 1) + base; 259 | 260 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * col + row), k_num_dims) = 261 | Eigen::Vector2d{col * 1.0, -1.0 * row}; 262 | } 263 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * col + num_rows), k_num_dims) = 264 | Eigen::Vector2d{col * 1.0, -1.0 * num_rows}; 265 | } 266 | for (std::size_t row = 0; row < num_rows; ++row) 267 | { 268 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * num_cols + row), k_num_dims) = 269 | Eigen::Vector2d{num_cols * 1.0, -1.0 * row}; 270 | } 271 | m_mesh.x_rest.segment(k_num_dims * ((num_rows + 1) * num_cols + num_rows), k_num_dims) = 272 | Eigen::Vector2d{num_cols * 1.0, -1.0 * num_rows}; 273 | 274 | // Set transform 275 | m_mesh.x_rest *= 1.0 / static_cast(num_rows); 276 | for (std::size_t vert = 0; vert < num_verts; ++vert) 277 | { 278 | m_mesh.x_rest[2 * vert + 1] += 0.5; 279 | } 280 | m_mesh.x_rest *= size; 281 | 282 | // Initialize other values 283 | m_mesh.x = m_mesh.x_rest; 284 | m_mesh.v = Eigen::VectorXd::Zero(k_num_dims * num_verts); 285 | m_mesh.f = Eigen::VectorXd::Zero(k_num_dims * num_verts); 286 | 287 | // Set constraints 288 | for (std::size_t i = 0; i < num_rows + 1; ++i) 289 | { 290 | const auto motion = [&, i](double) -> Eigen::Vector2d 291 | { 292 | return m_mesh.x_rest.segment<2>(i * 2); 293 | }; 294 | 295 | m_constraints.push_back(Constraint{i, motion, k_spring_stiffness}); 296 | } 297 | for (std::size_t i = (num_rows + 1) * num_cols; i < (num_rows + 1) * (num_cols + 1); ++i) 298 | { 299 | const auto ease = [](double x) 300 | { 301 | return -(std::cos(3.14159265358979 * x) - 1.0) * 0.5; 302 | }; 303 | 304 | const auto motion = [&, i](double t) -> Eigen::Vector2d 305 | { 306 | const auto x_init = m_mesh.x_rest.segment<2>(i * 2); 307 | const auto dir = Eigen::Vector2d{3.0, 0.0}; 308 | const double t_0 = 0.8; 309 | const double t_1 = t_0 + 1.0; 310 | const double a = (t < t_0) ? 0.0 : ((t < t_1) ? t - t_0 : t_1 - t_0); 311 | const double b = ease(a); 312 | 313 | return x_init + b * dir; 314 | }; 315 | 316 | m_constraints.push_back(Constraint{i, motion, k_spring_stiffness}); 317 | } 318 | 319 | // Perform precomputation 320 | m_mesh.volume_array.resize(num_elems); 321 | m_mesh.rest_shape_mat_inv_array.resize(num_elems); 322 | m_mesh.vec_PFPx_array.resize(num_elems); 323 | for (std::size_t elem_index = 0; elem_index < num_elems; ++elem_index) 324 | { 325 | const auto& indices = m_mesh.elems.col(elem_index); 326 | 327 | m_mesh.volume_array[elem_index] = elasty::fem::calc2dTriangleArea(m_mesh.x_rest.segment<2>(2 * indices[0]), 328 | m_mesh.x_rest.segment<2>(2 * indices[1]), 329 | m_mesh.x_rest.segment<2>(2 * indices[2])); 330 | m_mesh.rest_shape_mat_inv_array[elem_index] = 331 | elasty::fem::calc2dShapeMatrix(m_mesh.x_rest.segment<2>(2 * indices[0]), 332 | m_mesh.x_rest.segment<2>(2 * indices[1]), 333 | m_mesh.x_rest.segment<2>(2 * indices[2])) 334 | .inverse(); 335 | m_mesh.vec_PFPx_array[elem_index] = 336 | elasty::fem::calcVecTrianglePartDeformGradPartPos(m_mesh.rest_shape_mat_inv_array[elem_index]); 337 | } 338 | m_mesh.lumped_mass = elasty::fem::calcTriangleMeshLumpedMass(m_mesh.x_rest, m_mesh.elems, m_mesh.mass); 339 | } 340 | 341 | /// \brief Getter of the delta physics time. 342 | /// 343 | /// \details The value equals to the delta frame time devided by the number of substeps. 344 | double getDeltaPhysicsTime() const { return m_delta_physics_time; } 345 | 346 | void setDeltaPhysicsTime(const double delta_physics_time) { m_delta_physics_time = delta_physics_time; } 347 | 348 | const TriangleMesh* getMesh() const { return &m_mesh; } 349 | 350 | private: 351 | double m_delta_physics_time = 1.0 / 60.0; 352 | double m_physics_time = 0.0; 353 | 354 | std::vector m_constraints; 355 | 356 | TriangleMesh m_mesh; 357 | }; 358 | 359 | int main(int argc, char** argv) 360 | { 361 | VariationalImplicit2dEngine engine; 362 | 363 | engine.initializeScene(); 364 | engine.setDeltaPhysicsTime(k_delta_time / static_cast(k_num_substeps)); 365 | 366 | const auto mesh = engine.getMesh(); 367 | const std::size_t num_verts = mesh->x_rest.size() / 2; 368 | const std::size_t num_elems = mesh->elems.cols(); 369 | 370 | auto alembic_manager = elasty::createTriangleMesh2dAlembicManager( 371 | "./out.abc", k_delta_time, num_verts, num_elems, mesh->x.data(), mesh->elems.data()); 372 | 373 | for (unsigned int frame = 0; frame < 240; ++frame) 374 | { 375 | timer::Timer t(std::to_string(frame)); 376 | 377 | alembic_manager->submitCurrentStatus(); 378 | 379 | for (int i = 0; i < k_num_substeps; ++i) 380 | { 381 | engine.proceedFrame(); 382 | } 383 | } 384 | 385 | return 0; 386 | } 387 | -------------------------------------------------------------------------------- /examples/variational-implicit-3d/mesh.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | template struct Mesh 8 | { 9 | using ElemList = Eigen::Matrix; 10 | 11 | ElemList elems; 12 | 13 | Eigen::VectorXd x_rest; 14 | Eigen::VectorXd x; 15 | Eigen::VectorXd v; 16 | Eigen::VectorXd f; 17 | 18 | double mass = 1.0; 19 | 20 | /// \details Should be precomputed 21 | Eigen::VectorXd lumped_mass; 22 | 23 | /// \details Should be precomputed 24 | std::vector volume_array; 25 | 26 | /// \details Should be precomputed 27 | std::vector> rest_shape_mat_inv_array; 28 | 29 | /// \details Should be precomputed 30 | std::vector> vec_PFPx_array; 31 | }; 32 | 33 | using TetraMesh = Mesh<3>; 34 | 35 | /// \brief Read a tetrahedral mesh from a ".mesh" file 36 | /// 37 | /// \details Currently, this function is only tested with the data generated by fTetWild: 38 | /// https://github.com/wildmeshing/fTetWild 39 | TetraMesh ReadTetraMesh(const std::string& path) 40 | { 41 | TetraMesh mesh; 42 | 43 | std::ifstream file(path.c_str()); 44 | std::string line; 45 | 46 | // Detect the vertices block 47 | while (std::getline(file, line)) 48 | { 49 | if (line == "Vertices") 50 | { 51 | break; 52 | } 53 | } 54 | std::getline(file, line); 55 | 56 | // Read the number of vertices 57 | const int num_verts = std::stoi(line); 58 | 59 | // Allocate the vertex data 60 | mesh.x_rest.resize(num_verts * 3); 61 | 62 | // Read the vertex data 63 | std::size_t vert_index = 0; 64 | while (std::getline(file, line)) 65 | { 66 | if (line == "Triangles") 67 | { 68 | break; 69 | } 70 | 71 | std::istringstream iss(line); 72 | 73 | double x, y, z, zero; 74 | if (!(iss >> x >> y >> z >> zero)) 75 | { 76 | assert(false); 77 | } 78 | 79 | mesh.x_rest.segment<3>(vert_index * 3) = Eigen::Vector3d(x, y, z); 80 | 81 | ++vert_index; 82 | } 83 | 84 | // This line reads the number of triangles, which should be zero 85 | std::getline(file, line); 86 | 87 | // Detect the tetrahedra block 88 | while (std::getline(file, line)) 89 | { 90 | if (line == "Tetrahedra") 91 | { 92 | break; 93 | } 94 | } 95 | std::getline(file, line); 96 | 97 | // Read the number of elements 98 | const int num_elems = std::stoi(line); 99 | 100 | // Allocate the element data 101 | mesh.elems.resize(4, num_elems); 102 | 103 | // Read the element data 104 | std::size_t elem_index = 0; 105 | while (std::getline(file, line)) 106 | { 107 | std::istringstream iss(line); 108 | 109 | int e0, e1, e2, e3, zero; 110 | if (!(iss >> e0 >> e1 >> e2 >> e3 >> zero)) 111 | { 112 | break; 113 | } 114 | 115 | // Note: the indices in .mesh files start from "1", not "0" 116 | mesh.elems(0, elem_index) = e0 - 1; 117 | mesh.elems(1, elem_index) = e1 - 1; 118 | mesh.elems(2, elem_index) = e2 - 1; 119 | mesh.elems(3, elem_index) = e3 - 1; 120 | 121 | ++elem_index; 122 | } 123 | 124 | // Initialize other values 125 | mesh.x = mesh.x_rest; 126 | mesh.v = Eigen::VectorXd::Zero(3 * num_verts); 127 | mesh.f = Eigen::VectorXd::Zero(3 * num_verts); 128 | 129 | return mesh; 130 | } 131 | -------------------------------------------------------------------------------- /include/elasty/alembic-manager.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_ALEMBIC_MANAGER_HPP 2 | #define ELASTY_ALEMBIC_MANAGER_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace elasty 10 | { 11 | class ClothSimObject; 12 | 13 | /// \brief An abstract class for making alembic export easier. 14 | class AbstractAlembicManager 15 | { 16 | public: 17 | /// \brief Submit the current frame to the managed alembic file. 18 | virtual void submitCurrentStatus() = 0; 19 | }; 20 | 21 | /// \brief A utility function to instantiating an alembic manager tailered for cloth objects 22 | std::shared_ptr 23 | createClothAlembicManager(const std::string& output_file_path, 24 | const std::shared_ptr cloth_sim_object, 25 | const double delta_time); 26 | 27 | /// \param positions Positions of the vertices in the format: [x_{0}, y_{0}, ..., x_{n - 1}, y_{n - 1}]. 28 | std::shared_ptr createTriangleMesh2dAlembicManager(const std::string& file_path, 29 | const double delta_time, 30 | const std::size_t num_verts, 31 | const std::size_t num_elems, 32 | const double* positions, 33 | const std::int32_t* indices); 34 | 35 | std::shared_ptr createTetraMeshAlembicManager(const std::string& file_path, 36 | const double delta_time, 37 | const std::size_t num_verts, 38 | const std::size_t num_elems, 39 | const double* positions, 40 | const std::int32_t* indices); 41 | } // namespace elasty 42 | 43 | #endif // ELASTY_ALEMBIC_MANAGER_HPP 44 | -------------------------------------------------------------------------------- /include/elasty/algorithm-type.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_ALGORITHM_TYPE_HPP 2 | #define ELASTY_ALGORITHM_TYPE_HPP 3 | 4 | namespace elasty 5 | { 6 | enum class AlgorithmType 7 | { 8 | Pbd, 9 | Xpbd 10 | }; 11 | } 12 | 13 | #endif // ELASTY_ALGORITHM_TYPE_HPP 14 | -------------------------------------------------------------------------------- /include/elasty/cloth-sim-object.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_CLOTH_SIM_OBJECT_HPP 2 | #define ELASTY_CLOTH_SIM_OBJECT_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace elasty 10 | { 11 | class ClothSimObject : public SimObject 12 | { 13 | public: 14 | enum class InPlaneStrategy 15 | { 16 | EdgeDistance, 17 | ContinuumTriangle, 18 | Both, 19 | }; 20 | 21 | enum class OutOfPlaneStrategy 22 | { 23 | Bending, 24 | IsometricBending, 25 | Cross, 26 | }; 27 | 28 | using TriangleList = Eigen::Matrix; 29 | using UvList = Eigen::Matrix; 30 | 31 | ClothSimObject(const unsigned resolution, 32 | const double in_plane_stiffness = 1.000, ///< PBD 33 | const double in_plane_compliance = 0.001, ///< XPBD 34 | const double out_of_plane_stiffness = 0.100, ///< PBD 35 | const double out_of_plane_compliance = 0.010, ///< XPBD 36 | const double dt = 1.0 / 60.0, ///< XPBD 37 | const Eigen::Affine3d& transform = Eigen::Affine3d::Identity(), 38 | const InPlaneStrategy in_plane_strategy = InPlaneStrategy::Both, 39 | const OutOfPlaneStrategy out_of_plane_strategy = OutOfPlaneStrategy::IsometricBending); 40 | 41 | TriangleList m_triangle_list; 42 | UvList m_uv_list; 43 | 44 | bool hasUv() const { return m_uv_list.size() != 0; } 45 | 46 | /// \brief Getter of the areas of the triangles. 47 | const Eigen::VectorXd& getAreaList() const { return m_area_list; } 48 | 49 | /// \brief Apply aerodynamic forces to the relevant particles. 50 | /// 51 | /// \details The aerodynamics model is based on [Wilson+14]. This member function is intended to be called in 52 | /// the setExternalForces() routine in engines. 53 | /// 54 | /// \param drag_coeff The coefficient for drag forces. This value should be larger than lift_coeff. 55 | /// 56 | /// \param lift_coeff The coefficient for lift forces. This value should be smaller than drag_coeff. 57 | void applyAerodynamicForces(const Eigen::Vector3d& global_velocity = Eigen::Vector3d::Zero(), 58 | const double drag_coeff = 0.100, 59 | const double lift_coeff = 0.060); 60 | 61 | private: 62 | Eigen::VectorXd m_area_list; 63 | 64 | /// \brief Calculate areas of the triangles. 65 | /// 66 | /// \details The result will be stored in m_area_list. This function needs to be called only once. 67 | void calculateAreas(); 68 | }; 69 | } // namespace elasty 70 | 71 | #endif // ELASTY_CLOTH_SIM_OBJECT_HPP 72 | -------------------------------------------------------------------------------- /include/elasty/constraint.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_CONSTRAINT_HPP 2 | #define ELASTY_CONSTRAINT_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace elasty 11 | { 12 | enum class ConstraintType 13 | { 14 | Bilateral, 15 | Unilateral 16 | }; 17 | 18 | class AbstractConstraint 19 | { 20 | public: 21 | AbstractConstraint(const std::vector>& particles, 22 | const double stiffness, ///< for PBD 23 | const double compliance, ///< for XPBD 24 | const double delta_time ///< for XPBD 25 | ) 26 | : m_stiffness(stiffness), 27 | m_lagrange_multiplier(0.0), 28 | m_compliance(compliance), 29 | m_delta_time(delta_time), 30 | m_particles(particles) 31 | { 32 | } 33 | 34 | /// \brief Calculates the constraint function value C(x). 35 | virtual double calculateValue() = 0; 36 | 37 | /// \brief Calculate the derivative of the constraint function grad C(x). 38 | /// 39 | /// \details As constraints can have different vector sizes, it will store the result to the passed raw buffer 40 | /// that should be allocated in the caller, rather than returning a dynamically allocated variable-length 41 | /// vector. This method does not check whether the buffer is adequately allocated, or not. 42 | virtual void calculateGrad(double* grad_C) = 0; 43 | 44 | /// \brief Manipulate the associated particles by projecting them to the constraint manifold. 45 | /// 46 | /// \details This method should be called by the core engine. As this method directly updates the predicted 47 | /// positions of the associated particles, it is intended to be used in a Gauss-Seidel-style solver. 48 | virtual void projectParticles(const AlgorithmType type) = 0; 49 | 50 | /// \brief Return the constraint type (i.e., either unilateral or bilateral). 51 | virtual ConstraintType getType() = 0; 52 | 53 | /// \brief Stiffness of this constraint, which should be in [0, 1]. 54 | /// 55 | /// \details This value will not be used in XPBD; instead, the compliance value will be used. 56 | double m_stiffness; 57 | 58 | /// \brief Lagrange multipler for this constraint, used in XPBD only. 59 | double m_lagrange_multiplier; 60 | 61 | /// \brief Compliance (inverse stiffness), used in XPBD only. 62 | /// 63 | /// \details Should be set to zero if the constraint is hard (e.g., collision constraints). 64 | double m_compliance; 65 | 66 | /// \brief Time step, used in XPBD only. 67 | double m_delta_time; 68 | 69 | protected: 70 | /// \brief Associated particles. 71 | /// 72 | /// \details The number of particles is (in most cases) solely determined in each constraint. For example, a 73 | /// distance constraint need to have exactly two particles. Some special constraints (e.g., shape-matching 74 | /// constraint) could have a variable number of particles. 75 | std::vector> m_particles; 76 | }; 77 | 78 | template class FixedNumAbstractConstraint : public AbstractConstraint 79 | { 80 | public: 81 | FixedNumAbstractConstraint(const std::vector>& particles, 82 | const double stiffness, ///< for PBD 83 | const double compliance, ///< for XPBD 84 | const double delta_time ///< for XPBD 85 | ) 86 | : AbstractConstraint(particles, stiffness, compliance, delta_time), 87 | m_inv_M(constructInverseMassMatrix(m_particles)) 88 | { 89 | } 90 | 91 | virtual double calculateValue() = 0; 92 | virtual void calculateGrad(double* grad_C) = 0; 93 | 94 | void projectParticles(const AlgorithmType type) final 95 | { 96 | // Calculate the constraint function value 97 | const double C = calculateValue(); 98 | 99 | // Skip if it is a unilateral constraint and is satisfied 100 | if (getType() == ConstraintType::Unilateral && C >= 0.0) 101 | { 102 | return; 103 | } 104 | 105 | // Calculate the derivative of the constraint function 106 | const Eigen::Matrix grad_C = calculateGrad(); 107 | 108 | // Skip if the gradient is sufficiently small 109 | constexpr double very_small_value = 1e-12; 110 | if (grad_C.norm() < very_small_value) 111 | { 112 | return; 113 | } 114 | 115 | switch (type) 116 | { 117 | case AlgorithmType::Pbd: 118 | { 119 | // Calculate s 120 | const double s = -C / (grad_C.transpose() * m_inv_M.asDiagonal() * grad_C); 121 | 122 | // Calculate \Delta x 123 | const Eigen::Matrix delta_x = m_stiffness * s * m_inv_M.asDiagonal() * grad_C; 124 | assert(!delta_x.hasNaN()); 125 | 126 | // Update predicted positions 127 | for (unsigned int j = 0; j < Num; ++j) 128 | { 129 | m_particles[j]->p += delta_x.segment(3 * j, 3); 130 | } 131 | 132 | break; 133 | } 134 | case AlgorithmType::Xpbd: 135 | { 136 | // Calculate time-scaled compliance 137 | const double alpha_tilde = m_compliance / (m_delta_time * m_delta_time); 138 | 139 | // Calculate \Delta lagrange multiplier 140 | const double delta_lagrange_multiplier = 141 | (-C - alpha_tilde * m_lagrange_multiplier) / 142 | (grad_C.transpose() * m_inv_M.asDiagonal() * grad_C + alpha_tilde); 143 | 144 | // Calculate \Delta x 145 | const Eigen::Matrix delta_x = 146 | delta_lagrange_multiplier * m_inv_M.asDiagonal() * grad_C; 147 | assert(!delta_x.hasNaN()); 148 | 149 | // Update predicted positions 150 | for (unsigned int j = 0; j < Num; ++j) 151 | { 152 | m_particles[j]->p += delta_x.segment(3 * j, 3); 153 | } 154 | 155 | // Update the lagrange multiplier 156 | m_lagrange_multiplier += delta_lagrange_multiplier; 157 | 158 | break; 159 | } 160 | } 161 | } 162 | 163 | private: 164 | const Eigen::Matrix m_inv_M; 165 | 166 | Eigen::Matrix calculateGrad() 167 | { 168 | Eigen::Matrix grad_C; 169 | calculateGrad(grad_C.data()); 170 | return grad_C; 171 | } 172 | 173 | static Eigen::Matrix 174 | constructInverseMassMatrix(const std::vector>& particles) 175 | { 176 | Eigen::Matrix inv_M; 177 | for (unsigned int j = 0; j < Num; ++j) 178 | { 179 | inv_M(j * 3 + 0) = particles[j]->w; 180 | inv_M(j * 3 + 1) = particles[j]->w; 181 | inv_M(j * 3 + 2) = particles[j]->w; 182 | } 183 | return inv_M; 184 | } 185 | }; 186 | 187 | class VariableNumConstraint : public AbstractConstraint 188 | { 189 | public: 190 | VariableNumConstraint(const std::vector>& particles, 191 | const double stiffness, ///< for PBD 192 | const double compliance, ///< for XPBD 193 | const double delta_time ///< for XPBD 194 | ) 195 | : AbstractConstraint(particles, stiffness, compliance, delta_time), 196 | m_inv_M(constructInverseMassMatrix(m_particles)) 197 | { 198 | } 199 | 200 | virtual double calculateValue() = 0; 201 | virtual void calculateGrad(double* grad_C) = 0; 202 | 203 | virtual void projectParticles(const AlgorithmType type) 204 | { 205 | // Calculate the constraint function value 206 | const double C = calculateValue(); 207 | 208 | // Skip if it is a unilateral constraint and is satisfied 209 | if (getType() == ConstraintType::Unilateral && C >= 0.0) 210 | { 211 | return; 212 | } 213 | 214 | // Calculate the derivative of the constraint function 215 | const Eigen::VectorXd grad_C = calculateGrad(); 216 | 217 | // Skip if the gradient is sufficiently small 218 | constexpr double very_small_value = 1e-12; 219 | if (grad_C.norm() < very_small_value) 220 | { 221 | return; 222 | } 223 | 224 | switch (type) 225 | { 226 | case AlgorithmType::Pbd: 227 | { 228 | // Calculate s 229 | const double s = -C / (grad_C.transpose() * m_inv_M.asDiagonal() * grad_C); 230 | 231 | // Calculate \Delta x 232 | const Eigen::VectorXd delta_x = m_stiffness * s * m_inv_M.asDiagonal() * grad_C; 233 | assert(!delta_x.hasNaN()); 234 | 235 | // Update predicted positions 236 | for (unsigned int j = 0; j < m_particles.size(); ++j) 237 | { 238 | m_particles[j]->p += delta_x.segment(3 * j, 3); 239 | } 240 | 241 | break; 242 | } 243 | case AlgorithmType::Xpbd: 244 | { 245 | // Calculate time-scaled compliance 246 | const double alpha_tilde = m_compliance / (m_delta_time * m_delta_time); 247 | 248 | // Calculate \Delta lagrange multiplier 249 | const double delta_lagrange_multiplier = 250 | (-C - alpha_tilde * m_lagrange_multiplier) / 251 | (grad_C.transpose() * m_inv_M.asDiagonal() * grad_C + alpha_tilde); 252 | 253 | // Calculate \Delta x 254 | const Eigen::VectorXd delta_x = delta_lagrange_multiplier * m_inv_M.asDiagonal() * grad_C; 255 | assert(!delta_x.hasNaN()); 256 | 257 | // Update predicted positions 258 | for (unsigned int j = 0; j < m_particles.size(); ++j) 259 | { 260 | m_particles[j]->p += delta_x.segment(3 * j, 3); 261 | } 262 | 263 | // Update the lagrange multiplier 264 | m_lagrange_multiplier += delta_lagrange_multiplier; 265 | 266 | break; 267 | } 268 | } 269 | } 270 | 271 | private: 272 | const Eigen::VectorXd m_inv_M; 273 | 274 | Eigen::VectorXd calculateGrad() 275 | { 276 | Eigen::VectorXd grad_C(m_particles.size() * 3); 277 | calculateGrad(grad_C.data()); 278 | return grad_C; 279 | } 280 | 281 | static Eigen::VectorXd 282 | constructInverseMassMatrix(const std::vector>& particles) 283 | { 284 | Eigen::VectorXd inv_M(particles.size() * 3); 285 | for (unsigned int j = 0; j < particles.size(); ++j) 286 | { 287 | inv_M(j * 3 + 0) = particles[j]->w; 288 | inv_M(j * 3 + 1) = particles[j]->w; 289 | inv_M(j * 3 + 2) = particles[j]->w; 290 | } 291 | return inv_M; 292 | } 293 | }; 294 | 295 | class BendingConstraint final : public FixedNumAbstractConstraint<4> 296 | { 297 | public: 298 | BendingConstraint(const std::shared_ptr p_0, 299 | const std::shared_ptr p_1, 300 | const std::shared_ptr p_2, 301 | const std::shared_ptr p_3, 302 | const double stiffness, ///< for PBD 303 | const double compliance, ///< for XPBD 304 | const double delta_time, ///< for XPBD 305 | const double dihedral_angle); 306 | 307 | double calculateValue() override; 308 | void calculateGrad(double* grad_C) override; 309 | ConstraintType getType() override { return ConstraintType::Bilateral; } 310 | 311 | private: 312 | const double m_dihedral_angle; 313 | }; 314 | 315 | class ContinuumTriangleConstraint final : public FixedNumAbstractConstraint<3> 316 | { 317 | public: 318 | ContinuumTriangleConstraint(const std::shared_ptr p_0, 319 | const std::shared_ptr p_1, 320 | const std::shared_ptr p_2, 321 | const double stiffness, ///< for PBD 322 | const double compliance, ///< for XPBD 323 | const double delta_time, ///< for XPBD 324 | const double youngs_modulus, 325 | const double poisson_ratio); 326 | 327 | double calculateValue() override; 328 | void calculateGrad(double* grad_C) override; 329 | ConstraintType getType() override { return ConstraintType::Bilateral; } 330 | 331 | private: 332 | const double m_first_lame; 333 | const double m_second_lame; 334 | 335 | double m_rest_area; 336 | Eigen::Matrix2d m_rest_D_inv; 337 | }; 338 | 339 | /// \details When the two particles are degenerated (i.e., at an exactly same posinion), the gradient will be set as 340 | /// a random direction to recover from the degenerated state. 341 | class DistanceConstraint final : public FixedNumAbstractConstraint<2> 342 | { 343 | public: 344 | DistanceConstraint(const std::shared_ptr p_0, 345 | const std::shared_ptr p_1, 346 | const double stiffness, ///< for PBD 347 | const double compliance, ///< for XPBD 348 | const double delta_time, ///< for XPBD 349 | const double d); 350 | 351 | double calculateValue() override; 352 | void calculateGrad(double* grad_C) override; 353 | ConstraintType getType() override { return ConstraintType::Bilateral; } 354 | 355 | private: 356 | const double m_d; 357 | }; 358 | 359 | class EnvironmentalCollisionConstraint final : public FixedNumAbstractConstraint<1> 360 | { 361 | public: 362 | EnvironmentalCollisionConstraint(const std::shared_ptr p_0, 363 | const double stiffness, ///< for PBD 364 | const double compliance, ///< for XPBD 365 | const double delta_time, ///< for XPBD 366 | const Eigen::Vector3d& n, 367 | const double d); 368 | 369 | double calculateValue() override; 370 | void calculateGrad(double* grad_C) override; 371 | ConstraintType getType() override { return ConstraintType::Unilateral; } 372 | 373 | private: 374 | const Eigen::Vector3d m_n; 375 | const double m_d; 376 | }; 377 | 378 | class FixedPointConstraint final : public FixedNumAbstractConstraint<1> 379 | { 380 | public: 381 | FixedPointConstraint(const std::shared_ptr p_0, 382 | const double stiffness, ///< for PBD 383 | const double compliance, ///< for XPBD 384 | const double delta_time, ///< for XPBD 385 | const Eigen::Vector3d& point); 386 | 387 | double calculateValue() override; 388 | void calculateGrad(double* grad_C) override; 389 | ConstraintType getType() override { return ConstraintType::Bilateral; } 390 | 391 | private: 392 | const Eigen::Vector3d m_point; 393 | }; 394 | 395 | class IsometricBendingConstraint final : public FixedNumAbstractConstraint<4> 396 | { 397 | public: 398 | IsometricBendingConstraint(const std::shared_ptr p_0, 399 | const std::shared_ptr p_1, 400 | const std::shared_ptr p_2, 401 | const std::shared_ptr p_3, 402 | const double stiffness, ///< for PBD 403 | const double compliance, ///< for XPBD 404 | const double delta_time ///< for XPBD 405 | ); 406 | 407 | double calculateValue() override; 408 | void calculateGrad(double* grad_C) override; 409 | ConstraintType getType() override { return ConstraintType::Bilateral; } 410 | 411 | private: 412 | Eigen::Matrix4d m_Q; 413 | }; 414 | 415 | class ShapeMatchingConstraint final : public VariableNumConstraint 416 | { 417 | public: 418 | ShapeMatchingConstraint(const std::vector>& particles, 419 | const double stiffness, ///< for PBD 420 | const double compliance, ///< for XPBD 421 | const double delta_time ///< for XPBD 422 | ); 423 | 424 | /// \details This method should never be called. 425 | double calculateValue() override; 426 | 427 | /// \details This method should never be called. 428 | void calculateGrad(double* grad_C) override; 429 | 430 | ConstraintType getType() override { return ConstraintType::Bilateral; } 431 | 432 | /// \details Unlike other constraints, ShapeMatchingConstraint does not offer a closed-form constraint function 433 | /// (i.e., it cannot be represented as a cost function), but does offer a particle projection procedure 434 | /// directly. This means that does not support XPBD. 435 | /// 436 | /// Thus, even when the "type" is specified as XPBD, it uses the PBD update scheme and the "stiffness" value to 437 | /// adjust the constraint stiffness. Thus, it is not generally recommended to use this constraint with XPBD 438 | /// scenes for consistency. 439 | /// 440 | /// This is why this class needs to override the particle projection procedure. 441 | void projectParticles(const AlgorithmType type) override; 442 | 443 | private: 444 | double m_total_mass; 445 | std::vector m_q; 446 | }; 447 | } // namespace elasty 448 | 449 | #endif // ELASTY_CONSTRAINT_HPP 450 | -------------------------------------------------------------------------------- /include/elasty/engine.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_ENGINE_HPP 2 | #define ELASTY_ENGINE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace elasty 9 | { 10 | class AbstractConstraint; 11 | struct Particle; 12 | 13 | class AbstractEngine 14 | { 15 | public: 16 | AbstractEngine(const double delta_frame_time = 1.0 / 60.0, 17 | const unsigned int num_constraint_iters = 1, 18 | const unsigned int num_substeps = 8, 19 | const AlgorithmType algorithm_type = AlgorithmType::Xpbd); 20 | 21 | void proceedFrame(); 22 | 23 | virtual void initializeScene() = 0; 24 | 25 | virtual void setExternalForces() = 0; 26 | virtual void generateCollisionConstraints() = 0; 27 | virtual void updateVelocities() = 0; 28 | 29 | void clearScene(); 30 | 31 | /// \brief Getter of the delta physics time. 32 | /// 33 | /// \details The value equals to the delta frame time devided by the number of substeps. 34 | double getDeltaPhysicsTime() const { return m_delta_physics_time; } 35 | 36 | /// \brief Getter of the delta frame time. 37 | /// 38 | /// \details The value equals to the delta physics time multipled by the number of substeps. 39 | double getDeltaFrameTime() const { return m_delta_frame_time; } 40 | 41 | /// \brief Getter of the current physics time. 42 | /// 43 | /// \details Routine functions for external forces, collisions, etc. can call this member function to retrieve 44 | /// the current physics time. 45 | double getCurrentPhysicsTime() const { return m_current_physics_time; } 46 | 47 | /// \brief Getter of the particles this engine manages. 48 | const std::vector>& getParticles() const { return m_particles; } 49 | 50 | protected: 51 | void addConstraint(std::shared_ptr constraint); 52 | void addInstantConstraint(std::shared_ptr constraint); 53 | 54 | std::vector> m_particles; 55 | 56 | std::vector> m_constraints; 57 | std::vector> m_instant_constraints; 58 | 59 | private: 60 | const double m_delta_physics_time; 61 | const double m_delta_frame_time; 62 | const unsigned int m_num_constraint_iters; 63 | const unsigned int m_num_substeps; 64 | const AlgorithmType m_algorithm_type; 65 | 66 | double m_current_physics_time = 0.0; 67 | 68 | void proceedSubstep(); 69 | }; 70 | } // namespace elasty 71 | 72 | #endif // ELASTY_ENGINE_HPP 73 | -------------------------------------------------------------------------------- /include/elasty/fem.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_FEM_HPP 2 | #define ELASTY_FEM_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // References: 9 | // 10 | // [1] Eftychios Sifakis and Jernej Barbic. 2012. FEM simulation of 3D deformable solids: a practitioner's guide to 11 | // theory, discretization and model reduction. In ACM SIGGRAPH 2012 Courses (SIGGRAPH '12). Association for Computing 12 | // Machinery, New York, NY, USA, Article 20, 1–50. DOI:https://doi.org/10.1145/2343483.2343501 13 | // 14 | // [2] Theodore Kim and David Eberle. 2020. Dynamic deformables: implementation and production practicalities. In ACM 15 | // SIGGRAPH 2020 Courses (SIGGRAPH '20). Association for Computing Machinery, New York, NY, USA, Article 23, 1–182. 16 | // DOI:https://doi.org/10.1145/3388769.3407490 17 | 18 | namespace elasty::fem 19 | { 20 | /// \brief Extract the rotational part of the given square matrix by performing polar decomposion. 21 | /// 22 | /// \details This implementation is based on SVD. It checks the determinant to avoid any reflection. 23 | template 24 | Eigen::Matrix 25 | extractRotation(const Eigen::MatrixBase& F) 26 | { 27 | const auto svd = F.jacobiSvd(Eigen::ComputeFullU | Eigen::ComputeFullV); 28 | const auto Sigma = svd.singularValues(); 29 | const auto U = svd.matrixU(); 30 | const auto V = svd.matrixV(); 31 | const auto R = (U * V.transpose()).eval(); 32 | 33 | assert(std::abs(std::abs(R.determinant()) - 1.0) < 1e-04); 34 | 35 | if constexpr (Derived::RowsAtCompileTime == 2) 36 | { 37 | // Just ignore reflection (if any) 38 | // TODO: Discuss whether this is a good strategy, or not 39 | return R; 40 | } 41 | else if constexpr (Derived::RowsAtCompileTime == 3) 42 | { 43 | // Correct reflection (if any) 44 | return (R.determinant() > 0) ? R : -R; 45 | } 46 | } 47 | 48 | /// \brief Calculate the first Lame parameter, $\lambda$. 49 | /// 50 | /// \details Reference: [1] 51 | template constexpr Scalar calcFirstLame(const Scalar youngs_modulus, const Scalar poisson_ratio) 52 | { 53 | return youngs_modulus * poisson_ratio / ((1.0 + poisson_ratio) * (1.0 - 2.0 * poisson_ratio)); 54 | } 55 | 56 | /// \brief Calculate the second Lame parameter, $\mu$. 57 | /// 58 | /// \details Reference: [1] 59 | template constexpr Scalar calcSecondLame(const Scalar youngs_modulus, const Scalar poisson_ratio) 60 | { 61 | return youngs_modulus / (2.0 * (1.0 + poisson_ratio)); 62 | } 63 | 64 | /// \brief Calculate either the "deformed" shape matrix (D_s) or "reference" shape matrix (D_m) of a triangle. 65 | /// 66 | /// \param x_0 A 2D vector. 67 | /// 68 | /// \param x_1 A 2D vector. 69 | /// 70 | /// \param x_2 A 2D vector. 71 | /// 72 | /// \details Reference: [1] 73 | template 74 | Eigen::Matrix calc2dShapeMatrix(const Eigen::MatrixBase& x_0, 75 | const Eigen::MatrixBase& x_1, 76 | const Eigen::MatrixBase& x_2) 77 | { 78 | using Mat = Eigen::Matrix; 79 | 80 | Mat shape_matrix; 81 | shape_matrix.col(0) = x_1 - x_0; 82 | shape_matrix.col(1) = x_2 - x_0; 83 | 84 | return shape_matrix; 85 | } 86 | 87 | /// \brief Calculate either the "deformed" shape matrix (D_s) or "reference" shape matrix (D_m) of a tetrahedron. 88 | /// 89 | /// \param x_0 A 3D vector. 90 | /// 91 | /// \param x_1 A 3D vector. 92 | /// 93 | /// \param x_2 A 3D vector. 94 | /// 95 | /// \param x_3 A 3D vector. 96 | /// 97 | /// \details Reference: [1] 98 | template 99 | Eigen::Matrix calc3dShapeMatrix(const Eigen::MatrixBase& x_0, 100 | const Eigen::MatrixBase& x_1, 101 | const Eigen::MatrixBase& x_2, 102 | const Eigen::MatrixBase& x_3) 103 | { 104 | using Mat = Eigen::Matrix; 105 | 106 | Mat shape_matrix; 107 | shape_matrix.col(0) = x_1 - x_0; 108 | shape_matrix.col(1) = x_2 - x_0; 109 | shape_matrix.col(2) = x_3 - x_0; 110 | 111 | return shape_matrix; 112 | } 113 | 114 | /// \brief Calculate the area of a triangle in 2D 115 | template 116 | typename Derived::Scalar calc2dTriangleArea(const Eigen::MatrixBase& x_0, 117 | const Eigen::MatrixBase& x_1, 118 | const Eigen::MatrixBase& x_2) 119 | { 120 | const auto r_1 = x_1 - x_0; 121 | const auto r_2 = x_2 - x_0; 122 | 123 | return 0.5 * std::abs(r_1(0) * r_2(1) - r_2(0) * r_1(1)); 124 | } 125 | 126 | /// \brief Calculate the volume of a tetrahedron 127 | template 128 | typename Derived::Scalar calcTetrahedronVolume(const Eigen::MatrixBase& x_0, 129 | const Eigen::MatrixBase& x_1, 130 | const Eigen::MatrixBase& x_2, 131 | const Eigen::MatrixBase& x_3) 132 | { 133 | const auto r_1 = x_1 - x_0; 134 | const auto r_2 = x_2 - x_0; 135 | const auto r_3 = x_3 - x_0; 136 | 137 | return std::abs(r_1.dot(r_2.cross(r_3))) / 6.0; 138 | } 139 | 140 | /// \brief Calculate the diagonal elements of the lumped mass matrix. 141 | /// 142 | /// \details This function takes the "barycentric" approach. See https://www.alecjacobson.com/weblog/?p=1146 . 143 | template 144 | Eigen::Matrix 145 | calcTriangleMeshLumpedMass(const Eigen::MatrixBase& verts, 146 | const Eigen::MatrixBase& elems, 147 | const typename DerivedV::Scalar total_mass) 148 | { 149 | using Scalar = typename DerivedV::Scalar; 150 | using Vec = Eigen::Matrix; 151 | 152 | assert(verts.cols() == 1); 153 | assert(verts.size() % 2 == 0); 154 | assert(elems.rows() == 3); 155 | 156 | const auto num_verts = verts.size() / 2; 157 | 158 | Scalar total_area = 0; 159 | Vec masses = Vec::Zero(verts.size()); 160 | 161 | for (std::size_t elem_index = 0; elem_index < elems.cols(); ++elem_index) 162 | { 163 | const auto& indices = elems.col(elem_index); 164 | 165 | const Scalar area = calc2dTriangleArea(verts.template segment<2>(2 * indices[0]), 166 | verts.template segment<2>(2 * indices[1]), 167 | verts.template segment<2>(2 * indices[2])); 168 | 169 | const Scalar one_third_area = (1.0 / 3.0) * area; 170 | 171 | masses(2 * indices[0] + 0) += one_third_area; 172 | masses(2 * indices[0] + 1) += one_third_area; 173 | masses(2 * indices[1] + 0) += one_third_area; 174 | masses(2 * indices[1] + 1) += one_third_area; 175 | masses(2 * indices[2] + 0) += one_third_area; 176 | masses(2 * indices[2] + 1) += one_third_area; 177 | 178 | total_area += area; 179 | } 180 | 181 | assert(total_mass > 0); 182 | assert(total_area > 0); 183 | 184 | return (total_mass / total_area) * masses; 185 | } 186 | 187 | /// \brief Calculate the diagonal elements of the lumped mass matrix. 188 | template 189 | Eigen::Matrix 190 | calcTetraMeshLumpedMass(const Eigen::MatrixBase& verts, 191 | const Eigen::MatrixBase& elems, 192 | const typename DerivedV::Scalar total_mass) 193 | { 194 | using Scalar = typename DerivedV::Scalar; 195 | using Vec = Eigen::Matrix; 196 | 197 | assert(verts.cols() == 1); 198 | assert(verts.size() % 3 == 0); 199 | assert(elems.rows() == 4); 200 | 201 | const auto num_verts = verts.size() / 3; 202 | const auto num_elems = elems.cols(); 203 | 204 | Scalar total_vol = 0; 205 | Vec masses = Vec::Zero(verts.size()); 206 | 207 | for (std::size_t elem_index = 0; elem_index < num_elems; ++elem_index) 208 | { 209 | const auto& indices = elems.col(elem_index); 210 | 211 | const Scalar vol = calcTetrahedronVolume(verts.template segment<3>(3 * indices[0]), 212 | verts.template segment<3>(3 * indices[1]), 213 | verts.template segment<3>(3 * indices[2]), 214 | verts.template segment<3>(3 * indices[3])); 215 | 216 | const Scalar one_fourth_vol = (1.0 / 4.0) * vol; 217 | 218 | masses.template segment<3>(3 * indices[0]) += one_fourth_vol * Eigen::Vector3d::Ones(); 219 | masses.template segment<3>(3 * indices[1]) += one_fourth_vol * Eigen::Vector3d::Ones(); 220 | masses.template segment<3>(3 * indices[2]) += one_fourth_vol * Eigen::Vector3d::Ones(); 221 | masses.template segment<3>(3 * indices[3]) += one_fourth_vol * Eigen::Vector3d::Ones(); 222 | 223 | total_vol += vol; 224 | } 225 | 226 | assert(total_mass > 0); 227 | assert(total_vol > 0); 228 | assert(masses.minCoeff() > 0.0); 229 | 230 | return (total_mass / total_vol) * masses; 231 | } 232 | 233 | /// \brief Calculate the Green strain tensor for a finite element. 234 | /// 235 | /// \param deform_grad The deformation gradient matrix, which should be either 2-by-2 (2D element in 2D), 3-by-3 (3D 236 | /// element in 3D), or 3-by-2 (2D element in 3D). 237 | template 238 | Eigen::Matrix 239 | calcGreenStrain(const Eigen::MatrixBase& deform_grad) 240 | { 241 | using Mat = Eigen::Matrix; 242 | 243 | return 0.5 * (deform_grad.transpose() * deform_grad - Mat::Identity()); 244 | } 245 | 246 | /// \details Eq. 3.4 in [1] 247 | template 248 | typename Derived::Scalar calcCoRotationalEnergyDensity(const Eigen::MatrixBase& deform_grad, 249 | const typename Derived::Scalar first_lame, 250 | const typename Derived::Scalar second_lame) 251 | { 252 | using Mat = Eigen::Matrix; 253 | 254 | const auto R = extractRotation(deform_grad); 255 | const auto S = R.transpose() * deform_grad; 256 | const auto I = Mat::Identity(); 257 | const auto trace = (S - I).trace(); 258 | 259 | assert(deform_grad.isApprox(R * S)); 260 | 261 | return second_lame * (deform_grad - R).squaredNorm() + 0.5 * first_lame * trace * trace; 262 | } 263 | 264 | /// \details Eq. 3.5 in [1] 265 | template 266 | Eigen::Matrix 267 | calcCoRotationalPiolaStress(const Eigen::MatrixBase& deform_grad, 268 | const typename Derived::Scalar first_lame, 269 | const typename Derived::Scalar second_lame) 270 | { 271 | using Mat = Eigen::Matrix; 272 | 273 | const auto R = extractRotation(deform_grad); 274 | const auto S = R.transpose() * deform_grad; 275 | const auto I = Mat::Identity(); 276 | const auto trace = (S - I).trace(); 277 | 278 | assert(deform_grad.isApprox(R * S)); 279 | 280 | return 2.0 * second_lame * (deform_grad - R) + first_lame * trace * R; 281 | } 282 | 283 | /// \details The first equation in Sec. 3.3 in [1] 284 | template 285 | typename Derived::Scalar calcStVenantKirchhoffEnergyDensity(const Eigen::MatrixBase& deform_grad, 286 | const typename Derived::Scalar first_lame, 287 | const typename Derived::Scalar second_lame) 288 | { 289 | const auto E = calcGreenStrain(deform_grad); 290 | const auto trace = E.trace(); 291 | 292 | return second_lame * E.squaredNorm() + 0.5 * first_lame * trace * trace; 293 | } 294 | 295 | /// \details Eq. 3.3 in [1] 296 | template 297 | Eigen::Matrix 298 | calcStVenantKirchhoffPiolaStress(const Eigen::MatrixBase& deform_grad, 299 | const typename Derived::Scalar first_lame, 300 | const typename Derived::Scalar second_lame) 301 | { 302 | const auto E = calcGreenStrain(deform_grad); 303 | 304 | return 2.0 * second_lame * deform_grad * E + first_lame * E.trace() * deform_grad; 305 | } 306 | 307 | template 308 | Eigen::Matrix 309 | calc2dTriangleDeformGrad(const Eigen::MatrixBase& x_0, 310 | const Eigen::MatrixBase& x_1, 311 | const Eigen::MatrixBase& x_2, 312 | const Eigen::MatrixBase& rest_shape_mat_inv) 313 | { 314 | const auto D_s = elasty::fem::calc2dShapeMatrix(x_0, x_1, x_2); 315 | const auto F = D_s * rest_shape_mat_inv; 316 | 317 | return F; 318 | } 319 | 320 | template 321 | Eigen::Matrix 322 | calcTetrahedronDeformGrad(const Eigen::MatrixBase& x_0, 323 | const Eigen::MatrixBase& x_1, 324 | const Eigen::MatrixBase& x_2, 325 | const Eigen::MatrixBase& x_3, 326 | const Eigen::MatrixBase& rest_shape_mat_inv) 327 | { 328 | const auto D_s = elasty::fem::calc3dShapeMatrix(x_0, x_1, x_2, x_3); 329 | const auto F = D_s * rest_shape_mat_inv; 330 | 331 | return F; 332 | } 333 | 334 | /// \brief Calculate analytic partial derivatives of the deformation gradient $\mathbf{F}$ with respect to the 335 | /// vertex positions $\mathbf{x}$ (i.e., $\frac{\partial \mathbf{F}}{\partial \mathbf{x}}$) and return it in the 336 | /// "flattened" format. 337 | /// 338 | /// \details The result is a 2-by-2-by-6 third-order tensor but flattened into a 4-by-6 matrix. 339 | /// 340 | /// Reference: [2, Appendix D] 341 | template 342 | Eigen::Matrix 343 | calcVecTrianglePartDeformGradPartPos(const Eigen::MatrixBase& rest_shape_mat_inv) 344 | { 345 | using Scalar = typename Derived::Scalar; 346 | 347 | // Analytics partial derivatives $\frac{\partial \mathbf{D}_\text{s}{\partial x_{i}}$ 348 | Eigen::Matrix PDsPx[6]; 349 | 350 | // x[0] (= x_0) 351 | PDsPx[0] << -1.0, -1.0, 0.0, 0.0; 352 | 353 | // x[1] (= y_0) 354 | PDsPx[1] << 0.0, 0.0, -1.0, -1.0; 355 | 356 | // x[2] (= x_1) 357 | PDsPx[2] << 1.0, 0.0, 0.0, 0.0; 358 | 359 | // x[3] (= y_1) 360 | PDsPx[3] << 0.0, 0.0, 1.0, 0.0; 361 | 362 | // x[4] (= x_2) 363 | PDsPx[4] << 0.0, 1.0, 0.0, 0.0; 364 | 365 | // x[5] (= y_2) 366 | PDsPx[5] << 0.0, 0.0, 0.0, 1.0; 367 | 368 | Eigen::Matrix vec_PFPx; 369 | for (std::size_t i = 0; i < 6; ++i) 370 | { 371 | const Eigen::Matrix PFPx_i = PDsPx[i] * rest_shape_mat_inv; 372 | 373 | vec_PFPx.col(i) = Eigen::Map>(PFPx_i.data(), PFPx_i.size()); 374 | } 375 | 376 | return vec_PFPx; 377 | } 378 | 379 | /// \brief Calculate analytic partial derivatives of the deformation gradient $\mathbf{F}$ with respect to the 380 | /// vertex positions $\mathbf{x}$ (i.e., $\frac{\partial \mathbf{F}}{\partial \mathbf{x}}$) and return it in the 381 | /// "flattened" format. 382 | /// 383 | /// \details The result is a 3-by-3-by-12 third-order tensor but flattened into a 9-by-12 matrix. 384 | /// 385 | /// Reference: [2, Appendix D] 386 | template 387 | Eigen::Matrix 388 | calcVecTetrahedronPartDeformGradPartPos(const Eigen::MatrixBase& rest_shape_mat_inv) 389 | { 390 | using Scalar = typename Derived::Scalar; 391 | 392 | // Analytics partial derivatives $\frac{\partial \mathbf{D}_\text{s}{\partial x_{i}}$ 393 | Eigen::Matrix PDsPx[12]; 394 | 395 | PDsPx[0] << -1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0; 396 | PDsPx[1] << 0.0, 0.0, 0.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0; 397 | PDsPx[2] << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0; 398 | 399 | PDsPx[3] << 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0; 400 | PDsPx[4] << 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0; 401 | PDsPx[5] << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0; 402 | 403 | PDsPx[6] << 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0; 404 | PDsPx[7] << 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0; 405 | PDsPx[8] << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0; 406 | 407 | PDsPx[9] << 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0; 408 | PDsPx[10] << 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0; 409 | PDsPx[11] << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0; 410 | 411 | Eigen::Matrix vec_PFPx; 412 | for (std::size_t i = 0; i < 12; ++i) 413 | { 414 | const Eigen::Matrix PFPx_i = PDsPx[i] * rest_shape_mat_inv; 415 | 416 | vec_PFPx.col(i) = Eigen::Map>(PFPx_i.data(), PFPx_i.size()); 417 | } 418 | 419 | return vec_PFPx; 420 | } 421 | } // namespace elasty::fem 422 | 423 | #endif // ELASTY_FEM_HPP 424 | -------------------------------------------------------------------------------- /include/elasty/particle.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_PARTICLE_HPP 2 | #define ELASTY_PARTICLE_HPP 3 | 4 | #include 5 | 6 | namespace elasty 7 | { 8 | struct Particle 9 | { 10 | Particle(const Eigen::Vector3d& x, const Eigen::Vector3d& v, const double m) 11 | : x(x), v(v), p(x), f(Eigen::Vector3d::Zero()), m(m), w(1.0 / m) 12 | { 13 | } 14 | 15 | Eigen::Vector3d x; 16 | Eigen::Vector3d v; 17 | Eigen::Vector3d p; 18 | Eigen::Vector3d f; 19 | double m; 20 | double w; 21 | }; 22 | } // namespace elasty 23 | 24 | #endif // ELASTY_PARTICLE_HPP 25 | -------------------------------------------------------------------------------- /include/elasty/sim-object.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_SIM_OBJECT_HPP 2 | #define ELASTY_SIM_OBJECT_HPP 3 | 4 | #include 5 | #include 6 | 7 | namespace elasty 8 | { 9 | struct Particle; 10 | class AbstractConstraint; 11 | 12 | class SimObject 13 | { 14 | public: 15 | std::vector> m_particles; 16 | std::vector> m_constraints; 17 | }; 18 | } // namespace elasty 19 | 20 | #endif // ELASTY_SIM_OBJECT_HPP 21 | -------------------------------------------------------------------------------- /include/elasty/utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELASTY_UTILS_HPP 2 | #define ELASTY_UTILS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace elasty 10 | { 11 | struct Particle; 12 | class AbstractConstraint; 13 | class ClothSimObject; 14 | 15 | constexpr double pi() 16 | { 17 | return 3.14159265358979323846264338327950288; 18 | } 19 | 20 | /// \param particles an array of N particles 21 | /// 22 | /// \return an array of 3 * N values: 23 | /// std::vector 24 | /// { 25 | /// p[0].x, p[0].y, p[0].z, 26 | /// p[1].x, p[1].y, p[1].z, 27 | /// ... 28 | /// } 29 | std::vector packParticlePositions(const std::vector>& particles); 30 | 31 | void setRandomVelocities(const std::vector>& particles, const double scale = 1.0); 32 | 33 | void generateFixedPointConstraints(const Eigen::Vector3d& search_position, 34 | const Eigen::Vector3d& fixed_position, 35 | const std::vector>& particles, 36 | std::vector>& constraints); 37 | 38 | /// \brief Export the current cloth state as an obj file. 39 | void exportCurrentClothStateAsObj(const std::string& output_file_path, 40 | const std::shared_ptr cloth_sim_object); 41 | 42 | /// \brief Generate rectangular plane mesh for cloth simulation in the obj string format. 43 | /// 44 | /// \param vertical_resolution Need to be an even number. 45 | std::string generateClothMeshObjData(const double width = 2.0, 46 | const double height = 2.0, 47 | const unsigned horizontal_resolution = 50, 48 | const unsigned vertical_resolution = 50); 49 | } // namespace elasty 50 | 51 | #endif // ELASTY_UTILS_HPP 52 | -------------------------------------------------------------------------------- /src/alembic-manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | /// \brief A class for implementing common operations for alembic manager classes 8 | class AlembicManagerBase : public elasty::AbstractAlembicManager 9 | { 10 | public: 11 | AlembicManagerBase(const std::string& output_file_path, const double delta_time, const std::string& object_name) 12 | : m_archive(Alembic::AbcCoreOgawa::WriteArchive(), output_file_path.c_str()) 13 | { 14 | using namespace Alembic::Abc; 15 | using namespace Alembic::AbcGeom; 16 | 17 | const TimeSampling time_sampling(delta_time, 0); 18 | const std::uint32_t time_sampling_index = m_archive.addTimeSampling(time_sampling); 19 | 20 | m_mesh_obj = OPolyMesh(OObject(m_archive, kTop), object_name.c_str()); 21 | m_mesh_obj.getSchema().setTimeSampling(time_sampling_index); 22 | } 23 | 24 | void submitCurrentStatus() 25 | { 26 | using namespace Alembic::Abc; 27 | using namespace Alembic::AbcGeom; 28 | 29 | if (m_is_first) 30 | { 31 | submitCurrentStatusFirstTime(); 32 | 33 | m_is_first = false; 34 | 35 | return; 36 | } 37 | 38 | const OPolyMeshSchema::Sample sample(V3fArraySample((const V3f*) getAlembicPositionArray(), getNumVerts())); 39 | 40 | m_mesh_obj.getSchema().set(sample); 41 | } 42 | 43 | /// \brief A function that is called at the first frame of the alembic export. 44 | /// 45 | /// \details At the first frame, the sample needs to include indices, UVs, etc., so special care is necessary for 46 | /// each individual inherited class. 47 | virtual void submitCurrentStatusFirstTime() = 0; 48 | 49 | protected: 50 | virtual const float* getAlembicPositionArray() = 0; 51 | virtual std::int32_t getNumVerts() const = 0; 52 | 53 | bool m_is_first = true; 54 | 55 | Alembic::Abc::OArchive m_archive; 56 | Alembic::AbcGeom::OPolyMesh m_mesh_obj; 57 | }; 58 | 59 | class TriangleMesh2dAlembicManager : public AlembicManagerBase 60 | { 61 | public: 62 | TriangleMesh2dAlembicManager(const std::string& output_file_path, 63 | const double delta_time, 64 | const std::size_t num_verts, 65 | const std::size_t num_elems, 66 | const double* positions, 67 | const std::int32_t* indices) 68 | : AlembicManagerBase(output_file_path, delta_time, "Mesh"), 69 | m_num_verts(num_verts), 70 | m_num_elems(num_elems), 71 | m_positions(positions), 72 | m_indices(indices) 73 | { 74 | m_alembic_position_buffer = Eigen::Matrix::Zero(3, m_num_verts); 75 | } 76 | 77 | void submitCurrentStatusFirstTime() override 78 | { 79 | using namespace Alembic::Abc; 80 | using namespace Alembic::AbcGeom; 81 | 82 | const std::size_t num_counts = m_num_elems; 83 | const std::vector counts(num_counts, 3); 84 | 85 | // Ignore UV 86 | const OV2fGeomParam::Sample geom_param_sample = OV2fGeomParam::Sample(); 87 | 88 | const OPolyMeshSchema::Sample sample(V3fArraySample((const V3f*) getAlembicPositionArray(), getNumVerts()), 89 | Int32ArraySample(m_indices, m_num_elems * 3), 90 | Int32ArraySample(counts.data(), num_counts), 91 | geom_param_sample); 92 | 93 | m_mesh_obj.getSchema().set(sample); 94 | } 95 | 96 | private: 97 | const float* getAlembicPositionArray() override 98 | { 99 | m_alembic_position_buffer.block(0, 0, 2, m_num_verts) = 100 | Eigen::Map(m_positions, 2, m_num_verts).cast(); 101 | 102 | return m_alembic_position_buffer.data(); 103 | } 104 | 105 | std::int32_t getNumVerts() const override { return m_num_verts; } 106 | 107 | const std::size_t m_num_verts; 108 | const std::size_t m_num_elems; 109 | 110 | const double* m_positions; 111 | const std::int32_t* m_indices; 112 | 113 | /// \brief A buffer object to store the vertex position array in the alembic format. 114 | Eigen::Matrix m_alembic_position_buffer; 115 | }; 116 | 117 | class TetraMeshAlembicManager : public AlembicManagerBase 118 | { 119 | public: 120 | TetraMeshAlembicManager(const std::string& output_file_path, 121 | const double delta_time, 122 | const std::size_t num_verts, 123 | const std::size_t num_elems, 124 | const double* positions, 125 | const std::int32_t* indices) 126 | : AlembicManagerBase(output_file_path, delta_time, "Mesh"), 127 | m_num_verts(num_verts), 128 | m_num_elems(num_elems), 129 | m_positions(positions), 130 | m_indices(indices) 131 | { 132 | for (std::size_t elem_index = 0; elem_index < m_num_elems; ++elem_index) 133 | { 134 | m_index_map[elem_index * 4 + 0] = m_indices[elem_index * 4 + 0]; 135 | m_index_map[elem_index * 4 + 1] = m_indices[elem_index * 4 + 1]; 136 | m_index_map[elem_index * 4 + 2] = m_indices[elem_index * 4 + 2]; 137 | m_index_map[elem_index * 4 + 3] = m_indices[elem_index * 4 + 3]; 138 | } 139 | 140 | m_alembic_position_buffer.resize(3, m_num_elems * 4); 141 | } 142 | 143 | void submitCurrentStatusFirstTime() override 144 | { 145 | using namespace Alembic::Abc; 146 | using namespace Alembic::AbcGeom; 147 | 148 | const std::size_t num_counts = 4 * m_num_elems; 149 | const std::vector counts(num_counts, 3); 150 | 151 | m_alembic_index_buffer = Eigen::Matrix{3, num_counts}; 152 | for (std::size_t elem_index = 0; elem_index < m_num_elems; ++elem_index) 153 | { 154 | const auto i_0 = elem_index * 4 + 0; 155 | const auto i_1 = elem_index * 4 + 1; 156 | const auto i_2 = elem_index * 4 + 2; 157 | const auto i_3 = elem_index * 4 + 3; 158 | 159 | m_alembic_index_buffer.col(elem_index * 4 + 0) << i_0, i_2, i_1; 160 | m_alembic_index_buffer.col(elem_index * 4 + 1) << i_3, i_1, i_2; 161 | m_alembic_index_buffer.col(elem_index * 4 + 2) << i_0, i_3, i_2; 162 | m_alembic_index_buffer.col(elem_index * 4 + 3) << i_0, i_1, i_3; 163 | } 164 | 165 | // Ignore UV 166 | const OV2fGeomParam::Sample geom_param_sample = OV2fGeomParam::Sample(); 167 | 168 | const OPolyMeshSchema::Sample sample(V3fArraySample((const V3f*) getAlembicPositionArray(), getNumVerts()), 169 | Int32ArraySample(m_alembic_index_buffer.data(), m_num_elems * 4 * 3), 170 | Int32ArraySample(counts.data(), num_counts), 171 | geom_param_sample); 172 | 173 | m_mesh_obj.getSchema().set(sample); 174 | } 175 | 176 | private: 177 | const float* getAlembicPositionArray() override 178 | { 179 | const auto pos = Eigen::Map(m_positions, 3, m_num_verts).cast(); 180 | 181 | for (std::size_t alembic_vert_index = 0; alembic_vert_index < m_num_elems * 4; ++alembic_vert_index) 182 | { 183 | m_alembic_position_buffer.col(alembic_vert_index) = pos.col(m_index_map[alembic_vert_index]); 184 | } 185 | 186 | return m_alembic_position_buffer.data(); 187 | } 188 | 189 | std::int32_t getNumVerts() const override { return m_num_elems * 4; } 190 | 191 | const std::size_t m_num_verts; 192 | const std::size_t m_num_elems; 193 | 194 | const double* m_positions; 195 | const std::int32_t* m_indices; 196 | 197 | /// \brief A buffer object to store the vertex position array in the alembic format. 198 | Eigen::Matrix m_alembic_position_buffer; 199 | 200 | Eigen::Matrix m_alembic_index_buffer; 201 | 202 | std::map m_index_map; 203 | }; 204 | 205 | /// \details This manager assumes that the number of particles does not change during simulation 206 | class ClothAlembicManager : public AlembicManagerBase 207 | { 208 | public: 209 | ClothAlembicManager(const std::string& output_file_path, 210 | const double delta_time, 211 | const std::shared_ptr cloth_sim_object) 212 | : AlembicManagerBase(output_file_path, delta_time, "Cloth"), m_cloth_sim_object(cloth_sim_object) 213 | { 214 | } 215 | 216 | void submitCurrentStatusFirstTime() override 217 | { 218 | using namespace Alembic::Abc; 219 | using namespace Alembic::AbcGeom; 220 | 221 | const std::size_t num_indices = m_cloth_sim_object->m_triangle_list.size(); 222 | const std::size_t num_counts = m_cloth_sim_object->m_triangle_list.rows(); 223 | const std::vector counts(num_counts, 3); 224 | 225 | // If the model has UV info, include it into the geometry sample 226 | const OV2fGeomParam::Sample geom_param_sample = 227 | m_cloth_sim_object->hasUv() 228 | ? OV2fGeomParam::Sample(V2fArraySample((const V2f*) m_cloth_sim_object->m_uv_list.data(), num_indices), 229 | kFacevaryingScope) 230 | : OV2fGeomParam::Sample(); 231 | 232 | const OPolyMeshSchema::Sample sample(V3fArraySample((const V3f*) getAlembicPositionArray(), getNumVerts()), 233 | Int32ArraySample(m_cloth_sim_object->m_triangle_list.data(), num_indices), 234 | Int32ArraySample(counts.data(), num_counts), 235 | geom_param_sample); 236 | 237 | m_mesh_obj.getSchema().set(sample); 238 | } 239 | 240 | private: 241 | const float* getAlembicPositionArray() override 242 | { 243 | m_alembic_position_buffer = packParticlePositions(m_cloth_sim_object->m_particles); 244 | 245 | return m_alembic_position_buffer.data(); 246 | } 247 | 248 | std::int32_t getNumVerts() const override { return m_cloth_sim_object->m_particles.size(); } 249 | 250 | bool m_is_first = true; 251 | 252 | const std::shared_ptr m_cloth_sim_object; 253 | 254 | /// \brief A buffer object to store the vertex position array in the alembic format. 255 | std::vector m_alembic_position_buffer; 256 | }; 257 | 258 | std::shared_ptr 259 | elasty::createClothAlembicManager(const std::string& file_path, 260 | const std::shared_ptr cloth_sim_object, 261 | const double delta_time) 262 | { 263 | return std::make_shared(file_path, delta_time, cloth_sim_object); 264 | } 265 | 266 | std::shared_ptr elasty::createTriangleMesh2dAlembicManager(const std::string& file_path, 267 | const double delta_time, 268 | const std::size_t num_verts, 269 | const std::size_t num_elems, 270 | const double* positions, 271 | const std::int32_t* indices) 272 | { 273 | return std::make_shared( 274 | file_path, delta_time, num_verts, num_elems, positions, indices); 275 | } 276 | 277 | std::shared_ptr elasty::createTetraMeshAlembicManager(const std::string& file_path, 278 | const double delta_time, 279 | const std::size_t num_verts, 280 | const std::size_t num_elems, 281 | const double* positions, 282 | const std::int32_t* indices) 283 | { 284 | return std::make_shared(file_path, delta_time, num_verts, num_elems, positions, indices); 285 | } 286 | -------------------------------------------------------------------------------- /src/cloth-sim-object.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | elasty::ClothSimObject::ClothSimObject(const unsigned resolution, 11 | const double in_plane_stiffness, 12 | const double in_plane_compliance, 13 | const double out_of_plane_stiffness, 14 | const double out_of_plane_compliance, 15 | const double dt, 16 | const Eigen::Affine3d& transform, 17 | const InPlaneStrategy in_plane_strategy, 18 | const OutOfPlaneStrategy out_of_plane_strategy) 19 | { 20 | std::istringstream obj_data_stream(generateClothMeshObjData(2.0, 2.0, resolution, resolution)); 21 | 22 | tinyobj::attrib_t attrib; 23 | std::vector shapes; 24 | std::vector materials; 25 | 26 | std::string warn; 27 | std::string err; 28 | const bool return_value = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, &obj_data_stream); 29 | 30 | if (!warn.empty()) 31 | { 32 | std::cerr << warn << std::endl; 33 | } 34 | if (!err.empty()) 35 | { 36 | std::cerr << err << std::endl; 37 | } 38 | if (!return_value) 39 | { 40 | throw std::runtime_error(""); 41 | } 42 | if (attrib.vertices.empty()) 43 | { 44 | throw std::runtime_error(""); 45 | } 46 | 47 | const auto& shape = shapes[0]; 48 | 49 | assert(shapes.size() == 1); 50 | assert(attrib.vertices.size() % 3 == 0); 51 | assert(shape.mesh.indices.size() % 3 == 0); 52 | 53 | m_triangle_list.resize(shape.mesh.indices.size() / 3, 3); 54 | for (unsigned int i = 0; i < shape.mesh.indices.size() / 3; ++i) 55 | { 56 | m_triangle_list(i, 0) = shape.mesh.indices[i * 3 + 0].vertex_index; 57 | m_triangle_list(i, 1) = shape.mesh.indices[i * 3 + 1].vertex_index; 58 | m_triangle_list(i, 2) = shape.mesh.indices[i * 3 + 2].vertex_index; 59 | } 60 | 61 | if (!attrib.texcoords.empty()) 62 | { 63 | m_uv_list.resize(shape.mesh.indices.size() / 3, 2 * 3); 64 | for (unsigned int i = 0; i < shape.mesh.indices.size() / 3; ++i) 65 | { 66 | m_uv_list(i, 2 * 0 + 0) = attrib.texcoords[2 * shape.mesh.indices[i * 3 + 0].texcoord_index + 0]; 67 | m_uv_list(i, 2 * 0 + 1) = attrib.texcoords[2 * shape.mesh.indices[i * 3 + 0].texcoord_index + 1]; 68 | m_uv_list(i, 2 * 1 + 0) = attrib.texcoords[2 * shape.mesh.indices[i * 3 + 1].texcoord_index + 0]; 69 | m_uv_list(i, 2 * 1 + 1) = attrib.texcoords[2 * shape.mesh.indices[i * 3 + 1].texcoord_index + 1]; 70 | m_uv_list(i, 2 * 2 + 0) = attrib.texcoords[2 * shape.mesh.indices[i * 3 + 2].texcoord_index + 0]; 71 | m_uv_list(i, 2 * 2 + 1) = attrib.texcoords[2 * shape.mesh.indices[i * 3 + 2].texcoord_index + 1]; 72 | } 73 | } 74 | 75 | std::map> map_from_obj_vertex_index_to_particle; 76 | for (unsigned int i = 0; i < attrib.vertices.size() / 3; ++i) 77 | { 78 | const Eigen::Vector3d position{ 79 | attrib.vertices[3 * i + 0], attrib.vertices[3 * i + 1], attrib.vertices[3 * i + 2]}; 80 | 81 | const Eigen::Vector3d x = transform * position; 82 | const Eigen::Vector3d v = Eigen::Vector3d::Zero(); 83 | const double m = 1.0 / double(attrib.vertices.size()); 84 | 85 | auto particle = std::make_shared(x, v, m); 86 | 87 | map_from_obj_vertex_index_to_particle[i] = particle; 88 | 89 | m_particles.push_back(particle); 90 | } 91 | 92 | for (unsigned int i = 0; i < shape.mesh.indices.size() / 3; ++i) 93 | { 94 | const auto p_0 = map_from_obj_vertex_index_to_particle[shape.mesh.indices[i * 3 + 0].vertex_index]; 95 | const auto p_1 = map_from_obj_vertex_index_to_particle[shape.mesh.indices[i * 3 + 1].vertex_index]; 96 | const auto p_2 = map_from_obj_vertex_index_to_particle[shape.mesh.indices[i * 3 + 2].vertex_index]; 97 | 98 | if (in_plane_strategy == InPlaneStrategy::EdgeDistance || in_plane_strategy == InPlaneStrategy::Both) 99 | { 100 | m_constraints.push_back(std::make_shared( 101 | p_0, p_1, in_plane_stiffness, in_plane_compliance, dt, (p_0->x - p_1->x).norm())); 102 | m_constraints.push_back(std::make_shared( 103 | p_0, p_2, in_plane_stiffness, in_plane_compliance, dt, (p_0->x - p_2->x).norm())); 104 | m_constraints.push_back(std::make_shared( 105 | p_1, p_2, in_plane_stiffness, in_plane_compliance, dt, (p_1->x - p_2->x).norm())); 106 | } 107 | 108 | if (in_plane_strategy == InPlaneStrategy::ContinuumTriangle || in_plane_strategy == InPlaneStrategy::Both) 109 | { 110 | m_constraints.push_back(std::make_shared( 111 | p_0, p_1, p_2, in_plane_stiffness, in_plane_compliance, dt, 1000.0, 0.10)); 112 | } 113 | } 114 | 115 | using vertex_t = unsigned int; 116 | using triangle_t = unsigned int; 117 | using edge_t = std::pair; 118 | std::map> edges_and_triangles; 119 | for (unsigned int i = 0; i < shape.mesh.indices.size() / 3; ++i) 120 | { 121 | const vertex_t index_0 = shape.mesh.indices[i * 3 + 0].vertex_index; 122 | const vertex_t index_1 = shape.mesh.indices[i * 3 + 1].vertex_index; 123 | const vertex_t index_2 = shape.mesh.indices[i * 3 + 2].vertex_index; 124 | 125 | const edge_t e_01 = std::make_pair(std::min(index_0, index_1), std::max(index_0, index_1)); 126 | const edge_t e_02 = std::make_pair(std::min(index_0, index_2), std::max(index_0, index_2)); 127 | const edge_t e_12 = std::make_pair(std::min(index_1, index_2), std::max(index_1, index_2)); 128 | 129 | auto register_edge = [&](const edge_t& edge) 130 | { 131 | if (edges_and_triangles.find(edge) == edges_and_triangles.end()) 132 | { 133 | edges_and_triangles[edge] = {i}; 134 | } 135 | else 136 | { 137 | edges_and_triangles[edge].push_back(i); 138 | } 139 | }; 140 | 141 | register_edge(e_01); 142 | register_edge(e_02); 143 | register_edge(e_12); 144 | } 145 | 146 | for (const auto& key_value : edges_and_triangles) 147 | { 148 | const edge_t& edge = key_value.first; 149 | const std::vector& triangles = key_value.second; 150 | 151 | assert(triangles.size() == 1 || triangles.size() == 2); 152 | 153 | // Boundary 154 | if (triangles.size() == 1) 155 | { 156 | continue; 157 | } 158 | 159 | auto obtain_another_vertex = [&](const triangle_t& triangle, const edge_t& edge) 160 | { 161 | const vertex_t vertex_0 = shape.mesh.indices[3 * triangle + 0].vertex_index; 162 | const vertex_t vertex_1 = shape.mesh.indices[3 * triangle + 1].vertex_index; 163 | const vertex_t vertex_2 = shape.mesh.indices[3 * triangle + 2].vertex_index; 164 | 165 | if (vertex_0 != edge.first && vertex_0 != edge.second) 166 | { 167 | return vertex_0; 168 | } 169 | else if (vertex_1 != edge.first && vertex_1 != edge.second) 170 | { 171 | return vertex_1; 172 | } 173 | else 174 | { 175 | assert(vertex_2 != edge.first && vertex_2 != edge.second); 176 | return vertex_2; 177 | } 178 | }; 179 | 180 | const triangle_t another_vertex_0 = obtain_another_vertex(triangles[0], edge); 181 | const triangle_t another_vertex_1 = obtain_another_vertex(triangles[1], edge); 182 | 183 | switch (out_of_plane_strategy) 184 | { 185 | case OutOfPlaneStrategy::Bending: 186 | { 187 | const auto p_0 = map_from_obj_vertex_index_to_particle[edge.first]; 188 | const auto p_1 = map_from_obj_vertex_index_to_particle[edge.second]; 189 | const auto p_2 = map_from_obj_vertex_index_to_particle[another_vertex_0]; 190 | const auto p_3 = map_from_obj_vertex_index_to_particle[another_vertex_1]; 191 | 192 | const Eigen::Vector3d& x_0 = p_0->x; 193 | const Eigen::Vector3d& x_1 = p_1->x; 194 | const Eigen::Vector3d& x_2 = p_2->x; 195 | const Eigen::Vector3d& x_3 = p_3->x; 196 | 197 | const Eigen::Vector3d p_10 = x_1 - x_0; 198 | const Eigen::Vector3d p_20 = x_2 - x_0; 199 | const Eigen::Vector3d p_30 = x_3 - x_0; 200 | 201 | const Eigen::Vector3d n_0 = p_10.cross(p_20).normalized(); 202 | const Eigen::Vector3d n_1 = p_10.cross(p_30).normalized(); 203 | 204 | assert(!n_0.hasNaN()); 205 | assert(!n_1.hasNaN()); 206 | 207 | // Typical value is 0.0 or pi 208 | const double dihedral_angle = std::acos(std::clamp(n_0.dot(n_1), -1.0, 1.0)); 209 | 210 | assert(!std::isnan(dihedral_angle)); 211 | 212 | m_constraints.push_back(std::make_shared( 213 | p_0, p_1, p_2, p_3, out_of_plane_stiffness, out_of_plane_compliance, dt, dihedral_angle)); 214 | 215 | break; 216 | } 217 | case OutOfPlaneStrategy::IsometricBending: 218 | { 219 | const auto p_0 = map_from_obj_vertex_index_to_particle[edge.first]; 220 | const auto p_1 = map_from_obj_vertex_index_to_particle[edge.second]; 221 | const auto p_2 = map_from_obj_vertex_index_to_particle[another_vertex_0]; 222 | const auto p_3 = map_from_obj_vertex_index_to_particle[another_vertex_1]; 223 | 224 | m_constraints.push_back(std::make_shared( 225 | p_0, p_1, p_2, p_3, out_of_plane_stiffness, out_of_plane_compliance, dt)); 226 | 227 | break; 228 | } 229 | case OutOfPlaneStrategy::Cross: 230 | { 231 | const auto p_2 = map_from_obj_vertex_index_to_particle[another_vertex_0]; 232 | const auto p_3 = map_from_obj_vertex_index_to_particle[another_vertex_1]; 233 | 234 | const Eigen::Vector3d& x_2 = p_2->x; 235 | const Eigen::Vector3d& x_3 = p_3->x; 236 | 237 | m_constraints.push_back(std::make_shared( 238 | p_2, p_3, out_of_plane_stiffness, out_of_plane_compliance, dt, (x_2 - x_3).norm())); 239 | 240 | break; 241 | } 242 | } 243 | } 244 | 245 | calculateAreas(); 246 | } 247 | 248 | void elasty::ClothSimObject::applyAerodynamicForces(const Eigen::Vector3d& global_velocity, 249 | const double drag_coeff, 250 | const double lift_coeff) 251 | { 252 | constexpr double rho = 1.225; // Taken from Wikipedia: https://en.wikipedia.org/wiki/Density_of_air 253 | 254 | assert(drag_coeff >= lift_coeff); 255 | 256 | const int num_triangles = m_triangle_list.rows(); 257 | 258 | for (int i = 0; i < num_triangles; ++i) 259 | { 260 | const auto& x_0 = m_particles[m_triangle_list.row(i)(0)]->x; 261 | const auto& x_1 = m_particles[m_triangle_list.row(i)(1)]->x; 262 | const auto& x_2 = m_particles[m_triangle_list.row(i)(2)]->x; 263 | 264 | const auto& v_0 = m_particles[m_triangle_list.row(i)(0)]->v; 265 | const auto& v_1 = m_particles[m_triangle_list.row(i)(1)]->v; 266 | const auto& v_2 = m_particles[m_triangle_list.row(i)(2)]->v; 267 | 268 | const auto& m_0 = m_particles[m_triangle_list.row(i)(0)]->m; 269 | const auto& m_1 = m_particles[m_triangle_list.row(i)(1)]->m; 270 | const auto& m_2 = m_particles[m_triangle_list.row(i)(2)]->m; 271 | 272 | const double m_sum = m_0 + m_1 + m_2; 273 | 274 | // Calculate the weighted average of the particle velocities 275 | const Eigen::Vector3d v_triangle = (m_0 * v_0 + m_1 * v_1 + m_2 * v_2) / m_sum; 276 | 277 | // Calculate the relative velocity of the triangle 278 | const Eigen::Vector3d v_rel = v_triangle - global_velocity; 279 | const double v_rel_squared = v_rel.squaredNorm(); 280 | 281 | const auto cross = (x_1 - x_0).cross(x_2 - x_0); 282 | const double area = 0.5 * cross.norm(); 283 | const auto n_either_side = cross.normalized(); 284 | const Eigen::Vector3d n = (n_either_side.dot(v_rel) > 0.0) ? n_either_side : -n_either_side; 285 | 286 | const double coeff = 0.5 * rho * area; 287 | 288 | // Note: This wind force model was proposed by [Wilson+14] 289 | const Eigen::Vector3d f = 290 | -coeff * ((drag_coeff - lift_coeff) * v_rel.dot(n) * v_rel + lift_coeff * v_rel_squared * n); 291 | 292 | m_particles[m_triangle_list.row(i)(0)]->f += (m_0 / m_sum) * f; 293 | m_particles[m_triangle_list.row(i)(1)]->f += (m_1 / m_sum) * f; 294 | m_particles[m_triangle_list.row(i)(2)]->f += (m_2 / m_sum) * f; 295 | } 296 | } 297 | 298 | void elasty::ClothSimObject::calculateAreas() 299 | { 300 | const int num_triangles = m_triangle_list.rows(); 301 | 302 | m_area_list = Eigen::VectorXd(num_triangles); 303 | 304 | for (int i = 0; i < num_triangles; ++i) 305 | { 306 | const auto& x_0 = m_particles[m_triangle_list.row(i)[0]]->x; 307 | const auto& x_1 = m_particles[m_triangle_list.row(i)[1]]->x; 308 | const auto& x_2 = m_particles[m_triangle_list.row(i)[2]]->x; 309 | 310 | const double area = 0.5 * (x_1 - x_0).cross(x_2 - x_0).norm(); 311 | 312 | m_area_list(i) = area; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/constraint.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | inline Eigen::Matrix3d convertVecToCrossOp(const Eigen::Vector3d& vec) 12 | { 13 | Eigen::Matrix3d mat = Eigen::Matrix3d::Zero(); 14 | 15 | mat(0, 1) = -vec(2); 16 | mat(0, 2) = +vec(1); 17 | mat(1, 0) = +vec(2); 18 | mat(1, 2) = -vec(0); 19 | mat(2, 0) = -vec(1); 20 | mat(2, 1) = +vec(0); 21 | 22 | return mat; 23 | }; 24 | 25 | inline double calculateCotTheta(const Eigen::Vector3d& x, const Eigen::Vector3d& y) 26 | { 27 | const double scaled_cos_theta = x.dot(y); 28 | const double scaled_sin_theta = x.cross(y).norm(); 29 | return scaled_cos_theta / scaled_sin_theta; 30 | } 31 | } // namespace 32 | 33 | elasty::BendingConstraint::BendingConstraint(const std::shared_ptr p_0, 34 | const std::shared_ptr p_1, 35 | const std::shared_ptr p_2, 36 | const std::shared_ptr p_3, 37 | const double stiffness, 38 | const double compliance, 39 | const double delta_time, 40 | const double dihedral_angle) 41 | : FixedNumAbstractConstraint(std::vector>{p_0, p_1, p_2, p_3}, 42 | stiffness, 43 | compliance, 44 | delta_time), 45 | m_dihedral_angle(dihedral_angle) 46 | { 47 | } 48 | 49 | double elasty::BendingConstraint::calculateValue() 50 | { 51 | const Eigen::Vector3d& x_0 = m_particles[0]->p; 52 | const Eigen::Vector3d& x_1 = m_particles[1]->p; 53 | const Eigen::Vector3d& x_2 = m_particles[2]->p; 54 | const Eigen::Vector3d& x_3 = m_particles[3]->p; 55 | 56 | const Eigen::Vector3d p_10 = x_1 - x_0; 57 | const Eigen::Vector3d p_20 = x_2 - x_0; 58 | const Eigen::Vector3d p_30 = x_3 - x_0; 59 | 60 | const Eigen::Vector3d n_0 = p_10.cross(p_20).normalized(); 61 | const Eigen::Vector3d n_1 = p_10.cross(p_30).normalized(); 62 | 63 | const double current_dihedral_angle = std::acos(std::clamp(n_0.dot(n_1), -1.0, 1.0)); 64 | 65 | assert(n_0.norm() > 0.0); 66 | assert(n_1.norm() > 0.0); 67 | assert(!std::isnan(current_dihedral_angle)); 68 | 69 | return current_dihedral_angle - m_dihedral_angle; 70 | } 71 | 72 | // See the appendix of the original paper by Muller et al. (2007) for details. 73 | void elasty::BendingConstraint::calculateGrad(double* grad_C) 74 | { 75 | const Eigen::Vector3d& x_0 = m_particles[0]->p; 76 | const Eigen::Vector3d& x_1 = m_particles[1]->p; 77 | const Eigen::Vector3d& x_2 = m_particles[2]->p; 78 | const Eigen::Vector3d& x_3 = m_particles[3]->p; 79 | 80 | // Assuming that p_0 = [ 0, 0, 0 ]^T without loss of generality 81 | const Eigen::Vector3d p_1 = x_1 - x_0; 82 | const Eigen::Vector3d p_2 = x_2 - x_0; 83 | const Eigen::Vector3d p_3 = x_3 - x_0; 84 | 85 | const Eigen::Vector3d p_1_cross_p_2 = p_1.cross(p_2); 86 | const Eigen::Vector3d p_1_cross_p_3 = p_1.cross(p_3); 87 | 88 | const Eigen::Vector3d n_0 = p_1_cross_p_2.normalized(); 89 | const Eigen::Vector3d n_1 = p_1_cross_p_3.normalized(); 90 | 91 | const double d = n_0.dot(n_1); 92 | 93 | // If the current dihedral angle is sufficiently small or large (i.e., zero or pi), return zeros. 94 | // This is only an ad-hoc solution for stability and it needs to be solved in a more theoretically grounded way. 95 | constexpr double epsilon = 1e-12; 96 | if (1.0 - d * d < epsilon) 97 | { 98 | std::fill(grad_C, grad_C + 12, 0.0); 99 | return; 100 | } 101 | 102 | const double common_coeff = -1.0 / std::sqrt(1.0 - d * d); 103 | 104 | auto calc_grad_of_normalized_cross_prod_wrt_p_a = 105 | [](const Eigen::Vector3d& p_a, const Eigen::Vector3d& p_b, const Eigen::Vector3d& n) -> Eigen::Matrix3d 106 | { 107 | return +(1.0 / p_a.cross(p_b).norm()) * (-convertVecToCrossOp(p_b) + n * (n.cross(p_b)).transpose()); 108 | }; 109 | 110 | auto calc_grad_of_normalized_cross_prod_wrt_p_b = 111 | [](const Eigen::Vector3d& p_a, const Eigen::Vector3d& p_b, const Eigen::Vector3d& n) -> Eigen::Matrix3d 112 | { 113 | return -(1.0 / p_a.cross(p_b).norm()) * (-convertVecToCrossOp(p_a) + n * (n.cross(p_a)).transpose()); 114 | }; 115 | 116 | const Eigen::Matrix3d partial_n_0_per_partial_p_1 = calc_grad_of_normalized_cross_prod_wrt_p_a(p_1, p_2, n_0); 117 | const Eigen::Matrix3d partial_n_1_per_partial_p_1 = calc_grad_of_normalized_cross_prod_wrt_p_a(p_1, p_3, n_1); 118 | const Eigen::Matrix3d partial_n_0_per_partial_p_2 = calc_grad_of_normalized_cross_prod_wrt_p_b(p_1, p_2, n_0); 119 | const Eigen::Matrix3d partial_n_1_per_partial_p_3 = calc_grad_of_normalized_cross_prod_wrt_p_b(p_1, p_3, n_1); 120 | 121 | const Eigen::Vector3d grad_C_wrt_p_1 = 122 | common_coeff * (partial_n_0_per_partial_p_1.transpose() * n_1 + partial_n_1_per_partial_p_1.transpose() * n_0); 123 | const Eigen::Vector3d grad_C_wrt_p_2 = common_coeff * partial_n_0_per_partial_p_2.transpose() * n_1; 124 | const Eigen::Vector3d grad_C_wrt_p_3 = common_coeff * partial_n_1_per_partial_p_3.transpose() * n_0; 125 | const Eigen::Vector3d grad_C_wrt_p_0 = -grad_C_wrt_p_1 - grad_C_wrt_p_2 - grad_C_wrt_p_3; 126 | 127 | std::memcpy(grad_C + (3 * 0), grad_C_wrt_p_0.data(), sizeof(double) * 3); 128 | std::memcpy(grad_C + (3 * 1), grad_C_wrt_p_1.data(), sizeof(double) * 3); 129 | std::memcpy(grad_C + (3 * 2), grad_C_wrt_p_2.data(), sizeof(double) * 3); 130 | std::memcpy(grad_C + (3 * 3), grad_C_wrt_p_3.data(), sizeof(double) * 3); 131 | } 132 | 133 | elasty::ContinuumTriangleConstraint::ContinuumTriangleConstraint(const std::shared_ptr p_0, 134 | const std::shared_ptr p_1, 135 | const std::shared_ptr p_2, 136 | const double stiffness, 137 | const double compliance, 138 | const double delta_time, 139 | const double youngs_modulus, 140 | const double poisson_ratio) 141 | : FixedNumAbstractConstraint(std::vector>{p_0, p_1, p_2}, 142 | stiffness, 143 | compliance, 144 | delta_time), 145 | m_first_lame(fem::calcFirstLame(youngs_modulus, poisson_ratio)), 146 | m_second_lame(fem::calcSecondLame(youngs_modulus, poisson_ratio)) 147 | { 148 | const Eigen::Vector3d& x_0 = m_particles[0]->x; 149 | const Eigen::Vector3d& x_1 = m_particles[1]->x; 150 | const Eigen::Vector3d& x_2 = m_particles[2]->x; 151 | 152 | // Calculate the two axes for defining material coordinates 153 | const Eigen::Vector3d r_1 = x_1 - x_0; 154 | const Eigen::Vector3d r_2 = x_2 - x_0; 155 | const Eigen::Vector3d cross = r_1.cross(r_2); 156 | const Eigen::Vector3d axis_1 = r_1.normalized(); 157 | const Eigen::Vector3d axis_2 = cross.cross(axis_1).normalized(); 158 | 159 | // Calculate the rest positions in the material coordinates 160 | const Eigen::Vector2d mat_x_0(axis_1.dot(x_0), axis_2.dot(x_0)); 161 | const Eigen::Vector2d mat_x_1(axis_1.dot(x_1), axis_2.dot(x_1)); 162 | const Eigen::Vector2d mat_x_2(axis_1.dot(x_2), axis_2.dot(x_2)); 163 | 164 | // Calculate the rest shape matrix 165 | Eigen::Matrix2d rest_D; 166 | rest_D.col(0) = mat_x_1 - mat_x_0; 167 | rest_D.col(1) = mat_x_2 - mat_x_0; 168 | 169 | // Calculate the inverse of the rest shape matrix 170 | assert(rest_D.determinant() > 0.0); 171 | m_rest_D_inv = rest_D.inverse(); 172 | 173 | // Calculate the area of the rest configuration 174 | m_rest_area = 0.5 * cross.norm(); 175 | } 176 | 177 | double elasty::ContinuumTriangleConstraint::calculateValue() 178 | { 179 | const Eigen::Vector3d& x_0 = m_particles[0]->p; 180 | const Eigen::Vector3d& x_1 = m_particles[1]->p; 181 | const Eigen::Vector3d& x_2 = m_particles[2]->p; 182 | 183 | // Calculate the shape matrix 184 | Eigen::Matrix D; 185 | D.col(0) = x_1 - x_0; 186 | D.col(1) = x_2 - x_0; 187 | 188 | // Calculate the deformation gradient (a 3-by-2 matrix) 189 | const auto F = D * m_rest_D_inv; 190 | 191 | // Calculate the strain energy density 192 | const double psi = fem::calcStVenantKirchhoffEnergyDensity(F, m_first_lame, m_second_lame); 193 | 194 | // Return the constraint value 195 | return m_rest_area * psi; 196 | } 197 | 198 | void elasty::ContinuumTriangleConstraint::calculateGrad(double* grad_C) 199 | { 200 | const Eigen::Vector3d& x_0 = m_particles[0]->p; 201 | const Eigen::Vector3d& x_1 = m_particles[1]->p; 202 | const Eigen::Vector3d& x_2 = m_particles[2]->p; 203 | 204 | // Calculate the shape matrix 205 | Eigen::Matrix D; 206 | D.col(0) = x_1 - x_0; 207 | D.col(1) = x_2 - x_0; 208 | 209 | // Calculate the deformation gradient (a 3-by-2 matrix) 210 | const auto F = D * m_rest_D_inv; 211 | 212 | // Calculate the first Piola-Kirchhoff stress tensor (a 3-by-2 matrix) 213 | const auto P = fem::calcStVenantKirchhoffPiolaStress(F, m_first_lame, m_second_lame); 214 | 215 | // Calculate the gradient of the constraint 216 | const Eigen::Matrix grad_12 = m_rest_area * P * m_rest_D_inv.transpose(); 217 | const Eigen::Vector3d grad_0 = -grad_12.col(0) - grad_12.col(1); 218 | 219 | // Copy the results 220 | std::memcpy(grad_C + 0, grad_0.data(), sizeof(double) * 3); 221 | std::memcpy(grad_C + 3, grad_12.data(), sizeof(double) * 6); 222 | } 223 | 224 | elasty::DistanceConstraint::DistanceConstraint(const std::shared_ptr p_0, 225 | const std::shared_ptr p_1, 226 | const double stiffness, 227 | const double compliance, 228 | const double delta_time, 229 | const double d) 230 | : FixedNumAbstractConstraint(std::vector>{p_0, p_1}, stiffness, compliance, delta_time), 231 | m_d(d) 232 | { 233 | assert(d >= 0.0); 234 | } 235 | 236 | double elasty::DistanceConstraint::calculateValue() 237 | { 238 | const Eigen::Vector3d& x_0 = m_particles[0]->p; 239 | const Eigen::Vector3d& x_1 = m_particles[1]->p; 240 | 241 | return (x_0 - x_1).norm() - m_d; 242 | } 243 | 244 | void elasty::DistanceConstraint::calculateGrad(double* grad_C) 245 | { 246 | const Eigen::Vector3d& x_0 = m_particles[0]->p; 247 | const Eigen::Vector3d& x_1 = m_particles[1]->p; 248 | 249 | const Eigen::Vector3d r = x_0 - x_1; 250 | 251 | const double dist = r.norm(); 252 | 253 | constexpr double epsilon = 1e-24; 254 | 255 | // Calculate a normalized vector, where a random direction is selected when the points are degenerated 256 | const Eigen::Vector3d n = (dist < epsilon) ? Eigen::Vector3d::Random().normalized() : (1.0 / dist) * r; 257 | 258 | grad_C[0] = +n(0); 259 | grad_C[1] = +n(1); 260 | grad_C[2] = +n(2); 261 | grad_C[3] = -n(0); 262 | grad_C[4] = -n(1); 263 | grad_C[5] = -n(2); 264 | } 265 | 266 | elasty::EnvironmentalCollisionConstraint::EnvironmentalCollisionConstraint(const std::shared_ptr p_0, 267 | const double stiffness, 268 | const double compliance, 269 | const double delta_time, 270 | const Eigen::Vector3d& n, 271 | const double d) 272 | : FixedNumAbstractConstraint(std::vector>{p_0}, stiffness, compliance, delta_time), 273 | m_n(n), 274 | m_d(d) 275 | { 276 | } 277 | 278 | double elasty::EnvironmentalCollisionConstraint::calculateValue() 279 | { 280 | const Eigen::Vector3d& x = m_particles[0]->p; 281 | return m_n.transpose() * x - m_d; 282 | } 283 | 284 | void elasty::EnvironmentalCollisionConstraint::calculateGrad(double* grad_C) 285 | { 286 | std::memcpy(grad_C, m_n.data(), sizeof(double) * 3); 287 | } 288 | 289 | elasty::FixedPointConstraint::FixedPointConstraint(const std::shared_ptr p_0, 290 | const double stiffness, 291 | const double compliance, 292 | const double delta_time, 293 | const Eigen::Vector3d& point) 294 | : FixedNumAbstractConstraint(std::vector>{p_0}, stiffness, compliance, delta_time), 295 | m_point(point) 296 | { 297 | } 298 | 299 | double elasty::FixedPointConstraint::calculateValue() 300 | { 301 | const Eigen::Vector3d& x = m_particles[0]->p; 302 | return (x - m_point).norm(); 303 | } 304 | 305 | void elasty::FixedPointConstraint::calculateGrad(double* grad_C) 306 | { 307 | const Eigen::Vector3d& x = m_particles[0]->p; 308 | const Eigen::Vector3d r = x - m_point; 309 | const double dist = r.norm(); 310 | 311 | constexpr double epsilon = 1e-24; 312 | 313 | // Calculate a normalized vector, where a random direction is selected when the points are degenerated 314 | const Eigen::Vector3d n = (dist < epsilon) ? Eigen::Vector3d::Random().normalized() : (1.0 / dist) * r; 315 | 316 | std::memcpy(grad_C, n.data(), sizeof(double) * 3); 317 | } 318 | 319 | elasty::IsometricBendingConstraint::IsometricBendingConstraint(const std::shared_ptr p_0, 320 | const std::shared_ptr p_1, 321 | const std::shared_ptr p_2, 322 | const std::shared_ptr p_3, 323 | const double stiffness, 324 | const double compliance, 325 | const double delta_time) 326 | : FixedNumAbstractConstraint(std::vector>{p_0, p_1, p_2, p_3}, 327 | stiffness, 328 | compliance, 329 | delta_time) 330 | { 331 | const Eigen::Vector3d& x_0 = p_0->x; 332 | const Eigen::Vector3d& x_1 = p_1->x; 333 | const Eigen::Vector3d& x_2 = p_2->x; 334 | const Eigen::Vector3d& x_3 = p_3->x; 335 | 336 | const Eigen::Vector3d e0 = x_1 - x_0; 337 | const Eigen::Vector3d e1 = x_2 - x_1; 338 | const Eigen::Vector3d e2 = x_0 - x_2; 339 | const Eigen::Vector3d e3 = x_3 - x_0; 340 | const Eigen::Vector3d e4 = x_1 - x_3; 341 | 342 | const double cot_01 = calculateCotTheta(e0, -e1); 343 | const double cot_02 = calculateCotTheta(e0, -e2); 344 | const double cot_03 = calculateCotTheta(e0, e3); 345 | const double cot_04 = calculateCotTheta(e0, e4); 346 | 347 | const Eigen::Vector4d K = Eigen::Vector4d(cot_01 + cot_04, cot_02 + cot_03, -cot_01 - cot_02, -cot_03 - cot_04); 348 | 349 | const double A_0 = 0.5 * e0.cross(e1).norm(); 350 | const double A_1 = 0.5 * e0.cross(e3).norm(); 351 | 352 | m_Q = (3.0 / (A_0 + A_1)) * K * K.transpose(); 353 | } 354 | 355 | double elasty::IsometricBendingConstraint::calculateValue() 356 | { 357 | double sum = 0.0; 358 | for (unsigned int i = 0; i < 4; ++i) 359 | { 360 | for (unsigned int j = 0; j < 4; ++j) 361 | { 362 | sum += m_Q(i, j) * double(m_particles[i]->p.transpose() * m_particles[j]->p); 363 | } 364 | } 365 | return 0.5 * sum; 366 | } 367 | 368 | void elasty::IsometricBendingConstraint::calculateGrad(double* grad_C) 369 | { 370 | for (unsigned int i = 0; i < 4; ++i) 371 | { 372 | Eigen::Vector3d sum = Eigen::Vector3d::Zero(); 373 | for (unsigned int j = 0; j < 4; ++j) 374 | { 375 | sum += m_Q(i, j) * m_particles[j]->p; 376 | } 377 | std::memcpy(grad_C + (3 * i), sum.data(), sizeof(double) * 3); 378 | } 379 | } 380 | 381 | elasty::ShapeMatchingConstraint::ShapeMatchingConstraint(const std::vector>& particles, 382 | const double stiffness, 383 | const double compliance, 384 | const double delta_time) 385 | : VariableNumConstraint(particles, stiffness, compliance, delta_time) 386 | { 387 | // Calculate the initial center of mass and the total mass 388 | Eigen::Vector3d x_0_cm = Eigen::Vector3d::Zero(); 389 | m_total_mass = 0.0; 390 | for (int i = 0; i < m_particles.size(); ++i) 391 | { 392 | m_total_mass += m_particles[i]->m; 393 | x_0_cm += m_particles[i]->m * m_particles[i]->p; 394 | } 395 | x_0_cm /= m_total_mass; 396 | 397 | // Calculate q 398 | m_q.resize(m_particles.size()); 399 | for (int i = 0; i < m_particles.size(); ++i) 400 | { 401 | m_q[i] = m_particles[i]->x - x_0_cm; 402 | } 403 | } 404 | 405 | double elasty::ShapeMatchingConstraint::calculateValue() 406 | { 407 | throw std::runtime_error("ShapeMatchingConstraint does not directly provide its cost value or the gradient."); 408 | } 409 | 410 | void elasty::ShapeMatchingConstraint::calculateGrad(double* grad_C) 411 | { 412 | throw std::runtime_error("ShapeMatchingConstraint does not directly provide its cost value or the gradient."); 413 | } 414 | 415 | void elasty::ShapeMatchingConstraint::projectParticles(const AlgorithmType type) 416 | { 417 | // Calculate the current center of mass 418 | Eigen::Vector3d x_cm = Eigen::Vector3d::Zero(); 419 | for (int i = 0; i < m_particles.size(); ++i) 420 | { 421 | x_cm += m_particles[i]->m * m_particles[i]->p; 422 | } 423 | x_cm /= m_total_mass; 424 | 425 | // Calculate A_pq 426 | Eigen::Matrix3d A_pq = Eigen::Matrix3d::Zero(); 427 | for (int i = 0; i < m_particles.size(); ++i) 428 | { 429 | A_pq += m_particles[i]->m * (m_particles[i]->p - x_cm) * m_q[i].transpose(); 430 | } 431 | 432 | // Calculate the rotation matrix 433 | const auto ATA = A_pq.transpose() * A_pq; 434 | const auto eigen_solver = Eigen::SelfAdjointEigenSolver(ATA); 435 | const auto S_inv = eigen_solver.operatorInverseSqrt(); 436 | const Eigen::Matrix3d R = A_pq * S_inv; 437 | 438 | assert(R.determinant() > 0); 439 | 440 | // Update the particle positions 441 | for (int i = 0; i < m_particles.size(); ++i) 442 | { 443 | // Calculate the goal position 444 | const Eigen::Vector3d g = R * m_q[i] + x_cm; 445 | 446 | // Move the particle 447 | m_particles[i]->p = m_stiffness * g + (1.0 - m_stiffness) * m_particles[i]->p; 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /src/engine.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | elasty::AbstractEngine::AbstractEngine(const double delta_frame_time, 6 | const unsigned int num_iters, 7 | const unsigned int num_substeps, 8 | const AlgorithmType algorithm_type) 9 | : m_delta_physics_time(delta_frame_time / static_cast(num_substeps)), 10 | m_delta_frame_time(delta_frame_time), 11 | m_num_constraint_iters(num_iters), 12 | m_num_substeps(num_substeps), 13 | m_algorithm_type(algorithm_type) 14 | { 15 | } 16 | 17 | void elasty::AbstractEngine::proceedFrame() 18 | { 19 | for (unsigned int i = 0; i < m_num_substeps; ++i) 20 | { 21 | proceedSubstep(); 22 | } 23 | } 24 | 25 | void elasty::AbstractEngine::addConstraint(std::shared_ptr constraint) 26 | { 27 | m_constraints.push_back(constraint); 28 | } 29 | 30 | void elasty::AbstractEngine::addInstantConstraint(std::shared_ptr constraint) 31 | { 32 | m_instant_constraints.push_back(constraint); 33 | } 34 | 35 | void elasty::AbstractEngine::clearScene() 36 | { 37 | m_particles.clear(); 38 | m_constraints.clear(); 39 | m_instant_constraints.clear(); 40 | } 41 | 42 | void elasty::AbstractEngine::proceedSubstep() 43 | { 44 | // Apply external forces 45 | setExternalForces(); 46 | for (auto& particle : m_particles) 47 | { 48 | particle->v = particle->v + m_delta_physics_time * particle->w * particle->f; 49 | } 50 | 51 | // Calculate predicted positions 52 | for (auto& particle : m_particles) 53 | { 54 | particle->p = particle->x + m_delta_physics_time * particle->v; 55 | } 56 | 57 | // Generate collision constraints 58 | generateCollisionConstraints(); 59 | 60 | // Reset Lagrange multipliers (only necessary for XPBD) 61 | for (auto constraint : m_constraints) 62 | { 63 | constraint->m_lagrange_multiplier = 0.0; 64 | } 65 | 66 | // Solve constraints 67 | for (unsigned int i = 0; i < m_num_constraint_iters; ++i) 68 | { 69 | for (auto constraint : m_constraints) 70 | { 71 | constraint->projectParticles(m_algorithm_type); 72 | } 73 | 74 | for (auto constraint : m_instant_constraints) 75 | { 76 | constraint->projectParticles(m_algorithm_type); 77 | } 78 | } 79 | 80 | // Apply the results 81 | for (auto& particle : m_particles) 82 | { 83 | particle->v = (particle->p - particle->x) * (1.0 / m_delta_physics_time); 84 | particle->x = particle->p; 85 | } 86 | 87 | // Update velocities 88 | updateVelocities(); 89 | 90 | // Clear instant constraints 91 | m_instant_constraints.clear(); 92 | 93 | // Increment the current physics time 94 | m_current_physics_time += m_delta_physics_time; 95 | } 96 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | std::vector elasty::packParticlePositions(const std::vector>& particles) 11 | { 12 | std::vector verts; 13 | verts.reserve(3 * particles.size()); 14 | for (const auto& particle : particles) 15 | { 16 | verts.push_back(particle->x(0)); 17 | verts.push_back(particle->x(1)); 18 | verts.push_back(particle->x(2)); 19 | } 20 | return verts; 21 | } 22 | 23 | void elasty::setRandomVelocities(const std::vector>& particles, const double scale) 24 | { 25 | for (const auto& particle : particles) 26 | { 27 | particle->v = scale * Eigen::Vector3d::Random(); 28 | } 29 | } 30 | 31 | void elasty::generateFixedPointConstraints(const Eigen::Vector3d& search_position, 32 | const Eigen::Vector3d& fixed_position, 33 | const std::vector>& particles, 34 | std::vector>& constraints) 35 | { 36 | constexpr double dummy_delta_time = 1.0 / 60.0; 37 | 38 | for (const auto& particle : particles) 39 | { 40 | if (particle->x.isApprox(search_position)) 41 | { 42 | constraints.push_back( 43 | std::make_shared(particle, 1.0, 0.0, dummy_delta_time, fixed_position)); 44 | } 45 | } 46 | } 47 | 48 | void elasty::exportCurrentClothStateAsObj(const std::string& output_file_path, 49 | const std::shared_ptr cloth_sim_object) 50 | { 51 | const auto& particles = cloth_sim_object->m_particles; 52 | const auto& triangles = cloth_sim_object->m_triangle_list; 53 | const auto& uvs = cloth_sim_object->m_uv_list; 54 | 55 | // Convert to the OBJ format 56 | std::ofstream file(output_file_path); 57 | for (const auto& particle : particles) 58 | { 59 | file << "v " << particle->x(0) << " " << particle->x(1) << " " << particle->x(2) << std::endl; 60 | } 61 | for (unsigned row = 0; row < uvs.rows(); ++row) 62 | { 63 | file << "vt " << uvs.row(row)(0) << " " << uvs.row(row)(1) << std::endl; 64 | } 65 | for (unsigned row = 0; row < triangles.rows(); ++row) 66 | { 67 | file << "f "; 68 | file << triangles.row(row)(0) + 1 << "/" << triangles.row(row)(0) + 1 << " "; 69 | file << triangles.row(row)(1) + 1 << "/" << triangles.row(row)(1) + 1 << " "; 70 | file << triangles.row(row)(2) + 1 << "/" << triangles.row(row)(2) + 1 << std::endl; 71 | } 72 | file.close(); 73 | } 74 | 75 | std::string elasty::generateClothMeshObjData(const double width, 76 | const double height, 77 | const unsigned horizontal_resolution, 78 | const unsigned vertical_resolution) 79 | { 80 | std::vector vertices; 81 | std::vector triangles; 82 | std::vector uvs; 83 | 84 | // Vertices 85 | for (unsigned v_index = 0; v_index <= vertical_resolution; ++v_index) 86 | { 87 | for (unsigned h_index = 0; h_index <= horizontal_resolution; ++h_index) 88 | { 89 | const double u = [&]() 90 | { 91 | if (v_index % 2 == 0 || h_index == 0) 92 | { 93 | return static_cast(h_index) / static_cast(horizontal_resolution); 94 | } 95 | else 96 | { 97 | return (static_cast(h_index) - 0.5) / static_cast(horizontal_resolution); 98 | } 99 | }(); 100 | const double v = static_cast(v_index) / static_cast(vertical_resolution); 101 | 102 | const double x = (u - 0.5) * width; 103 | const double y = (v - 0.5) * height; 104 | 105 | vertices.push_back(Eigen::Vector3d(x, 0.0, y)); 106 | uvs.push_back(Eigen::Vector2d(u, v)); 107 | 108 | // Additional vetex at the even-indexed row 109 | if (v_index % 2 == 1 && h_index == horizontal_resolution) 110 | { 111 | vertices.push_back(Eigen::Vector3d(0.5 * width, 0.0, y)); 112 | uvs.push_back(Eigen::Vector2d(1.0, v)); 113 | } 114 | } 115 | } 116 | 117 | // Triangles 118 | for (unsigned v_index = 0; v_index < vertical_resolution; ++v_index) 119 | { 120 | if (v_index % 2 == 0) 121 | { 122 | const unsigned top_row_begin = (2 * (horizontal_resolution + 1) + 1) * (v_index / 2); 123 | const unsigned bottom_row_begin = top_row_begin + horizontal_resolution + 1; 124 | 125 | for (unsigned h_index = 0; h_index <= horizontal_resolution; ++h_index) 126 | { 127 | if (h_index == 0) 128 | { 129 | triangles.push_back( 130 | Eigen::Vector3i(top_row_begin + h_index, bottom_row_begin + 0, bottom_row_begin + 1)); 131 | } 132 | else 133 | { 134 | triangles.push_back(Eigen::Vector3i( 135 | top_row_begin + h_index, top_row_begin + h_index - 1, bottom_row_begin + h_index)); 136 | triangles.push_back(Eigen::Vector3i( 137 | top_row_begin + h_index, bottom_row_begin + h_index, bottom_row_begin + h_index + 1)); 138 | } 139 | } 140 | } 141 | else 142 | { 143 | const unsigned top_row_begin = 144 | (2 * (horizontal_resolution + 1) + 1) * ((v_index - 1) / 2) + horizontal_resolution + 1; 145 | const unsigned bottom_row_begin = top_row_begin + horizontal_resolution + 2; 146 | 147 | for (unsigned h_index = 0; h_index <= horizontal_resolution; ++h_index) 148 | { 149 | if (h_index == 0) 150 | { 151 | triangles.push_back(Eigen::Vector3i( 152 | top_row_begin + h_index, bottom_row_begin + h_index, top_row_begin + h_index + 1)); 153 | } 154 | else 155 | { 156 | triangles.push_back(Eigen::Vector3i( 157 | top_row_begin + h_index, bottom_row_begin + h_index - 1, bottom_row_begin + h_index)); 158 | triangles.push_back(Eigen::Vector3i( 159 | top_row_begin + h_index, bottom_row_begin + h_index, top_row_begin + h_index + 1)); 160 | } 161 | } 162 | } 163 | } 164 | 165 | // Convert to the OBJ format 166 | std::stringstream sstream; 167 | for (const auto& vertex : vertices) 168 | { 169 | sstream << "v " << vertex(0) << " " << vertex(1) << " " << vertex(2) << std::endl; 170 | } 171 | for (const auto& uv : uvs) 172 | { 173 | sstream << "vt " << uv(0) << " " << uv(1) << std::endl; 174 | } 175 | for (const auto& triangle : triangles) 176 | { 177 | sstream << "f "; 178 | sstream << triangle(0) + 1 << "/" << triangle(0) + 1 << " "; 179 | sstream << triangle(1) + 1 << "/" << triangle(1) + 1 << " "; 180 | sstream << triangle(2) + 1 << "/" << triangle(2) + 1 << std::endl; 181 | } 182 | 183 | return sstream.str(); 184 | } 185 | -------------------------------------------------------------------------------- /tests/constraint-test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using Eigen::Matrix; 8 | using Eigen::Vector3d; 9 | 10 | template 11 | void calculateNumericalDerivative(const std::shared_ptr particles[], 12 | const std::shared_ptr constraint, 13 | double* grad) 14 | { 15 | constexpr double delta = 1e-06; 16 | 17 | for (int i = 0; i < Num; ++i) 18 | { 19 | for (int j = 0; j < 3; ++j) 20 | { 21 | Vector3d eps3d = Vector3d::Zero(); 22 | eps3d(j) = delta; 23 | 24 | const Vector3d orig_pos = particles[i]->p; 25 | 26 | particles[i]->p = orig_pos + eps3d; 27 | const double cost_plus = constraint->calculateValue(); 28 | 29 | particles[i]->p = orig_pos - eps3d; 30 | const double cost_minus = constraint->calculateValue(); 31 | 32 | particles[i]->p = orig_pos; 33 | 34 | grad[i * 3 + j] = (cost_plus - cost_minus) / (2.0 * delta); 35 | } 36 | } 37 | } 38 | 39 | template bool checkZeroSumCondition(double* grad) 40 | { 41 | constexpr double epsilon = 1e-12; 42 | 43 | Vector3d sum = Vector3d::Zero(); 44 | for (int i = 0; i < Num; ++i) 45 | { 46 | sum(0) += grad[i * 3 + 0]; 47 | sum(1) += grad[i * 3 + 1]; 48 | sum(2) += grad[i * 3 + 2]; 49 | } 50 | 51 | return sum.norm() < epsilon; 52 | } 53 | 54 | TEST(ConstraintTest, BendingRestState) 55 | { 56 | constexpr double dt = 1.0 / 60.0; 57 | 58 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 59 | auto p_1 = std::make_shared(Vector3d(0.0, 1.0, 0.0), Vector3d::Zero(), 1.0); 60 | auto p_2 = std::make_shared(Vector3d(-0.5, 0.5, 0.0), Vector3d::Zero(), 1.0); 61 | auto p_3 = std::make_shared(Vector3d(+0.5, 0.5, 0.0), Vector3d::Zero(), 1.0); 62 | 63 | const Vector3d& x_0 = p_0->x; 64 | const Vector3d& x_1 = p_1->x; 65 | const Vector3d& x_2 = p_2->x; 66 | const Vector3d& x_3 = p_3->x; 67 | 68 | const Vector3d p_10 = x_1 - x_0; 69 | const Vector3d p_20 = x_2 - x_0; 70 | const Vector3d p_30 = x_3 - x_0; 71 | 72 | const Vector3d n_0 = p_10.cross(p_20).normalized(); 73 | const Vector3d n_1 = p_10.cross(p_30).normalized(); 74 | 75 | EXPECT_FALSE(n_0.hasNaN()); 76 | EXPECT_FALSE(n_1.hasNaN()); 77 | 78 | const double dihedral_angle = std::acos(std::clamp(n_0.dot(n_1), -1.0, 1.0)); 79 | 80 | EXPECT_FALSE(std::isnan(dihedral_angle)); 81 | 82 | const auto constraint = 83 | std::make_shared(p_0, p_1, p_2, p_3, 1.0, 0.0, dt, dihedral_angle); 84 | 85 | const double value = constraint->calculateValue(); 86 | 87 | Matrix grad; 88 | constraint->calculateGrad(grad.data()); 89 | 90 | constexpr double epsilon = 1e-20; 91 | 92 | EXPECT_TRUE(std::abs(value) < epsilon); 93 | EXPECT_TRUE(grad.norm() < epsilon); 94 | } 95 | 96 | TEST(ConstraintTest, BendingDerivative) 97 | { 98 | constexpr double dt = 1.0 / 60.0; 99 | 100 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 101 | auto p_1 = std::make_shared(Vector3d(0.0, 1.0, 0.0), Vector3d::Zero(), 1.0); 102 | auto p_2 = std::make_shared(Vector3d(-0.5, 0.5, 0.0), Vector3d::Zero(), 1.0); 103 | auto p_3 = std::make_shared(Vector3d(+0.5, 0.5, 0.0), Vector3d::Zero(), 1.0); 104 | 105 | const std::shared_ptr particles[] = {p_0, p_1, p_2, p_3}; 106 | 107 | const Vector3d& x_0 = p_0->x; 108 | const Vector3d& x_1 = p_1->x; 109 | const Vector3d& x_2 = p_2->x; 110 | const Vector3d& x_3 = p_3->x; 111 | 112 | const Vector3d p_10 = x_1 - x_0; 113 | const Vector3d p_20 = x_2 - x_0; 114 | const Vector3d p_30 = x_3 - x_0; 115 | 116 | const Vector3d n_0 = p_10.cross(p_20).normalized(); 117 | const Vector3d n_1 = p_10.cross(p_30).normalized(); 118 | 119 | EXPECT_FALSE(n_0.hasNaN()); 120 | EXPECT_FALSE(n_1.hasNaN()); 121 | 122 | const double dihedral_angle = std::acos(std::clamp(n_0.dot(n_1), -1.0, 1.0)); 123 | 124 | EXPECT_FALSE(std::isnan(dihedral_angle)); 125 | 126 | const auto constraint = 127 | std::make_shared(p_0, p_1, p_2, p_3, 1.0, 0.0, dt, dihedral_angle); 128 | 129 | constexpr double epsilon = 1e-04; 130 | 131 | for (auto particle : particles) 132 | { 133 | particle->p = particle->x + Vector3d::Random(); 134 | } 135 | 136 | Matrix analytic_grad; 137 | constraint->calculateGrad(analytic_grad.data()); 138 | 139 | Matrix numerical_grad; 140 | calculateNumericalDerivative<4>(particles, constraint, numerical_grad.data()); 141 | 142 | EXPECT_TRUE((numerical_grad - analytic_grad).cwiseAbs().maxCoeff() < epsilon); 143 | EXPECT_TRUE(checkZeroSumCondition<4>(analytic_grad.data())); 144 | } 145 | 146 | TEST(ConstraintTest, IsometricBendingDerivative) 147 | { 148 | constexpr double dt = 1.0 / 60.0; 149 | 150 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 151 | auto p_1 = std::make_shared(Vector3d(0.0, 1.0, 0.0), Vector3d::Zero(), 1.0); 152 | auto p_2 = std::make_shared(Vector3d(-0.5, 0.5, 0.0), Vector3d::Zero(), 1.0); 153 | auto p_3 = std::make_shared(Vector3d(+0.5, 0.5, 0.0), Vector3d::Zero(), 1.0); 154 | 155 | const std::shared_ptr particles[] = {p_0, p_1, p_2, p_3}; 156 | 157 | const auto constraint = std::make_shared(p_0, p_1, p_2, p_3, 1.0, 0.0, dt); 158 | 159 | constexpr double epsilon = 1e-04; 160 | 161 | for (auto particle : particles) 162 | { 163 | particle->p = particle->x + Vector3d::Random(); 164 | } 165 | 166 | Matrix analytic_grad; 167 | constraint->calculateGrad(analytic_grad.data()); 168 | 169 | Matrix numerical_grad; 170 | calculateNumericalDerivative<4>(particles, constraint, numerical_grad.data()); 171 | 172 | EXPECT_TRUE((numerical_grad - analytic_grad).cwiseAbs().maxCoeff() < epsilon); 173 | EXPECT_TRUE(checkZeroSumCondition<4>(analytic_grad.data())); 174 | } 175 | 176 | TEST(ConstraintTest, DistanceDegenerated) 177 | { 178 | constexpr double dt = 1.0 / 60.0; 179 | 180 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 181 | auto p_1 = std::make_shared(Vector3d(0.0, 1.0, 0.0), Vector3d::Zero(), 1.0); 182 | 183 | const double dist = (p_0->x - p_1->x).norm(); 184 | 185 | const auto constraint = std::make_shared(p_0, p_1, 1.0, 0.0, dt, dist); 186 | 187 | const std::shared_ptr particles[] = {p_0, p_1}; 188 | 189 | constexpr double epsilon = 1e-04; 190 | 191 | for (auto particle : particles) 192 | { 193 | particle->p = Vector3d::Zero(); 194 | } 195 | 196 | Matrix analytic_grad; 197 | constraint->calculateGrad(analytic_grad.data()); 198 | 199 | EXPECT_TRUE(std::abs(analytic_grad.segment<3>(0 * 3).norm() - 1.0) < epsilon); 200 | EXPECT_TRUE(std::abs(analytic_grad.segment<3>(1 * 3).norm() - 1.0) < epsilon); 201 | EXPECT_TRUE(checkZeroSumCondition<2>(analytic_grad.data())); 202 | } 203 | 204 | TEST(ConstraintTest, DistanceZeroDegenerated) 205 | { 206 | constexpr double dt = 1.0 / 60.0; 207 | 208 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 209 | auto p_1 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 210 | 211 | const double dist = (p_0->x - p_1->x).norm(); 212 | 213 | const auto constraint = std::make_shared(p_0, p_1, 1.0, 0.0, dt, dist); 214 | 215 | const std::shared_ptr particles[] = {p_0, p_1}; 216 | 217 | constexpr double epsilon = 1e-04; 218 | 219 | Matrix analytic_grad; 220 | constraint->calculateGrad(analytic_grad.data()); 221 | 222 | const double value = constraint->calculateValue(); 223 | 224 | EXPECT_TRUE(std::abs(value) < epsilon); 225 | EXPECT_TRUE(std::abs(analytic_grad.segment<3>(0 * 3).norm() - 1.0) < epsilon); 226 | EXPECT_TRUE(std::abs(analytic_grad.segment<3>(1 * 3).norm() - 1.0) < epsilon); 227 | EXPECT_TRUE(checkZeroSumCondition<2>(analytic_grad.data())); 228 | } 229 | 230 | TEST(ConstraintTest, DistanceDerivative) 231 | { 232 | constexpr double dt = 1.0 / 60.0; 233 | 234 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 235 | auto p_1 = std::make_shared(Vector3d(0.0, 1.0, 0.0), Vector3d::Zero(), 1.0); 236 | 237 | const double dist = (p_0->x - p_1->x).norm(); 238 | 239 | const auto constraint = std::make_shared(p_0, p_1, 1.0, 0.0, dt, dist); 240 | 241 | const std::shared_ptr particles[] = {p_0, p_1}; 242 | 243 | constexpr double epsilon = 1e-04; 244 | 245 | for (auto particle : particles) 246 | { 247 | particle->p = particle->x + Vector3d::Random(); 248 | } 249 | 250 | Matrix analytic_grad; 251 | constraint->calculateGrad(analytic_grad.data()); 252 | 253 | Matrix numerical_grad; 254 | calculateNumericalDerivative<2>(particles, constraint, numerical_grad.data()); 255 | 256 | EXPECT_TRUE((numerical_grad - analytic_grad).cwiseAbs().maxCoeff() < epsilon); 257 | } 258 | 259 | TEST(ConstraintTest, ContinuumTriangleDerivative) 260 | { 261 | constexpr double dt = 1.0 / 60.0; 262 | 263 | constexpr double youngs_modulus = 1000.0; 264 | constexpr double poisson_ratio = 0.10; 265 | 266 | auto p_0 = std::make_shared(Vector3d(0.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 267 | auto p_1 = std::make_shared(Vector3d(0.0, 1.0, 0.0), Vector3d::Zero(), 1.0); 268 | auto p_2 = std::make_shared(Vector3d(1.0, 0.0, 0.0), Vector3d::Zero(), 1.0); 269 | 270 | const auto constraint = std::make_shared( 271 | p_0, p_1, p_2, 1.0, 0.0, dt, youngs_modulus, poisson_ratio); 272 | 273 | const std::shared_ptr particles[] = {p_0, p_1, p_2}; 274 | 275 | constexpr double epsilon = 1e-04; 276 | 277 | for (auto particle : particles) 278 | { 279 | particle->p = particle->x + Vector3d::Random(); 280 | } 281 | 282 | Matrix analytic_grad; 283 | constraint->calculateGrad(analytic_grad.data()); 284 | 285 | Matrix numerical_grad; 286 | calculateNumericalDerivative<3>(particles, constraint, numerical_grad.data()); 287 | 288 | EXPECT_TRUE((numerical_grad - analytic_grad).cwiseAbs().maxCoeff() < epsilon); 289 | } 290 | 291 | TEST(ConstraintTest, FixedPointDerivative) 292 | { 293 | constexpr double dt = 1.0 / 60.0; 294 | 295 | auto p_0 = std::make_shared(Vector3d::Random(), Vector3d::Zero(), 1.0); 296 | 297 | const auto constraint = std::make_shared(p_0, 1.0, 1.0, dt, Vector3d::Random()); 298 | 299 | const std::shared_ptr particles[] = {p_0}; 300 | 301 | constexpr double epsilon = 1e-04; 302 | 303 | Matrix analytic_grad; 304 | constraint->calculateGrad(analytic_grad.data()); 305 | 306 | Matrix numerical_grad; 307 | calculateNumericalDerivative<1>(particles, constraint, numerical_grad.data()); 308 | 309 | EXPECT_TRUE((numerical_grad - analytic_grad).cwiseAbs().maxCoeff() < epsilon); 310 | } 311 | 312 | TEST(ConstraintTest, FixedPointRestState) 313 | { 314 | constexpr double dt = 1.0 / 60.0; 315 | 316 | auto p_0 = std::make_shared(Vector3d::Random(), Vector3d::Zero(), 1.0); 317 | 318 | const auto constraint = std::make_shared(p_0, 1.0, 1.0, dt, p_0->x); 319 | 320 | const std::shared_ptr particles[] = {p_0}; 321 | 322 | constexpr double epsilon = 1e-04; 323 | 324 | Matrix analytic_grad; 325 | constraint->calculateGrad(analytic_grad.data()); 326 | 327 | Matrix numerical_grad; 328 | calculateNumericalDerivative<1>(particles, constraint, numerical_grad.data()); 329 | 330 | const double value = constraint->calculateValue(); 331 | 332 | EXPECT_TRUE(std::abs(value) < epsilon); 333 | EXPECT_TRUE(std::abs(analytic_grad.segment<3>(0 * 3).norm() - 1.0) < epsilon); 334 | } 335 | 336 | int main(int argc, char** argv) 337 | { 338 | ::testing::InitGoogleTest(&argc, argv); 339 | 340 | return RUN_ALL_TESTS(); 341 | } 342 | -------------------------------------------------------------------------------- /tests/fem-test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace 6 | { 7 | constexpr double k_youngs_modulus = 800.0; 8 | constexpr double k_poisson_ratio = 0.40; 9 | 10 | constexpr double k_first_lame = elasty::fem::calcFirstLame(k_youngs_modulus, k_poisson_ratio); 11 | constexpr double k_second_lame = elasty::fem::calcSecondLame(k_youngs_modulus, k_poisson_ratio); 12 | } // namespace 13 | 14 | TEST(FemTest, StVenantKirchhoff2d) 15 | { 16 | auto calcPiolaStress = [](const Eigen::Matrix2d& F) -> Eigen::Matrix2d 17 | { 18 | return elasty::fem::calcStVenantKirchhoffPiolaStress(F, k_first_lame, k_second_lame); 19 | }; 20 | auto calcEnergyDensity = [](const Eigen::Matrix2d& F) -> double 21 | { 22 | return elasty::fem::calcStVenantKirchhoffEnergyDensity(F, k_first_lame, k_second_lame); 23 | }; 24 | 25 | Eigen::MatrixXd x_rest{2, 3}; 26 | x_rest.col(0) = Eigen::Vector2d{0.0, 0.0}; 27 | x_rest.col(1) = Eigen::Vector2d{1.0, 0.0}; 28 | x_rest.col(2) = Eigen::Vector2d{0.0, 1.0}; 29 | 30 | Eigen::MatrixXd x{2, 3}; 31 | x.col(0) = Eigen::Vector2d{0.0, 0.5}; 32 | x.col(1) = Eigen::Vector2d{2.0, 0.0}; 33 | x.col(2) = Eigen::Vector2d{0.0, 1.5}; 34 | 35 | // Calculate $\frac{\partial \Psi}{\partial \mathbf{x}}$ analytically 36 | const auto D_m = elasty::fem::calc2dShapeMatrix(x_rest.col(0), x_rest.col(1), x_rest.col(2)); 37 | const auto D_m_inv = D_m.inverse(); 38 | const auto F = elasty::fem::calc2dTriangleDeformGrad(x.col(0), x.col(1), x.col(2), D_m_inv); 39 | const auto P = calcPiolaStress(F); 40 | const auto vec_P = Eigen::Map{P.data(), P.size()}; 41 | const auto vec_PFPx = elasty::fem::calcVecTrianglePartDeformGradPartPos(D_m_inv); 42 | const auto PPsiPx = vec_PFPx.transpose() * vec_P; 43 | 44 | // Calculate $\frac{\partial \Psi}{\partial \mathbf{x}}$ numerically 45 | Eigen::MatrixXd x_temp = x; 46 | Eigen::VectorXd diff{6}; 47 | for (size_t i = 0; i < 3; ++i) 48 | { 49 | for (size_t j = 0; j < 2; ++j) 50 | { 51 | constexpr double eps = 1e-06; 52 | 53 | x_temp(j, i) += eps; 54 | const auto F_p = 55 | elasty::fem::calc2dTriangleDeformGrad(x_temp.col(0), x_temp.col(1), x_temp.col(2), D_m_inv); 56 | const auto e_p = calcEnergyDensity(F_p); 57 | 58 | x_temp = x; 59 | 60 | x_temp(j, i) -= eps; 61 | const auto F_m = 62 | elasty::fem::calc2dTriangleDeformGrad(x_temp.col(0), x_temp.col(1), x_temp.col(2), D_m_inv); 63 | const auto e_m = calcEnergyDensity(F_m); 64 | 65 | x_temp = x; 66 | 67 | diff[i * 2 + j] = (e_p - e_m) / (2.0 * eps); 68 | } 69 | } 70 | 71 | EXPECT_TRUE((PPsiPx - diff).norm() < 1e-04); 72 | } 73 | 74 | TEST(FemTest, CoRotational2d) 75 | { 76 | auto calcPiolaStress = [](const Eigen::Matrix2d& F) -> Eigen::Matrix2d 77 | { 78 | return elasty::fem::calcCoRotationalPiolaStress(F, k_first_lame, k_second_lame); 79 | }; 80 | auto calcEnergyDensity = [](const Eigen::Matrix2d& F) -> double 81 | { 82 | return elasty::fem::calcCoRotationalEnergyDensity(F, k_first_lame, k_second_lame); 83 | }; 84 | 85 | Eigen::MatrixXd x_rest{2, 3}; 86 | x_rest.col(0) = Eigen::Vector2d{0.0, 0.0}; 87 | x_rest.col(1) = Eigen::Vector2d{1.0, 0.0}; 88 | x_rest.col(2) = Eigen::Vector2d{0.0, 1.0}; 89 | 90 | Eigen::MatrixXd x{2, 3}; 91 | x.col(0) = Eigen::Vector2d{0.0, 0.5}; 92 | x.col(1) = Eigen::Vector2d{2.0, 0.0}; 93 | x.col(2) = Eigen::Vector2d{0.0, 1.5}; 94 | 95 | // Calculate $\frac{\partial \Psi}{\partial \mathbf{x}}$ analytically 96 | const auto D_m = elasty::fem::calc2dShapeMatrix(x_rest.col(0), x_rest.col(1), x_rest.col(2)); 97 | const auto D_m_inv = D_m.inverse(); 98 | const auto F = elasty::fem::calc2dTriangleDeformGrad(x.col(0), x.col(1), x.col(2), D_m_inv); 99 | const auto P = calcPiolaStress(F); 100 | const auto vec_P = Eigen::Map{P.data(), P.size()}; 101 | const auto vec_PFPx = elasty::fem::calcVecTrianglePartDeformGradPartPos(D_m_inv); 102 | const auto PPsiPx = vec_PFPx.transpose() * vec_P; 103 | 104 | // Calculate $\frac{\partial \Psi}{\partial \mathbf{x}}$ numerically 105 | Eigen::MatrixXd x_temp = x; 106 | Eigen::VectorXd diff{6}; 107 | for (size_t i = 0; i < 3; ++i) 108 | { 109 | for (size_t j = 0; j < 2; ++j) 110 | { 111 | constexpr double eps = 1e-06; 112 | 113 | x_temp(j, i) += eps; 114 | const auto F_p = 115 | elasty::fem::calc2dTriangleDeformGrad(x_temp.col(0), x_temp.col(1), x_temp.col(2), D_m_inv); 116 | const auto e_p = calcEnergyDensity(F_p); 117 | 118 | x_temp = x; 119 | 120 | x_temp(j, i) -= eps; 121 | const auto F_m = 122 | elasty::fem::calc2dTriangleDeformGrad(x_temp.col(0), x_temp.col(1), x_temp.col(2), D_m_inv); 123 | const auto e_m = calcEnergyDensity(F_m); 124 | 125 | x_temp = x; 126 | 127 | diff[i * 2 + j] = (e_p - e_m) / (2.0 * eps); 128 | } 129 | } 130 | 131 | EXPECT_TRUE((PPsiPx - diff).norm() < 1e-04); 132 | } 133 | 134 | int main(int argc, char** argv) 135 | { 136 | ::testing::InitGoogleTest(&argc, argv); 137 | 138 | return RUN_ALL_TESTS(); 139 | } 140 | --------------------------------------------------------------------------------