├── .gitignore ├── LICENSE ├── README.md ├── data ├── direct_vs_cg.gif ├── direct_vs_cg.mp4 ├── fem.gif ├── fem.mp4 ├── mass-spring.gif └── mass-spring.mp4 ├── fem-explicit.py ├── implicit_mass_spring_system.py ├── main.py ├── mass-spring-explicit.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Pycharm 2 | .idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TaichiCourse 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 | # 太极图形课S1-显式弹性物体仿真 2 | 3 | ## 背景简介 4 | 本文基于显式时间积分和弹簧质点系统以及线性有限元系统,实现了弹性悬臂梁的仿真。 5 | 基于隐式时间积分和弹簧质点系统,实现了布料的仿真,参考文件[1](https://www.cs.cmu.edu/~baraff/papers/sig98.pdf)。 6 | ## 成功效果展示 7 | 8 | ### FEM demo 9 | ![fem demo](./data/fem.gif) 10 | 11 | 12 | ### Implicit mass spring demo 13 | 左侧:Direct solver;右侧: Conjugate Gradient (CG) solver。 14 | ![Mass spring demo](./data/direct_vs_cg.gif) 15 | 16 | ## 运行环境 17 | ``` 18 | [Taichi] version 0.8.3, llvm 10.0.0, commit 021af5d2, win, python 3.8.10 19 | ``` 20 | 21 | ## 运行方式 22 | 在安装了taichi的情况下,可以直接运行: 23 | ``` 24 | python3 [*].py 25 | ``` 26 | 27 | 在运行 `implicit_mass_spring_system.py`时,可以通过命令行参数 `-cg` 来控制是否使用 **CG** solver。 28 | ``` 29 | # 使用Direct solver 30 | python implicit_mass_spring_system.py 31 | 32 | # 使用CG solver 33 | python implicit_mass_spring_system.py -cg 34 | ``` -------------------------------------------------------------------------------- /data/direct_vs_cg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taichiCourse01/--Deformables/0c895bc8f4dae11aecd54a460106b1a82d01356f/data/direct_vs_cg.gif -------------------------------------------------------------------------------- /data/direct_vs_cg.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taichiCourse01/--Deformables/0c895bc8f4dae11aecd54a460106b1a82d01356f/data/direct_vs_cg.mp4 -------------------------------------------------------------------------------- /data/fem.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taichiCourse01/--Deformables/0c895bc8f4dae11aecd54a460106b1a82d01356f/data/fem.gif -------------------------------------------------------------------------------- /data/fem.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taichiCourse01/--Deformables/0c895bc8f4dae11aecd54a460106b1a82d01356f/data/fem.mp4 -------------------------------------------------------------------------------- /data/mass-spring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taichiCourse01/--Deformables/0c895bc8f4dae11aecd54a460106b1a82d01356f/data/mass-spring.gif -------------------------------------------------------------------------------- /data/mass-spring.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taichiCourse01/--Deformables/0c895bc8f4dae11aecd54a460106b1a82d01356f/data/mass-spring.mp4 -------------------------------------------------------------------------------- /fem-explicit.py: -------------------------------------------------------------------------------- 1 | import taichi as ti 2 | import math 3 | 4 | ti.init(arch=ti.cpu) 5 | 6 | # global control 7 | paused = True 8 | damping_toggle = ti.field(ti.i32, ()) 9 | curser = ti.Vector.field(2, ti.f32, ()) 10 | picking = ti.field(ti.i32,()) 11 | using_auto_diff = False 12 | 13 | # procedurally setting up the cantilever 14 | init_x, init_y = 0.1, 0.6 15 | N_x = 20 16 | N_y = 4 17 | # N_x = 2 18 | # N_y = 2 19 | N = N_x*N_y 20 | N_edges = (N_x-1)*N_y + N_x*(N_y - 1) + (N_x-1) * \ 21 | (N_y-1) # horizontal + vertical + diagonal springs 22 | N_triangles = 2 * (N_x-1) * (N_y-1) 23 | dx = 1/32 24 | curser_radius = dx/2 25 | 26 | # physical quantities 27 | m = 1 28 | g = 9.8 29 | YoungsModulus = ti.field(ti.f32, ()) 30 | PoissonsRatio = ti.field(ti.f32, ()) 31 | LameMu = ti.field(ti.f32, ()) 32 | LameLa = ti.field(ti.f32, ()) 33 | 34 | # time-step size (for simulation, 16.7ms) 35 | h = 16.7e-3 36 | # substepping 37 | substepping = 100 38 | # time-step size (for time integration) 39 | dh = h/substepping 40 | 41 | # simulation components 42 | x = ti.Vector.field(2, ti.f32, N, needs_grad=True) 43 | v = ti.Vector.field(2, ti.f32, N) 44 | total_energy = ti.field(ti.f32, (), needs_grad=True) 45 | grad = ti.Vector.field(2, ti.f32, N) 46 | elements_Dm_inv = ti.Matrix.field(2, 2, ti.f32, N_triangles) 47 | elements_V0 = ti.field(ti.f32, N_triangles) 48 | 49 | # geometric components 50 | triangles = ti.Vector.field(3, ti.i32, N_triangles) 51 | edges = ti.Vector.field(2, ti.i32, N_edges) 52 | 53 | 54 | def ij_2_index(i, j): return i * N_y + j 55 | 56 | 57 | # -----------------------meshing and init---------------------------- 58 | @ti.kernel 59 | def meshing(): 60 | # setting up triangles 61 | for i,j in ti.ndrange(N_x - 1, N_y - 1): 62 | # triangle id 63 | tid = (i * (N_y - 1) + j) * 2 64 | triangles[tid][0] = ij_2_index(i, j) 65 | triangles[tid][1] = ij_2_index(i + 1, j) 66 | triangles[tid][2] = ij_2_index(i, j + 1) 67 | 68 | tid = (i * (N_y - 1) + j) * 2 + 1 69 | triangles[tid][0] = ij_2_index(i, j + 1) 70 | triangles[tid][1] = ij_2_index(i + 1, j + 1) 71 | triangles[tid][2] = ij_2_index(i + 1, j) 72 | 73 | # setting up edges 74 | # edge id 75 | eid_base = 0 76 | 77 | # horizontal edges 78 | for i in range(N_x-1): 79 | for j in range(N_y): 80 | eid = eid_base+i*N_y+j 81 | edges[eid] = [ij_2_index(i, j), ij_2_index(i+1, j)] 82 | 83 | eid_base += (N_x-1)*N_y 84 | # vertical edges 85 | for i in range(N_x): 86 | for j in range(N_y-1): 87 | eid = eid_base+i*(N_y-1)+j 88 | edges[eid] = [ij_2_index(i, j), ij_2_index(i, j+1)] 89 | 90 | eid_base += N_x*(N_y-1) 91 | # diagonal edges 92 | for i in range(N_x-1): 93 | for j in range(N_y-1): 94 | eid = eid_base+i*(N_y-1)+j 95 | edges[eid] = [ij_2_index(i+1, j), ij_2_index(i, j+1)] 96 | 97 | @ti.kernel 98 | def initialize(): 99 | YoungsModulus[None] = 1e6 100 | paused = True 101 | # init position and velocity 102 | for i, j in ti.ndrange(N_x, N_y): 103 | index = ij_2_index(i, j) 104 | x[index] = ti.Vector([init_x + i * dx, init_y + j * dx]) 105 | v[index] = ti.Vector([0.0, 0.0]) 106 | 107 | @ti.func 108 | def compute_D(i): 109 | a = triangles[i][0] 110 | b = triangles[i][1] 111 | c = triangles[i][2] 112 | return ti.Matrix.cols([x[b] - x[a], x[c] - x[a]]) 113 | 114 | @ti.kernel 115 | def initialize_elements(): 116 | for i in range(N_triangles): 117 | Dm = compute_D(i) 118 | elements_Dm_inv[i] = Dm.inverse() 119 | elements_V0[i] = ti.abs(Dm.determinant())/2 120 | 121 | # ----------------------core----------------------------- 122 | @ti.func 123 | def compute_R_2D(F): 124 | R, S = ti.polar_decompose(F, ti.f32) 125 | return R 126 | 127 | @ti.kernel 128 | def compute_gradient(): 129 | # clear gradient 130 | for i in grad: 131 | grad[i] = ti.Vector([0, 0]) 132 | 133 | # gradient of elastic potential 134 | for i in range(N_triangles): 135 | Ds = compute_D(i) 136 | F = Ds@elements_Dm_inv[i] 137 | # co-rotated linear elasticity 138 | R = compute_R_2D(F) 139 | Eye = ti.Matrix.cols([[1.0, 0.0], [0.0, 1.0]]) 140 | # first Piola-Kirchhoff tensor 141 | P = 2*LameMu[None]*(F-R) + LameLa[None]*((R.transpose())@F-Eye).trace()*R 142 | #assemble to gradient 143 | H = elements_V0[i] * P @ (elements_Dm_inv[i].transpose()) 144 | a,b,c = triangles[i][0],triangles[i][1],triangles[i][2] 145 | gb = ti.Vector([H[0,0], H[1, 0]]) 146 | gc = ti.Vector([H[0,1], H[1, 1]]) 147 | ga = -gb-gc 148 | grad[a] += ga 149 | grad[b] += gb 150 | grad[c] += gc 151 | 152 | @ti.kernel 153 | def compute_total_energy(): 154 | for i in range(N_triangles): 155 | Ds = compute_D(i) 156 | F = Ds @ elements_Dm_inv[i] 157 | # co-rotated linear elasticity 158 | R = compute_R_2D(F) 159 | Eye = ti.Matrix.cols([[1.0, 0.0], [0.0, 1.0]]) 160 | element_energy_density = LameMu[None]*((F-R)@(F-R).transpose()).trace() + 0.5*LameLa[None]*(R.transpose()@F-Eye).trace()**2 161 | 162 | total_energy[None] += element_energy_density * elements_V0[i] 163 | 164 | @ti.kernel 165 | def update(): 166 | # perform time integration 167 | for i in range(N): 168 | # symplectic integration 169 | # elastic force + gravitation force, divding mass to get the acceleration 170 | if using_auto_diff: 171 | acc = -x.grad[i]/m - ti.Vector([0.0, g]) 172 | v[i] += dh*acc 173 | else: 174 | acc = -grad[i]/m - ti.Vector([0.0, g]) 175 | v[i] += dh*acc 176 | x[i] += dh*v[i] 177 | 178 | # explicit damping (ether drag) 179 | for i in v: 180 | if damping_toggle[None]: 181 | v[i] *= ti.exp(-dh*5) 182 | 183 | # enforce boundary condition 184 | for i in range(N): 185 | if picking[None]: 186 | r = x[i]-curser[None] 187 | if r.norm() < curser_radius: 188 | x[i] = curser[None] 189 | v[i] = ti.Vector([0.0, 0.0]) 190 | pass 191 | 192 | for j in range(N_y): 193 | ind = ij_2_index(0, j) 194 | v[ind] = ti.Vector([0, 0]) 195 | x[ind] = ti.Vector([init_x, init_y + j * dx]) # rest pose attached to the wall 196 | 197 | for i in range(N): 198 | if x[i][0] < init_x: 199 | x[i][0] = init_x 200 | v[i][0] = 0 201 | 202 | 203 | @ti.kernel 204 | def updateLameCoeff(): 205 | E = YoungsModulus[None] 206 | nu = PoissonsRatio[None] 207 | LameLa[None] = E*nu / ((1+nu)*(1-2*nu)) 208 | LameMu[None] = E / (2*(1+nu)) 209 | 210 | # init once and for all 211 | meshing() 212 | initialize() 213 | initialize_elements() 214 | updateLameCoeff() 215 | 216 | gui = ti.GUI('Linear FEM', (800, 800)) 217 | while gui.running: 218 | 219 | picking[None]=0 220 | 221 | # key events 222 | for e in gui.get_events(ti.GUI.PRESS): 223 | if e.key in [ti.GUI.ESCAPE, ti.GUI.EXIT]: 224 | exit() 225 | elif e.key == 'r': 226 | initialize() 227 | elif e.key == '0': 228 | YoungsModulus[None] *= 1.1 229 | elif e.key == '9': 230 | YoungsModulus[None] /= 1.1 231 | if YoungsModulus[None] <= 0: 232 | YoungsModulus[None] = 0 233 | elif e.key == '8': 234 | PoissonsRatio[None] = PoissonsRatio[None]*0.9+0.05 # slowly converge to 0.5 235 | if PoissonsRatio[None] >= 0.499: 236 | PoissonsRatio[None] = 0.499 237 | elif e.key == '7': 238 | PoissonsRatio[None] = PoissonsRatio[None]*1.1-0.05 239 | if PoissonsRatio[None] <= 0: 240 | PoissonsRatio[None] = 0 241 | elif e.key == ti.GUI.SPACE: 242 | paused = not paused 243 | elif e.key =='d' or e.key == 'D': 244 | damping_toggle[None] = not damping_toggle[None] 245 | elif e.key =='p' or e.key == 'P': # step-forward 246 | for i in range(substepping): 247 | if using_auto_diff: 248 | total_energy[None]=0 249 | with ti.Tape(total_energy): 250 | compute_total_energy() 251 | else: 252 | compute_gradient() 253 | update() 254 | updateLameCoeff() 255 | 256 | if gui.is_pressed(ti.GUI.LMB): 257 | curser[None] = gui.get_cursor_pos() 258 | picking[None] = 1 259 | 260 | # numerical time integration 261 | if not paused: 262 | for i in range(substepping): 263 | if using_auto_diff: 264 | total_energy[None]=0 265 | with ti.Tape(total_energy): 266 | compute_total_energy() 267 | else: 268 | compute_gradient() 269 | update() 270 | 271 | # render 272 | pos = x.to_numpy() 273 | for i in range(N_edges): 274 | a, b = edges[i][0], edges[i][1] 275 | gui.line((pos[a][0], pos[a][1]), 276 | (pos[b][0], pos[b][1]), 277 | radius=1, 278 | color=0xFFFF00) 279 | gui.line((init_x, 0.0), (init_x, 1.0), color=0xFFFFFF, radius=4) 280 | 281 | if picking[None]: 282 | gui.circle((curser[None][0], curser[None][1]), radius=curser_radius*800, color=0xFF8888) 283 | 284 | # text 285 | gui.text( 286 | content=f'9/0: (-/+) Young\'s Modulus {YoungsModulus[None]:.1f}', pos=(0.6, 0.9), color=0xFFFFFF) 287 | gui.text( 288 | content=f'7/8: (-/+) Poisson\'s Ratio {PoissonsRatio[None]:.3f}', pos=(0.6, 0.875), color=0xFFFFFF) 289 | if damping_toggle[None]: 290 | gui.text( 291 | content='D: Damping On', pos=(0.6, 0.85), color=0xFFFFFF) 292 | else: 293 | gui.text( 294 | content='D: Damping Off', pos=(0.6, 0.85), color=0xFFFFFF) 295 | gui.show() -------------------------------------------------------------------------------- /implicit_mass_spring_system.py: -------------------------------------------------------------------------------- 1 | # https://www.cs.cmu.edu/~baraff/papers/sig98.pdf 2 | import argparse 3 | 4 | import numpy as np 5 | 6 | import taichi as ti 7 | 8 | import time 9 | 10 | 11 | @ti.data_oriented 12 | class Cloth: 13 | def __init__(self, N): 14 | self.N = N 15 | self.NF = 2 * N**2 # number of faces 16 | self.NV = (N + 1)**2 # number of vertices 17 | self.NE = 2 * N * (N + 1) + 2 * N * N # numbser of edges 18 | self.initPos = ti.Vector.field(2, ti.f32, self.NV) 19 | self.pos = ti.Vector.field(2, ti.f32, self.NV) 20 | self.vel = ti.Vector.field(2, ti.f32, self.NV) 21 | self.force = ti.Vector.field(2, ti.f32, self.NV) 22 | self.mass = ti.field(ti.f32, self.NV) 23 | self.spring = ti.Vector.field(2, ti.i32, self.NE) 24 | self.rest_len = ti.field(ti.f32, self.NE) 25 | self.ks = 1000.0 # spring stiffness 26 | self.kf = 1.0e5 # Attachment point stiffness 27 | self.Jx = ti.Matrix.field(2, 2, ti.f32, self.NE) # Force Jacobian 28 | self.Jf = ti.Matrix.field(2, 2, ti.f32, 2) # Attachment Jacobian 29 | 30 | self.init_pos() 31 | self.init_edges() 32 | 33 | # For sparse matrix solver, PPT: P45 34 | max_num_triplets = 10000 35 | self.MBuilder = ti.linalg.SparseMatrixBuilder(2 * self.NV, 2 * self.NV, 36 | max_num_triplets) 37 | self.init_mass_sp(self.MBuilder) 38 | self.M = self.MBuilder.build() 39 | self.KBuilder = ti.linalg.SparseMatrixBuilder(2 * self.NV, 2 * self.NV, 40 | max_num_triplets) 41 | 42 | # For conjugate gradient method, PPT: P106 43 | self.x = ti.Vector.field(2, ti.f32, self.NV) 44 | self.Ax = ti.Vector.field(2, ti.f32, self.NV) 45 | self.b = ti.Vector.field(2, ti.f32, self.NV) 46 | self.r = ti.Vector.field(2, ti.f32, self.NV) 47 | self.d = ti.Vector.field(2, ti.f32, self.NV) 48 | self.Ad = ti.Vector.field(2, ti.f32, self.NV) 49 | 50 | @ti.kernel 51 | def init_pos(self): 52 | for i, j in ti.ndrange(self.N + 1, self.N + 1): 53 | k = i * (self.N + 1) + j 54 | self.initPos[k] = ti.Vector([i, j]) / self.N * 0.5 + ti.Vector( 55 | [0.25, 0.25]) 56 | self.pos[k] = self.initPos[k] 57 | self.vel[k] = ti.Vector([0, 0]) 58 | self.mass[k] = 1.0 59 | 60 | @ti.kernel 61 | def init_edges(self): 62 | pos, spring, N, rest_len = ti.static(self.pos, self.spring, self.N, 63 | self.rest_len) 64 | for i, j in ti.ndrange(N + 1, N): 65 | idx, idx1 = i * N + j, i * (N + 1) + j 66 | spring[idx] = ti.Vector([idx1, idx1 + 1]) 67 | start = N * (N + 1) 68 | for i, j in ti.ndrange(N, N + 1): 69 | idx, idx1, idx2 = start + i + j * N, i * (N + 1) + j, i * ( 70 | N + 1) + j + N + 1 71 | spring[idx] = ti.Vector([idx1, idx2]) 72 | start = 2 * N * (N + 1) 73 | for i, j in ti.ndrange(N, N): 74 | idx, idx1, idx2 = start + i * N + j, i * (N + 1) + j, (i + 1) * ( 75 | N + 1) + j + 1 76 | spring[idx] = ti.Vector([idx1, idx2]) 77 | start = 2 * N * (N + 1) + N * N 78 | for i, j in ti.ndrange(N, N): 79 | idx, idx1, idx2 = start + i * N + j, i * (N + 1) + j + 1, ( 80 | i + 1) * (N + 1) + j 81 | spring[idx] = ti.Vector([idx1, idx2]) 82 | for i in range(self.NE): 83 | idx1, idx2 = spring[i] 84 | rest_len[i] = (pos[idx1] - pos[idx2]).norm() 85 | 86 | @ti.kernel 87 | def init_mass_sp(self, M: ti.linalg.sparse_matrix_builder()): 88 | for i in range(self.NV): 89 | M[2 * i + 0, 2 * i + 0] += self.mass[i] 90 | M[2 * i + 1, 2 * i + 1] += self.mass[i] 91 | 92 | @ti.func 93 | def clear_force(self): 94 | for i in self.force: 95 | self.force[i] = ti.Vector([0.0, 0.0]) 96 | 97 | @ti.kernel 98 | def compute_force(self): 99 | self.clear_force() 100 | gravity = ti.Vector([0.0, -2.0]) 101 | for i in self.force: 102 | self.force[i] += gravity * self.mass[i] 103 | 104 | for i in self.spring: 105 | idx1, idx2 = self.spring[i][0], self.spring[i][1] 106 | pos1, pos2 = self.pos[idx1], self.pos[idx2] 107 | dis = pos1 - pos2 108 | # Hook's law 109 | force = self.ks * (dis.norm() - 110 | self.rest_len[i]) * dis.normalized() 111 | self.force[idx1] -= force 112 | self.force[idx2] += force 113 | # Attachment constraint force 114 | self.force[self.N] += self.kf * (self.initPos[self.N] - 115 | self.pos[self.N]) 116 | self.force[self.NV - 1] += self.kf * (self.initPos[self.NV - 1] - 117 | self.pos[self.NV - 1]) 118 | 119 | @ti.kernel 120 | def compute_force_Jacobians(self): 121 | for i in self.spring: 122 | idx1, idx2 = self.spring[i][0], self.spring[i][1] 123 | pos1, pos2 = self.pos[idx1], self.pos[idx2] 124 | dx = pos1 - pos2 125 | I = ti.Matrix([[1.0, 0.0], [0.0, 1.0]]) 126 | dxtdx = ti.Matrix([[dx[0] * dx[0], dx[0] * dx[1]], 127 | [dx[1] * dx[0], dx[1] * dx[1]]]) 128 | l = dx.norm() 129 | if l != 0.0: 130 | l = 1.0 / l 131 | self.Jx[i] = (I - self.rest_len[i] * l * 132 | (I - dxtdx * l**2)) * self.ks 133 | # Attachment constraint force Jacobian 134 | self.Jf[0] = ti.Matrix([[-self.kf, 0], [0, -self.kf]]) 135 | self.Jf[1] = ti.Matrix([[-self.kf, 0], [0, -self.kf]]) 136 | 137 | @ti.kernel 138 | def assemble_K(self, K: ti.linalg.sparse_matrix_builder()): 139 | for i in self.spring: 140 | idx1, idx2 = self.spring[i][0], self.spring[i][1] 141 | for m, n in ti.static(ti.ndrange(2, 2)): 142 | K[2 * idx1 + m, 2 * idx1 + n] -= self.Jx[i][m, n] 143 | K[2 * idx1 + m, 2 * idx2 + n] += self.Jx[i][m, n] 144 | K[2 * idx2 + m, 2 * idx1 + n] += self.Jx[i][m, n] 145 | K[2 * idx2 + m, 2 * idx2 + n] -= self.Jx[i][m, n] 146 | for m, n in ti.static(ti.ndrange(2, 2)): 147 | K[2 * self.N + m, 2 * self.N + n] += self.Jf[0][m, n] 148 | K[2 * (self.NV - 1) + m, 2 * (self.NV - 1) + n] += self.Jf[1][m, n] 149 | 150 | @ti.kernel 151 | def directUpdatePosVel(self, h: ti.f32, v_next: ti.ext_arr()): 152 | for i in self.pos: 153 | self.vel[i] = ti.Vector([v_next[2 * i], v_next[2 * i + 1]]) 154 | self.pos[i] += h * self.vel[i] 155 | 156 | def update_direct(self, h): 157 | self.compute_force() 158 | self.compute_force_Jacobians() 159 | # Assemble global system 160 | self.assemble_K(self.KBuilder) 161 | K = self.KBuilder.build() 162 | A = self.M - h**2 * K 163 | solver = ti.linalg.SparseSolver(solver_type="LLT") 164 | solver.analyze_pattern(A) 165 | solver.factorize(A) 166 | 167 | vel = self.vel.to_numpy().reshape(2 * self.NV) 168 | force = self.force.to_numpy().reshape(2 * self.NV) 169 | b = h * force + self.M @ vel 170 | 171 | v_next = solver.solve(b) 172 | # flag = solver.info() 173 | # print("solver flag: ", flag) 174 | self.directUpdatePosVel(h, v_next) 175 | 176 | @ti.kernel 177 | def cgUpdatePosVel(self, h: ti.f32): 178 | for i in self.pos: 179 | self.vel[i] = self.x[i] 180 | self.pos[i] += h * self.vel[i] 181 | 182 | @ti.kernel 183 | def compute_RHS(self, h: ti.f32): 184 | #rhs = b = h * force + M @ v 185 | for i in range(self.NV): 186 | self.b[i] = h * self.force[i] + self.mass[i] * self.vel[i] 187 | 188 | @ti.func 189 | def dot(self, v1, v2): 190 | result = 0.0 191 | for i in range(self.NV): 192 | result += v1[i][0] * v2[i][0] 193 | result += v1[i][1] * v2[i][1] 194 | return result 195 | 196 | @ti.func 197 | def A_mult_x(self, h, dst, src): 198 | coeff = -h**2 199 | for i in range(self.NV): 200 | dst[i] = self.mass[i] * src[i] 201 | for i in range(self.NE): 202 | idx1, idx2 = self.spring[i][0], self.spring[i][1] 203 | temp = self.Jx[i] @ (src[idx1] - src[idx2]) 204 | dst[idx1] -= coeff * temp 205 | dst[idx2] += coeff * temp 206 | # Attachment constraint 207 | Attachment1, Attachment2 = self.N, self.NV - 1 208 | dst[Attachment1] -= coeff * self.kf * src[Attachment1] 209 | dst[Attachment2] -= coeff * self.kf * src[Attachment2] 210 | 211 | # conjugate gradient solving 212 | # https://www.cs.cmu.edu/~quake-papers/painless-conjugate-gradient.pdf 213 | 214 | @ti.kernel 215 | def before_ite(self) -> ti.f32: 216 | for i in range(self.NV): 217 | self.x[i] = ti.Vector([0.0, 0.0]) 218 | self.A_mult_x(h, self.Ax, self.x) # Ax = A @ x 219 | for i in range(self.NV): # r = b - A @ x 220 | self.r[i] = self.b[i] - self.Ax[i] 221 | for i in range(self.NV): # d = r 222 | self.d[i] = self.r[i] 223 | delta_new = self.dot(self.r, self.r) 224 | return delta_new 225 | 226 | @ti.kernel 227 | def run_iteration(self, delta_new: ti.f32) -> ti.f32: 228 | self.A_mult_x(h, self.Ad, self.d) # Ad = A @ d 229 | alpha = delta_new / self.dot(self.d, 230 | self.Ad) # alpha = (r^T * r) / dot(d, Ad) 231 | for i in range(self.NV): 232 | self.x[i] += alpha * self.d[i] # x^{i+1} = x^{i} + alpha * d 233 | self.r[i] -= alpha * self.Ad[i] # r^{i+1} = r^{i} + alpha * Ad 234 | delta_old = delta_new 235 | delta_new = self.dot(self.r, self.r) 236 | beta = delta_new / delta_old 237 | for i in range(self.NV): 238 | self.d[i] = self.r[i] + beta * self.d[ 239 | i] #p^{i+1} = r^{i+1} + beta * p^{i} 240 | return delta_new 241 | 242 | def cg(self, h: ti.f32): 243 | delta_new = self.before_ite() 244 | ite, iteMax = 0, 2 * self.NV 245 | while ite < iteMax and delta_new > 1.0e-6: 246 | delta_new = self.run_iteration(delta_new) 247 | ite += 1 248 | 249 | def update_cg(self, h): 250 | self.compute_force() 251 | self.compute_force_Jacobians() 252 | self.compute_RHS(h) 253 | self.cg(h) 254 | self.cgUpdatePosVel(h) 255 | 256 | def display(self, gui, radius=5, color=0xffffff): 257 | springs, pos = self.spring.to_numpy(), self.pos.to_numpy() 258 | line_Begin = np.zeros(shape=(springs.shape[0], 2)) 259 | line_End = np.zeros(shape=(springs.shape[0], 2)) 260 | for i in range(springs.shape[0]): 261 | idx1, idx2 = springs[i][0], springs[i][1] 262 | line_Begin[i], line_End[i] = pos[idx1], pos[idx2] 263 | gui.lines(line_Begin, line_End, radius=2, color=0x0000ff) 264 | gui.circles(self.pos.to_numpy(), radius, color) 265 | 266 | 267 | if __name__ == "__main__": 268 | ti.init(arch=ti.cpu) 269 | cloth = Cloth(N=5) 270 | parser = argparse.ArgumentParser() 271 | parser.add_argument('-cg', 272 | '--use_cg', 273 | action='store_true', 274 | help='Solve Ax=b with conjugate gradient method (CG).') 275 | args, unknowns = parser.parse_known_args() 276 | use_cg = args.use_cg 277 | 278 | gui = ti.GUI('Implicit Mass Spring System', res=(500, 500)) 279 | pause = False 280 | h, max_step = 0.01, 3 281 | while gui.running: 282 | for e in gui.get_events(): 283 | if e.key == gui.ESCAPE: 284 | gui.running = False 285 | elif e.key == gui.SPACE: 286 | pause = not pause 287 | if not pause: 288 | for i in range(max_step): 289 | if use_cg: 290 | cloth.update_cg(h) 291 | else: 292 | cloth.update_direct(h) 293 | cloth.display(gui) 294 | gui.show() 295 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import taichi as ti 2 | 3 | ti.init(arch=ti.gpu) 4 | 5 | n = 320 6 | pixels = ti.field(dtype=float, shape=(n * 2, n)) 7 | 8 | @ti.func 9 | def complex_sqr(z): 10 | return ti.Vector([z[0]**2 - z[1]**2, z[1] * z[0] * 2]) 11 | 12 | @ti.kernel 13 | def paint(t: float): 14 | for i, j in pixels: # Parallelized over all pixels 15 | c = ti.Vector([-0.8, ti.cos(t) * 0.2]) 16 | z = ti.Vector([i / n - 1, j / n - 0.5]) * 2 17 | iterations = 0 18 | while z.norm() < 20 and iterations < 50: 19 | z = complex_sqr(z) + c 20 | iterations += 1 21 | pixels[i, j] = 1 - iterations * 0.02 22 | 23 | gui = ti.GUI("Julia Set", res=(n * 2, n)) 24 | 25 | for i in range(1000000): 26 | paint(i * 0.03) 27 | gui.set_image(pixels) 28 | gui.show() 29 | -------------------------------------------------------------------------------- /mass-spring-explicit.py: -------------------------------------------------------------------------------- 1 | import taichi as ti 2 | import math 3 | 4 | ti.init(arch=ti.cpu) 5 | 6 | # global control 7 | paused = True 8 | damping_toggle = ti.field(ti.i32, ()) 9 | curser = ti.Vector.field(2, ti.f32, ()) 10 | picking = ti.field(ti.i32,()) 11 | 12 | # integration method 13 | # 1: explicit euler 14 | # 2: symplectic euler 15 | # 3: implicit euler (you bet) 16 | integration = 2 17 | 18 | # procedurally setting up the cantilever 19 | init_x, init_y = 0.1, 0.6 20 | N_x = 20 21 | N_y = 4 22 | # N_x = 2 23 | # N_y = 2 24 | N = N_x*N_y 25 | N_edges = (N_x-1)*N_y + N_x*(N_y - 1) + (N_x-1) * \ 26 | (N_y-1) # horizontal + vertical + diagonal springs 27 | N_triangles = 2 * (N_x-1) * (N_y-1) 28 | dx = 1/32 29 | curser_radius = dx/2 30 | 31 | # physical quantities 32 | m = 1 33 | g = 9.8 34 | YoungsModulus = ti.field(ti.f32, ()) 35 | 36 | # time-step size (for simulation, 16.7ms) 37 | h = 16.7e-3 38 | # substepping 39 | substepping = 100 40 | # time-step size (for time integration) 41 | dh = h/substepping 42 | 43 | # simulation components 44 | x = ti.Vector.field(2, ti.f32, N) 45 | v = ti.Vector.field(2, ti.f32, N) 46 | grad = ti.Vector.field(2, ti.f32, N) 47 | spring_length = ti.field(ti.f32, N_edges) 48 | 49 | # geometric components 50 | triangles = ti.Vector.field(3, ti.i32, N_triangles) 51 | edges = ti.Vector.field(2, ti.i32, N_edges) 52 | 53 | def ij_2_index(i, j): return i * N_y + j 54 | 55 | # -----------------------meshing and init---------------------------- 56 | @ti.kernel 57 | def meshing(): 58 | # setting up triangles 59 | for i,j in ti.ndrange(N_x - 1, N_y - 1): 60 | # triangle id 61 | tid = (i * (N_y - 1) + j) * 2 62 | triangles[tid][0] = ij_2_index(i, j) 63 | triangles[tid][1] = ij_2_index(i + 1, j) 64 | triangles[tid][2] = ij_2_index(i, j + 1) 65 | 66 | tid = (i * (N_y - 1) + j) * 2 + 1 67 | triangles[tid][0] = ij_2_index(i, j + 1) 68 | triangles[tid][1] = ij_2_index(i + 1, j + 1) 69 | triangles[tid][2] = ij_2_index(i + 1, j) 70 | 71 | # setting up edges 72 | # edge id 73 | eid_base = 0 74 | 75 | # horizontal edges 76 | for i in range(N_x-1): 77 | for j in range(N_y): 78 | eid = eid_base+i*N_y+j 79 | edges[eid] = [ij_2_index(i, j), ij_2_index(i+1, j)] 80 | 81 | eid_base += (N_x-1)*N_y 82 | # vertical edges 83 | for i in range(N_x): 84 | for j in range(N_y-1): 85 | eid = eid_base+i*(N_y-1)+j 86 | edges[eid] = [ij_2_index(i, j), ij_2_index(i, j+1)] 87 | 88 | eid_base += N_x*(N_y-1) 89 | # diagonal edges 90 | for i in range(N_x-1): 91 | for j in range(N_y-1): 92 | eid = eid_base+i*(N_y-1)+j 93 | edges[eid] = [ij_2_index(i+1, j), ij_2_index(i, j+1)] 94 | 95 | 96 | @ti.kernel 97 | def initialize(): 98 | YoungsModulus[None] = 3e4 99 | paused = True 100 | # init position and velocity 101 | for i, j in ti.ndrange(N_x, N_y): 102 | index = ij_2_index(i, j) 103 | x[index] = ti.Vector([init_x + i * dx, init_y + j * dx]) 104 | v[index] = ti.Vector([0.0, 0.0]) 105 | 106 | @ti.kernel 107 | def initialize_springs(): 108 | # init spring rest-length 109 | for i in range(N_edges): 110 | a, b = edges[i][0], edges[i][1] 111 | r = x[a]-x[b] 112 | spring_length[i] = r.norm() 113 | 114 | # ----------------------core----------------------------- 115 | @ti.kernel 116 | def compute_gradient(): 117 | # clear gradient 118 | for i in grad: 119 | grad[i] = ti.Vector([0, 0]) 120 | 121 | # gradient of elastic potential 122 | for i in range(N_edges): 123 | a, b = edges[i][0], edges[i][1] 124 | r = x[a]-x[b] 125 | l = r.norm() 126 | l0 = spring_length[i] 127 | k = YoungsModulus[None]/l0 # stiffness in Hooke's law 128 | gradient = k*(l-l0)*r/l 129 | grad[a] += gradient 130 | grad[b] += -gradient 131 | 132 | @ti.kernel 133 | def update(): 134 | # perform time integration 135 | for i in range(N): 136 | if integration == 1: 137 | # explicit euler integration 138 | x[i] += dh*v[i] 139 | # elastic force + gravitation force, divding mass to get the acceleration 140 | acc = -grad[i]/m - ti.Vector([0.0, g]) 141 | v[i] += dh*acc 142 | elif integration == 2: 143 | # symplectic integration 144 | # elastic force + gravitation force, divding mass to get the acceleration 145 | acc = -grad[i]/m - ti.Vector([0.0, g]) 146 | v[i] += dh*acc 147 | x[i] += dh*v[i] 148 | 149 | # explicit damping (ether drag) 150 | for i in v: 151 | if damping_toggle[None]: 152 | v[i] *= ti.exp(-dh*5) 153 | 154 | # enforce boundary condition 155 | for i in range(N): 156 | if picking[None]: 157 | r = x[i]-curser[None] 158 | if r.norm() < curser_radius: 159 | x[i] = curser[None] 160 | v[i] = ti.Vector([0.0, 0.0]) 161 | pass 162 | 163 | for j in range(N_y): 164 | ind = ij_2_index(0, j) 165 | v[ind] = ti.Vector([0, 0]) 166 | x[ind] = ti.Vector([init_x, init_y + j * dx]) # rest pose attached to the wall 167 | 168 | for i in range(N): 169 | if x[i][0] < init_x: 170 | x[i][0] = init_x 171 | v[i][0] = 0 172 | 173 | 174 | # init once and for all 175 | meshing() 176 | initialize() 177 | initialize_springs() 178 | 179 | gui = ti.GUI('mass-spring system', (800, 800)) 180 | while gui.running: 181 | 182 | picking[None]=0 183 | 184 | # key events 185 | for e in gui.get_events(ti.GUI.PRESS): 186 | if e.key in [ti.GUI.ESCAPE, ti.GUI.EXIT]: 187 | exit() 188 | elif e.key == 'r': 189 | initialize() 190 | elif e.key == '0': 191 | YoungsModulus[None] *= 1.1 192 | elif e.key == '9': 193 | YoungsModulus[None] /= 1.1 194 | elif e.key == ti.GUI.SPACE: 195 | paused = not paused 196 | elif e.key =='d' or e.key == 'D': 197 | damping_toggle[None] = not damping_toggle[None] 198 | elif e.key == 'p' or e.key == 'P': 199 | for i in range(substepping): 200 | compute_gradient() 201 | update() 202 | 203 | if gui.is_pressed(ti.GUI.LMB): 204 | curser[None] = gui.get_cursor_pos() 205 | picking[None] = 1 206 | 207 | # numerical time integration 208 | if not paused: 209 | for i in range(substepping): 210 | compute_gradient() 211 | update() 212 | 213 | # render 214 | pos = x.to_numpy() 215 | for i in range(N_edges): 216 | a, b = edges[i][0], edges[i][1] 217 | gui.line((pos[a][0], pos[a][1]), 218 | (pos[b][0], pos[b][1]), 219 | radius=1, 220 | color=0xFFFF00) 221 | gui.line((init_x, 0.0), (init_x, 1.0), color=0xFFFFFF, radius=4) 222 | 223 | if picking[None]: 224 | gui.circle((curser[None][0], curser[None][1]), radius=curser_radius*800, color=0xFF8888) 225 | 226 | # text 227 | gui.text( 228 | content=f'9/0: (-/+) Young\'s Modulus {YoungsModulus[None]:.1f}', pos=(0.6, 0.9), color=0xFFFFFF) 229 | if damping_toggle[None]: 230 | gui.text( 231 | content='D: Damping On', pos=(0.6, 0.875), color=0xFFFFFF) 232 | else: 233 | gui.text( 234 | content='D: Damping Off', pos=(0.6, 0.875), color=0xFFFFFF) 235 | gui.show() 236 | 237 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | taichi 2 | --------------------------------------------------------------------------------