Taichi Introduction

引言

太极图形课会分成「太极」语言与计算机「图形学」两部分。前半部分主讲太极编程语言的基础语法,高级用法,以及调试和优化太极程序的方式。后半部分侧重于图形学知识,我们将以实践者的角度来聊一聊基于太极语言的渲染和仿真。

课程导览

本季课程将通过 太极图形科技 微信公众号、太极图形 bilibili账号,太极图形 知乎机构号进行直播。 课程直播时间为每周二晚7点(中秋顺延),答疑时间则为每周四晚7点,详细时间表如图所示。

时间内容答疑安排
第00讲:09/14导览:什么是图形学?什么是太极?课件09/16
第01讲:09/22Hello World:编写你的第一个太极程序 课件09/23
第02讲:09/28复用你的太极代码:元编程和面向对象编程 课件09/30
第03讲:10/12大规模计算的关键:高级数据结构 课件10/14
第04讲:10/19调试和优化你的太极程序 课件10/21
第05讲:10/26程序动画:生成你的第一个二维动画 课件10/28
第06讲:11/02渲染01:光线追踪概念和基础11/04
第07讲:11/09渲染02:光线追踪实战11/11
第08讲:11/16弹性物体仿真01:时间和空间离散化11/18
第09讲:11/23弹性物体仿真02:隐式积分和数值解法11/25
第10讲:11/30流体仿真01:拉格朗日视角12/02
第11讲:12/07流体仿真02:欧拉视角12/09
第12讲:12/14神秘嘉宾客串:从太极的使用者变成太极的贡献者

课时】:共安排13节讲堂及12节答疑,每节课约45分钟。

授课形式】:中文授课,英文课件。当日直播,次日于 太极图形 bilibili账号发布课程录播。(由于B站审核时间的问题,请同学们耐心地常刷常新~)

课程需求】:了解Python基础语法,有线性代数和高中物理知识更佳。

Instructor

这门课的主讲人是刘天添老师,本科毕业于浙江大学,硕士和博士毕业于宾夕法尼亚大学。回国在微软亚洲研究所做了三年研究员,目前是太极图形的资深研究科学家,研究领域包括实时物理仿真、高性能几何处理和高效数值算法。

Lead Teaching Assistant

GAMES201

GAMES201是高级物理引擎实战指南2020,许多计算机图形学的华人老师都活跃在GAMES论坛中,开设课程涵盖渲染、仿真、几何、三维视觉等各个领域的内容,欢迎学有余力的同学按需求学习。

The Taichi Graphics Course - S1

去年GAMES201胡渊鸣教授的口号是“下接地气上不封顶,适合 0 到 99 岁人群适量学习”,所以刘老师想把宣传语改为“有手就行”,但转念一想过度宣传课程的简单容易增加学习者的沮丧感,还有凡尔赛之嫌,所以就不这么说了。总之今年会从基础讲起,争取做到手把手入门太极和计算机图形学。

Homework and Final Project

每一节课的课下作业是可选的,在 Taichi Zoo 也可以运行自己的小作业。

最后的大作业不定题材,只要是用太极写的图形学程序我们都认可。如果想要一份结业证书,大作业是必须上交的,截稿日期是2022年1月3日,上交方式以开源平台为主。

Where shall I raise questions?

什么是计算机图形学

我们先来聊聊什么是计算机图形学。

Me & Computer Graphics

和许多人一样,老师接触计算机图形学是因为小时候非常喜欢玩游戏,想像李逍遥一样仗剑天涯。

而第一次听到 CG 这个名词是因为最终幻想的过场动画,当时的硬件基础还是算法都没办法去支持大规模高质量的图片。

本科学了一学期的 OpenGL,只能说是图形学的一部分,没有办法涵盖图形学的全部。所以太极图形课更多的是让大家认识图形学是什么样子的。

The Knowledge-Image Cycle

这边有一个知识-图片的循环,很多时候被人描述计算机图形学和计算机视觉的不同。计算机视觉更多是用来如何去理解图片,知道这张图片哪里是车哪里有标牌等等;而计算机图形学则更多是用计算机生成人也难以分辨的图片,它的“G”是用来生成的:

The G in Graphics is for Generation!

程序化的内容生成就是一种计算机图形学的表现方式,它的生成主要是用创造者给定的规则创造出创造者想要的数字内容,比如给树木设定一个生长的规则,最后可以长出形态各异的树。

又或者说这些生成可以是基于物理的渲染方式,我们可以通过前人总结的物理公式来生成以假乱真的虚拟世界的图形。

基于物理的动画也是如此。我们在虚拟世界可以生成一条可以扭动的龙,可以拖拽,它背后的原理也许只是牛顿运动定理而已。而根据这些物理原理,我们就可以用计算机图形学的知识来生成可以交互的动画。

当然,用于生成的内容未必是人类能够理解的公式,它们也可以来自大数据。比如这张图片来自生成对抗网络,相片被用于生成很多不同画家风格的画,比如梵高、莫奈甚至是浮世绘风格,也可以看到计算机视觉与计算机图形学的界限越来越模糊,我认为所有用于生成数字资产的技术都可以被认为是图形学关心的技术。

What can we generate?

那我们都可以拿计算机图形学的工具来生成什么样的东西呢?

We generate movies

上图是动画电影《冰雪奇缘》,计算机图形学可以生成这些拥有丰富表现力的动画和特效。

最近国内的动画电影也有非常大的进步,不管是哪吒还是白蛇青蛇,都是最近涌现的非常高质量的依托于计算机图形学技术制作的动画电影。

We generate games

除了电影,自然也有游戏。上面的截图来源于《黑神话:悟空》,是非常期待的国产游戏,预计2023年发布。游戏中除了很棒的实时渲染,还拥有非常多的物理交互,我可以破坏物体、看到毛发这些东西的二级运动以及和雪做交互等等,这些都极大提高了游戏的体验,让我们仿佛置身其中一样。

We generate design tools

我们还能用图形学技术来制作生产工具。以往我们只认为机床、流水线或缝纫机才是生产工具,在当今智能制造生产流程当中,虚拟生产线也是非常重要的生产工具,上图是截取的一张凌迪科技的 Style3D 工具截图。

We generate the Reality in the virtual space

一定程度上所有的数字内容生成都是计算机图形学感兴趣的工作,所以你在虚拟世界中看得到摸得到感受得到的东西都会是计算机图形学生成和服务的对象。

Maybe ultimately…

那最终我们是不是可以迎来一个拥有超级丰富内容的虚拟世界呢?这幅图来自 2012 年非常火的动画片《刀剑神域》,该动画片中大部分精彩的故事都在虚拟世界中发生的。

… but apparently we are not there yet. Why?

首先是好用的工具不足。在虚拟数字内容的生成当中会需要使用非常多的工具,上面每一项工具都需要很长的时间去学习才能创作出满意的虚拟数字内容。我认为这些工具本身是非常棒的,但它们的学习曲线却非常陡峭,也许我们需要更加好用的数字内容生产工具,可以让人们生产出更多的数字内容。

另一点是这些工具确实不是那么容易被生产的。一个是因为我们很难复现别人的代码,另一个是因为代码的计算效率非常依赖程序员的功底。

什么是太极

What do we want from a programming language for computer graphics?

Productivity

大部分科研代码都是用 C++ 或者 CUDA 来写的,图中截取了一部分源代码,而一个约束的处理就达到了 1207 行,再包括用户交互所有代码可能要达到 1w+ 行。而在计算机图形学中,这种规模的代码并不是非常令人吃惊的东西,这就造成了要上手计算机图形学代码是一件非常难的事情,生产力被加上了非常多的制约。

Portability

我们的硬件后端非常多,你可能希望自己的代码在 Intel 或 AppleM1 上可以跑起来,而这些设备通常有不同的指令集。在不同的操作系统上 Windows、MacOS 和 Linux 你的代码可能会以不同方式编译来支持这些指令集,比如在 Windows 的 Visual Studio 上写的代码很难在 Linux 上复现,开发者更喜欢 MacOS 和 Linux。

Performance

通常来说高效执行的语言比如 C++ 易读性和可维护性都不理想,会限制你的生产力。而脚本语言比如 Python 这样的工具非常好用,但很难被高效编译成想要的程序,Matlab 算是两者之间的折中,但还是很难满足计算机图形学实时计算的需求。

Taichi – A DSL for Computer Graphics

而太极就是为了高效的并行计算或图形计算而设计出的一种领域特定语言,它在生产力、可移植性做了非常好的优化。

大家认识太极可能是源自胡渊鸣的 99 行《冰雪奇缘》帖子。

他的标题党在于《冰雪奇缘》不仅仅是一个 mpm 仿真程序而已,更多的是高质量虚拟内容的创作,仿真只是很小的一部分。这个程序麻雀虽小,五脏俱全,你可以去理解代码的逻辑。

What you can do using Taichi

太极最初用于物理仿真,图中是基于物质点法的仿真程序,可以看到使用太极可以以比较少的代码行数去把整个仿真程序搞定。

太极还可以写渲染。一位来自 AMD 的产品经理表示太极非常好用,它可以成为 GPU 计算中的未来。

如何安装 Taichi

Taichi Installation

太极的安装非常简单,无论什么开发环境都可以在 Python 下调用:

python3 -m pip install taichi

Taichi Zoo

如果本地没有 Python 环境或者说 pip install 有些问题,这里还可以使用 Taichi Zoo

还可以修改默认的 Julia Set 代码体会:

import taichi as ti
ti.init(arch=ti.gpu)

n = 640
pixels = ti.field(dtype=float, shape=(n, n))


@ti.func
def complex_sqr(z):
return ti.Vector([z[0]**2 - z[1]**2, z[1] * z[0] * 2])


@ti.kernel
def paint(t: float):
for i, j in pixels: # Parallized over all pixels
# c = ti.Vector([-0.8, ti.cos(t) * 0.2])
c = ti.Vector([ti.cos(t), ti.sin(t)]) * 0.7885
z = ti.Vector([i / n - 0.5, j / n - 0.5]) * 2
iterations = 0
while z.norm() < 20 and iterations < 50:
z = complex_sqr(z) + c
iterations += 1
pixels[i, j] = 1 - iterations * 0.02


gui = ti.GUI("Julia Set", res=(n, n))

for i in range(1000000):
paint(i * 0.01)
gui.set_image(pixels)
gui.show()

Since Zoo is still in BETA…

目前 Zoo 还处于测试阶段,如果遇到意想不到的问题欢迎提 issues。

Taichi Documentation

太极文档放在 https://docs.taichi.graphics/docs/ 中,可以查阅各种 feature。

初始化一个 Taichi 程序

Hello World!

def Hello():
print("Hello World!")
Hello()

相信大家在很多语言课中第一节课都是在屏幕中打印出一个 “Hello World”。

Hello World @ Taichi

太极的 “Hello World” 可能稍微复杂一点:

import taichi as ti

ti.init(ti.gpu)

# global control
paused = ti.field(ti.i32, ())

# gravitational constant 6.67408e-11, using 1 for simplicity
G = 1
PI = 3.141592653

# number of planets
N = 1000
# unit mass
m = 5
# galaxy size
galaxy_size = 0.4
# planet radius (for rendering)
planet_radius = 2
# init vel
init_vel = 120

# time-step size
h = 1e-5
# substepping
substepping = 10

# pos, vel and force of the planets
# Nx2 vectors
pos = ti.Vector.field(2, ti.f32, N)
vel = ti.Vector.field(2, ti.f32, N)
force = ti.Vector.field(2, ti.f32, N)

@ti.kernel
def initialize():
center=ti.Vector([0.5, 0.5])
for i in range(N):
theta = ti.random() * 2 * PI
r = (ti.sqrt(ti.random()) * 0.7 + 0.3) * galaxy_size
offset = r * ti.Vector([ti.cos(theta), ti.sin(theta)])
pos[i] = center+offset
vel[i] = [-offset.y, offset.x]
vel[i] *= init_vel

@ti.kernel
def compute_force():

# clear force
for i in range(N):
force[i] = ti.Vector([0.0, 0.0])

# compute gravitational force
for i in range(N):
p = pos[i]
for j in range(i): # bad memory footprint and load balance, but better CPU performance
diff = p-pos[j]
r = diff.norm(1e-5)

# gravitational force -(GMm / r^2) * (diff/r) for i
f = -G * m * m * (1.0/r)**3 * diff

# assign to each particle
force[i] += f
force[j] += -f
# for j in range(N):# double the computation for a better memory footprint and load balance
# if i != j:
# diff = p-pos[j]
# r = diff.norm(1e-5)

# # gravitational force -(GMm / r^2) * (diff/r) for i
# f = -G * m * m * (1.0/r)**3 * diff

# # assign to each particle
# force[i] += f

@ti.kernel
def update():
dt = h/substepping
for i in range(N):
#symplectic euler
vel[i] += dt*force[i]/m
pos[i] += dt*vel[i]

gui = ti.GUI('N-body problem', (512, 512))

initialize()
while gui.running:

for i in range(substepping):
compute_force()
update()

gui.clear(0x112F41)
gui.circles(pos.to_numpy(), color=0xffffff, radius=planet_radius)
gui.show()

Build your first project…

Initialization

import taichi as ti

在所有太极程序中,都必须有这两段入口代码:

import taichi as ti
ti.init(arch = ti.gpu)
ti.init()

ti.init() 是所有太极程序的入口,最重要的参数是 arch 的计算硬件是什么:

arch = ti.cpu / ti.gpu / ti.arm / ti.x64 / ti.cuda...

当设备没有 gpu 时会回滚到 cpu 执行。

Taichi v.s. Python

The Python frontend of Taichi

可以看到这段代码中只有 @ti.kernel 是在太极作用域,剩下都是 Python 作用域。

import taichi as ti

ti.init(arch = ti.gpu)

d = 1

def foo():
d_python = d
print("d_python = ", d_python)

@ti.kernel
def bar():
d_taichi = d
print("d_taichi = ", d_taichi)

d = d + 1 # d = 2
foo() # d_python = 2
bar() # d_taichi = 2
d = d + 1 # d = 3
foo() # d_python = 3
bar() # d_taichi = 2
Taichi-scope v.s. Python-scope

Python-scope 就是在代码里几乎所有东西。下面的代码虽然调用了太极,却没有任何需要处理的内容,所以与一般的 Python 一致:

import taichi as ti

ti.init(arch = ti.gpu)

def foo():
print("This is a normal python function")

foo()

而当你开始写 @ti.kernel 太极计算核的时候才会被太极编译成高性能计算程序:

import taichi as ti

ti.init(arch = ti.cpu)

@ti.kernel
def foo():
print("This is now a Taichi kernel")

foo()

组织 Taichi 数据

Primitive types

太极拥有无/有符号的整型,也支持浮点型,默认类型都是 32 位。

对于不同的架构我们支持的类型也有所不同,在 CPU 和 CUDA 上我们支持所有的数据类型,但在 Metal 或 OpenGL 中仅支持部分数据类型。

Default types

可以通过 ti.init() 初始化中修改默认的数据类型:

ti.init(default_fp = ti.f32) # float = ti.f32
ti.init(default_fp = ti.f64) # float = ti.f64

ti.init(default_ip = ti.i32) # int = ti.i32
ti.init(default_ip = ti.i64) # int = ti.i64

Type promotions

精度不同的类型计算,太极会把低精度的自动升级为高精度的类型。

Type Casts

太极还可以进行隐式转换,如果在太极作用域中浮点型会被转变为整型。

显式转换使用 ti.cast() 把不同类型之间的数据做转换。

Compound types

之前这些都是基础的标量数据格式,在太极作用域中同样可以定义矢量。用户可以使用 ti.types. 来定义想要使用的容器结构,图中定义了向量、矩阵以及结构体。

当然,一些常用的数据类型太极已经定义好了,可以调用 ti.Vector()ti.Matrix() 等等。

可以使用 [] 去访问这些向量、矩阵结构中每一个 index 元素。

ti.field

太极与其他计算语言不同的是这个 ti.field,这边展示出一个示例:

heat_field = ti.field(dtype=ti.f32, shape=(256, 256))

假如现在有一个方形的平底锅,大小是 256 x 256。锅上任意一点都会有温度,我想定义一个温度场表达每一点的温度是什么,就可以使用 ti.field,大小由 shape 定义,数据类型由 dtype 定义。

它是一个 N 维数组,当 N 为 0 时为标量;当 N 为 1 时为向量;当 N 为 2 时为矩阵。

ti.field examples

Example: N-body

这是之前说的 “Hello World” 的 N 体系统源码中数据声明与定义部分:

import taichi as ti

ti.init(ti.gpu)

# global control
paused = ti.field(ti.i32, ())

# gravitational constant 6.67408e-11, using 1 for simplicity
G = 1
PI = 3.141592653

# number of planets
N = 1000
# unit mass
m = 5
# galaxy size
galaxy_size = 0.4
# planet radius (for rendering)
planet_radius = 2
# init vel
init_vel = 120

# time-step size
h = 1e-5
# substepping
substepping = 10

# pos, vel and force of the planets
# Nx2 vectors
pos = ti.Vector.field(2, ti.f32, N)
vel = ti.Vector.field(2, ti.f32, N)
force = ti.Vector.field(2, ti.f32, N)

了解 Taichi 的计算核

Kernels

@ti.kernel 是太极的计算核,修饰的函数可以被 Python 作用域所调用。需要注意的是不可以在一个 @ti.kernel 中再调用另一个 @ti.kernel 函数,这样会直接报错。

For-loops in a @ti.kernel

我们为什么要使用太极计算核呢?因为计算核的效率要比传统的 Python 要快得多,并且当你被检测到你的程序是在并行化环境中运行时候,for 循环的最外层会被自动并行化:

@ti.kernel
def fill():
for i in range(10): # Parallelized
x[i] += i

s = 0
for j in range(5): # Serialized in each parallel thread
s += j

y[i] = s

for k in range(20): # Parallelized
z[k] = k

最外层不单单只循环层,也包括一些条件跳转层:

import taichi as ti
ti.init(arch=ti.cpu)
@ti.kernel
def foo(k: ti.i32):
for i in range(10): # Parallelized :-)
if k > 42:
...

@ti.kernel
def bar(k: ti.i32):
if k > 42:
for i in range(10): # Serial :-(
...

这时候就需要好好设计一下你的程序了,我们想要最大化去优化最外层的可并行性:

def my_for_loop():
for i in range(10): # I don't want to parallelize this for
for j in range(100): # I want to parallelize this for
...
my_for_loop()

此时我们想要并行里层循环,可以选择把里层逻辑写到 Python scope,就像写一个 CUDA kernel 一样:

def my_for_loop():
for i in range(10):
my_taichi_for()

@ti.kernel
def my_taichi_for()
for j in range(100):
...

my_for_loop();

既然 @ti.kernel 会被并行执行,那么一些在 for 中正常使用的语句在最外层就不可以被调用了:

@ti.kernel
def foo():
for i in range(10):
...
break # Error!

@ti.kernel
def foo():
for i in range(10):
for j in range(10):
...
break # OK!

并行还带来占用问题,太极中的 += 是原子操作:

@ti.kernel
def sum():
for i in range(10):
# 1. OK
total[None] += x[i]

# 2. OK
ti.atomic_add(total[None], x[i])

# 3. data race
total[None] = total[None] + x[i]

struct-for 可以遍历 ti.field,但仅支持最外层。

Kernel arguments
  • At most 8 parameters
  • Pass from the Python scope to the Taichi scope
  • Must be type-hinted
  • Scalar Only
  • Pass by value
Return value of a @ti.kernel

传参中只能返回一个标量,并且必须表明数据类型。

Functions

@ti.func 代表太极所有的 function,它和 @ti.kernel 最大的不同是它只能从 @ti.kernel 中被调用。

该功能主要是帮助复用代码。

@ti.func 可以嵌 @ti.func,在目前的实现中是强制内联的,所以函数目前不支持递归。

Arguments and return values

因为 @ti.func 是从 Taichi-scope 中调进来的,所以我们不需要知道传的参数是什么东西,太极程序已经知道它们是什么了。

需要注意的是为了保持程序传参的一致性,我们专门做了一次拷贝。

Anything in @ti.kernel and @ti.func are in the Taichi scope
  • Static data type in the Taichi scope
  • Static lexical scope in the Taichi scope

太极中数据是静态的,上图整型和向量没有隐式转换的方式,所以编译器会报错。

太极的词法作用域也是静态的,这个和我们常写的 Python 程序可能会有区别,上面的 y 只作用于跳转语句,出去之后就被释放掉了。

左边的 a 不是全局的,所以变动后 @ti.kernel 并不知道变动。如果需要这样的全局变量,请设置 ti.field

Math ops

太极支持大多数 Python 的数学运算,包括矩阵的转置、求逆等等。

Example: N-body

可视化你的 Taichi 程序

Print

因为最外层的 for 是 GPU 并行处理的,所以打印出来的东西并不是按序的(除非在 CPU 环境下)。

打印的时间也不一定和你想像的时间一样。

GUI

太极目前提供了一套比较简单的可视化交互系统。

2D

3D