Creating Fern Animation
3D Voxel Animation: Fern
This guide walks you through how to generate a looping 3D voxel animation of a fern using SpatialStudio.
The script creates a realistic fern plant that gently sways and grows 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
- Generates a procedural fern with:
- A brown woody stem/trunk
- Multiple branching fronds with natural curves
- Gradient green coloring from dark to bright
- Subtle swaying motion in the wind
- Animates the fern swaying naturally for 8 seconds at 30 FPS
- Outputs the file
fern.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
). -
Stem structure The main trunk is drawn as a vertical brown cylinder that gets thicker toward the base.
-
Frond generation Fern fronds are created using fractal-like branching patterns, with smaller leaflets growing off main branches.
-
Natural colors The fern uses a gradient from dark forest green at the base to bright green at the tips of fronds.
-
Swaying animation A gentle wind effect makes the fronds sway using sine waves, with different frequencies for natural movement.
-
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 fern.py
and run:
python fern.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/fern.splv"
# Fern settings
STEM_HEIGHT = 45
FROND_COUNT = 12
MAX_FROND_LENGTH = 25
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_stem(volume, cx, cy, cz, t):
stem_color = (101, 67, 33) # Brown
sway_base = np.sin(t * 0.8) * 1.5
for i in range(STEM_HEIGHT):
progress = i / STEM_HEIGHT
# Stem gets thicker at base and sways more at top
thickness = max(1, int(3 * (1 - progress * 0.7)))
sway = sway_base * progress * progress
stem_x = int(cx + sway)
stem_y = cy - (SIZE // 3) + i
stem_z = cz
# Draw stem with thickness
for dx in range(-thickness, thickness + 1):
for dz in range(-thickness, thickness + 1):
if dx*dx + dz*dz <= thickness*thickness:
add_voxel(volume, stem_x + dx, stem_y, stem_z + dz, stem_color)
def generate_frond(volume, start_x, start_y, start_z, angle, height_offset, t):
# Color gradient from dark to bright green
base_green = (34, 87, 34)
tip_green = (50, 205, 50)
frond_length = MAX_FROND_LENGTH + int(5 * np.sin(height_offset * 0.3))
for i in range(frond_length):
progress = i / frond_length
# Main frond curve
curve = np.sin(progress * np.pi) * 0.3
wind_sway = np.sin(t * 1.2 + height_offset * 0.1 + angle) * progress * 3
# Position along frond
fx = int(start_x + np.cos(angle) * i * 1.5 + wind_sway)
fy = int(start_y + curve * 8 + np.sin(t * 0.7 + i * 0.1) * progress * 2)
fz = int(start_z + np.sin(angle) * i * 1.5)
# Color interpolation
color = tuple(int(base_green[j] + (tip_green[j] - base_green[j]) * progress) for j in range(3))
# Main frond stem
add_voxel(volume, fx, fy, fz, color)
# Generate leaflets along the frond
if i % 3 == 0 and i > 2:
leaflet_count = max(1, int(6 * (1 - progress)))
for side in [-1, 1]:
for j in range(leaflet_count):
leaflet_progress = j / max(1, leaflet_count - 1)
# Leaflet positions
lx = fx + int(side * (j + 1) * np.cos(angle + np.pi/2))
ly = fy - j
lz = fz + int(side * (j + 1) * np.sin(angle + np.pi/2))
# Leaflet color (slightly darker)
leaflet_color = tuple(max(0, int(c * (0.8 + leaflet_progress * 0.2))) for c in color)
add_voxel(volume, lx, ly, lz, leaflet_color)
def generate_fern(volume, cx, cy, cz, t):
# Generate main stem
generate_stem(volume, cx, cy, cz, t)
# Generate fronds at different heights and angles
for i in range(FROND_COUNT):
height_progress = i / FROND_COUNT
height_offset = int(STEM_HEIGHT * height_progress * 0.8)
# Spiral arrangement of fronds
angle = (i / FROND_COUNT) * 4 * np.pi + t * 0.1
# Frond starting position
sway = np.sin(t * 0.8) * height_progress * height_progress * 1.5
start_x = int(cx + sway)
start_y = cy - (SIZE // 3) + height_offset + 10
start_z = cz
generate_frond(volume, start_x, start_y, start_z, angle, height_offset, t)
def add_ground_detail(volume, cx, cy, cz):
# Add some ground/soil detail
ground_color = (139, 115, 85) # Sandy brown
ground_y = cy - (SIZE // 3) - 5
for dx in range(-8, 9):
for dz in range(-8, 9):
if dx*dx + dz*dz <= 64: # Circular ground patch
for dy in range(3):
if np.random.random() > 0.3: # Sparse ground detail
add_voxel(volume, cx + dx, ground_y + dy, cz + dz, ground_color)
def generate_scene(volume, t):
generate_fern(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
if t == 0: # Only add ground detail once
add_ground_detail(volume, CENTER_X, CENTER_Y, CENTER_Z)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating fern"):
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
FROND_COUNT
to make the fern bushier or sparser - Modify the color gradients in
base_green
andtip_green
for different fern varieties - Change the swaying frequency by adjusting the multipliers in the
np.sin(t * ...)
expressions - Add multiple ferns by calling
generate_fern()
with different center positions - Experiment with
MAX_FROND_LENGTH
to create larger or smaller fern varieties