Creating Stone Wall Animation
This guide walks you through how to generate a looping 3D voxel animation of a stone wall using SpatialStudio.
The script creates a weathered stone wall with realistic textures, subtle movements, and atmospheric effects 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 stone wall with:
- Individual stone blocks with varied textures
- Realistic weathering and color variations
- Subtle mortar between stones
- Gentle swaying motion to simulate wind
- Animates subtle movements for 8 seconds at 30 FPS
- Outputs the file
stone_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
). -
Stone blocks Individual stones are drawn as rounded rectangles with noise-based texture variations for realism.
-
Mortar joints Gray mortar fills the gaps between stones, creating authentic masonry appearance.
-
Texture variation Each stone gets unique color variations and surface roughness using procedural noise.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating gentle swaying and texture shifts. -
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 stone_wall.py
and run:
python stone_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/stone_wall.splv"
# Wall settings
WALL_WIDTH = 80
WALL_HEIGHT = 60
WALL_DEPTH = 12
STONE_HEIGHT = 8
STONE_WIDTH_MIN = 12
STONE_WIDTH_MAX = 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 noise_3d(x, y, z, scale=0.1):
"""Simple 3D noise function for texture variation"""
return (np.sin(x * scale) * np.cos(y * scale) * np.sin(z * scale) + 1) / 2
def generate_stone_block(volume, start_x, start_y, start_z, width, height, depth, stone_id, t):
"""Generate a single stone block with texture"""
base_colors = [
(120, 120, 110), # Gray stone
(130, 125, 115), # Light gray
(110, 105, 95), # Dark gray
(125, 115, 100), # Brownish gray
(115, 110, 105), # Medium gray
]
base_color = base_colors[stone_id % len(base_colors)]
# Add gentle movement
sway_x = int(np.sin(t * 0.5 + stone_id * 0.3) * 0.5)
sway_z = int(np.cos(t * 0.7 + stone_id * 0.2) * 0.3)
for dx in range(width):
for dy in range(height):
for dz in range(depth):
x = start_x + dx + sway_x
y = start_y + dy
z = start_z + dz + sway_z
# Create rounded edges
edge_factor = 1.0
if dx == 0 or dx == width-1 or dy == 0 or dy == height-1:
edge_factor = 0.9
if dz == 0 or dz == depth-1:
edge_factor *= 0.95
# Add texture noise
noise_val = noise_3d(x + stone_id*10, y, z + t*2, 0.3)
texture_factor = 0.8 + 0.4 * noise_val
# Combine factors
final_factor = edge_factor * texture_factor
final_color = tuple(int(c * final_factor) for c in base_color)
add_voxel(volume, x, y, z, final_color)
def generate_mortar(volume, start_x, start_y, start_z, width, height, depth):
"""Generate mortar between stones"""
mortar_color = (90, 85, 80)
for dx in range(width):
for dy in range(height):
for dz in range(depth):
x = start_x + dx
y = start_y + dy
z = start_z + dz
add_voxel(volume, x, y, z, mortar_color)
def generate_wall_pattern(volume, t):
"""Generate the complete stone wall with mortar"""
wall_start_x = CENTER_X - WALL_WIDTH // 2
wall_start_y = CENTER_Y - WALL_HEIGHT // 2
wall_start_z = CENTER_Z - WALL_DEPTH // 2
stone_id = 0
current_y = wall_start_y
# Build wall row by row
while current_y < wall_start_y + WALL_HEIGHT:
current_x = wall_start_x
row_height = min(STONE_HEIGHT, wall_start_y + WALL_HEIGHT - current_y)
# Offset every other row for realistic brick pattern
if (current_y - wall_start_y) // STONE_HEIGHT % 2 == 1:
current_x += STONE_WIDTH_MIN // 2
while current_x < wall_start_x + WALL_WIDTH:
# Random stone width
stone_width = STONE_WIDTH_MIN + (stone_id * 7) % (STONE_WIDTH_MAX - STONE_WIDTH_MIN)
stone_width = min(stone_width, wall_start_x + WALL_WIDTH - current_x - 2)
if stone_width < 6: # Skip very small stones
break
# Generate mortar first (1 voxel thick)
if current_x > wall_start_x:
generate_mortar(volume, current_x, current_y, wall_start_z,
1, row_height, WALL_DEPTH)
current_x += 1
if current_y > wall_start_y:
generate_mortar(volume, current_x, current_y, wall_start_z,
stone_width, 1, WALL_DEPTH)
# Generate stone block
stone_y = current_y + (1 if current_y > wall_start_y else 0)
stone_height = row_height - (1 if current_y > wall_start_y else 0)
generate_stone_block(volume, current_x, stone_y, wall_start_z + 1,
stone_width, stone_height, WALL_DEPTH - 2, stone_id, t)
current_x += stone_width
stone_id += 1
current_y += row_height
def add_atmospheric_effects(volume, t):
"""Add subtle atmospheric effects like dust particles"""
dust_color = (200, 195, 190)
for i in range(20): # Add some floating dust particles
x = int(CENTER_X + 30 * np.sin(t * 0.3 + i * 0.5))
y = int(CENTER_Y + 20 * np.cos(t * 0.4 + i * 0.3) + i * 2)
z = int(CENTER_Z + 15 * np.sin(t * 0.2 + i * 0.7))
if abs(y - CENTER_Y) < 25: # Keep dust near wall level
add_voxel(volume, x, y, z, dust_color)
def generate_scene(volume, t):
"""Generate the complete stone wall scene"""
generate_wall_pattern(volume, t)
add_atmospheric_effects(volume, t)
# Initialize encoder
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
# Generate frames
for frame in tqdm(range(FRAMES), desc="Building stone 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
WALL_WIDTH
andWALL_HEIGHT
to change wall dimensions. - Modify
base_colors
to create walls with different stone types. - Increase
STONE_WIDTH_MAX
for larger, more irregular stones. - Add
ivy
ormoss
by introducing green voxels with organic growth patterns. - Create a
crumbling
effect by randomly removing some stone voxels over time.