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 起来还是需要一些思维的转换