Creating Prism Animation
3D Voxel Animation Tutorial: Prismatic Light Show
This guide walks you through how to generate a looping 3D voxel animation of a crystal prism using SpatialStudio.
The script creates a rotating glass prism that refracts light into rainbow beams 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 crystal prism with:
- Transparent triangular glass structure
- Reflective surfaces with highlights
- Rainbow light beams refracting through it
- Particle sparkles around the prism
- Animates smooth rotation and light dispersion for 8 seconds at 30 FPS
- Outputs the file
prism.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
). -
Prism geometry The prism is drawn as a triangular structure using geometric calculations for clean edges and faces.
-
Light refraction White light enters the prism and splits into colored beams that spread outward at different angles.
-
Surface effects Semi-transparent glass material with bright white highlights to simulate crystal reflections.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, rotating the prism and shifting the light patterns 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 prism.py
and run:
python prism.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/prism.splv"
# Prism settings
PRISM_HEIGHT = 30
PRISM_WIDTH = 20
BEAM_LENGTH = 40
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 generate_prism_body(volume, cx, cy, cz, rotation):
glass_color = (200, 230, 255)
cos_r, sin_r = np.cos(rotation), np.sin(rotation)
for y in range(-PRISM_HEIGHT//2, PRISM_HEIGHT//2):
for x in range(-PRISM_WIDTH, PRISM_WIDTH):
for z in range(-PRISM_WIDTH, PRISM_WIDTH):
# Rotate coordinates
rx = x * cos_r - z * sin_r
rz = x * sin_r + z * cos_r
# Triangular prism shape
if (abs(rx) <= PRISM_WIDTH//2 and
abs(rz) <= PRISM_WIDTH//2 and
rx + rz >= -PRISM_WIDTH//2 and
rx - rz <= PRISM_WIDTH//2 and
-rx + rz <= PRISM_WIDTH//2):
# Edge detection for glass effect
edge_dist = min(PRISM_WIDTH//2 - abs(rx), PRISM_WIDTH//2 - abs(rz))
if edge_dist <= 2:
brightness = 1.2 + 0.3 * np.sin(y * 0.2 + rotation)
final_color = tuple(min(255, int(c * brightness)) for c in glass_color)
add_voxel(volume, cx + int(rx), cy + y, cz + int(rz), final_color, 180)
def generate_light_beams(volume, cx, cy, cz, rotation, t):
# Rainbow colors for refracted light
beam_colors = [
(255, 0, 0), # Red
(255, 127, 0), # Orange
(255, 255, 0), # Yellow
(0, 255, 0), # Green
(0, 0, 255), # Blue
(75, 0, 130), # Indigo
(148, 0, 211), # Violet
]
for i, color in enumerate(beam_colors):
angle_offset = (i / len(beam_colors)) * np.pi * 0.5
beam_angle = rotation + angle_offset + np.sin(t * 2.0) * 0.2
for length in range(BEAM_LENGTH):
intensity = max(0, 1.0 - length / BEAM_LENGTH)
if intensity > 0.1:
spread = int(length * 0.15)
bx = cx + int(length * np.cos(beam_angle))
bz = cz + int(length * np.sin(beam_angle))
by = cy + int(np.sin(length * 0.1 + t * 3.0) * 3)
# Create beam with slight spread
for dx in range(-spread, spread + 1):
for dz in range(-spread, spread + 1):
if dx*dx + dz*dz <= spread*spread:
final_color = tuple(int(c * intensity) for c in color)
alpha = int(intensity * 150)
add_voxel(volume, bx + dx, by, bz + dz, final_color, alpha)
def generate_sparkles(volume, cx, cy, cz, t):
sparkle_color = (255, 255, 255)
for i in range(20):
# Pseudo-random sparkle positions based on time and index
angle = (i * 2.7 + t * 1.5) % (2 * np.pi)
radius = 25 + 10 * np.sin(i * 0.8 + t * 2.0)
height = 15 * np.sin(i * 1.2 + t * 1.8)
sx = cx + int(radius * np.cos(angle))
sy = cy + int(height)
sz = cz + int(radius * np.sin(angle))
# Twinkling effect
brightness = 0.5 + 0.5 * np.sin(t * 4.0 + i * 0.5)
if brightness > 0.7:
final_color = tuple(int(c * brightness) for c in sparkle_color)
add_voxel(volume, sx, sy, sz, final_color)
# Add small cross pattern for sparkle effect
for dx, dz in [(1,0), (-1,0), (0,1), (0,-1)]:
add_voxel(volume, sx + dx, sy, sz + dz, final_color, 180)
def generate_prism_highlights(volume, cx, cy, cz, rotation):
highlight_color = (255, 255, 255)
cos_r, sin_r = np.cos(rotation), np.sin(rotation)
# Add bright highlights to prism edges
for y in range(-PRISM_HEIGHT//2, PRISM_HEIGHT//2, 3):
for edge in range(3):
angle = edge * (2*np.pi/3) + rotation
hx = cx + int(PRISM_WIDTH//2 * np.cos(angle))
hz = cz + int(PRISM_WIDTH//2 * np.sin(angle))
add_voxel(volume, hx, cy + y, hz, highlight_color)
def generate_scene(volume, t):
rotation = t * 0.5 # Slow rotation
generate_prism_body(volume, CENTER_X, CENTER_Y, CENTER_Z, rotation)
generate_light_beams(volume, CENTER_X, CENTER_Y, CENTER_Z, rotation, t)
generate_prism_highlights(volume, CENTER_X, CENTER_Y, CENTER_Z, rotation)
generate_sparkles(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating prism"):
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
PRISM_HEIGHT
andPRISM_WIDTH
to change the crystal size. - Modify
beam_colors
to create different light spectrums. - Increase
BEAM_LENGTH
for longer light rays. - Add more sparkles by changing the range in
generate_sparkles()
. - Try different rotation speeds by modifying the
rotation = t * 0.5
multiplier.