Note
Go to the end to download the full example code.
Vortex (Helical) Solar System Model#
In this tutorial, we will create a vortex (helical) animation of the solar system. In the actual universe, the Sun is not stationary, it travels through the Milky Way galaxy at a high speed. This causes the planets to trace out beautiful 3D helical/corkscrew trajectories (vortexes) through space rather than closed, static ellipses.
import numpy as np
from scipy.spatial.transform import Rotation as Rot
from fury import actor, window
from fury.ui import PlaybackPanel, TextBlock2D
from fury.data import fetch_viz_textures, read_viz_textures
from fury.lib import EventType
Create a scene to start.
scene = window.Scene(background=(0.01, 0.01, 0.02))
Define information relevant for each planet actor including its texture name, relative orbital position, and scale. We also add a custom color for the trailing helical orbit to match the planet’s visual aesthetic.
planets_data = [
{
"filename": "8k_mercury.jpg",
"position": 8,
"earth_days": 58.0,
"scale": (0.3, 0.3, 0.3),
"trail_color": (0.6, 0.6, 0.6), # Gray
},
{
"filename": "8k_venus_surface.jpg",
"position": 10,
"earth_days": 243.0,
"scale": (0.76, 0.76, 0.76),
"trail_color": (0.8, 0.6, 0.4), # Bronze
},
{
"filename": "1_earth_8k.jpg",
"position": 12,
"earth_days": 1.0,
"scale": (0.8, 0.8, 0.8),
"trail_color": (0.2, 0.6, 1.0), # Sky blue
},
{
"filename": "8k_mars.jpg",
"position": 14,
"earth_days": 1.03,
"scale": (0.42, 0.42, 0.42),
"trail_color": (0.9, 0.3, 0.2), # Red
},
{
"filename": "jupiter.jpg",
"position": 20,
"earth_days": 0.41,
"scale": (2.5, 2.5, 2.5),
"trail_color": (0.8, 0.5, 0.3), # Brownish orange
},
{
"filename": "8k_saturn.jpg",
"position": 28,
"earth_days": 0.45,
"scale": (2.1, 2.1, 2.1),
"trail_color": (0.9, 0.8, 0.5), # Yellow
},
{
"filename": "8k_saturn_ring_alpha.png",
"position": 28,
"earth_days": 0.45,
"scale": (3.15, 0.5, 3.15),
"trail_color": (0.9, 0.8, 0.5),
},
{
"filename": "2k_uranus.jpg",
"position": 38,
"earth_days": 0.72,
"scale": (1.3, 1.3, 1.3),
"trail_color": (0.4, 0.8, 0.8), # Cyan
},
{
"filename": "2k_neptune.jpg",
"position": 49,
"earth_days": 0.67,
"scale": (1.25, 1.25, 1.25),
"trail_color": (0.2, 0.4, 0.9), # Deep blue
},
{
"filename": "8k_sun.jpg",
"position": 0,
"earth_days": 27.0,
"scale": (6.0, 6.0, 6.0),
"trail_color": (1.0, 0.7, 0.1), # Glowing gold/orange
},
]
fetch_viz_textures()
Dataset is already in place. If you want to fetch it again please first remove the folder C:\Users\magor\.fury\textures
({'1_earth_8k.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/1_earth_8k.jpg', '0D66DC62768C43D763D3288CE67128AAED27715B11B0529162DC4117F710E26F'), '2_no_clouds_8k.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/2_no_clouds_8k.jpg', '5CF740C72287AF7B3ACCF080C3951944ADCB1617083B918537D08CBD9F2C5465'), '5_night_8k.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/5_night_8k.jpg', 'DF443F3E20C7724803690A350D9F6FDB36AD8EBC011B0345FB519A8B321F680A'), 'earth.ppm': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/earth.ppm', '34CE9AD183D7C7B11E2F682D7EBB84C803E661BE09E01ADB887175AE60C58156'), 'jupiter.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/jupiter.jpg', '5DF6A384E407BD0D5F18176B7DB96AAE1EEA3CFCFE570DDCE0D34B4F0E493668'), 'masonry.bmp': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/masonry.bmp', '045E30B2ABFEAE6318C2CF955040C4A37E6DE595ACE809CE6766D397C0EE205D'), 'moon-8k.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/moon_8k.jpg', '7397A6C2CE0348E148C66EBEFE078467DDB9D0370FF5E63434D0451477624839'), '8k_mercury.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/8k_mercury.jpg', '5C8BD885AE3571C6BA2CD34B3446B9C6D767E314BF0EE8C1D5C147CADD388FC3'), '8k_venus_surface.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/8k_venus_surface.jpg', '9BC21A50577ED8AC734CDA91058724C7A741C19427AA276224CE349351432C5B'), '8k_mars.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/8k_mars.jpg', '4CC52149924ABC6AE507D63032F994E1D42A55CB82C09E002D1A567FF66C23EE'), '8k_saturn.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/8k_saturn.jpg', '0D39A4A490C87C3EDABE00A3881A29BB3418364178C79C534FE0986E97E09853'), '8k_saturn_ring_alpha.png': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/8k_saturn_ring_alpha.png', 'F1F826933C9FF87D64ECF0518D6256B8ED990B003722794F67E96E3D2B876AE4'), '2k_uranus.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/2k_uranus.jpg', 'D15239D46F82D3EA13D2B260B5B29B2A382F42F2916DAE0694D0387B1204A09D'), '2k_neptune.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/2k_neptune.jpg', 'CB42EA82709741D28B0AF44D8B283CBC6DBD0C521A7F0E1E1E010ADE00977DF6'), '8k_sun.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/8k_sun.jpg', 'F22B1CFB306DDCE72A7E3B628668A0175B745038CE6268557CB2F7F1BDF98B9D'), '1_earth_16k.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/1_earth_16k.jpg', '7DD1DAC926101B5D7B7F2E952E53ACF209421B5CCE57C03168BCE0AAD675998A'), 'clouds.jpg': ('https://raw.githubusercontent.com/fury-gl/fury-data/master/textures/clouds.jpg', '85043336E023C4C9394CFD6D48D257A5564B4F895BFCEC01C70E4898CC77F003')}, 'C:\\Users\\magor\\.fury\\textures')
Initialize all planet actors.
planets = []
for p_data in planets_data:
filename = p_data["filename"]
pos = p_data["position"]
earth_days = p_data["earth_days"]
scale = p_data["scale"]
trail_color = p_data["trail_color"]
planet_file = read_viz_textures(filename)
initial_pos = [float(pos), 0.0, 0.0]
actor_obj = actor.texture_on_sphere(planet_file, theta=60, phi=60)
actor_obj.local.scale = scale
actor_obj.local.position = initial_pos
scene.add(actor_obj)
is_ring = "saturn_ring" in filename
is_sun = "sun" in filename
p_info = {
"filename": filename,
"actor": actor_obj,
"radius": float(pos),
"earth_days": float(earth_days),
"is_ring": is_ring,
"is_sun": is_sun,
"trail_color": trail_color,
}
planets.append(p_info)
sun_info = next(p for p in planets if p["is_sun"])
Physics of the Helical/Vortex Orbit: Gravity constant G and central mass of the Sun.
g_exponent = np.float_power(10, -11)
g_constant = 6.673 * g_exponent
m_exponent = 1073741824
m_constant = 1.989 * m_exponent
miu = m_constant * g_constant
# Unit travel direction and velocity vector of the Sun through space
u_vec = np.array([0.25, 0.12, -0.05])
v_sun = np.linalg.norm(u_vec)
u_hat = u_vec / v_sun
# Determine two perpendicular unit vectors p_hat and q_hat defining the orbital plane
if abs(u_hat[2]) < 0.9:
p_vec = np.cross(u_hat, np.array([0.0, 0.0, 1.0]))
else:
p_vec = np.cross(u_hat, np.array([1.0, 0.0, 0.0]))
p_hat = p_vec / np.linalg.norm(p_vec)
q_hat = np.cross(u_hat, p_hat)
R_align_mat = np.column_stack((p_hat, u_hat, -q_hat))
R_align = Rot.from_matrix(R_align_mat)
def get_orbit_period(radius):
if radius == 0.0:
return 1.0
return 2.0 * np.pi * np.sqrt(np.power(radius, 3) / miu)
def get_relative_position(p_info, t):
"""Compute the position relative to the Sun at time t."""
if p_info["is_sun"]:
return np.array([0.0, 0.0, 0.0])
orbit_period = get_orbit_period(p_info["radius"])
theta = -2.0 * np.pi * t / orbit_period
# Calculate relative position in the perpendicular orbital plane
pos_orbit = p_info["radius"] * (np.cos(theta) * p_hat + np.sin(theta) * q_hat)
return pos_orbit
State management dictionary.
state = {
"current_time": 0.0,
"rotation_speed": 1.0,
"show_trails": True,
"orbit_actor": None,
"camera_mode": "cinematic",
}
def update_planet_transforms(p_info, t):
actor_obj = p_info["actor"]
if p_info["is_sun"]:
angle_axial = (50.0 / p_info["earth_days"]) * t
R_axial = Rot.from_euler("y", angle_axial, degrees=True)
actor_obj.local.rotation = (R_align * R_axial).as_quat()
actor_obj.local.position = get_relative_position(p_info, t)
else:
pos = get_relative_position(p_info, t)
actor_obj.local.position = pos
if not p_info["is_ring"]:
angle_axial = (50.0 / p_info["earth_days"]) * t
R_axial = Rot.from_euler("y", angle_axial, degrees=True)
actor_obj.local.rotation = (R_align * R_axial).as_quat()
else:
actor_obj.local.rotation = R_align.as_quat()
def update_helical_trails(t):
"""Draw trailing helical corkscrew trails for all bodies in the scene."""
global scene
if state["orbit_actor"] is not None:
scene.remove(state["orbit_actor"])
state["orbit_actor"] = None
if not state["show_trails"]:
return
all_tracks = []
all_colors = []
trail_length = 1000.0
start_t = max(0.0, t - trail_length)
if t <= 1.0:
return
for p in planets:
track_points = []
for s in np.linspace(start_t, t, 150):
pos_orbit = get_relative_position(p, s)
drawn_pos = pos_orbit - u_vec * (t - s)
track_points.append(drawn_pos)
all_tracks.append(np.array(track_points))
all_colors.append(p["trail_color"])
trail_actor = actor.line(all_tracks, colors=all_colors)
trail_actor.local.position = (0.0, 0.0, 0.0)
scene.add(trail_actor)
state["orbit_actor"] = trail_actor
Initialize UI and HUD.
hud_text = TextBlock2D(
text="Vortex Solar System (Helical Model)\n"
"Press [T] to Toggle Trailing Orbits: ON\n"
"Press [C] to Toggle 2D/3D View\n"
"Press [R] to Reset Camera",
position=(50, 650),
font_size=18,
color=(1.0, 1.0, 1.0),
dynamic_bbox=True,
)
scene.add(hud_text)
playback_ui = PlaybackPanel(position=(50, 50), width=700, loop=True)
playback_ui.final_time = 10000.0
scene.add(playback_ui)
Set up camera and follow behavior.
showm = window.ShowManager(
scene=scene, size=(900, 768), title="FURY Vortex Solar System Animation"
)
camera = showm.screens[0].camera
camera.local.position = np.array([-30.0, 75.0, 142.5])
camera.look_at((0.0, 0.0, 0.0))
Interactive events and keyboard controls.
def handle_key_event(event):
if event.key.lower() == "t":
state["show_trails"] = not state["show_trails"]
status = "ON" if state["show_trails"] else "OFF"
hud_text.message = (
"Vortex Solar System (Helical Model)\n"
f"Press [T] to Toggle Trailing Orbits: {status}\n"
"Press [C] to Toggle 2D/3D View\n"
"Press [R] to Reset Camera"
)
update_helical_trails(state["current_time"])
showm.render()
elif event.key.lower() == "c":
if state["camera_mode"] == "cinematic":
state["camera_mode"] = "top_down"
camera.local.position = u_vec * 150.0
camera.look_at((0.0, 0.0, 0.0))
else:
state["camera_mode"] = "cinematic"
camera.local.position = np.array([-30.0, 75.0, 142.5])
camera.look_at((0.0, 0.0, 0.0))
controller = showm.screens[0].controller
if hasattr(controller, "target"):
controller.target = np.array([0.0, 0.0, 0.0])
showm.render()
elif event.key.lower() == "r":
if state["camera_mode"] == "cinematic":
camera.local.position = np.array([-30.0, 75.0, 142.5])
else:
camera.local.position = u_vec * 150.0
camera.look_at((0.0, 0.0, 0.0))
controller = showm.screens[0].controller
if hasattr(controller, "target"):
controller.target = np.array([0.0, 0.0, 0.0])
showm.render()
showm.renderer.add_event_handler(handle_key_event, EventType.KEY_UP)
<function handle_key_event at 0x0000026BD3041620>
Callback functions for ShowManager progress synchronization.
def on_progress_changed(t):
state["current_time"] = t
for p in planets:
update_planet_transforms(p, t)
update_helical_trails(t)
def on_speed_changed(s):
state["rotation_speed"] = s
playback_ui.on_progress_bar_changed = on_progress_changed
playback_ui.on_speed_changed = on_speed_changed
def update_playback_logic(show_manager_obj=None):
if playback_ui._playing:
step = 1.0 * state["rotation_speed"]
state["current_time"] += step
if state["current_time"] > playback_ui.final_time:
if playback_ui._loop:
state["current_time"] = 0.0
else:
state["current_time"] = playback_ui.final_time
playback_ui.pause()
playback_ui.current_time = state["current_time"]
on_progress_changed(state["current_time"])
showm.register_callback(update_playback_logic, 0.01, True, "PlaybackSync", showm)
on_progress_changed(0.0)
Start the interactive simulation window.
if __name__ == "__main__":
showm.start()

Total running time of the script: (0 minutes 10.160 seconds)