Creating Wooden Planks Animation
3D Voxel Animation: Wooden Planks
This guide walks you through how to generate a looping 3D voxel animation of wooden planks using SpatialStudio.
The script creates realistic wooden planks with grain textures that gently sway and shift 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 wooden planks, each with:
- Realistic wood grain patterns
- Natural brown color variations
- Subtle weathering effects
- Metal bolts and hardware details
- Animates them with gentle swaying motion for 8 seconds at 30 FPS
- Outputs the file
wooden_planks.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
). -
Wood grain texture Planks feature realistic wood grain using noise functions and layered sine waves to simulate natural patterns.
-
Color variation Multiple shades of brown create depth, with darker areas for grain lines and lighter spots for worn areas.
-
Metal hardware Each plank gets metal bolts and corner brackets with metallic gray coloring for authenticity.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating 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 wooden_planks.py
and run:
python wooden_planks.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/wooden_planks.splv"
# Plank settings
PLANK_COUNT = 6
PLANK_WIDTH = 8
PLANK_HEIGHT = 4
PLANK_LENGTH = 35
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 wood_grain_noise(x, y, z, t):
"""Generate wood grain pattern using layered noise"""
grain1 = np.sin(y * 0.3 + t * 0.1) * 0.3
grain2 = np.sin(y * 0.15 + x * 0.1) * 0.2
grain3 = np.sin(y * 0.45 + z * 0.05 + t * 0.05) * 0.15
return grain1 + grain2 + grain3
def get_wood_color(base_x, base_y, base_z, t):
"""Generate realistic wood color with grain"""
# Base wood colors (different browns)
base_colors = [
(139, 90, 43), # Dark brown
(160, 100, 50), # Medium brown
(180, 120, 70), # Light brown
(120, 80, 40), # Very dark brown
]
# Get wood grain influence
grain = wood_grain_noise(base_x, base_y, base_z, t)
# Select base color with some variation
color_index = int(abs(grain * 10) % len(base_colors))
base_r, base_g, base_b = base_colors[color_index]
# Apply grain darkening/lightening
multiplier = 1.0 + grain * 0.4
r = max(20, min(255, int(base_r * multiplier)))
g = max(15, min(255, int(base_g * multiplier)))
b = max(10, min(255, int(base_b * multiplier)))
return (r, g, b)
def generate_plank_body(volume, cx, cy, cz, width, height, length, rotation, t):
"""Generate a wooden plank with grain texture"""
half_w, half_h, half_l = width//2, height//2, length//2
cos_r, sin_r = np.cos(rotation), np.sin(rotation)
for dx in range(-half_l, half_l+1):
for dy in range(-half_h, half_h+1):
for dz in range(-half_w, half_w+1):
# Apply rotation around Y axis
rx = int(dx * cos_r - dz * sin_r)
rz = int(dx * sin_r + dz * cos_r)
final_x = cx + rx
final_y = cy + dy
final_z = cz + rz
# Get wood color for this position
wood_color = get_wood_color(final_x, final_y, final_z, t)
add_voxel(volume, final_x, final_y, final_z, wood_color)
def generate_metal_hardware(volume, cx, cy, cz, width, height, length, rotation, t):
"""Add metal bolts and corner brackets to planks"""
half_w, half_h, half_l = width//2, height//2, length//2
cos_r, sin_r = np.cos(rotation), np.sin(rotation)
metal_color = (105, 105, 105) # Dark gray metal
# Corner bolts
bolt_positions = [
(-half_l+2, 0, -half_w+1),
(-half_l+2, 0, half_w-1),
(half_l-2, 0, -half_w+1),
(half_l-2, 0, half_w-1),
]
for bx, by, bz in bolt_positions:
# Apply rotation
rx = int(bx * cos_r - bz * sin_r)
rz = int(bx * sin_r + bz * cos_r)
# Small bolt head
for dx in range(-1, 2):
for dy in range(-1, 2):
for dz in range(-1, 2):
if dx*dx + dy*dy + dz*dz <= 2:
final_x = cx + rx + dx
final_y = cy + by + dy + half_h + 1
final_z = cz + rz + dz
add_voxel(volume, final_x, final_y, final_z, metal_color)
def generate_plank_arrangement(volume, t):
"""Generate multiple wooden planks in a stack arrangement"""
plank_spacing = 6
for i in range(PLANK_COUNT):
# Stack planks with slight offset
plank_y = CENTER_Y - (PLANK_COUNT * plank_spacing // 2) + (i * plank_spacing)
plank_x = CENTER_X + int(np.sin(t * 0.5 + i * 0.8) * 3)
plank_z = CENTER_Z + int(np.cos(t * 0.3 + i * 0.5) * 2)
# Slight rotation for natural look
rotation = (i * 0.2) + np.sin(t * 0.4 + i * 0.6) * 0.1
# Vary plank dimensions slightly
width = PLANK_WIDTH + (i % 3) - 1
height = PLANK_HEIGHT
length = PLANK_LENGTH + int(np.sin(i * 1.2) * 5)
generate_plank_body(volume, plank_x, plank_y, plank_z, width, height, length, rotation, t)
generate_metal_hardware(volume, plank_x, plank_y, plank_z, width, height, length, rotation, t)
def generate_wood_debris(volume, t):
"""Add small wood chips and sawdust for realism"""
debris_color = (101, 67, 33) # Dark wood debris
for i in range(20):
# Random positions around the planks
debris_x = CENTER_X + int(np.sin(t * 1.2 + i * 2.1) * 25)
debris_y = CENTER_Y - 20 + int(np.cos(t * 0.8 + i * 1.7) * 5)
debris_z = CENTER_Z + int(np.sin(t * 0.9 + i * 1.3) * 20)
# Small debris pieces
if np.sin(t * 2.0 + i) > 0.7: # Only show some debris
add_voxel(volume, debris_x, debris_y, debris_z, debris_color)
# Occasionally add a slightly larger piece
if i % 5 == 0:
add_voxel(volume, debris_x+1, debris_y, debris_z, debris_color)
def generate_scene(volume, t):
generate_plank_arrangement(volume, t)
generate_wood_debris(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating wooden planks"):
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
PLANK_COUNT
to create more or fewer planks in your stack. - Modify the
base_colors
array to experiment with different wood types (oak, pine, mahogany). - Adjust
plank_spacing
to make planks more tightly packed or spread apart. - Add more metal hardware by expanding the
bolt_positions
array. - Create weathered effects by adding more color variation in
get_wood_color()
. - Make planks fall or rotate by modifying the position calculations with time-based offsets.