Creating Pine Tree Animation
Pine Tree - 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a pine tree using SpatialStudio.
The script creates a detailed pine tree with layered branches that sway in the wind 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
- Builds 1 detailed pine tree with:
- A brown trunk with bark texture
- Multiple layers of green branches
- Natural wind-swaying animation
- Realistic proportions and tapering
- Animates gentle wind movement for 8 seconds at 30 FPS
- Outputs the file
pine_tree.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
). -
Tree trunk The trunk is drawn as a tapered cylinder with brown bark texture that gets thinner toward the top.
-
Branch layers Pine branches are generated in horizontal layers, each smaller than the one below, creating the classic cone shape.
-
Wind animation Branches sway using sine waves with different frequencies to simulate natural wind movement.
-
Texture details Bark texture is added to the trunk, and branch density varies to look more organic.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the swaying motion 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 pine_tree.py
and run:
python pine_tree.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 8
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/pine_tree.splv"
# Tree settings
TREE_HEIGHT = 60
TRUNK_HEIGHT = 20
TRUNK_BASE_RADIUS = 4
BRANCH_LAYERS = 12
MAX_BRANCH_RADIUS = 20
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_trunk(volume, cx, cy, cz, t):
trunk_colors = [(101, 67, 33), (139, 69, 19), (160, 82, 45)]
for y in range(TRUNK_HEIGHT):
# Trunk tapers slightly toward the top
radius = max(2, TRUNK_BASE_RADIUS - (y * 0.1))
# Add slight sway to trunk
sway_x = int(np.sin(t * 0.8) * (y * 0.05))
sway_z = int(np.cos(t * 0.6) * (y * 0.03))
for dx in range(-int(radius), int(radius)+1):
for dz in range(-int(radius), int(radius)+1):
if dx*dx + dz*dz <= radius*radius:
# Add bark texture
texture = int(np.sin(dx*0.5 + dz*0.3 + y*0.2) * 2)
color_idx = (texture + 1) % len(trunk_colors)
color = trunk_colors[color_idx]
add_voxel(volume,
cx + dx + sway_x,
cy - TRUNK_HEIGHT + y,
cz + dz + sway_z,
color)
def generate_branch_layer(volume, cx, cy, cz, layer_y, layer_radius, t):
branch_colors = [(34, 139, 34), (0, 100, 0), (46, 125, 50), (76, 175, 80)]
# Wind effect - higher layers sway more
wind_intensity = (layer_y / TREE_HEIGHT) * 0.3
wind_x = np.sin(t * 1.2 + layer_y * 0.1) * wind_intensity * 8
wind_z = np.cos(t * 0.9 + layer_y * 0.15) * wind_intensity * 6
for angle in np.linspace(0, 2*np.pi, int(layer_radius * 3)):
for r in range(1, int(layer_radius)):
# Create irregular branch edges
r_variation = r + np.sin(angle * 4 + t * 0.5) * 1.5
if r_variation <= 0:
continue
x = int(r_variation * np.cos(angle) + wind_x)
z = int(r_variation * np.sin(angle) + wind_z)
# Add some vertical variation to branches
for dy in range(-2, 3):
if np.random.random() > 0.3: # Not completely dense
# Vary branch color for realism
color_variation = int(np.sin(angle + r + layer_y) * 2)
color = branch_colors[color_variation % len(branch_colors)]
add_voxel(volume, cx + x, cy + layer_y + dy, cz + z, color)
def generate_pine_tree(volume, cx, cy, cz, t):
# Generate trunk
generate_trunk(volume, cx, cy, cz, t)
# Generate branch layers from bottom to top
for layer in range(BRANCH_LAYERS):
# Layer position - start above trunk and go up
layer_y = -TRUNK_HEIGHT + 5 + (layer * (TREE_HEIGHT - TRUNK_HEIGHT) // BRANCH_LAYERS)
# Branch radius - larger at bottom, smaller at top (cone shape)
progress = layer / BRANCH_LAYERS
layer_radius = MAX_BRANCH_RADIUS * (1.0 - progress * 0.8)
# Skip very small top layers
if layer_radius > 2:
generate_branch_layer(volume, cx, cy, cz, layer_y, layer_radius, t)
def generate_scene(volume, t):
# Generate single pine tree at center
generate_pine_tree(volume, CENTER_X, CENTER_Y + 20, CENTER_Z, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Growing pine tree"):
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
- Adjust
BRANCH_LAYERS
to make the tree more or less detailed - Change
MAX_BRANCH_RADIUS
to create wider or narrower trees - Modify wind intensity by changing the multipliers in the wind calculations
- Add multiple trees by calling
generate_pine_tree()
with different positions - Experiment with
branch_colors
for autumn or winter effects - Add snow by placing white voxels on top of branches