Voxel Pac-man

早在很久以前就看到了 Taichivoxel-challenge 这个仓库。因为我自己也是非常喜欢像素风的,对于体素自然也是有所关注,比如我之前还做过个 deep learning for pixel art 的调研。随便翻了翻仓库就看到了一个 issue #1,然后我非常兴奋的自己动手做了一下,代码在这里,大概长下面这个样子:

当然,后面才知道这原来是内测(x,我说我怎么找半天公众号也没找到这 challenge 的推送

正式赛开始以后,我就修改了一下内容,稍微调节了一下参数改进了一下,加了几个 feed ball,投稿了这个 Voxel Pac-man,看起来有那么一点高级感了(x:

代码大概 96 行,整体的思路也比较简单,首先看看 pac-man 都有什么成分,一个是整个的球面,但是嘴巴张开了,是不完整的,嘴巴上下应该算另一个面,然后就是眼睛,最后 feed ball。

我在代码前面加了很多个性化的参数,可以很方便的来调节,n 是整个空间的大小,因为当时也没自己看代码,所以并不知道空间限制是(-64,64),就自己随便填了,剩下的就是半径和中心点,面朝的方向之类的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@ti.kernel
def initialize_voxels():
    n = 60
    r = 20
    p_center = vec3(0, n // 2, -n // 2)

    vec_face_to = vec3(0, 0, 1)
    vec_z = vec3(0, 1, 0)
    vec_normal = normalize(cross(vec_face_to, vec_z))

    mouse_angle = pi / 5
    mouse_angle_min = mouse_angle
    mouse_angle_max = mouse_angle
    mouse_angle_cos_min = ti.cos(mouse_angle_min) - 0.05
    mouse_angle_cos_max = ti.cos(mouse_angle_max) + 0.05
    skin_thickness = 0.5

    eye_angle = mouse_angle + pi / 18
    he_angle = pi / 5

    vec_eye_left = rotate(rotate(vec_face_to, vec_normal, -eye_angle), vec_z, he_angle).normalized()
    vec_eye_right = rotate(rotate(vec_face_to, vec_normal, -eye_angle), vec_z,
                           -he_angle).normalized()

    p_eye_left = p_center + vec_eye_left * r
    p_eye_right = p_center + vec_eye_right * r
    eye_size = 4

核心代码是这样的,首先先画皮肤表面,枚举 x,y,z ,如果在皮肤内部,那么需要进行判断,如果在嘴巴的部分,就不能画出来,不在嘴巴的部分可以直接填上皮肤的颜色。如何判断是嘴巴的部分呢?直接从侧面来看,如果这个点在中间面上的投影和pac-man 面对的方向的夹角在我们嘴张开的夹角范围内,那么就是了。最后再来画,嘴巴内部的情况,也是同理,首先要在皮肤内部,然后在判断是否在嘴巴中间,否则就在嘴巴张开的上下一定范围画出来即可,因为是体素可能不太标准,所以需要加点参数让他看起来舒服一些。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    for i, j, k in ti.ndrange((-n, n), (-n, n), (-n, n)):
        x = ivec3(i, j, k)
        color = vec3(2, 2, 2)
        # surface
        if distance(x, p_center) < r + skin_thickness and distance(x,
                                                                   p_center) > r - skin_thickness:
            # mouse
            # project to the plane
            vec_mouse = vec3(i, j, k) - p_center
            vec_mouse_projected = vec_mouse - vec_normal * dot(vec_mouse, vec_normal)
            print(vec_mouse_projected)
            # angle to face to
            angle_cos = dot(vec_mouse_projected,
                            vec_face_to) / (vec_mouse_projected.norm() * vec_face_to.norm())
            if angle_cos <= mouse_angle_cos_max:
                color = vec3(1, 1, 0.)
            if distance(vec3(i, j, k), p_eye_left) <= eye_size or distance(
                    vec3(i, j, k), p_eye_right) <= eye_size:
                color = vec3(0.01, 0.01, 0.01)

        elif distance(x, p_center) <= r - skin_thickness:
            # mouse
            # project to the plane
            vec_mouse = vec3(i, j, k) - p_center
            vec_mouse_projected = vec_mouse - vec_normal * dot(vec_mouse, vec_normal)
            angle_cos = dot(vec_mouse_projected,
                            vec_face_to) / (vec_mouse_projected.norm() * vec_face_to.norm())

            if mouse_angle_cos_min <= angle_cos and angle_cos <= mouse_angle_cos_max or vec_mouse_projected.norm(
            ) < 3:
                color = vec3(0.0, 0.0, 0.0)
        if any(color != vec3(2, 2, 2)):
            scene.set_voxel(vec3(i, j, k), 2, color)

最后画 feed ball 就比较简单了

1
2
3
4
5
6
@ti.func
def create_feed_ball(feed_r, feed_p, feed_color):
    for i, j, k in ti.ndrange((-feed_r, feed_r), (-feed_r, feed_r), (-feed_r, feed_r)):
        x = ivec3(i, j, k)
        if distance(x, vec3(0, 0, 0)) + 0.5 <= feed_r:
            scene.set_voxel(feed_p + vec3(i, j, k), 2, feed_color)

但是我在做完 Voxel Fortress 之后,感觉这个其实没有那么复杂,可能 50 来行就能搞定了。

我们再来拆解一下整个 pac-man,表面是一个球,嘴巴也是一个内部的球,眼睛是个球,feed ball 也是一个球,那么主要的元素就都已经完成了。嘴巴张开怎么做呢?只需要把 voxelmat 设置成 0,其实就是删除,那么往嘴巴的部分塞一个横着的半圆柱就可以直接完成了!!

Voxel Fortress

现实中有没有什么体素组成的东西呢?除了 LEGO 就是砖块啦!那么就要做个堡垒要塞!(可能后面也想做个 GW!)

这个堡垒做起来非常简单,一个立方体的四周 + 上封顶 + 四周的凸起的砖块就好啦,一个函数搞定!

1
2
3
4
5
6
7
8
9
@ti.func
def build_fortress(pos, sz1, sz2, height, color, color_noise):
    for x, y in ti.ndrange((-sz1, sz1 + 1), (-sz2, sz2 + 1)):
        if x == -sz1 or x == sz1 or y == -sz2 or y == sz2:
            for z in range(height):
                set_color_voxel(pos + vec3(x, z, y), 1, color, color_noise, 0.8)
            if (x + y) % 4 == 0 or (x + y) % 4 == 1:
                set_color_voxel(pos + vec3(x, height, y), 1, color, color_noise)
        set_color_voxel(pos + vec3(x, height - 2, y), 1, color, color_noise, 0.8)

然后我们中间建造 1 个大的,4 个角建造 4 个,就 ok 啦

然后我们建造四周的城墙来把它围起来,城墙其实不就一个 block 吗,直接创建一个矩形就 ok 了,给左下右上两个顶点,中间填充,当然城墙要比四周的要塞低一点。

1
2
3
4
5
6
@ti.func
def build_block(pos1, pos2, color, color_noise, prob=1, mat=1):
    x_min, y_min, z_min = min(pos1.x, pos2.x), min(pos1.y, pos2.y), min(pos1.z, pos2.z)
    x_max, y_max, z_max = max(pos1.x, pos2.x), max(pos1.y, pos2.y), max(pos1.z, pos2.z)
    for x, y, z in ti.ndrange((x_min, x_max + 1), (y_min, y_max + 1), (z_min, z_max + 1)):
        set_color_voxel(vec3(x, y, z), mat, color, color_noise, prob)

这城墙看起来有点脆弱…好像一碰就碎了,让他变厚点,也应该留出中间的位置让士兵可以在上边站岗,但是有要保护士兵不能被打到,所以和堡垒一样建造就可以了!原先的堡垒的面都是正方形,把他改成矩形,给他长宽的参数就可以建造出厚厚坚实的墙了!

好像有内味了~

来建一个城门吧,不然咋进来嘞?门就一个扇形和一个矩形,so easy~门框就是一个更大一圈的门嘛

1
2
3
4
    for i in ti.ndrange((d_ - 2, d_ + 3)):
        build_door(vec3(0, 6, i), 6, 4, vec3(0.6, 0.6, 0.6), vec3(0))
        build_door(vec3(0, 5, i), 5, 3, vec3(0, 0, 0), vec3(0), 1, 0)
    build_door(vec3(0, 5, d_), 5, 3, vec3(0.43, 0.352, 0.156), vec3(0))

看起来还不错,再加个地面吧,一层土一层草,顺便加一个门前的路~再给左右的塔开个窗户,其实就是删掉了一个 door

注入灵魂!!加上小火把!!

1
2
3
4
@ti.func
def build_fire(pos):
    scene.set_voxel(pos, 2, vec3(1, 1, 0))
    scene.set_voxel(pos + vec3(0, -1, 0), 1, vec3(0.43, 0.352, 0.156))

最后在加上个小小的门把手,大功告成!

最最最后,优化了一下光线和曝光,添加了夜景模式

一些其他的话

因为我确实是好久没有用 Taichi 了,之前在 B 站看过 Taichi 的图形课,可惜当时忙于组会和各种期末结课,并没有完成当时的大作业,现在还觉得有些可惜,正好有这次自己喜欢的东西也正好练手!我的代码有些地方确实写得不太好(主要还是感觉不太熟练,很多地方与 Python 的常用方法略有冲突,所以 Code 起来还是需要一些思维的转换