Creating Brick Wall Animation
Creating a 3D Voxel Brick Wall Animation
This guide walks you through how to generate a looping 3D voxel animation of a brick wall using SpatialStudio.
The script creates a realistic brick wall with weathering effects, mortar joints, and subtle aging animations 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 a structured brick wall with:
- Individual brick blocks with realistic proportions
- Mortar joints between bricks
- Weathering and aging effects
- Subtle color variations for realism
- Animates subtle weathering and lighting changes for 8 seconds at 30 FPS
- Outputs the file
brick_wall.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
). -
Brick layout Bricks are arranged in a traditional offset pattern with proper mortar spacing.
-
Mortar joints Gray mortar fills the gaps between bricks, creating realistic joint lines.
-
Weathering effects Random variations in brick color and subtle aging patterns make the wall look authentic.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating subtle lighting and weathering changes. -
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 brick_wall.py
and run:
python brick_wall.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/brick_wall.splv"
# Brick wall settings
BRICK_WIDTH = 12
BRICK_HEIGHT = 6
BRICK_DEPTH = 8
MORTAR_THICKNESS = 2
WALL_WIDTH = 80
WALL_HEIGHT = 80
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 get_brick_color(base_color, x, y, z, t):
# Add weathering and variation
noise = np.sin(x * 0.1 + y * 0.15 + z * 0.2 + t * 0.3) * 0.1
age_factor = np.sin(x * 0.05 + y * 0.08 + t * 0.5) * 0.15
r, g, b = base_color
r = max(0, min(255, int(r * (1 + noise + age_factor))))
g = max(0, min(255, int(g * (1 + noise * 0.5 + age_factor * 0.7))))
b = max(0, min(255, int(b * (1 + noise * 0.3 + age_factor * 0.5))))
return (r, g, b)
def generate_brick(volume, start_x, start_y, start_z, brick_id, t):
# Base brick colors with variation
brick_colors = [
(180, 65, 45), # Classic red brick
(165, 75, 55), # Darker red
(195, 85, 65), # Lighter red
(170, 60, 40), # Deep red
]
base_color = brick_colors[brick_id % len(brick_colors)]
for dx in range(BRICK_WIDTH):
for dy in range(BRICK_HEIGHT):
for dz in range(BRICK_DEPTH):
x, y, z = start_x + dx, start_y + dy, start_z + dz
# Add texture to brick surface
if dx == 0 or dx == BRICK_WIDTH-1 or dy == 0 or dy == BRICK_HEIGHT-1:
# Edge highlighting
edge_brightness = 1.1 + 0.05 * np.sin(t * 2 + dx + dy)
color = tuple(min(255, int(c * edge_brightness)) for c in base_color)
else:
color = get_brick_color(base_color, x, y, z, t)
add_voxel(volume, x, y, z, color)
def generate_mortar(volume, x, y, z, t):
# Mortar color with slight variation
base_gray = 120
variation = int(15 * np.sin(x * 0.1 + y * 0.1 + t * 0.2))
gray_value = max(100, min(140, base_gray + variation))
mortar_color = (gray_value, gray_value, gray_value)
add_voxel(volume, x, y, z, mortar_color)
def generate_wall_structure(volume, t):
wall_start_x = CENTER_X - WALL_WIDTH // 2
wall_start_y = CENTER_Y - WALL_HEIGHT // 2
wall_start_z = CENTER_Z - BRICK_DEPTH // 2
brick_id = 0
# Generate brick rows
y = wall_start_y
row = 0
while y < wall_start_y + WALL_HEIGHT:
x = wall_start_x
# Offset every other row for traditional brick pattern
if row % 2 == 1:
x += BRICK_WIDTH // 2
while x < wall_start_x + WALL_WIDTH:
# Check if we have space for a full brick
if x + BRICK_WIDTH <= wall_start_x + WALL_WIDTH and y + BRICK_HEIGHT <= wall_start_y + WALL_HEIGHT:
generate_brick(volume, x, y, wall_start_z, brick_id, t)
brick_id += 1
x += BRICK_WIDTH + MORTAR_THICKNESS
y += BRICK_HEIGHT + MORTAR_THICKNESS
row += 1
def generate_mortar_joints(volume, t):
wall_start_x = CENTER_X - WALL_WIDTH // 2
wall_start_y = CENTER_Y - WALL_HEIGHT // 2
wall_start_z = CENTER_Z - BRICK_DEPTH // 2
# Fill mortar joints
for x in range(wall_start_x, wall_start_x + WALL_WIDTH):
for y in range(wall_start_y, wall_start_y + WALL_HEIGHT):
for z in range(wall_start_z, wall_start_z + BRICK_DEPTH):
# Check if this position should be mortar
brick_x = (x - wall_start_x) % (BRICK_WIDTH + MORTAR_THICKNESS)
brick_y = (y - wall_start_y) % (BRICK_HEIGHT + MORTAR_THICKNESS)
if brick_x >= BRICK_WIDTH or brick_y >= BRICK_HEIGHT:
generate_mortar(volume, x, y, z, t)
def add_wall_details(volume, t):
# Add some weathering stains and moss effects
wall_start_x = CENTER_X - WALL_WIDTH // 2
wall_start_y = CENTER_Y - WALL_HEIGHT // 2
wall_start_z = CENTER_Z - BRICK_DEPTH // 2
for i in range(20): # Add some random weathering spots
spot_x = wall_start_x + int((i * 13.7) % WALL_WIDTH)
spot_y = wall_start_y + int((i * 7.3) % WALL_HEIGHT)
# Weathering stain
stain_size = 3 + int(2 * np.sin(t * 0.5 + i))
for dx in range(-stain_size, stain_size):
for dy in range(-stain_size, stain_size):
if dx*dx + dy*dy <= stain_size*stain_size:
x, y = spot_x + dx, spot_y + dy
z = wall_start_z - 1 # Slightly in front
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
# Dark weathering stain
darkness = 0.7 + 0.1 * np.sin(t + dx + dy)
if i % 5 == 0:
# Moss effect (green tint)
color = (int(60 * darkness), int(80 * darkness), int(40 * darkness))
else:
# Dirt/water stain
gray = int(80 * darkness)
color = (gray, gray, gray)
add_voxel(volume, x, y, z, color)
def generate_scene(volume, t):
generate_mortar_joints(volume, t)
generate_wall_structure(volume, t)
add_wall_details(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Building brick wall"):
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
BRICK_WIDTH
,BRICK_HEIGHT
to create different brick proportions. - Modify
brick_colors
array to use different brick color palettes. - Increase
WALL_WIDTH
andWALL_HEIGHT
for larger walls. - Add more weathering effects by expanding the
add_wall_details
function. - Create curved walls by modifying the brick positioning logic.