Creating Earth Orbit Animation
Earth Orbit Animation - 3D Voxel Learning Example
This guide walks you through how to generate a looping 3D voxel animation of Earth orbiting the Sun using SpatialStudio.
The script creates a realistic solar system scene with Earth rotating on its axis while orbiting around a glowing Sun, then saves the animation to a .splv
file.
What this script does
- Creates a 3D scene of size 128×128×128
- Renders a glowing Sun at the center with:
- Dynamic solar flares
- Pulsing brightness effects
- Radiating heat particles
- Animates Earth with realistic motion:
- Orbital revolution around the Sun
- Axial rotation (day/night cycle)
- Blue oceans and green/brown continents
- Atmospheric glow effect
- Runs for 12 seconds at 30 FPS for a complete orbit
- Outputs the file
earth_orbit.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
). -
Sun rendering The Sun is drawn as a large glowing sphere with animated surface details and particle effects.
-
Earth generation Earth features procedurally generated continents using noise functions and rotates to show different faces.
-
Orbital mechanics Earth follows an elliptical path around the Sun while spinning on its own axis at a realistic speed ratio.
-
Lighting effects The Sun-facing side of Earth is brighter, with atmospheric scattering creating a blue rim effect.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring one complete orbit 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 earth_orbit.py
and run:
python earth_orbit.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 12
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/earth_orbit.splv"
# Celestial body settings
SUN_RADIUS = 12
EARTH_RADIUS = 4
ORBIT_RADIUS = 35
EARTH_ROTATION_SPEED = 8.0 # Earth spins faster than it orbits
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 noise_3d(x, y, z, scale=0.1):
"""Simple 3D noise function for terrain generation"""
return np.sin(x*scale) * np.cos(y*scale*1.3) * np.sin(z*scale*0.7)
def generate_sun(volume, cx, cy, cz, t):
"""Generate animated sun with solar flares"""
base_color = (255, 200, 50)
for dx in range(-SUN_RADIUS-3, SUN_RADIUS+4):
for dy in range(-SUN_RADIUS-3, SUN_RADIUS+4):
for dz in range(-SUN_RADIUS-3, SUN_RADIUS+4):
dist = np.sqrt(dx*dx + dy*dy + dz*dz)
# Sun core
if dist <= SUN_RADIUS:
flare = np.sin(dx*0.2 + dy*0.3 + dz*0.1 + t*2.0) * 0.3
pulse = 1.0 + np.sin(t*3.0) * 0.2
intensity = (1.0 + flare) * pulse
color = tuple(min(255, max(0, int(c * intensity))) for c in base_color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, color)
# Solar corona
elif dist <= SUN_RADIUS + 3:
corona_intensity = max(0, 1.0 - (dist - SUN_RADIUS) / 3.0)
corona_flicker = np.sin(t*4.0 + dist*0.5) * 0.5 + 0.5
final_intensity = corona_intensity * corona_flicker * 0.7
if final_intensity > 0.3:
corona_color = (int(255*final_intensity), int(150*final_intensity), int(20*final_intensity))
add_voxel(volume, cx+dx, cy+dy, cz+dz, corona_color)
def generate_earth(volume, cx, cy, cz, rotation_t, t):
"""Generate Earth with continents, oceans, and atmosphere"""
for dx in range(-EARTH_RADIUS-2, EARTH_RADIUS+3):
for dy in range(-EARTH_RADIUS-2, EARTH_RADIUS+3):
for dz in range(-EARTH_RADIUS-2, EARTH_RADIUS+3):
dist = np.sqrt(dx*dx + dy*dy + dz*dz)
if dist <= EARTH_RADIUS:
# Rotate the sampling point
rot_x = dx * np.cos(rotation_t) - dz * np.sin(rotation_t)
rot_z = dx * np.sin(rotation_t) + dz * np.cos(rotation_t)
# Generate terrain
terrain = noise_3d(rot_x, dy, rot_z, 0.3)
# Determine surface type
if terrain > 0.1: # Land
if dy > EARTH_RADIUS * 0.3: # Mountains/arctic
color = (139, 90, 43) if terrain > 0.3 else (34, 139, 34)
else: # Forests/plains
color = (34, 139, 34) if terrain > 0.25 else (160, 82, 45)
else: # Ocean
depth = abs(terrain)
blue_intensity = int(255 - depth * 100)
color = (0, 50, max(100, blue_intensity))
# Apply lighting based on sun direction
sun_dir = np.array([-cx + CENTER_X, -cy + CENTER_Y, -cz + CENTER_Z])
if np.linalg.norm(sun_dir) > 0:
sun_dir = sun_dir / np.linalg.norm(sun_dir)
normal = np.array([dx, dy, dz]) / dist if dist > 0 else np.array([0, 1, 0])
lighting = max(0.3, np.dot(normal, sun_dir) * 0.7 + 0.3)
color = tuple(int(c * lighting) for c in color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, color)
# Atmosphere
elif dist <= EARTH_RADIUS + 2:
atmo_intensity = max(0, 1.0 - (dist - EARTH_RADIUS) / 2.0)
if atmo_intensity > 0.4:
atmo_color = (int(100*atmo_intensity), int(150*atmo_intensity), int(255*atmo_intensity))
add_voxel(volume, cx+dx, cy+dy, cz+dz, atmo_color)
def generate_orbit_trail(volume, cx, cy, cz, t, trail_length=50):
"""Generate a faint orbital trail behind Earth"""
trail_color = (100, 100, 200)
for i in range(trail_length):
trail_t = t - (i / trail_length) * 0.5
trail_x = cx + int(ORBIT_RADIUS * np.cos(trail_t))
trail_z = cz + int(ORBIT_RADIUS * np.sin(trail_t))
alpha = max(0, 1.0 - (i / trail_length))
if alpha > 0.1:
faded_color = tuple(int(c * alpha * 0.3) for c in trail_color)
add_voxel(volume, trail_x, cy, trail_z, faded_color)
def generate_scene(volume, t):
"""Generate complete solar system scene"""
# Generate Sun at center
generate_sun(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
# Calculate Earth position (orbital motion)
earth_x = CENTER_X + int(ORBIT_RADIUS * np.cos(t))
earth_z = CENTER_Z + int(ORBIT_RADIUS * np.sin(t))
earth_y = CENTER_Y + int(3 * np.sin(t * 0.1)) # Slight vertical oscillation
# Calculate Earth rotation (spinning on axis)
earth_rotation = t * EARTH_ROTATION_SPEED
# Generate orbital trail
generate_orbit_trail(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
# Generate Earth
generate_earth(volume, earth_x, earth_y, earth_z, earth_rotation, t)
# Initialize encoder
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
# Generate animation frames
for frame in tqdm(range(FRAMES), desc="Generating Earth orbit"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 2 * np.pi # Complete orbit over animation duration
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
ORBIT_RADIUS
to change Earth's distance from the Sun - Modify
EARTH_ROTATION_SPEED
to change day/night cycle speed - Add more planets by duplicating the Earth generation with different parameters
- Experiment with
SECONDS
to create longer or shorter orbital periods - Try adding a moon by creating a smaller sphere orbiting Earth
- Enhance the terrain generation by tweaking the
noise_3d
function parameters