#! https://zhuanlan.zhihu.com/p/339879519

论文阅读:Position Based Dynamics

本文为对Position Based Dynamics的论文阅读。本文并不会涵盖论文的所有内容,同时也会做相当多的补充,因此内容上会有很大的差异,但所介绍、使用的方法都是原论文中的内容。

下文中略称Position Based Dynamics为PBD。

一、PBD的含义与出发点

Position based是相对于传统的force based而言的。一般进行物理模拟时,都是对t时刻的物体进行受力分析,然后根据牛顿第二定律F=amF=am求出t时刻的加速度a,再根据加速度a进行时间积分,由当前时刻的系统状态(速度、位移)求出下一时刻的系统状态:

v=v+tt+Δtadtx=x+tt+Δtvdt \begin{aligned} v' &= v+\int_t^{t+\Delta t}adt \\ x' &= x+\int_t^{t+\Delta t}vdt \end{aligned}

因为a通常是根据时间发生变化的,所以积分tt+Δtadt\int_t^{t+\Delta t}adt并不好求,一般都只能得到在时间区间[t,t+Δt][t, t+\Delta t]中的几个点上的a,所以通常都是看成常微分方程,然后使用一些数值方法来计算。

v=v+tt+Δtadtv'=v+\int_t^{t+\Delta t}adt其实是根据dvdt=a\frac{dv}{dt} = a得到的,这也就是个常微分方程。例如用欧拉法求解的话就是vn+1=vn+Δtanv_{n+1}=v_n+\Delta ta_{n},本质就是把上面的那个积分近似成tt+ΔtadtatΔt\int_t^{t+\Delta t}adt \approx a|_t\Delta t

Force based方法因为不直接操纵位移,所以很容易出现overshooting问题。

通常来说,时间步长Δt\Delta t越小,方法越数值稳定,也就越不容易出现overshooting问题。但是Δt\Delta t越小,为了经过相同的时间跨度所需要的迭代次数就越多,计算量也就越大。Δt\Delta t缩小10倍,那迭代次数就得增大10倍。

所以在步长不能足够小时,通常使用隐式方法来避免overshooting问题,因为隐式方法相对而言比较容易数值稳定。不过隐式方法的计算量通常都大得多,所以force based方法的overshooting问题并不是完全解决的,通常都需要在数值稳定性和计算复杂度之间做个取舍,以求用尽量快的速度获得尽量好的效果。

Position based方法则是抛掉加速度,而直接处理位移x。很多力都可以表示为当前系统中质点位移的函数(例如保守力),例如磁铁的排斥力、弹簧的拉力。

PBD的优势在于:

二、算法

Position Based Dynamics在每个时间步的位移更新大致可以分成两步:

  1. 没化成位移约束的力的作用
  2. 约束投影(constraint projection)

可以想象成把力分为了两个批次,先后对物体产生作用。显然因为现实中力的作用是同时的,所以这种近似必定会带来一定的误差。具体这个误差是多少,我暂时没想明白咋算。

第一个批次的计算在此不做讨论,原论文中也仅仅只是使用显式欧拉法vn+1=vn+Δtanv_{n+1}=v_{n}+\Delta t a_n来简单得到的。

第二个批次的关键点在于如何计算位移变化量,本节剩下的篇幅都将讨论这一问题。

1. 约束投影

(1). 形式化定义

目标是计算位移变化量Δx\Delta x,使得x+Δxx+\Delta x对每个约束都成立。

首先要做的是将约束形式化定义。原论文中将其分为两类:

原论文中处理inequality约束的方法是首先检查当前是否已经满足该约束,如果满足的话那就跳过,否则将其转换成equality约束进行处理(也就是不等号变等号)。

如此处理inequality约束后,就只剩下equality约束了,而每个equality约束实际上就是一个关于Δx\Delta x的方程,所有的equality约束就构成一个关于Δx\Delta x的方程组。想要得到Δx\Delta x满足所有约束,就是要求解这个方程组。

形式化定义一下。设有nn个质点,第i个质点的原位移为xix_i,并设x=[x1,x2,...,xn]Tx=[x_1, x_2, ..., x_n]^T。设有m个约束函数(包括由inequality约束转换得到的),第j个约束函数为Cj(x)C_j(x)。所要求的就是某个Δx=[Δx1 Δx2 ... Δxn]T\Delta x = [\Delta x_1 \ \Delta x_2 \ ... \ \Delta x_n]^T,使得对j=1,2,...,mj=1,2,...,m,都有Cj(x+Δx)=0C_j(x+\Delta x)=0。也就是求解如下关于Δx\Delta x的方程组:

{C1(x+Δx)=0C2(x+Δx)=0...Cm(x+Δx)=0\left\{\begin{aligned} C_1(x+\Delta x) &= 0 \\ C_2(x+\Delta x) &= 0 \\ ... \\ C_m(x+\Delta x) &= 0 \\ \end{aligned}\right.

(2). 方程组性质分析

Cj(x)C_j(x)通常都是一个非线性函数,例如就连最简单的距离约束函数C(x1,x2)=x1x2lC(x_1, x_2)=||x_1-x_2||-l都是非线性的,所以该方程组通常也都是非线性的。

而另一方面,如果假设在三维空间的话,要求Δx\Delta x就意味着有3n3n个变量要求,而方程数量mm并不一定等于3n3n,这就意味着该方程组在一般情况下要么欠定(m<3nm<3n,解不唯一),要么过定(m>3nm>3n,很可能无解)。

(3). n元方程组化为m个一元方程

原文中为了求解这一方程组,使用了他称之为“Gauss-Seidel-type iteration”的方法,我并没有找到该词的出处,推测是作者自己造的。这个词的含义是引用了求解线性方程组的Gauss-Seidel方法的不可并行性。求解线性方程组的Gauss-Seidel方法因为迭代计算时第n+1n+1个变量的迭代需要在第nn个变量迭代完才能进行,所以每次迭代都只能串行地逐个迭代每个变量,而不能并行地同时迭代所有变量。“Gauss-Seidel-type”指的就是每次迭代都串行计算的概念。

这里它的串行是“逐个约束”的串行,也就是按照C1=0,C2=0,...,Cm=0C_1=0, C_2=0, ..., C_m=0的顺序,逐个求解Δx\Delta x。每个约束处理完,就根据求解的Δx\Delta x更新xx,然后再处理下一个约束。显然这一方法的话,只逐个处理一次很容易会处理完最后一个以后破坏第一个约束,所以作者将这一过程迭代多次。

这里存在的一个问题是,求解每个约束Cj(x+Δx)=0C_j(x+\Delta x)=0时,因为有3n3n个变量,而方程仅仅只有一个,所以有无穷多解。需要增加另外的约束才能求解。

我是觉得这个方程组的求解比较像最优化问题。在欠定的情况下,找到一个尽可能好的解(使某个奖励函数的值尽可能大)。在过定的情况下,找到一个尽可能不差的解(使某个惩罚函数的值尽可能小)。而这个最优化的目标就构成了上面提到的“另外的约束”。

不过原文没有使用最优化的思路,而是直接指定了Δx\Delta x的方向,这样对每个约束计算Δx\Delta x的时候仅仅只需要求Δx\Delta x的大小,这一个变量,就行了。

Δx\Delta x的方向,原文是基于“动量、角动量守恒”这一出发点指定的。“动量、角动量守恒”是指系统在质点们的位移为xx时具有的动量、角动量,应与位移为x+Δxx+\Delta x时具有的相同。为什么应该相同呢?

接下来的问题是如何找到这样一个不改变动量、角动量的方向?注意这里Δx\Delta x的方向指的并非单纯的质点的“位移方向”,还包括了不同质点之间的位移距离的比例。事实上,如果假设所有质点的质量都为1的话,整个系统的质心位移就是xc=1ni=1nxix_c=\frac{1}{n}\sum_{i=1}^n x_i,位移变化量就是Δxc=1ni=1nΔxi\Delta x_c = \frac{1}{n}\sum_{i=1}^n \Delta x_i,也就是Δx\Delta x的所有分量之和除以n。显然,Δx\Delta x描述了系统质心位移的变化量Δxc\Delta x_c

原文的思路其实就是找一个方向,在这个方向上的Δx\Delta x都会使Δxc=0\Delta x_c=0,也就是系统质心位移不变,那么动量与角动量当然也不变(速度可以看成是当前时刻位移与上一时刻位移的差值除以时间步长,所以当前时刻位移不变的话,速度当然也不变)。

问题就变成怎么找一个方向使系统质心位移不变。

这里得回过头去看一下约束函数Cj(x)C_j(x),不难发现,当这一约束函数描述的力产生作用时,其函数值会发生改变。例如,距离约束C(x1,x2)=x1x2lC(x_1, x_2)=||x_1-x_2||-l描述的是棒子的弹力,当棒子把两个小球拉近/弹远一点的时候,函数值就变小了点。

所以约束函数Cj(x)C_j(x)的值的变化可以看成是内力的作用。而内力的作用是不会改变系统质心位移的,设ΔCj(x)=Cj(x+Δx)Cj(x)\Delta C_j(x) = C_j(x+\Delta x)-C_j(x),若物体只做刚体运动(平移、旋转)(此时ΔCj(x)\Delta C_j(x)一定是内力作用的结果),则有如下命题成立:

ΔCj(x)0Δxc=0 \Delta C_j(x)\neq 0 \Rightarrow \Delta x_c=0

所以问题就变成了怎么找一个方向使得约束函数Cj(x)C_j(x)的值一定随xx的改变发生改变。这个问题很简单,沿梯度方向Cj=Cjx\nabla C_j = \frac{\partial C_j}{\partial x}基本是肯定会改变函数值的。原论文就采用了这一方向。

结果就是,取xx处的Cj\nabla C_j作为Δx\Delta x的方向,即设Δx=λCj(x)\Delta x = \lambda \nabla C_j(x),其中λR\lambda \in R

(4). 具体求解

回过头去看一下想要求解的方程组:

{C1(x+Δx)=0C2(x+Δx)=0...Cm(x+Δx)=0\left\{\begin{aligned} C_1(x+\Delta x) &= 0 \\ C_2(x+\Delta x) &= 0 \\ ... \\ C_m(x+\Delta x) &= 0 \\ \end{aligned}\right.

根据(3)部分的论述,求解的思路就是对每个约束C(x+Δx)=0C(x+\Delta x)=0,使Δx=λC(x)\Delta x=\lambda \nabla C(x),则得到mm个关于λ\lambda的一元方程C(x+λC(x))=0C(x+\lambda \nabla C(x))=0

使x0x_0表示进行约束投影前的质点位移,设xj=xj1+λjCj(xj1)x_j=x_{j-1}+\lambda_j \nabla C_j(x_{j-1}),其中λj\lambda_j为关于λj\lambda_j的一元方程Cj(xj1+λjCj(xj1))=0C_j(x_{j-1}+\lambda_j \nabla C_j(x_{j-1}))=0的解。则xmx_m就是经过一次约束投影后得到的质点位移。至此,就得到了一个完整的约束投影的算法,逐个求解xjx_j即可。

不过一元方程C(x+λC(x))=0C(x+\lambda \nabla C(x))=0通常是个非线性方程,挺难求解的。原文中为了降低计算量,用泰勒展开进行了线性化的近似处理:

C(x+λC(x))C(x)+λC(x)C(x)=0 C(x+\lambda \nabla C(x)) \approx C(x)+\lambda\nabla C(x) \cdot \nabla C(x) = 0

解出λ\lambda

λ=C(x)C(x)2 \lambda = - \frac{C(x)}{||\nabla C(x)||^2}

则对应的Δx\Delta x就是:

Δx=C(x)C(x)2C(x) \Delta x = - \frac{C(x)}{||\nabla C(x)||^2} \nabla C(x)

(5). 关于质量

注意到上面我们在确保系统质心位移不变时,曾假定所有质点的质量都为1,当时是为了方便后面叙述,下面考虑质点质量不同的情况。设质点ii的质量为mim_i,并为了方便而设wi=1miw_i=\frac{1}{m_i}。为了方便叙述,设Δx\Delta x为质点质量都为1的时候求出来的位移变化量,而Δx\Delta x'为质点质量不相同的时候求出来的位移变化量。由前述(1)~(4)的方法我们可以求出Δx\Delta x,现在想要求Δx\Delta x'

Δx\Delta x'也依然需要满足

  1. C(x+Δx)=0C(x+\Delta x')=0
  2. Δxc=1mii=1nmiΔxi=0\Delta x_c' = \frac{1}{\sum m_i}\sum_{i=1}^n m_i \Delta x_i' = 0

为了满足条件2,使系统质心位移不变,假设Δxi=kwiΔxi\Delta x_i' = kw_i\Delta x_i,其中k为待定系数,则Δxc=1mii=1nmikwiΔxi=kmii=1nΔxi\Delta x_c' = \frac{1}{\sum m_i}\sum_{i=1}^n m_i k w_i \Delta x_i = \frac{k}{\sum m_i}\sum_{i=1}^n \Delta x_i,因为Δx\Delta x是质量都为1时候求出来的位移变化量,所以i=1nxi=0\sum_{i=1}^n x_i=0,故Δxc=0\Delta x_c'=0

然后为了满足条件1,将其线性化,得到:

C(x+Δx)C(x)+ΔxC(x)=C(x)+ki=1nwiΔxixiC(x)\begin{aligned} C(x+\Delta x') &\approx C(x)+\Delta x' \cdot \nabla C(x) \\ &= C(x) + k\sum_{i=1}^nw_i\Delta x_i \cdot \nabla_{x_i}C(x) \end{aligned}

而因为C(x+Δx)=0C(x+\Delta x)=0,所以将其线性化后可以得到:

C(x+Δx)C(x)+ΔxC(x)=0C(x)=i=1nΔxixiC(x)\begin{aligned} C(x+\Delta x) \approx& C(x) + \Delta x \cdot \nabla C(x) = 0 \\ \Rightarrow & C(x) = - \sum_{i=1}^n \Delta x_i \cdot \nabla_{x_i}C(x) \end{aligned}

将上两式代入C(x+Δx)=0C(x+\Delta x')=0得到:

C(x+Δx)=i=1nΔxixiC(x)+ki=1nwiΔxixiC(x)=0k=i=1nΔxixiC(x)i=1nwiΔxixiC(x)\begin{aligned} C(x+\Delta x') &= - \sum_{i=1}^n \Delta x_i \cdot \nabla_{x_i}C(x) + k\sum_{i=1}^nw_i\Delta x_i \cdot \nabla_{x_i}C(x) = 0 \\ &\Rightarrow k = \frac{\sum_{i=1}^n \Delta x_i \cdot \nabla_{x_i}C(x)}{\sum_{i=1}^nw_i\Delta x_i \cdot \nabla_{x_i}C(x)} \end{aligned}

注意到ΔxixiC(x)=C(x)C(x)2xiC(x)2\Delta x_i \cdot \nabla_{x_i}C(x) = -\frac{C(x)}{||\nabla C(x)||^2} || \nabla_{x_i} C(x) ||^2

则有:

k=i=1nxiC(x)2i=1nwixiC(x)2 k = \frac{\sum_{i=1}^n ||\nabla_{x_i}C(x)||^2}{\sum_{i=1}^nw_i||\nabla_{x_i}C(x)||^2}

接下来只是推测。

我推测应该存在某个条件能够估计出xiC(x)21||\nabla_{x_i}C(x)||^2 \approx 1,因为代入C(x1,x2)=x1x2lC(x_1, x_2) = ||x_1-x_2|| - l,得到C=[x1x2x1x2 x2x1x1x2]T\nabla C = [\frac{x_1-x_2}{||x_1-x_2||}\ \frac{x_2-x_1}{||x_1-x_2||}]^T,显然满足xiC(x)2=1||\nabla_{x_i}C(x)||^2 = 1

假设xiC(x)2=1||\nabla_{x_i}C(x)||^2 = 1,就能得到:

k=ni=1nwi k = \frac{n}{\sum_{i=1}^n w_i}

由此得到最终的位移变化量:

Δxi=nwij=1nwjΔxi=nwij=1nwjC(x)C(x)2xiC(x)\begin{aligned} \Delta x'_i &= \frac{n w_i}{\sum_{j=1}^n w_j} \Delta x_i \\ &= - \frac{n w_i}{\sum_{j=1}^n w_j} \frac{C(x)}{||\nabla C(x)||^2} \nabla_{x_i} C(x) \end{aligned}

三、刚度系数、碰撞检测、阻尼

TODO 讲讲这些和算法核心无关的内容

呃,这章大概不写了,感觉我也写不出来啥和原论文有差别的地方,我也解读不出来更多东西。

四、Demo

原文里是拿个布料模拟来做demo,那个太NB了,我自己写的简单很多很多。

模拟的东西很简单,就是五个质点x0,x1,x2,x3,x4x_0, x_1, x_2, x_3, x_4,相邻两个之间有长度约束xi+1xi=l||x_{i+1}-x_{i}=l||

先忽略x0,x1x_0, x_1之间的长度约束,它俩比较特别。

用PBD的思路做的话,就是有这么一组约束函数:Ci(x)=xi+1xili=1,2,3C_i(x)=||x_{i+1}-x_i||-l\quad i=1,2,3

第k次迭代按照如下步骤:

  1. 更新加速度:因为非约束力的外力只有重力,而重力带来的加速度固定是重力加速度gg,所以每个质点的加速度都是ak=ga^k=g
  2. 预计算速度:根据加速度预计算只考虑了非约束力外力的速度v'^k=v^{k-1}+a^k\Delta t。
  3. 预计算位移:根据速度预计算只考虑非约束力外力的位移x'^k=x^{k-1}+\frac{(v^{k-1}+v'^k)\Delta t}{2}。
  4. 投影约束:迭代若干轮(我迭代了两轮),对每个约束计算其对应的位移修正量。设每个约束函数修正前为xx,修正量为Δx\Delta x,约束为Cj(x)=xj+1xjl=0C_j(x)=||x_{j+1}-x_j||-l=0。由前文一堆论述可知:
  5. 更新速度:计算vk=2Δt(xkxk1)vk1v^k = \frac{2}{\Delta t}(x^k-x^{k-1})-v^{k-1}

最后提一下关于x0x_0的处理,在原论文里这块就叫attachment,因为质点0是直接被我提着跑的,它不参与物理计算。所以x0x_0在每次迭代的时候都是被指定的,而非被物理计算出来的。因此包含x0x_0的约束C0(x)=x1x0l=0C_0(x)=||x_1-x_0||-l=0导出的Δx\Delta x虽然包含对x0x_0的更新,但却会被无效化。所以我们希望投影约束过程中能自动地让Δx0\Delta x_0为0。为了实现这一目的,原论文中是将质点0的质量设为无限大,也就是使质量的倒数w0=0w_0=0。这确实是很好的策略。我自己耍着玩,用了另一种策略。

我在处理约束C0C_0时,不使用C0\nabla C_0作为Δx\Delta x的方向,而是直接指定[0 x1x0x1x0 0...0][0 \ \frac{x_1-x_0}{||x_1-x_0||} \ 0 ... 0]为其方向。因为Δx\Delta x关于这个方向是线性关系,所以Δx0\Delta x_0就等于0了。这么做的话Δx\Delta x不会保证动量守恒,不过其实假设质点0质量无穷大的时候,由质点0和质点1构成的这个系统的动量也已经无穷大了,所以这块儿守不守恒我感觉问题不大。就结果来看也确实没啥问题。

最后taichi的代码附在附录二中。

附录一、四球模拟

import taichi as ti
from random import random
from tqdm import tqdm
from math import sin, pi, sqrt

################
# Support Functions
################

################
# Init taichi
################
ti.init(arch=ti.gpu)

################
# Input data
################
x1 = [-sqrt(3)/2, -1/2]
x2 = [sqrt(3)/2, -1/2]
x3 = [0, 1]
x = [0, 0]
v = [random()*2-1, random()*2-1]
gravityConstant = 0.5
timestep = 0.01


################
# Taichi Tensors
################


################
# Kernels
################


################
# Time Loop Functions
################
def calGravity(x, y):
    # From x to y
    d = [y[0]-x[0], y[1]-x[1]]
    k = d[0]*d[0] + d[1]*d[1]
    k = pow(k, 1.5)
    k = gravityConstant / k
    return [k*d[0], k*d[1]]

def TimeTick(t):
    a_list = [
        calGravity(x, x1),
        calGravity(x, x2),
        calGravity(x, x3),
    ]
    a = [0, 0]
    for sa in a_list:
        a[0] += sa[0]
        a[1] += sa[1]
    
    # update
    x[0] += timestep * v[0]
    x[1] += timestep * v[1]
    v[0] += timestep * a[0]
    v[1] += timestep * a[1]

################
# Init tensors
################


################
# GUI Settings
################
videoManager = ti.VideoManager(output_dir="./video", framerate=24, automatic_build=False)
gui = ti.GUI(background_color=0x222222)
colors = [0xFF0000, 0x00FFFF, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FF00]
def world2screen(x, y):
    L = 1.5
    x = (x-(-L))/(2*L)
    y = (y-(-L))/(2*L)
    return (x,y)

show_on_screen = True

seconds = 5
fps = 24
if show_on_screen:
    fps = 60
timestepsPerFrame = int(1.0/fps/timestep)
totalFrame = seconds*fps
if show_on_screen:
    totalFrame = 1000000


################
# Main Loop
################
print("v0: ", v)
for frameCount in tqdm(range(totalFrame)):
    print(x)
    for t in range(timestepsPerFrame):
        TimeTick(timestep*t + frameCount/fps)

    gui.circle(world2screen(x1[0],x1[1]), color=colors[0], radius=5)
    gui.circle(world2screen(x2[0],x2[1]), color=colors[0], radius=5)
    gui.circle(world2screen(x3[0],x3[1]), color=colors[0], radius=5)
    gui.circle(world2screen(x[0],x[1]), color=colors[1], radius=5)

    if show_on_screen:
        gui.show()
    else:
        img = gui.get_image()
        videoManager.write_frame(img)
        gui.clear()
if not show_on_screen:
    videoManager.make_video(gif=True, mp4=False)

附录二、简单的PBD demo

import taichi as ti
from random import random
from tqdm import tqdm
from math import sin, pi, sqrt

################
# Support Functions
################
def assignArray2Tensor(t, i, a):
    for j in range(len(a)):
        t[i][j] = a[j]

def diffDotProd(a, b):
    res = 0
    for i in range(3):
        res += (a[i]-b[i]) * (a[i]-b[i])
    return sqrt(res)

################
# Init taichi
################
ti.init(arch=ti.gpu)

################
# Input data
################
def Calx0(t):
    loopInt = 4 # s # 2s for a loop
    x = sin(t/loopInt*2*pi)
    y = sin(t/loopInt*2*pi)
    return [x, y, 0]
n = 5
lConst = 0.1
xInitRel = [[0, -lConst*i, 0] for i in range(5)]
gravityConstant = 9.8
timestep = 0.01


################
# Taichi Tensors
################
x = ti.Vector(3, dt=ti.f32, shape=(n))
v = ti.Vector(3, dt=ti.f32, shape=(n))
vPer = ti.Vector(3, dt=ti.f32, shape=(n))
xPer = ti.Vector(3, dt=ti.f32, shape=(n))
a = ti.Vector(3, dt=ti.f32, shape=(n))
l = ti.field(ti.f32, shape=(n))


################
# Kernels
################
@ti.kernel
def CalvPer():
    for i in v:
        vPer[i] = v[i] + a[i] * timestep

@ti.kernel
def CalxPer_rest():
    for i in x:
        xPer[i] = x[i] + timestep/2 * (v[i] + vPer[i])

@ti.kernel
def Updatev():
    for i in v:
        v[i] = 2/timestep * (xPer[i]-x[i]) - v[i]
    
@ti.kernel
def Updatex():
    for i in x:
        x[i] = xPer[i]


################
# Time Loop Functions
################
def CalxPer(t):
    CalxPer_rest()
    xPer[0] = Calx0(t)

def CalPer(t):
    CalvPer()
    CalxPer(t)

def SolveConstraints():
    ITER_NUM = 2
    for i in range(ITER_NUM):
        # C0
        k = l[0]/diffDotProd(xPer[1], xPer[0])-1
        for u in range(3):
            xPer[1][u] += k * (xPer[1][u]-xPer[0][u])
        # Cj
        for j in range(1, n-1):
            k = 0.5 * (l[j]/diffDotProd(xPer[j+1], xPer[j])-1)
            tmp = [0, 0, 0]
            for u in range(3):
                tmp[u] = k * (xPer[j][u]-xPer[j+1][u])
            for u in range(3):
                xPer[j+1][u] += k * (xPer[j+1][u]-xPer[j][u])
            for u in range(3):
                xPer[j][u] += tmp[u]

def Update():
    Updatev()
    Updatex()

def TimeTick(t):
    CalPer(t)
    SolveConstraints()
    Update()

################
# Init tensors
################
x0 = Calx0(0)
assignArray2Tensor(x, 0, Calx0(0))
for i in range(1, n):
    xi = [0, 0, 0]
    for u in range(3):
        xi[u] = x0[u] + xInitRel[i][u]
    assignArray2Tensor(x, i, xi)

for i in range(n):
    assignArray2Tensor(v, i, [0,0,0])

assignArray2Tensor(a, 0, [0,0,0])
for i in range(1, n):
    assignArray2Tensor(a, i, [0, -gravityConstant, 0])

for i in range(n):
    l[i] = lConst

################
# GUI Settings
################
videoManager = ti.VideoManager(output_dir="./video", framerate=24, automatic_build=False)
gui = ti.GUI(background_color=0x222222)
colors = [0xFF0000, 0x00FFFF, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FF00]
def world2screen(x, y):
    L = 1.5
    x = (x-(-L))/(2*L)
    y = (y-(-L))/(2*L)
    return (x,y)

show_on_screen = False

seconds = 5
fps = 24
if show_on_screen:
    fps = 60
timestepsPerFrame = int(1.0/fps/timestep)
totalFrame = seconds*fps
if show_on_screen:
    totalFrame = 1000000


################
# Main Loop
################
for frameCount in tqdm(range(totalFrame)):
    print(x)
    for t in range(timestepsPerFrame):
        TimeTick(timestep*t + frameCount/fps)

    for i in range(n):
        gui.circle(world2screen(x[i][0],x[i][1]), color=colors[i>0], radius=5)

    if show_on_screen:
        gui.show()
    else:
        img = gui.get_image()
        videoManager.write_frame(img)
        gui.clear()
if not show_on_screen:
    videoManager.make_video(gif=True, mp4=False)