Torus Knot round 2
#!/usr/bin/env python3
"""
torus_knot_spiced.py
15 s voxel animation:
• Trefoil (p=2,q=3) torus knot
• Breathing minor radius
• Tube thickness (3 samples per point)
• Gentle wander phase (unordered → ordered → unordered)
• Rainbow gradient + slow global hue drift
• Depth-based value fade for fake fog
Run:
pip install spatialstudio numpy
python torus_knot_spiced.py
Outputs:
torus_knot_spiced.splv
"""
import math
import numpy as np
from colorsys import hsv_to_rgb
from spatialstudio import splv
# Grid & timing
GRID = 128
FPS = 30
DURATION = 15
TOTAL_FRAMES = FPS * DURATION
# Torus-knot geometry
P, Q = 2, 3 # trefoil
R_MAJOR = GRID * 0.28 # distance from centre to tube centre
R_MINOR_BASE = GRID * 0.08
TUBE_R = GRID * 0.008 # voxel-shell thickness
# Points & wander
COUNT = 800
np.random.seed(1)
u_vals = np.linspace(0.0, 2 * math.pi, COUNT, endpoint=False)
base_pos = np.random.rand(COUNT, 3) * GRID
phase_offsets = np.random.rand(COUNT, 3) * 2 * math.pi
WANDER_AMP = 4
# Output
OUTPUT = "../outputs/torus_knot_spiced.splv"
CENTER = np.array([GRID // 2] * 3)
# HSV helpers
def hsv_bytes(h, s=1.0, v=1.0):
r, g, b = hsv_to_rgb(h, s, v)
return int(r * 255), int(g * 255), int(b * 255)
# Easing
def smoothstep(a, b, x):
t = max(0.0, min(1.0, (x - a) / (b - a)))
return t * t * (3 - 2 * t)
def lerp(a, b, t):
return a * (1 - t) + b * t
# Geometry
def torus_knot_pos(u, minor):
x = (R_MAJOR + minor * math.cos(Q * u)) * math.cos(P * u)
y = (R_MAJOR + minor * math.cos(Q * u)) * math.sin(P * u)
z = minor * math.sin(Q * u)
return np.array([x, y, z])
def rotate(vec, ax, angle):
ax = ax / np.linalg.norm(ax)
return (
vec * math.cos(angle)
+ np.cross(ax, vec) * math.sin(angle)
+ ax * np.dot(ax, vec) * (1 - math.cos(angle))
)
def tube_sample(pos, tangent):
# Build orthonormal basis around tangent
n = np.cross(tangent, [0, 0, 1])
if np.linalg.norm(n) < 1e-6:
n = np.cross(tangent, [0, 1, 0])
n /= np.linalg.norm(n)
b = np.cross(tangent, n)
phi = np.random.rand() * 2 * math.pi
return pos + (math.cos(phi) * n + math.sin(phi) * b) * TUBE_R
enc = splv.Encoder(GRID, GRID, GRID, framerate=FPS, outputPath=OUTPUT)
print(f"Encoding {TOTAL_FRAMES} frames …")
cam_z = CENTER[2] - GRID * 0.6 # fixed camera for depth fade
for f in range(TOTAL_FRAMES):
t = f / TOTAL_FRAMES
# Cluster phase (unordered → ordered → unordered)
if t < 0.2:
cluster = 0.0
elif t < 0.3:
cluster = smoothstep(0.2, 0.3, t)
elif t < 0.8:
cluster = 1.0
else:
cluster = 1.0 - smoothstep(0.8, 1.0, t)
ordered_t = 0.0 if t < 0.3 else 1.0 if t > 0.8 else smoothstep(0.3, 0.8, t)
# Rotate shape (Y then X)
rot_y = ordered_t * 2 * math.pi
rot_x = ordered_t * math.pi
# Breathing minor radius
minor_scale = 1.0 + 0.15 * math.sin(2 * math.pi * t)
# Global hue drift
base_hue = (f / TOTAL_FRAMES) * 0.2
frame = splv.Frame(GRID, GRID, GRID)
for i in range(COUNT):
u = u_vals[i]
obj = torus_knot_pos(u, R_MINOR_BASE * minor_scale)
# Tangent for tube normals (small forward difference)
eps = 1e-3
tangent = torus_knot_pos(u + eps, R_MINOR_BASE * minor_scale) - obj
tangent /= np.linalg.norm(tangent)
# Rotate knot
obj = rotate(obj, np.array([0, 1, 0]), rot_y)
obj = rotate(obj, np.array([1, 0, 0]), rot_x)
ordered_pos = CENTER + obj
# Wander position
npos = base_pos[i] + np.array(
[
math.sin(t * 2 * math.pi + phase_offsets[i, 0]) * WANDER_AMP,
math.cos(t * 2 * math.pi + phase_offsets[i, 1]) * WANDER_AMP,
math.sin(t * 1.5 * math.pi + phase_offsets[i, 2]) * WANDER_AMP,
]
)
pos_center = lerp(npos, ordered_pos, cluster)
# Tube: sample three voxels around centre
for _ in range(3):
p = tube_sample(pos_center, tangent)
x, y, z = p.astype(int)
if 0 <= x < GRID and 0 <= y < GRID and 0 <= z < GRID:
# Depth-based value fade
depth = (p[2] - cam_z) / GRID
v = max(0.0, 1.0 - 0.5 * depth ** 1.5)
# Rainbow gradient with global drift
hue = ((i / COUNT) + base_hue) % 1.0
frame.set_voxel(x, y, z, hsv_bytes(hue, 1.0, v))
enc.encode(frame)
if f % FPS == 0:
print(f" second {f // FPS + 1} / {DURATION}")
enc.finish()
print("Done. Saved", OUTPUT)