Voxel Pac-man#
早在很久以前就看到了 Taichi
的 voxel-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
| 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 也是一个球,那么主要的元素就都已经完成了。嘴巴张开怎么做呢?只需要把 voxel
的 mat
设置成 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 起来还是需要一些思维的转换