Creating Demon Animation
Creating a 3D Voxel Demon Animation
This guide walks you through how to generate a looping 3D voxel animation of a demon using SpatialStudio.
The script creates a menacing demon with glowing eyes, flickering horns, and swirling dark energy 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 1 animated demon, featuring:
- A dark red voxel body with muscular definition
- Two curved horns that glow and flicker
- Bright red glowing eyes
- Swirling dark energy particles around the body
- Animates the demon with breathing motion and energy effects for 8 seconds at 30 FPS
- Outputs the file
demon.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
). -
Demon body The body is constructed using ellipsoids with dark red gradients and surface texturing for a muscular appearance.
-
Horns Curved horn structures are drawn with procedural mathematics, featuring orange-to-yellow gradient with flickering effects.
-
Glowing eyes Bright red spherical eyes with white centers that pulse with intensity variations.
-
Dark energy Swirling particles generated using noise functions that orbit around the demon's body.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating breathing motion and energy swirls that loop 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 demon.py
and run:
python demon.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/demon.splv"
# Demon settings
DEMON_HEIGHT = 40
DEMON_WIDTH = 20
HORN_LENGTH = 15
EYE_RADIUS = 3
PARTICLE_COUNT = 50
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 generate_demon_body(volume, cx, cy, cz, t):
# Breathing effect
breath = 1.0 + 0.1 * np.sin(t * 3.0)
body_colors = [(120, 20, 20), (80, 10, 10), (60, 5, 5)]
for dy in range(-DEMON_HEIGHT//2, DEMON_HEIGHT//2):
for dx in range(-DEMON_WIDTH, DEMON_WIDTH+1):
for dz in range(-DEMON_WIDTH, DEMON_WIDTH+1):
# Create muscular body shape
body_radius = DEMON_WIDTH * (1.0 - abs(dy) / (DEMON_HEIGHT/2)) * breath
torso_width = body_radius * (0.8 + 0.2 * np.sin(dy * 0.2))
if np.sqrt(dx*dx + dz*dz) <= torso_width:
# Add surface detail with noise
surface_noise = np.sin(dx*0.3 + dy*0.2 + dz*0.4 + t*0.5)
muscle_def = int(surface_noise * 2)
# Choose color based on depth
depth = np.sqrt(dx*dx + dz*dz) / torso_width
color_idx = min(2, int(depth * 3))
base_color = body_colors[color_idx]
# Apply muscle definition
brightness = 1.0 + muscle_def * 0.15
final_color = tuple(min(255, int(c * brightness)) for c in base_color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color)
def generate_horns(volume, cx, cy, cz, t):
# Flickering horn colors
flicker = 0.8 + 0.4 * np.sin(t * 8.0)
horn_color = (int(255 * flicker), int(150 * flicker), int(50 * flicker))
for side in [-1, 1]: # Left and right horn
for i in range(HORN_LENGTH):
progress = i / HORN_LENGTH
# Curved horn shape
horn_x = cx + side * (5 + int(progress * 8))
horn_y = cy - DEMON_HEIGHT//2 + int(progress * 12)
horn_z = cz - int(progress * 4)
# Horn thickness decreases towards tip
thickness = max(1, int((1.0 - progress) * 3))
for dx in range(-thickness, thickness+1):
for dy in range(-thickness, thickness+1):
for dz in range(-thickness, thickness+1):
if dx*dx + dy*dy + dz*dz <= thickness*thickness:
add_voxel(volume, horn_x+dx, horn_y+dy, horn_z+dz, horn_color)
def generate_eyes(volume, cx, cy, cz, t):
# Pulsing eye glow
pulse = 0.7 + 0.3 * np.sin(t * 4.0)
eye_color = (int(255 * pulse), int(50 * pulse), int(50 * pulse))
eye_center = (255, 255, 255)
for side in [-1, 1]: # Left and right eye
eye_x = cx + side * 8
eye_y = cy - DEMON_HEIGHT//4
eye_z = cz + DEMON_WIDTH//2
# Eye socket (darker area)
for dx in range(-EYE_RADIUS-1, EYE_RADIUS+2):
for dy in range(-EYE_RADIUS-1, EYE_RADIUS+2):
for dz in range(-EYE_RADIUS-1, EYE_RADIUS+2):
if np.sqrt(dx*dx + dy*dy + dz*dz) <= EYE_RADIUS + 1:
add_voxel(volume, eye_x+dx, eye_y+dy, eye_z+dz, (40, 0, 0))
# Glowing eye
for dx in range(-EYE_RADIUS, EYE_RADIUS+1):
for dy in range(-EYE_RADIUS, EYE_RADIUS+1):
for dz in range(-EYE_RADIUS, EYE_RADIUS+1):
dist = np.sqrt(dx*dx + dy*dy + dz*dz)
if dist <= EYE_RADIUS:
if dist <= 1: # Eye center
add_voxel(volume, eye_x+dx, eye_y+dy, eye_z+dz, eye_center)
else: # Eye glow
add_voxel(volume, eye_x+dx, eye_y+dy, eye_z+dz, eye_color)
def generate_dark_energy(volume, cx, cy, cz, t):
# Swirling dark energy particles
energy_color = (100, 0, 100)
for i in range(PARTICLE_COUNT):
# Create swirling motion
angle = (i / PARTICLE_COUNT) * 2 * np.pi + t * 2.0
height_offset = (i / PARTICLE_COUNT) * DEMON_HEIGHT - DEMON_HEIGHT//2
# Spiral parameters
radius = 25 + 10 * np.sin(t + i * 0.5)
spiral_height = height_offset + 5 * np.sin(t * 1.5 + i * 0.3)
particle_x = cx + int(radius * np.cos(angle))
particle_y = cy + int(spiral_height)
particle_z = cz + int(radius * np.sin(angle))
# Add particle with some randomness
noise_x = int(3 * np.sin(t * 4 + i))
noise_z = int(3 * np.cos(t * 3 + i))
# Particle intensity based on position
intensity = 0.5 + 0.5 * np.sin(t * 3 + i * 0.7)
final_color = tuple(int(c * intensity) for c in energy_color)
add_voxel(volume, particle_x + noise_x, particle_y, particle_z + noise_z, final_color)
def generate_ground_glow(volume, cx, cy, cz, t):
# Red glow on the ground around demon's feet
glow_intensity = 0.3 + 0.2 * np.sin(t * 2.5)
ground_y = cy + DEMON_HEIGHT//2 + 5
for dx in range(-30, 31):
for dz in range(-30, 31):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= 30:
brightness = glow_intensity * (1.0 - distance / 30.0)
glow_color = (int(150 * brightness), int(30 * brightness), int(30 * brightness))
add_voxel(volume, cx+dx, ground_y, cz+dz, glow_color)
def generate_scene(volume, t):
generate_ground_glow(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_dark_energy(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_demon_body(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_horns(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_eyes(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 demon"):
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
PARTICLE_COUNT
to add more or fewer energy particles - Modify horn colors by changing the flicker calculation
- Add wing structures using similar ellipsoid techniques
- Create multiple demons by calling
generate_scene()
with different center positions - Experiment with different breathing rates by changing the multiplier in
np.sin(t * 3.0)