Creating Cactus Animation
3D Voxel Animation: Cactus
This guide walks you through how to generate a looping 3D voxel animation of cacti using SpatialStudio.
The script creates a desert scene with various cacti that sway gently and bloom with colorful flowers inside a cubic 3D space, then saves the animation to a .splv
file.
What this script does
-
Creates a 3D scene of size 128×128×128
-
Spawns 6 different cacti, each with:
- Segmented cylindrical stems
- Branching arms at various heights
- Colorful blooming flowers on top
- Subtle swaying motion in the wind
-
Adds a sandy desert floor for realism
-
Animates them swaying naturally for 10 seconds at 30 FPS
-
Outputs the file
cactus.splv
that you can play in your viewer
How it works (simplified)
-
Voxel volume Each frame is a 3D grid filled with RGBA values (
SIZE × SIZE × SIZE × 4
). -
Cactus stems Cacti are drawn as vertical cylinders with segmented texture patterns for realistic ridged appearance.
-
Branching arms Each cactus gets 1-3 horizontal arms that extend outward at different heights and angles.
-
Blooming flowers Colorful voxel clusters are placed on top of stems and arms to simulate desert flowers.
-
Desert floor Sandy-colored voxels create a textured ground surface with subtle height variations.
-
Wind animation A normalized time variable
t
creates gentle swaying motion that loops smoothly. -
Encoding Frames are passed into
splv.Encoder
, which writes them into the.splv
video file.
Try it yourself
Install requirements first:
pip install spatialstudio numpy tqdm
Then copy this script into cactus.py
and run:
python cactus.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 10
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/cactus.splv"
# Cactus settings
CACTUS_COUNT = 6
BASE_HEIGHT = 25
ARM_LENGTH = 12
def add_voxel(volume, x, y, z, color):
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
volume[x, y, z, :3] = color
volume[x, y, z, 3] = 255
def generate_desert_floor(volume):
sand_color = (194, 178, 128)
for x in range(SIZE):
for z in range(SIZE):
# Create subtle height variation
height_noise = int(np.sin(x*0.1) * np.cos(z*0.1) * 3)
floor_height = 15 + height_noise
for y in range(floor_height):
# Add texture variation to sand
brightness = 0.9 + 0.2 * np.sin(x*0.3 + z*0.2 + y*0.5)
final_color = tuple(int(c * brightness) for c in sand_color)
add_voxel(volume, x, y, z, final_color)
def generate_cactus_stem(volume, cx, cy, cz, height, t, sway_factor=1.0):
stem_color = (34, 139, 34) # Forest green
ridge_color = (28, 120, 28) # Darker green for ridges
for y in range(height):
# Calculate sway offset
sway_x = int(np.sin(t * 1.2 + cy * 0.05) * (y * 0.1) * sway_factor)
sway_z = int(np.cos(t * 0.8 + cy * 0.03) * (y * 0.08) * sway_factor)
# Draw cylindrical stem with ridges
for dx in range(-3, 4):
for dz in range(-3, 4):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= 3:
# Create vertical ridges
ridge = abs(dx) == 3 or abs(dz) == 3 or (abs(dx) == 2 and abs(dz) == 2)
color = ridge_color if ridge else stem_color
# Add segment texture
if y % 8 == 0:
color = tuple(max(20, c - 15) for c in color)
add_voxel(volume, cx + dx + sway_x, cy + y, cz + dz + sway_z, color)
def generate_cactus_arm(volume, cx, cy, cz, direction, length, t):
stem_color = (34, 139, 34)
arm_y = cy
for i in range(length):
# Calculate arm position with slight upward curve
curve = int(np.sin(i * 0.2) * 2)
sway = int(np.sin(t * 1.5 + i * 0.1) * 0.5)
if direction == 'x':
arm_x, arm_z = cx + i, cz + sway
else: # direction == 'z'
arm_x, arm_z = cx + sway, cz + i
# Draw arm cylinder (smaller than main stem)
for dx in range(-2, 3):
for dz in range(-2, 3):
if dx*dx + dz*dz <= 4:
add_voxel(volume, arm_x + dx, arm_y + curve, arm_z + dz, stem_color)
def generate_cactus_flower(volume, cx, cy, cz, color, t):
# Animated blooming effect
bloom_factor = 0.5 + 0.5 * np.sin(t * 2.0)
flower_size = int(3 * bloom_factor) + 1
for dx in range(-flower_size, flower_size + 1):
for dy in range(-2, 3):
for dz in range(-flower_size, flower_size + 1):
distance = np.sqrt(dx*dx + dy*dy*0.5 + dz*dz)
if distance <= flower_size:
# Add some sparkle effect
sparkle = int(np.sin(t * 5.0 + dx + dz) * 20)
final_color = tuple(min(255, max(0, c + sparkle)) for c in color)
add_voxel(volume, cx + dx, cy + dy, cz + dz, final_color)
def generate_single_cactus(volume, base_x, base_z, cactus_id, t):
flower_colors = [
(255, 100, 150), # Pink
(255, 200, 50), # Yellow
(255, 80, 80), # Red
(150, 100, 255), # Purple
(100, 255, 150), # Light green
(255, 150, 100), # Orange
]
# Vary cactus properties based on ID
height = BASE_HEIGHT + int(15 * np.sin(cactus_id * 1.3))
base_y = 15
# Generate main stem
generate_cactus_stem(volume, base_x, base_y, base_z, height, t)
# Add arms (not all cacti have arms)
if cactus_id % 2 == 0: # Even numbered cacti get arms
arm_height = height // 2 + int(5 * np.cos(cactus_id))
arm_y = base_y + arm_height
# Add 1-2 arms
if cactus_id % 4 == 0: # Some get two arms
generate_cactus_arm(volume, base_x, arm_y, base_z, 'x', ARM_LENGTH, t)
generate_cactus_arm(volume, base_x, arm_y - 8, base_z, 'z', ARM_LENGTH - 3, t)
# Flowers on arms
generate_cactus_flower(volume, base_x + ARM_LENGTH, arm_y + 2, base_z,
flower_colors[(cactus_id + 1) % len(flower_colors)], t)
generate_cactus_flower(volume, base_x, arm_y - 6, base_z + ARM_LENGTH - 3,
flower_colors[(cactus_id + 2) % len(flower_colors)], t)
else:
generate_cactus_arm(volume, base_x, arm_y, base_z, 'x', ARM_LENGTH, t)
# Flower on arm
generate_cactus_flower(volume, base_x + ARM_LENGTH, arm_y + 2, base_z,
flower_colors[(cactus_id + 1) % len(flower_colors)], t)
# Main flower on top
flower_color = flower_colors[cactus_id % len(flower_colors)]
generate_cactus_flower(volume, base_x, base_y + height + 2, base_z, flower_color, t)
def generate_cactus_garden(volume, t):
# Position cacti in a natural scattered pattern
positions = [
(CENTER_X - 25, CENTER_Z - 20),
(CENTER_X + 20, CENTER_Z - 25),
(CENTER_X - 15, CENTER_Z + 15),
(CENTER_X + 30, CENTER_Z + 10),
(CENTER_X - 35, CENTER_Z + 30),
(CENTER_X + 10, CENTER_Z - 35),
]
for i, (x, z) in enumerate(positions):
generate_single_cactus(volume, x, z, i, t)
def generate_scene(volume, t):
generate_desert_floor(volume)
generate_cactus_garden(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Growing cactus garden"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 2*np.pi
generate_scene(volume, t)
enc.encode(splv.Frame(volume, lrAxis="x", udAxis="y", fbAxis="z"))
enc.finish()
print(f"Created {OUT_PATH}")
Next steps
- Change
CACTUS_COUNT
to add more variety to your desert. - Edit
flower_colors
for different blooming patterns. - Modify
BASE_HEIGHT
to create taller or shorter cacti. - Add tumbleweeds by creating rolling sphere objects.
- Create a day/night cycle by adjusting the flower bloom timing.
- Add small desert creatures like lizards using similar voxel techniques.