写在前面

之前已经发过了两个作品了:Taichi Voxel Challenge 2022

最近忙完手头的几个比较急的工作,又手痒想整点活了 hhh,本来有几个想法,但最后就先做了 PVZ 这个。

PVZ 目前还是 WIP,后面应该会再完善一下下,后面如果时间充裕还可以再来整几个,不过快要答辩了 555。

作品思路

PVZ 这个目前只做了一个最简单的豌豆射手,但其实在做的过程中也遇到很多的问题。首先,还是来分析一下豌豆射手的结构吧!

豌豆射手大概就是有以下几个结构:主体炮筒,眼睛,枝干,后面的芽和下方的叶片。

主体炮筒:这个炮筒其实我们还可以再来拆解一下,其实可以把他看成一个圆柱,但圆柱的壁变成曲线,看起来很简单对不对?我们直接用 SDF 表示几何体然后直接构建就好啦!但是我不会 SDF(x),其实这样也是有点问题的,因为炮口应该会比较靠下,所以我们干脆拆成一个球体 + 几个圆即可。

眼睛:这个没啥难度,直接从球体上扣掉一个矩形,安放一个黑矩形当眼睛,白矩形点高光即可。

1
2
3
4
def create_eye(p):
    create_box(p, 2, 8, 3, 0, vec3(0))
    create_box(p, 2, 1, 3, 1, vec3(0))
    create_box(p + vec3(0, 1, 0), 1, 1, 1, 1, vec3(1))

枝干:这个枝干的曲线可是难倒我了,我并不会 bezier curve,所以我决定化繁为简,直接用正弦函数 hhh,由某条龙的作品启发 233,定义好振幅,我只选取了 [0, PI] 的参数范围。

1
2
3
4
5
@ti.func
def create_sine_curve(p, A, l, mat, color, dir1=vec3(0, 1, 0), dir2=vec3(0, 0, 1), tk=1):
    for x, tx in ti.ndrange((0, l + 1), (0, tk)):
        y = ti.cast(A * ti.sin(1.0 * x / l * ti.math.pi), ti.int32)
        scene.set_voxel(p + y * dir2 + tx * dir2 + x * dir1, mat, color)

后面的芽:直接也当成曲线画上即可

下面的叶子:原版的豌豆射手下方有 3 片叶子,2 大 1 小,但是太复杂了,这里也是简单表示,画 4 个方向的 4 片叶子。但是叶子这个又是个不规则的图形,好像又不好表示…我灵机一动,画了一片叶子(),这很像是两个对角的半圆拼接起来的重合区域,那就有了,指定一个起点和方向,然后左上右下或者右上左下作为圆心,判断重叠区域即可。但这样画出来是平的,没有关系,我们在 z 坐标还是用老方法,直接两个关于 x 和 y 的正弦函数相加就好了!(机智如我)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def create_leaf(p, r, dir, mat, color):
    if dir == 1:
        for x, y in ti.ndrange((0, r + 1), (0, r + 1)):
            if x * x + y * y <= r * r and (r - x) * (r - x) + (r - y) * (r - y) <= r * r:
                z = ti.cast(ti.floor(1 * ti.sin(ti.math.pi * x / r) + ti.sin(ti.math.pi * y / r)), ti.int32)
                scene.set_voxel(p + vec3(x, z, y), mat, color)
    elif dir == 2:
        for x, y in ti.ndrange((0, r + 1), (0, r + 1)):
            if x * x + (r - y) * (r - y) <= r * r and (r - x) * (r - x) + y * y <= r * r:
                z = ti.cast(ti.floor(1 * ti.sin(ti.math.pi * x / r) + ti.sin(ti.math.pi * y / r)), ti.int32)
                scene.set_voxel(p + vec3(x, z, y), mat, color)

到此,豌豆射手就结束了。构建豌豆射手的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@ti.func
def create_peashooter(p):
    create_ball(p + vec3(0, 18, 0), 8, 1, col_g)
    for i in range(6):
        create_circle(p + vec3(0, 16, 6 + i), 3.0, 1, col_g, 1)
    create_circle(p + vec3(0, 16, 12), 4.0, 1, col_g, 1);create_circle(p + vec3(0, 16, 13), 4.0, 1, col_g, 1)
    for i in range(8):
        create_circle(p + vec3(0, 16, 6 + i), ti.max(2.0, ti.min(i - 3.0, 3.0)), 0, col_g, 1)
    for x, y in ti.ndrange((-1, 1 + 1), (-1, 1 + 1)):
        create_sine_curve(p + vec3(x, 5, y), 2, 5, 1, col_gd, vec3(0, 1, 0), vec3(0, 0, -1))
        create_sine_curve(p + vec3(x, 0, y), 2, 5, 1, col_gd, vec3(0, 1, 0), vec3(0, 0, 1))
    create_sine_curve(p + vec3(0, 24, -5), 2, 5, 1, col_gd, vec3(0, 0, -1), vec3(0, 1, 0), 2)
    create_eye(p + vec3(-3, 20, 5));create_eye(p + vec3(2, 20, 5))
    create_leaf(p + vec3(0, 0, -6), 6, 1, 1, col_gdd);create_leaf(p + vec3(-7, 0, 1), 6, 1, 1, col_gdd)
    create_leaf(p + vec3(-6, 0, -6), 6, 2, 1, col_gdd);create_leaf(p + vec3(1, 0, 1), 6, 2, 1, col_gdd)

剩下的就是草坪了。因为有格子数的限制,没法实现原版的 6 * 9 的设置,也没法画全四周的篱笆了,所以就直接画了 6 *6 的场景。画完以后差点味儿,再给草坪周围加上噪声即可。

1
2
3
4
5
6
7
8
9
@ti.func
def create_grass(p, sx, sy, sz, mat, color):
    create_box(p, sx, sy, sz, mat, color)
    for x, y in ti.ndrange((0, sx + 1), (0, sy + 1)):
        if ti.random() > 0.8:
            scene.set_voxel(p + vec3(0, 0, (ti.random() - 0.5) * 4), mat, color)
            scene.set_voxel(p + vec3(x, 0, (ti.random() - 0.5) * 4), mat, color)
            scene.set_voxel(p + vec3((ti.random() - 0.5) * 4, 0, y), mat, color)
            scene.set_voxel(p + vec3((ti.random() - 0.5) * 4, 0, 0), mat, color)

最后做完了行数有点超,稍微压一下行就 99 行啦!

最后

有几个遇到的问题至今未解决 - -,首先一个就是我不知道怎么在 @ti.func 中交换 vec3 的维度,比如 vec3(x, y, z) 变成 vec3(z, y, x),不然在构建圆形的地方还能少几行,因为我是想着多方向的圆形,最后还是选择了最暴力的 if 算法。

还有一个小细节就是在圆形和球体的地方,为了让圆形更圆一下,就得给放宽一下边界条件,比如 x * x + y * y <= r * r + eps。在球体的时候,有时候会是上面多一个点,这时候缩一下边界条件会比较好。