Creating Kaleidoscope Animation
Kaleidoscope 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a kaleidoscope using SpatialStudio.
The script creates mesmerizing geometric patterns that rotate, shift, and shimmer 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 6 symmetrical segments, each with:
- Colorful geometric patterns
- Rotating triangular and diamond shapes
- Reflective symmetry like a real kaleidoscope
- Animates smooth rotation and color transitions for 8 seconds at 30 FPS
- Outputs the file
kaleidoscope.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
). -
Symmetrical segments The kaleidoscope is divided into 6 triangular segments, each mirrored to create perfect symmetry.
-
Geometric patterns Various shapes (triangles, diamonds, lines) are drawn with mathematical functions and rotated over time.
-
Color cycling HSV color space is used to create smooth rainbow transitions that shift throughout the animation.
-
3D rotation The entire pattern rotates around multiple axes, creating a hypnotic 3D kaleidoscope effect.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the motion 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 kaleidoscope.py
and run:
python kaleidoscope.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
import colorsys
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 8
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/kaleidoscope.splv"
# Kaleidoscope settings
SEGMENTS = 6
PATTERN_LAYERS = 4
MAX_RADIUS = SIZE // 3
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 hsv_to_rgb(h, s, v):
r, g, b = colorsys.hsv_to_rgb(h, s, v)
return (int(r * 255), int(g * 255), int(b * 255))
def rotate_point_3d(x, y, z, rx, ry, rz):
# Rotate around X axis
cos_rx, sin_rx = np.cos(rx), np.sin(rx)
y, z = y * cos_rx - z * sin_rx, y * sin_rx + z * cos_rx
# Rotate around Y axis
cos_ry, sin_ry = np.cos(ry), np.sin(ry)
x, z = x * cos_ry + z * sin_ry, -x * sin_ry + z * cos_ry
# Rotate around Z axis
cos_rz, sin_rz = np.cos(rz), np.sin(rz)
x, y = x * cos_rz - y * sin_rz, x * sin_rz + y * cos_rz
return x, y, z
def generate_triangle_pattern(volume, cx, cy, cz, t, layer):
for segment in range(SEGMENTS):
segment_angle = (segment / SEGMENTS) * 2 * np.pi
# Create triangular patterns
for i in range(8):
radius = (i + 1) * 4 + layer * 3
if radius > MAX_RADIUS:
continue
angle_offset = t * (0.5 + layer * 0.2) + segment_angle
for tri_point in range(3):
tri_angle = angle_offset + (tri_point / 3) * 2 * np.pi
x = radius * np.cos(tri_angle)
y = radius * np.sin(tri_angle)
z = np.sin(t * 2 + radius * 0.1 + layer) * 10
# 3D rotation
x, y, z = rotate_point_3d(x, y, z, t * 0.3, t * 0.2, t * 0.1)
final_x = int(cx + x)
final_y = int(cy + y)
final_z = int(cz + z)
# Color based on position and time
hue = (t * 0.1 + radius * 0.01 + segment * 0.15 + layer * 0.1) % 1.0
saturation = 0.8 + 0.2 * np.sin(t * 1.5 + radius * 0.05)
value = 0.7 + 0.3 * np.cos(t * 0.8 + layer * 0.3)
color = hsv_to_rgb(hue, saturation, value)
add_voxel(volume, final_x, final_y, final_z, color)
def generate_diamond_pattern(volume, cx, cy, cz, t, layer):
for segment in range(SEGMENTS):
segment_angle = (segment / SEGMENTS) * 2 * np.pi + t * 0.3
# Create diamond shapes
for diamond in range(4):
base_radius = 10 + diamond * 8 + layer * 2
if base_radius > MAX_RADIUS:
continue
diamond_rotation = t * (0.8 - layer * 0.1) + segment_angle
# Diamond vertices
vertices = [
(base_radius, 0, 0),
(0, base_radius * 0.7, 0),
(-base_radius, 0, 0),
(0, -base_radius * 0.7, 0)
]
for i in range(len(vertices)):
vx, vy, vz = vertices[i]
# Rotate diamond
cos_rot, sin_rot = np.cos(diamond_rotation), np.sin(diamond_rotation)
vx, vy = vx * cos_rot - vy * sin_rot, vx * sin_rot + vy * cos_rot
# Add Z oscillation
vz += np.sin(t * 1.2 + diamond * 0.8 + segment * 0.5) * 8
# 3D rotation
vx, vy, vz = rotate_point_3d(vx, vy, vz, t * 0.2, t * 0.4, t * 0.15)
final_x = int(cx + vx)
final_y = int(cy + vy)
final_z = int(cz + vz)
# Vibrant color cycling
hue = (t * 0.15 + diamond * 0.25 + segment * 0.1 + layer * 0.2) % 1.0
saturation = 0.9
value = 0.8 + 0.2 * np.sin(t * 2 + diamond + segment)
color = hsv_to_rgb(hue, saturation, value)
# Draw small cluster around vertex
for dx in range(-2, 3):
for dy in range(-2, 3):
for dz in range(-2, 3):
if dx*dx + dy*dy + dz*dz <= 4:
add_voxel(volume, final_x+dx, final_y+dy, final_z+dz, color)
def generate_radial_lines(volume, cx, cy, cz, t, layer):
for segment in range(SEGMENTS * 2):
line_angle = (segment / (SEGMENTS * 2)) * 2 * np.pi + t * 0.5
for radius in range(5, MAX_RADIUS, 3):
x = radius * np.cos(line_angle)
y = radius * np.sin(line_angle)
z = np.sin(t * 1.8 + radius * 0.05 + segment * 0.2) * 15
# 3D rotation
x, y, z = rotate_point_3d(x, y, z, t * 0.25, t * 0.35, t * 0.1)
final_x = int(cx + x)
final_y = int(cy + y)
final_z = int(cz + z)
# Color based on angle and radius
hue = (line_angle / (2 * np.pi) + t * 0.2 + radius * 0.005) % 1.0
saturation = 0.7 + 0.3 * np.sin(t + segment)
value = 0.6 + 0.4 * np.cos(t * 1.3 + radius * 0.02)
color = hsv_to_rgb(hue, saturation, value)
add_voxel(volume, final_x, final_y, final_z, color)
def generate_kaleidoscope_scene(volume, t):
for layer in range(PATTERN_LAYERS):
generate_triangle_pattern(volume, CENTER_X, CENTER_Y, CENTER_Z, t, layer)
generate_diamond_pattern(volume, CENTER_X, CENTER_Y, CENTER_Z, t, layer)
generate_radial_lines(volume, CENTER_X, CENTER_Y, CENTER_Z, t, layer)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating kaleidoscope"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 2 * np.pi
generate_kaleidoscope_scene(volume, t)
enc.encode(splv.Frame(volume, lrAxis="x", udAxis="y", fbAxis="z"))
enc.finish()
print(f"Created {OUT_PATH}")
Next steps
- Change
SEGMENTS
to create more or fewer symmetrical sections. - Adjust
PATTERN_LAYERS
for more complex overlapping patterns. - Modify rotation speeds by changing the multipliers in the time calculations.
- Experiment with
MAX_RADIUS
to make patterns larger or smaller. - Try different HSV color ranges for unique color palettes.