├── requirements.txt ├── data └── teaser.jpg ├── README.md ├── LICENSE └── tetris.py /requirements.txt: -------------------------------------------------------------------------------- 1 | taichi 2 | -------------------------------------------------------------------------------- /data/teaser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanming-hu/taichi_tetris/HEAD/data/teaser.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 太极软体俄罗斯方块 (太极图形课作业) Taichi Tetris (Taichi Graphics Course Homework) 2 | 3 | *by [@yuanming-hu](https://github.com/yuanming-hu) and [@k-ye](https://github.com/k-ye)* 4 | 5 | ![teaser](./data/teaser.jpg) 6 | 7 | ## 背景简介 Background 8 | 灵感来自[C4D4U的软体俄罗斯方块系列视频](https://www.youtube.com/watch?v=XAoNeNoa7eM&list=PLUiMrVMtq6VFBeBD1jSkkIfAQL4ocoot5)。 9 | 10 | Ideas are from [C4D4U's soft body tetris videos](https://www.youtube.com/watch?v=XAoNeNoa7eM&list=PLUiMrVMtq6VFBeBD1jSkkIfAQL4ocoot5). 11 | 12 | ## 成功效果展示 Demo 13 | 14 | 15 | 16 | ## 运行方式 Usage 17 | ```bash 18 | python3 tetris.py 19 | ``` 20 | 21 | 用 *鼠标* 控制下落位置,按 *空格键* 释放。 22 | 23 | Use your *mouse* to control the initial horizontal position of the new soft body, and press the *space* key to release it. 24 | 25 | ## 未来工作 TODOs 26 | 27 | 目前功能尚不完善,比如说消除、计分等功能还没有实现。我们俩估计是没时间继续写了,欢迎大家 fork 以后继续开发 :-) 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tetris.py: -------------------------------------------------------------------------------- 1 | # Authors: yuanming-hu and k-ye 2 | 3 | import taichi as ti 4 | import numpy as np 5 | import random 6 | import os 7 | 8 | write_to_disk = True 9 | ti.init(arch=ti.gpu) # Try to run on GPU 10 | 11 | quality = 1 # Use a larger value for higher-res simulations 12 | n_grid = (40 * quality, 80 * quality) 13 | dx, inv_dx = 1 / n_grid[1], n_grid[1] 14 | dt = 1e-4 / quality 15 | p_vol, p_rho = (dx * 0.5)**2, 1 16 | p_mass = p_vol * p_rho 17 | E, nu = 0.15e4, 0.2 # Young's modulus and Poisson's ratio 18 | mu_0, lambda_0 = E / (2 * (1 + nu)), E * nu / ( 19 | (1 + nu) * (1 - 2 * nu)) # Lame parameters 20 | 21 | max_num_particles = 1024 * 16 22 | dim = 2 23 | x = ti.Vector.field(dim, dtype=float) # position 24 | v = ti.Vector.field(dim, dtype=float) # velocity 25 | C = ti.Matrix.field(dim, dim, dtype=float) # affine velocity field 26 | F = ti.Matrix.field(dim, dim, dtype=float) # deformation gradient 27 | material = ti.field(dtype=int) # material id 28 | Jp = ti.field(dtype=float) # plastic deformation 29 | 30 | ti.root.dynamic(ti.i, max_num_particles).place(x, v, C, F, material, Jp) 31 | cur_num_particles = ti.field(ti.i32, shape=()) 32 | 33 | grid_v = ti.Vector.field(dim, dtype=float, 34 | shape=n_grid) # grid node momentum/velocity 35 | grid_m = ti.field(dtype=float, shape=n_grid) # grid node mass 36 | 37 | 38 | @ti.kernel 39 | def substep(): 40 | for i, j in grid_m: 41 | grid_v[i, j] = [0, 0] 42 | grid_m[i, j] = 0 43 | 44 | # p2g 45 | for p in x: 46 | base = (x[p] * inv_dx - 0.5).cast(int) 47 | fx = x[p] * inv_dx - base.cast(float) 48 | # Quadratic kernels [http://mpm.graphics Eqn. 123, with x=fx, fx-1,fx-2] 49 | w = [0.5 * (1.5 - fx)**2, 0.75 - (fx - 1)**2, 0.5 * (fx - 0.5)**2] 50 | F[p] = (ti.Matrix.identity(float, 2) + 51 | dt * C[p]) @ F[p] # deformation gradient update 52 | h = ti.exp( 53 | 10 * 54 | (1.0 - 55 | Jp[p])) # Hardening coefficient: snow gets harder when compressed 56 | if material[p] >= 2: # jelly, make it softer 57 | h = 0.3 58 | mu, la = mu_0 * h, lambda_0 * h 59 | if material[p] == 0: # liquid 60 | mu = 0.0 61 | U, sig, V = ti.svd(F[p]) 62 | J = 1.0 63 | for d in ti.static(range(2)): 64 | new_sig = sig[d, d] 65 | if material[p] == 1: # Snow 66 | new_sig = min(max(sig[d, d], 1 - 2.5e-2), 67 | 1 + 4.5e-3) # Plasticity 68 | Jp[p] *= sig[d, d] / new_sig 69 | sig[d, d] = new_sig 70 | J *= new_sig 71 | if material[ 72 | p] == 0: # Reset deformation gradient to avoid numerical instability 73 | F[p] = ti.Matrix.identity(float, 2) * ti.sqrt(J) 74 | elif material[p] == 1: 75 | F[p] = U @ sig @ V.transpose( 76 | ) # Reconstruct elastic deformation gradient after plasticity 77 | stress = 2 * mu * (F[p] - U @ V.transpose()) @ F[p].transpose( 78 | ) + ti.Matrix.identity(float, 2) * la * J * (J - 1) 79 | stress = (-dt * p_vol * 4 * inv_dx * inv_dx) * stress 80 | affine = stress + p_mass * C[p] 81 | for i, j in ti.static(ti.ndrange( 82 | 3, 3)): # Loop over 3x3 grid node neighborhood 83 | offset = ti.Vector([i, j]) 84 | dpos = (offset.cast(float) - fx) * dx 85 | weight = w[i][0] * w[j][1] 86 | grid_v[base + offset] += weight * (p_mass * v[p] + affine @ dpos) 87 | grid_m[base + offset] += weight * p_mass 88 | 89 | for i, j in grid_m: 90 | if grid_m[i, j] > 0: # No need for epsilon here 91 | grid_v[i, 92 | j] = (1 / grid_m[i, j]) * grid_v[i, 93 | j] # Momentum to velocity 94 | grid_v[i, j][1] -= dt * 50 # gravity 95 | if i < 3 and grid_v[i, j][0] < 0: 96 | grid_v[i, j][0] = 0 # Boundary conditions 97 | if i > n_grid[0] - 3 and grid_v[i, j][0] > 0: grid_v[i, j][0] = 0 98 | if j < 3 and grid_v[i, j][1] < 0: grid_v[i, j] = ti.Vector([0, 0]) 99 | if j > n_grid[1] - 3 and grid_v[i, j][1] > 0: grid_v[i, j][1] = 0 100 | 101 | # g2p 102 | for p in x: 103 | base = (x[p] * inv_dx - 0.5).cast(int) 104 | fx = x[p] * inv_dx - base.cast(float) 105 | w = [0.5 * (1.5 - fx)**2, 0.75 - (fx - 1.0)**2, 0.5 * (fx - 0.5)**2] 106 | new_v = ti.Vector.zero(float, 2) 107 | new_C = ti.Matrix.zero(float, 2, 2) 108 | for i, j in ti.static(ti.ndrange( 109 | 3, 3)): # loop over 3x3 grid node neighborhood 110 | dpos = ti.Vector([i, j]).cast(float) - fx 111 | g_v = grid_v[base + ti.Vector([i, j])] 112 | weight = w[i][0] * w[j][1] 113 | new_v += weight * g_v 114 | new_C += 4 * inv_dx * weight * g_v.outer_product(dpos) 115 | v[p], C[p] = new_v, new_C 116 | x[p] += dt * v[p] # advection 117 | 118 | 119 | num_per_tetromino_square = 128 120 | num_per_tetromino = num_per_tetromino_square * 4 121 | staging_tetromino_x = ti.Vector.field(dim, 122 | dtype=float, 123 | shape=num_per_tetromino) 124 | 125 | 126 | class StagingTetromino(object): 127 | def __init__(self, x): 128 | """ 129 | Stores the info of the staging tetromino. 130 | 131 | Args 132 | x: a taichi field. Note this class just keeps |x| as a reference, and 133 | is not a taichi data_oriented class. 134 | """ 135 | self._x_field = x 136 | self.material_idx = 0 137 | 138 | self._offsets = np.array([ 139 | [[0, -1], [1, 0], [0, -2]], 140 | [[1, 1], [-1, 0], [1, 0]], 141 | [[0, -1], [-1, 0], [0, -2]], 142 | [[0, 1], [1, 0], [1, -1]], 143 | [[1, 0], [2, 0], [-1, 0]], 144 | [[0, 1], [1, 1], [1, 0]], 145 | [[-1, 0], [1, 0], [0, 1]], 146 | ]) 147 | self._x_np_canonical = None 148 | self.left_width = 0 149 | self.right_width = 0 150 | self.lower_height = 0 151 | self.upper_height = 0 152 | 153 | def regenerate(self, mat, kind): 154 | self.material_idx = mat 155 | shape = (num_per_tetromino, dim) 156 | x = np.zeros(shape=shape, dtype=np.float32) 157 | for i in range(1, 4): 158 | # is there a more np-idiomatic way? 159 | begin = i * num_per_tetromino_square 160 | x[begin:(begin + 161 | num_per_tetromino_square)] = self._offsets[kind, (i - 1)] 162 | x += np.random.rand(*shape) 163 | scaling = 0.05 164 | x *= scaling 165 | self._x_np_canonical = x 166 | 167 | self.left_width= scaling * abs(min(self._offsets[kind, :][:, 0])) 168 | self.right_width= scaling * abs(max(self._offsets[kind, :][:, 0]) + 1) 169 | self.lower_height = scaling * abs(min(self._offsets[kind, :][:, 1])) 170 | self.upper_height = scaling * abs(max(self._offsets[kind, :][:, 1]) + 1) 171 | 172 | 173 | def update_center(self, center): 174 | self._x_field.from_numpy(np.clip(self._x_np_canonical + center, 0, 1)) 175 | 176 | def rotate(self): 177 | theta = np.radians(90) 178 | c, s = np.cos(theta), np.sin(theta) 179 | m = np.array([[c, -s], [s, c]], dtype=np.float32) 180 | x = m @ self._x_np_canonical.T 181 | self._x_np_canonical = x.T 182 | 183 | self.right_width, self.lower_height, self.left_width, self.upper_height = \ 184 | self.lower_height, self.left_width, self.upper_height, self.right_width 185 | 186 | def compute_center(self, mouse, l_bound, r_bound): 187 | r = staging_tetromino.right_width 188 | l = staging_tetromino.left_width 189 | 190 | if mouse[0] + r > r_bound: 191 | x = r_bound - r 192 | elif mouse[0] - l < l_bound: 193 | x = l_bound + l 194 | else: 195 | x = mouse[0] 196 | 197 | return np.array([x, 0.8], dtype=np.float32) 198 | 199 | staging_tetromino = StagingTetromino(staging_tetromino_x) 200 | 201 | 202 | @ti.kernel 203 | def drop_staging_tetromino(mat: int): 204 | base = cur_num_particles[None] 205 | for i in staging_tetromino_x: 206 | bi = base + i 207 | x[bi] = staging_tetromino_x[i] 208 | material[bi] = mat 209 | v[bi] = ti.Matrix([0, -2]) 210 | F[bi] = ti.Matrix([[1, 0], [0, 1]]) 211 | Jp[bi] = 1 212 | cur_num_particles[None] += num_per_tetromino 213 | 214 | 215 | def main(): 216 | os.makedirs('frames', exist_ok=True) 217 | gui = ti.GUI("Taichi MLS-MPM-99", 218 | res=(384, 768), 219 | background_color=0x112F41) 220 | 221 | def gen_mat_and_kind(): 222 | material_id = random.randint(0, 7) 223 | return material_id, random.randint(0, 6) 224 | 225 | staging_tetromino.regenerate(*gen_mat_and_kind()) 226 | 227 | last_action_frame = -1e10 228 | for f in range(100000): 229 | padding = 0.025 230 | segments = 20 231 | step = (1 - padding * 4) / (segments - 0.5) / 2 232 | for i in range(segments): 233 | gui.line(begin=(padding * 2 + step * 2 * i, 0.8), 234 | end=(padding * 2 + step * (2 * i + 1), 0.8), 235 | radius=1.5, 236 | color=0xFF8811) 237 | gui.line(begin=(padding * 2, padding), 238 | end=(1 - padding * 2, padding), 239 | radius=2) 240 | gui.line(begin=(padding * 2, 1 - padding), 241 | end=(1 - padding * 2, 1 - padding), 242 | radius=2) 243 | gui.line(begin=(padding * 2, padding), 244 | end=(padding * 2, 1 - padding), 245 | radius=2) 246 | gui.line(begin=(1 - padding * 2, padding), 247 | end=(1 - padding * 2, 1 - padding), 248 | radius=2) 249 | 250 | if gui.get_event(ti.GUI.PRESS): 251 | ev_key = gui.event.key 252 | if ev_key in [ti.GUI.ESCAPE, ti.GUI.EXIT]: break 253 | elif ev_key == ti.GUI.SPACE: 254 | if cur_num_particles[ 255 | None] + num_per_tetromino < max_num_particles: 256 | drop_staging_tetromino(staging_tetromino.material_idx) 257 | print('# particles =', cur_num_particles[None]) 258 | staging_tetromino.regenerate(*gen_mat_and_kind()) 259 | last_action_frame = f 260 | elif ev_key == 'r': 261 | staging_tetromino.rotate() 262 | mouse = gui.get_cursor_pos() 263 | mouse = (mouse[0] * 0.5, mouse[1]) 264 | 265 | right_bound = 0.5 - padding 266 | left_bound = padding 267 | 268 | staging_tetromino.update_center(staging_tetromino.compute_center(mouse, left_bound, right_bound)) 269 | 270 | for s in range(int(2e-3 // dt)): 271 | substep() 272 | colors = np.array([ 273 | 0xA6B5F7, 0xEEEEF0, 0xED553B, 0x3255A7, 0x6D35CB, 0xFE2E44, 274 | 0x26A5A7, 0xEDE53B 275 | ], 276 | dtype=np.uint32) 277 | particle_radius = 2.3 278 | gui.circles(x.to_numpy() * [[2, 1]], 279 | radius=particle_radius, 280 | color=colors[material.to_numpy()]) 281 | 282 | if last_action_frame + 40 < f: 283 | gui.circles(staging_tetromino_x.to_numpy() * [[2, 1]], 284 | radius=particle_radius, 285 | color=int(colors[staging_tetromino.material_idx])) 286 | 287 | if staging_tetromino.material_idx == 0: 288 | mat_text = 'Liquid' 289 | elif staging_tetromino.material_idx == 1: 290 | mat_text = 'Snow' 291 | else: 292 | mat_text = 'Jelly' 293 | gui.text(mat_text, (0.42, 0.97), 294 | font_size=30, 295 | color=colors[staging_tetromino.material_idx]) 296 | gui.text('Taichi Tetris', (0.07, 0.97), font_size=20) 297 | 298 | 299 | if write_to_disk: 300 | gui.show(f'frames/{f:05d}.png') 301 | else: 302 | gui.show() 303 | 304 | 305 | if __name__ == '__main__': 306 | main() 307 | --------------------------------------------------------------------------------