-
-
Save DanielHabib/3a42e691dfcb75ec0bece9e4dbafc65c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| crystalline_spiral.py | |
| 18-second 3D voxel animation of a crystalline spiral with hard angular corners. | |
| Features explosive particle formation, sharp geometric transitions, and collapse into singularity. | |
| The crystalline spiral uses sharp angular functions to create hard corners and faceted segments. | |
| Beginning: Particles explode from chaos into formation | |
| Middle: Sharp crystalline spiral rotates with angular segments | |
| End: Collapse into central singularity with energy flash | |
| Run: | |
| pip install spatialstudio numpy | |
| python crystalline_spiral.py | |
| Outputs: | |
| crystalline_spiral.splv | |
| """ | |
| import math | |
| import numpy as np | |
| from colorsys import hsv_to_rgb | |
| from spatialstudio import splv | |
| # ------------------------------------------------- | |
| GRID = 128 # cubic voxel grid size | |
| FPS = 30 # frames per second | |
| DURATION = 18 # seconds | |
| COUNT = 1000 # number of particles | |
| OUTPUT = "../outputs/crystalline_spiral.splv" | |
| # ------------------------------------------------- | |
| TOTAL_FRAMES = FPS * DURATION | |
| CENTER = np.array([GRID // 2] * 3, dtype=float) | |
| # Crystalline spiral parameters | |
| BASE_RADIUS = GRID * 0.18 # base radius | |
| HEIGHT_RANGE = GRID * 0.4 # total height range | |
| SEGMENTS = 8 # number of angular segments per turn | |
| TURNS = 3 # number of complete turns | |
| CORNER_SHARPNESS = 0.15 # controls how sharp the corners are (lower = sharper) | |
| # Animation phases | |
| EXPLOSION_PHASE = 0.15 # first 15% - explosion into formation | |
| SPIRAL_PHASE = 0.70 # middle 70% - crystalline spiral | |
| COLLAPSE_PHASE = 0.15 # final 15% - collapse to singularity | |
| def smoothstep(edge0: float, edge1: float, x: float) -> float: | |
| """Smooth interpolation function""" | |
| t = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0))) | |
| return t * t * (3 - 2 * t) | |
| def sharpstep(x: float, sharpness: float = 0.1) -> float: | |
| """Create sharp transitions - lower sharpness = sharper corners""" | |
| return 1.0 / (1.0 + math.exp(-(x - 0.5) / sharpness)) | |
| def hsv_bytes(h: float, s: float = 1.0, v: float = 1.0) -> tuple: | |
| """Convert HSV to RGB bytes""" | |
| # Ensure all values are in proper ranges | |
| h = max(0.0, min(1.0, h % 1.0)) | |
| s = max(0.0, min(1.0, s)) | |
| v = max(0.0, min(1.0, v)) | |
| r, g, b = hsv_to_rgb(h, s, v) | |
| return int(r * 255), int(g * 255), int(b * 255) | |
| def crystalline_spiral_pos(t_param: float, time_factor: float = 0.0) -> np.ndarray: | |
| """ | |
| Calculate position on crystalline spiral with smooth flowing curves and dynamic elements | |
| t_param: parameter along spiral (0 to 2π * TURNS) | |
| time_factor: global animation time (0 to 1) for morphing effects | |
| """ | |
| # Smooth base spiral with dynamic parameters | |
| base_angle = t_param | |
| # Multi-layered radius variations for visual interest | |
| # Primary spiral with breathing motion | |
| radius_base = BASE_RADIUS * (1 + 0.3 * math.sin(time_factor * 3 * math.pi)) | |
| # Secondary harmonic for interesting bulges | |
| harmonic_freq = 2.5 + math.sin(time_factor * 2 * math.pi) # varying frequency | |
| radius_harmonic = 0.4 * BASE_RADIUS * math.sin(harmonic_freq * t_param) | |
| # Tertiary modulation for fine detail | |
| detail_freq = 8 + 3 * math.cos(time_factor * 1.5 * math.pi) | |
| radius_detail = 0.15 * BASE_RADIUS * math.sin(detail_freq * t_param + time_factor * 4 * math.pi) | |
| # Combine radius components | |
| final_radius = radius_base + radius_harmonic + radius_detail | |
| # Smooth position calculation with flowing motion | |
| x = final_radius * math.cos(base_angle) | |
| z = final_radius * math.sin(base_angle) | |
| # Smooth Y position with gentle undulation | |
| y_progress = (t_param / (2 * math.pi * TURNS)) | |
| base_y = y_progress * HEIGHT_RANGE - HEIGHT_RANGE/2 | |
| # Add flowing vertical waves | |
| y_wave1 = 0.1 * HEIGHT_RANGE * math.sin(3 * t_param + time_factor * 2 * math.pi) | |
| y_wave2 = 0.05 * HEIGHT_RANGE * math.sin(7 * t_param - time_factor * 3 * math.pi) | |
| y = base_y + y_wave1 + y_wave2 | |
| # Add crystalline segments only at specific intervals for hard corners | |
| segment_angle = 2 * math.pi / SEGMENTS | |
| current_segment = math.floor((t_param % (2 * math.pi)) / segment_angle) | |
| segment_progress = ((t_param % (2 * math.pi)) % segment_angle) / segment_angle | |
| # Apply sharp angular corrections only near segment boundaries | |
| if segment_progress < 0.1 or segment_progress > 0.9: | |
| # Sharp angular transitions for crystalline edges | |
| sharp_progress = sharpstep(segment_progress, CORNER_SHARPNESS) | |
| sharp_angle = current_segment * segment_angle + sharp_progress * segment_angle | |
| # Blend between smooth and sharp | |
| blend_factor = 1.0 - abs(segment_progress - 0.5) * 2 # peaks at boundaries | |
| blend_factor = blend_factor * blend_factor # sharper transition | |
| sharp_x = final_radius * math.cos(sharp_angle) | |
| sharp_z = final_radius * math.sin(sharp_angle) | |
| x = x * (1 - blend_factor) + sharp_x * blend_factor | |
| z = z * (1 - blend_factor) + sharp_z * blend_factor | |
| # Enhanced crystalline texture with flowing patterns | |
| crystal_flow = 0.03 * BASE_RADIUS * ( | |
| math.sin(t_param * 11 + time_factor * 5 * math.pi) + | |
| math.cos(t_param * 13 - time_factor * 3 * math.pi) + | |
| math.sin(t_param * 17 + time_factor * 7 * math.pi) | |
| ) | |
| # Add swirling motion to the crystal texture | |
| swirl_angle = time_factor * 2 * math.pi + t_param * 0.5 | |
| swirl_x = crystal_flow * math.cos(swirl_angle) | |
| swirl_z = crystal_flow * math.sin(swirl_angle) | |
| return np.array([x + swirl_x, y, z + swirl_z]) | |
| def rotate_y(vec: np.ndarray, angle: float) -> np.ndarray: | |
| """Rotate vector around Y axis""" | |
| cos_a, sin_a = math.cos(angle), math.sin(angle) | |
| return np.array([ | |
| vec[0] * cos_a + vec[2] * sin_a, | |
| vec[1], | |
| -vec[0] * sin_a + vec[2] * cos_a | |
| ]) | |
| def rotate_x(vec: np.ndarray, angle: float) -> np.ndarray: | |
| """Rotate vector around X axis""" | |
| cos_a, sin_a = math.cos(angle), math.sin(angle) | |
| return np.array([ | |
| vec[0], | |
| vec[1] * cos_a - vec[2] * sin_a, | |
| vec[1] * sin_a + vec[2] * cos_a | |
| ]) | |
| def rotate_z(vec: np.ndarray, angle: float) -> np.ndarray: | |
| """Rotate vector around Z axis""" | |
| cos_a, sin_a = math.cos(angle), math.sin(angle) | |
| return np.array([ | |
| vec[0] * cos_a - vec[1] * sin_a, | |
| vec[0] * sin_a + vec[1] * cos_a, | |
| vec[2] | |
| ]) | |
| # Pre-compute parameter values along spiral | |
| t_vals = np.linspace(0, 2 * math.pi * TURNS, COUNT, endpoint=False) | |
| # Initialize random positions for explosion phase | |
| np.random.seed(42) | |
| explosion_positions = np.random.rand(COUNT, 3) * GRID | |
| explosion_velocities = (np.random.rand(COUNT, 3) - 0.5) * 2.0 | |
| enc = splv.Encoder(GRID, GRID, GRID, framerate=FPS, outputPath=OUTPUT) | |
| print(f"Encoding {TOTAL_FRAMES} frames for crystalline spiral animation...") | |
| for frame_idx in range(TOTAL_FRAMES): | |
| global_time = frame_idx / TOTAL_FRAMES # 0 to 1 | |
| # Determine animation phase | |
| if global_time < EXPLOSION_PHASE: | |
| # Explosion phase - particles form from chaos | |
| phase_progress = global_time / EXPLOSION_PHASE | |
| formation_factor = smoothstep(0.3, 1.0, phase_progress) | |
| elif global_time < EXPLOSION_PHASE + SPIRAL_PHASE: | |
| # Spiral phase - crystalline structure | |
| phase_progress = (global_time - EXPLOSION_PHASE) / SPIRAL_PHASE | |
| formation_factor = 1.0 | |
| else: | |
| # Collapse phase - singularity | |
| phase_progress = (global_time - EXPLOSION_PHASE - SPIRAL_PHASE) / COLLAPSE_PHASE | |
| formation_factor = 1.0 - smoothstep(0.0, 0.8, phase_progress) | |
| # Rotation angles with crystalline angular jumps | |
| if global_time < EXPLOSION_PHASE + SPIRAL_PHASE: | |
| rot_y = global_time * 1.5 * 2 * math.pi # faster rotation during spiral | |
| rot_x = math.sin(global_time * math.pi) * 0.4 | |
| rot_z = global_time * 0.5 * 2 * math.pi | |
| else: | |
| # Accelerating rotation during collapse | |
| collapse_speed = 1.0 + 5.0 * phase_progress | |
| rot_y = global_time * 1.5 * 2 * math.pi * collapse_speed | |
| rot_x = math.sin(global_time * math.pi) * 0.4 | |
| rot_z = global_time * 0.5 * 2 * math.pi * collapse_speed | |
| frame = splv.Frame(GRID, GRID, GRID) | |
| for i in range(COUNT): | |
| if global_time < EXPLOSION_PHASE: | |
| # Explosion phase - transition from random to spiral | |
| spiral_pos = crystalline_spiral_pos(t_vals[i], global_time) | |
| spiral_pos = rotate_y(spiral_pos, rot_y) | |
| spiral_pos = rotate_x(spiral_pos, rot_x) | |
| spiral_pos = rotate_z(spiral_pos, rot_z) | |
| target_pos = CENTER + spiral_pos | |
| # Explosive motion | |
| explosion_pos = explosion_positions[i] + explosion_velocities[i] * frame_idx * 2 | |
| # Interpolate from explosion to formation | |
| world_pos = explosion_pos * (1 - formation_factor) + target_pos * formation_factor | |
| elif global_time < EXPLOSION_PHASE + SPIRAL_PHASE: | |
| # Spiral phase - crystalline structure | |
| spiral_pos = crystalline_spiral_pos(t_vals[i], global_time) | |
| # Apply rotations | |
| rotated_pos = rotate_y(spiral_pos, rot_y) | |
| rotated_pos = rotate_x(rotated_pos, rot_x) | |
| rotated_pos = rotate_z(rotated_pos, rot_z) | |
| world_pos = CENTER + rotated_pos | |
| else: | |
| # Collapse phase - singularity | |
| spiral_pos = crystalline_spiral_pos(t_vals[i], global_time) | |
| rotated_pos = rotate_y(spiral_pos, rot_y) | |
| rotated_pos = rotate_x(rotated_pos, rot_x) | |
| rotated_pos = rotate_z(rotated_pos, rot_z) | |
| spiral_world_pos = CENTER + rotated_pos | |
| # Collapse toward center with acceleration | |
| collapse_acceleration = phase_progress * phase_progress | |
| world_pos = spiral_world_pos * (1 - collapse_acceleration) + CENTER * collapse_acceleration | |
| # Add energy discharge effect near the end | |
| if phase_progress > 0.8: | |
| energy_factor = (phase_progress - 0.8) / 0.2 | |
| energy_radius = 20 * (1 - energy_factor) | |
| energy_angle = i * 0.1 + frame_idx * 0.3 | |
| energy_offset = np.array([ | |
| energy_radius * math.cos(energy_angle), | |
| energy_radius * math.sin(energy_angle * 1.3), | |
| energy_radius * math.sin(energy_angle * 0.7) | |
| ]) | |
| world_pos = CENTER + energy_offset * math.sin(frame_idx * 0.5) | |
| x, y, z = world_pos.astype(int) | |
| if 0 <= x < GRID and 0 <= y < GRID and 0 <= z < GRID: | |
| # Color based on position along spiral and phase | |
| hue_shift = 0.0 # Initialize hue_shift | |
| if global_time < EXPLOSION_PHASE: | |
| # Explosion colors - hot oranges and reds | |
| hue_base = 0.05 + 0.1 * (i / COUNT) | |
| hue_shift = global_time * 0.3 | |
| saturation = 0.9 + 0.1 * math.sin(global_time * 10 + i * 0.1) | |
| brightness = 0.7 + 0.3 * formation_factor | |
| elif global_time < EXPLOSION_PHASE + SPIRAL_PHASE: | |
| # Enhanced crystalline colors - flowing rainbow with crystalline accents | |
| # Base hue flows along the spiral path | |
| hue_base = (i / COUNT) * 0.8 + global_time * 0.3 # flowing rainbow | |
| hue_shift = 0.0 # Already included in hue_base | |
| # Add crystalline facet highlights | |
| segment = int((t_vals[i] % (2 * math.pi)) / (2 * math.pi / SEGMENTS)) | |
| facet_highlight = 0.1 * math.sin(global_time * 4 * math.pi + segment * 2) | |
| # Dynamic saturation based on spiral position | |
| spiral_phase = (t_vals[i] / (2 * math.pi * TURNS)) % 1.0 | |
| saturation = 0.7 + 0.3 * math.sin(spiral_phase * 4 * math.pi + global_time * 3 * math.pi) | |
| # Pulsing brightness with harmonic variations | |
| brightness_base = 0.8 + 0.2 * math.sin(global_time * 2 * math.pi + i * 0.1) | |
| brightness_detail = 0.1 * math.sin(global_time * 6 * math.pi + t_vals[i] * 3) | |
| brightness = max(0.0, min(1.0, brightness_base + brightness_detail + facet_highlight)) | |
| else: | |
| # Collapse colors - bright white/energy discharge | |
| if phase_progress > 0.8: | |
| # Energy discharge - white hot | |
| hue_base = 0.0 | |
| hue_shift = 0.0 | |
| saturation = 0.2 | |
| brightness = 1.0 | |
| else: | |
| # Collapsing - intense colors | |
| hue_base = 0.8 + 0.2 * (i / COUNT) # magenta range | |
| hue_shift = global_time * 0.5 | |
| saturation = 1.0 | |
| brightness = 0.9 + 0.1 * math.sin(frame_idx * 0.2) | |
| hue = (hue_base + hue_shift) % 1.0 | |
| color = hsv_bytes(hue, saturation, brightness) | |
| # Enhanced particle size during key moments | |
| if global_time < EXPLOSION_PHASE and formation_factor < 0.5: | |
| # Larger particles during explosion | |
| for dx in [-1, 0, 1]: | |
| for dy in [-1, 0, 1]: | |
| for dz in [-1, 0, 1]: | |
| nx, ny, nz = x + dx, y + dy, z + dz | |
| if 0 <= nx < GRID and 0 <= ny < GRID and 0 <= nz < GRID: | |
| frame.set_voxel(nx, ny, nz, color) | |
| elif global_time > EXPLOSION_PHASE + SPIRAL_PHASE and phase_progress > 0.8: | |
| # Energy discharge effect | |
| energy_size = int(3 * (1 - (phase_progress - 0.8) / 0.2)) | |
| for dx in range(-energy_size, energy_size + 1): | |
| for dy in range(-energy_size, energy_size + 1): | |
| for dz in range(-energy_size, energy_size + 1): | |
| nx, ny, nz = x + dx, y + dy, z + dz | |
| if (0 <= nx < GRID and 0 <= ny < GRID and 0 <= nz < GRID and | |
| abs(dx) + abs(dy) + abs(dz) <= energy_size): | |
| bright_color = hsv_bytes(hue, saturation * 0.5, brightness) | |
| frame.set_voxel(nx, ny, nz, bright_color) | |
| else: | |
| # Normal particle | |
| frame.set_voxel(x, y, z, color) | |
| enc.encode(frame) | |
| # Progress indicator | |
| if frame_idx % FPS == 0: | |
| seconds_done = frame_idx // FPS + 1 | |
| phase_name = "Explosion" if global_time < EXPLOSION_PHASE else \ | |
| "Crystalline Spiral" if global_time < EXPLOSION_PHASE + SPIRAL_PHASE else \ | |
| "Collapse" | |
| print(f" second {seconds_done} / {DURATION} - {phase_name}") | |
| enc.finish() | |
| print("Done. Saved", OUTPUT) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment