Creating Vaporwave Animation
This guide walks you through how to generate a looping 3D voxel animation of a vaporwave scene using SpatialStudio.
The script creates a retro-futuristic landscape with a glowing grid floor, floating geometric shapes, and neon colors that pulse and shift 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 vaporwave aesthetic featuring:
- A glowing neon grid floor with perspective lines
- Floating geometric shapes (pyramids, cubes, spheres)
- Pulsing neon colors in pink, purple, and cyan
- Animated scanlines and digital effects
- Animates the scene with smooth color transitions for 8 seconds at 30 FPS
- Outputs the file
vaporwave.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
). -
Grid floor Creates a perspective grid pattern on the bottom plane using mathematical functions to simulate depth.
-
Geometric shapes Spawns floating pyramids, cubes, and spheres that rotate and pulse with neon colors.
-
Color cycling Uses sine waves to create smooth color transitions between classic vaporwave hues.
-
Scanlines & effects Adds horizontal scanlines and digital glitch effects that move across the scene.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring smooth looping motion and color 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 vaporwave.py
and run:
python vaporwave.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/vaporwave.splv"
# Vaporwave settings
GRID_SPACING = 8
SHAPE_COUNT = 6
SCANLINE_HEIGHT = 2
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_vaporwave_color(phase, t):
"""Generate classic vaporwave colors with time-based transitions"""
colors = [
(255, 20, 147), # Deep pink
(138, 43, 226), # Blue violet
(0, 255, 255), # Cyan
(255, 0, 255), # Magenta
(75, 0, 130) # Indigo
]
base_color = colors[phase % len(colors)]
intensity = 0.7 + 0.3 * np.sin(t * 2.0 + phase)
return tuple(int(c * intensity) for c in base_color)
def generate_grid_floor(volume, t):
"""Create a perspective neon grid on the floor"""
grid_color = get_vaporwave_color(0, t)
# Horizontal lines
for z in range(0, SIZE, GRID_SPACING):
for x in range(SIZE):
brightness = 0.8 + 0.2 * np.sin(t + z * 0.1)
final_color = tuple(int(c * brightness) for c in grid_color)
add_voxel(volume, x, 5, z, final_color)
add_voxel(volume, x, 6, z, final_color)
# Vertical lines
for x in range(0, SIZE, GRID_SPACING):
for z in range(SIZE):
brightness = 0.8 + 0.2 * np.sin(t + x * 0.1)
final_color = tuple(int(c * brightness) for c in grid_color)
add_voxel(volume, x, 5, z, final_color)
add_voxel(volume, x, 6, z, final_color)
def generate_pyramid(volume, cx, cy, cz, size, color):
"""Generate a voxel pyramid"""
for y in range(size):
level_size = size - y
for dx in range(-level_size, level_size + 1):
for dz in range(-level_size, level_size + 1):
if abs(dx) + abs(dz) <= level_size:
add_voxel(volume, cx + dx, cy + y, cz + dz, color)
def generate_cube(volume, cx, cy, cz, size, color):
"""Generate a voxel cube outline"""
for dx in range(-size, size + 1):
for dy in range(-size, size + 1):
for dz in range(-size, size + 1):
if (abs(dx) == size or abs(dy) == size or abs(dz) == size):
add_voxel(volume, cx + dx, cy + dy, cz + dz, color)
def generate_sphere(volume, cx, cy, cz, radius, color):
"""Generate a voxel sphere outline"""
for dx in range(-radius, radius + 1):
for dy in range(-radius, radius + 1):
for dz in range(-radius, radius + 1):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if radius - 1 <= distance <= radius:
add_voxel(volume, cx + dx, cy + dy, cz + dz, color)
def generate_floating_shapes(volume, t):
"""Create floating geometric shapes with rotation and pulsing"""
shapes = ['pyramid', 'cube', 'sphere']
for i in range(SHAPE_COUNT):
angle = (i / SHAPE_COUNT) * 2 * np.pi + t * 0.5
radius = 30 + 15 * np.sin(i * 0.7)
x = int(CENTER_X + radius * np.cos(angle))
z = int(CENTER_Z + radius * np.sin(angle))
y = int(CENTER_Y + 20 + 10 * np.sin(t * 1.2 + i * 0.8))
size = max(3, int(6 + 2 * np.sin(t * 2.0 + i)))
color = get_vaporwave_color(i, t)
shape_type = shapes[i % len(shapes)]
if shape_type == 'pyramid':
generate_pyramid(volume, x, y, z, size, color)
elif shape_type == 'cube':
generate_cube(volume, x, y, z, size, color)
elif shape_type == 'sphere':
generate_sphere(volume, x, y, z, size, color)
def generate_scanlines(volume, t):
"""Add moving scanlines for retro effect"""
scanline_color = (255, 255, 255) # White scanlines
# Moving horizontal scanlines
for i in range(3):
y_pos = int((SIZE * 0.3) + (SIZE * 0.4) * ((t * 0.5 + i * 0.3) % 1.0))
for x in range(SIZE):
for z in range(SIZE):
if y_pos < SIZE:
alpha = int(128 * (0.5 + 0.5 * np.sin(t * 3.0 + i)))
if volume[x, y_pos, z, 3] < alpha:
volume[x, y_pos, z, :3] = scanline_color
volume[x, y_pos, z, 3] = alpha
def generate_background_glow(volume, t):
"""Add subtle background glow effects"""
glow_color = get_vaporwave_color(2, t * 0.3)
for x in range(0, SIZE, 4):
for y in range(SIZE // 2, SIZE):
for z in range(0, SIZE, 4):
distance_from_center = np.sqrt((x - CENTER_X)**2 + (z - CENTER_Z)**2)
if distance_from_center > SIZE * 0.3:
intensity = max(0, 50 - int(distance_from_center * 0.5))
if intensity > 0 and volume[x, y, z, 3] == 0:
final_color = tuple(int(c * intensity / 255.0) for c in glow_color)
volume[x, y, z, :3] = final_color
volume[x, y, z, 3] = intensity
def generate_scene(volume, t):
generate_background_glow(volume, t)
generate_grid_floor(volume, t)
generate_floating_shapes(volume, t)
generate_scanlines(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating vaporwave"):
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
SHAPE_COUNT
to add more floating geometry. - Modify the
colors
array inget_vaporwave_color()
for different neon palettes. - Change
GRID_SPACING
to make the floor grid denser or sparser. - Add more geometric shapes by extending the
shapes
list. - Experiment with
SCANLINE_HEIGHT
for thicker retro scanlines.