Creating Crystallization Animation
Crystallization Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of crystallization using SpatialStudio.
The script creates growing crystal formations that emerge, shimmer, and evolve 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 crystal clusters, each with:
- A geometric crystal structure with faceted surfaces
- Growing formations that emerge over time
- Translucent surfaces with internal light refraction
-
Animates the crystallization process for 10 seconds at 30 FPS
-
Outputs the file
crystallization.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
). -
Crystal geometry Crystals are built using mathematical functions to create angular, faceted structures that grow outward from seed points.
-
Growth animation Each crystal starts small and expands over time, with different growth rates for realistic formation.
-
Surface effects Translucent surfaces with varying opacity create depth, while bright highlights simulate light refraction.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with crystals growing and shrinking in a smooth loop. -
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 crystallization.py
and run:
python crystallization.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 10
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/crystallization.splv"
# Crystal settings
CRYSTAL_COUNT = 6
MAX_CRYSTAL_SIZE = 18
GROWTH_SPEED = 1.5
def add_voxel(volume, x, y, z, color, alpha=255):
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
volume[x, y, z, :3] = color
volume[x, y, z, 3] = alpha
def crystal_shape(x, y, z, size):
# Create angular crystal shape using diamond/octahedron geometry
distance = (abs(x) + abs(y) + abs(z)) / 1.5
facet_distance = max(abs(x), abs(y), abs(z))
return distance <= size and facet_distance <= size * 0.8
def generate_crystal_core(volume, cx, cy, cz, color, size, t):
# Generate the main crystal body with faceted surfaces
for dx in range(-int(size), int(size)+1):
for dy in range(-int(size), int(size)+1):
for dz in range(-int(size), int(size)+1):
if crystal_shape(dx, dy, dz, size):
# Add surface variation based on facets
facet_intensity = abs(dx) + abs(dy) + abs(dz)
brightness = 0.7 + 0.3 * np.sin(facet_intensity * 0.5 + t)
final_color = tuple(min(255, int(c * brightness)) for c in color)
# Make edges more translucent
edge_distance = min(abs(dx), abs(dy), abs(dz))
alpha = int(120 + 135 * (edge_distance / size))
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color, alpha)
def generate_crystal_spikes(volume, cx, cy, cz, color, size, t):
# Generate smaller crystal spikes growing from the main body
directions = [(1,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)]
for i, (dx, dy, dz) in enumerate(directions):
spike_length = int(size * 0.6 * (1 + 0.3 * np.sin(t + i)))
spike_thickness = max(1, int(size * 0.2))
for length in range(int(size * 0.7), int(size * 0.7) + spike_length):
for thickness in range(-spike_thickness, spike_thickness + 1):
for thickness2 in range(-spike_thickness, thickness_thickness + 1):
if thickness*thickness + thickness2*thickness2 <= spike_thickness*spike_thickness:
spike_x = cx + dx * length + (dy if dx == 0 else 0) * thickness + (dz if dx == 0 and dy == 0 else 0) * thickness2
spike_y = cy + dy * length + (dx if dy == 0 else 0) * thickness + (dz if dy == 0 and dx == 0 else 0) * thickness2
spike_z = cz + dz * length + (dx if dz == 0 else 0) * thickness + (dy if dz == 0 and dx == 0 else 0) * thickness2
brightness = 0.8 + 0.2 * (length / (size + spike_length))
final_color = tuple(min(255, int(c * brightness)) for c in color)
add_voxel(volume, spike_x, spike_y, spike_z, final_color, 180)
def generate_crystal_highlights(volume, cx, cy, cz, size, t):
# Add bright highlights to simulate internal light refraction
highlight_count = 8
for i in range(highlight_count):
angle1 = (i / highlight_count) * 2 * np.pi + t * 0.5
angle2 = np.sin(t * 0.7 + i) * 0.5
highlight_radius = size * 0.4
hx = cx + int(highlight_radius * np.cos(angle1) * np.cos(angle2))
hy = cy + int(highlight_radius * np.sin(angle2))
hz = cz + int(highlight_radius * np.sin(angle1) * np.cos(angle2))
# Create small bright spots
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 <= 1:
brightness = int(200 + 55 * np.sin(t * 2 + i))
add_voxel(volume, hx+dx, hy+dy, hz+dz, (brightness, brightness, 255), 200)
def generate_crystal_cluster(volume, t):
# Crystal colors - cool tones like real minerals
colors = [
(100, 200, 255), # Ice blue
(150, 255, 200), # Mint green
(200, 150, 255), # Lavender
(255, 200, 150), # Warm amber
(150, 255, 255), # Cyan
(255, 180, 200), # Rose quartz
]
positions = [
(0.3, 0.7, 0.4), (0.7, 0.3, 0.6), (0.5, 0.5, 0.2),
(0.2, 0.4, 0.8), (0.8, 0.6, 0.3), (0.6, 0.8, 0.7)
]
for i in range(CRYSTAL_COUNT):
# Position crystals around the scene
px, py, pz = positions[i]
cx = int(SIZE * px)
cy = int(SIZE * py)
cz = int(SIZE * pz)
# Animate crystal growth with different phases
growth_phase = (t + i * np.pi / 3) % (2 * np.pi)
growth_factor = (np.sin(growth_phase * GROWTH_SPEED) + 1) / 2 # 0 to 1
size = MAX_CRYSTAL_SIZE * (0.3 + 0.7 * growth_factor)
color = colors[i % len(colors)]
# Generate crystal components
if size > 3: # Only draw if crystal is big enough
generate_crystal_core(volume, cx, cy, cz, color, size, t)
if size > 8:
generate_crystal_spikes(volume, cx, cy, cz, color, size, t)
generate_crystal_highlights(volume, cx, cy, cz, size, t)
def generate_scene(volume, t):
# Add subtle background particles for atmosphere
particle_count = 20
for i in range(particle_count):
px = int((SIZE * 0.1) + (SIZE * 0.8 * ((i * 47) % 100) / 100))
py = int((SIZE * 0.1) + (SIZE * 0.8 * ((i * 73) % 100) / 100))
pz = int((SIZE * 0.1) + (SIZE * 0.8 * ((i * 97) % 100) / 100))
brightness = int(30 + 25 * np.sin(t * 0.5 + i * 0.3))
add_voxel(volume, px, py, pz, (brightness, brightness, brightness + 20), 60)
generate_crystal_cluster(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Growing crystals"):
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
CRYSTAL_COUNT
to create more or fewer crystal formations. - Modify
colors
to experiment with different mineral types. - Change
GROWTH_SPEED
to make crystals form faster or slower. - Add
+ int(t*2)
to Y positions to make crystals rise while growing. - Experiment with different crystal shapes by modifying the
crystal_shape()
function.