Preface

I’ve already shared two creations before: Taichi Voxel Challenge 20221

Recently, after finishing a few urgent tasks at hand, I got the itch to create something new hhh. I had several ideas, but ultimately decided to go with the PVZ theme first.

The PVZ project is still a WIP; I plan to refine it further later. If time permits, I might create a few more, but I’m about to defend my thesis 555.

Design Concept

Currently, the PVZ project only features the simplest Peashooter, but I encountered many issues during the process. First, let’s analyze the Peashooter’s structure!

The Peashooter roughly consists of the following parts: the main cannon barrel, eyes, stem, the bud at the back, and the leaves at the bottom.

Main Cannon Barrel: We can further break this down. It can be viewed as a cylinder, but with curved walls. Seems simple, right? We could directly represent the geometry using SDFs and build it! But I don’t know SDFs (x). Actually, this approach has some issues because the muzzle should be positioned lower, so I simply decomposed it into a sphere plus a few circles.

Eyes: No difficulty here. Simply subtract a rectangle from a sphere, place a black rectangle for the eye, and add a white rectangle for the highlight.

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))

Stem: This stem’s curve stumped me; I don’t know Bezier curves. So, I decided to simplify things and just use a sine function hhh, inspired by a work from a certain dragon 233. I defined the amplitude and only selected the parameter range of [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)

The Bud at the Back: Simply draw this as a curve.

Leaves at the Bottom: The original Peashooter has three leaves at the bottom—two large and one small—but that’s too complex. Here, I simplified it by drawing four leaves in four directions. However, leaves are irregular shapes, which are hard to represent… Then I had a flash of inspiration: draw one leaf ()! This looks very much like the overlapping region of two diagonal semicircles. So, I defined a starting point and direction, then used top-left/bottom-right or top-right/bottom-left as the centers and checked for the overlapping region. But this results in a flat shape. No problem! For the z-coordinate, I used the old method again: simply add two sine functions regarding x and y! (So clever, me)

 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)

That concludes the Peashooter. The code to build the Peashooter is as follows:

 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)

The rest is the lawn. Due to the grid limit, I couldn’t implement the original 6 * 9 setup, nor could I draw the fences on all four sides, so I just created a 6 * 6 scene. After finishing, it felt a bit off, so I added noise around the lawn to fix it.

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)

Finally, the line count was slightly over, but after compressing it a bit, it’s down to 99 lines!

Taichi Voxel Peashooter Creation
Another View of the Voxel Peashooter

Conclusion

There are a few issues I still haven’t resolved –. First, I don’t know how to swap the dimensions of vec3 within @ti.func, for example, turning vec3(x, y, z) into vec3(z, y, x). Otherwise, I could have saved a few lines when building circles, as I was aiming for multi-directional circles, but I ultimately chose the most brute-force if algorithm.

Another small detail is that for circles and spheres, to make the circles rounder, I had to relax the boundary conditions, such as x * x + y * y <= r * r + eps. When dealing with spheres, sometimes an extra point appears at the top, so tightening the boundary conditions works better in that case.