diff --git a/.gitignore b/.gitignore
index e7b0893..e838828 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,8 @@ __pycache__/
.matplotlib/
.venv/
.DS_Store
+*.blend
*.blend1
+# Local debug / probe frames (never products)
+output/**/*_smoke_*
+output/**/overlay_probe.png
diff --git a/README.md b/README.md
index 7874b7f..239d7fc 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ SOLSYS/
│ ├── solar_system_animator.py # SolarSystemAnimator, renderAllAnimations
│ ├── camera_controller.py # CameraController
│ ├── animation_styles.py # Light/dark styles and timing constants
+│ ├── blender_body_sprites.py # Blender spin-loop frames for cinematic Earth/Moon
│ └── scenes/
│ ├── inner_system.py # Fixed 2D inner-system scene
│ ├── zoom_tour.py # Staged 3D Oort → inner zoom
@@ -84,6 +85,7 @@ SOLSYS/
├── tests/ # Unit tests (stdlib unittest)
│ ├── test_frame_transform.py # Sol ↔ α Cen frame transform
│ ├── test_sol_centauri_cinematic.py # Sol → α Cen cinematic helpers
+│ ├── test_blender_body_sprites.py # Blender sprites + cinematic billboards
│ └── test_blender_pipeline.py # Blender export / ingest scaffold
│
├── solsys/ # Shared libraries (no plotting)
@@ -111,7 +113,8 @@ SOLSYS/
│ ├── 2d/ # inner_solar_system_{light,dark}.gif
│ ├── 3d/ # solar_system_{light,dark}.gif
│ ├── alpha_centauri/ # ab / system_wide / proxima_planets GIFs
-│ ├── sol_centauri/ # sol_centauri_cinematic_{light,dark}.gif
+│ ├── sol_centauri/ # classic cinematic GIFs
+│ │ └── blender/ # cinematic with textured Earth/Moon
│ ├── barnards_star/ # barnards_star_planets_{light,dark}.gif
│ ├── trappist_1/ # trappist_1_planets_{light,dark}.gif
│ ├── tabbys_star/ # tabbys_star_dust_{light,dark}.gif
@@ -305,6 +308,7 @@ Or render by product:
.venv/bin/python render.py animate --dimension 3d
.venv/bin/python render.py animate --system alpha_centauri
.venv/bin/python render.py animate --system sol_centauri
+.venv/bin/python render.py animate --system sol_centauri --blender-bodies
.venv/bin/python render.py animate --system barnards_star
.venv/bin/python render.py animate --system trappist_1
.venv/bin/python render.py animate --system tabbys_star
@@ -314,12 +318,27 @@ Or render by product:
.venv/bin/python render.py static --dimension 2d
.venv/bin/python render.py neighborhood --ly 10
.venv/bin/python render.py blender --body Earth
-.venv/bin/python render.py blender --body Moon --flyby # Moon close-up GIFs
.venv/bin/python render.py blender --body Earth --load # optional debug ingest
.venv/bin/python render.py blender --body Earth --flyby # light/dark close-up GIFs
+.venv/bin/python render.py blender --body Moon --flyby # Moon close-up GIFs
+.venv/bin/python render.py blender --body Earth --spin # RGBA day/night loop for cinematic
+.venv/bin/python render.py blender --body Moon --spin # Moon spin loop for cinematic
+.venv/bin/python render.py blender --pipeline # Earth+Moon spins + blender cinematic
+```
+
+Blender close-ups: `render.py blender` exports Keplerian body-scene JSON (`#11`). `render.py blender --flyby` renders a body-centered close-up via EEVEE (`#12`/`#36`/`#41`) to `output/animate/blender/{planets,moons}/
/` (`*_flyby_{light,dark}.gif`). `render.py blender --spin` writes a fixed-camera transparent PNG day/night loop (`*_spin_/frame_*.png`) for cinematic reuse. Texture packs live under `data/textures/bodies/`.
+
+Blender → cinematic workflow:
+
+```bash
+.venv/bin/python render.py blender --body Earth --spin
+.venv/bin/python render.py blender --body Moon --spin
+.venv/bin/python render.py animate --system sol_centauri --blender-bodies
+# or one-shot:
+.venv/bin/python render.py blender --pipeline
```
-Blender close-ups: `render.py blender` exports Keplerian body-scene JSON (`#11`). `render.py blender --flyby` renders a body-centered close-up via EEVEE (`#12`/`#36`/`#41`) to `output/animate/blender/{planets,moons}//` (`*_flyby_{light,dark}.gif`). Texture packs live under `data/textures/bodies/`.
+Sol → Centauri cinematic with `--blender-bodies` (issue #42) keeps the same camera tour but composites those Blender spin frames at Earth/Moon **each `FuncAnimation` frame** (not GIF concat). Outputs go under `output/animate/sol_centauri/blender/` (classic dotted GIFs stay in `output/animate/sol_centauri/`).
Alpha Centauri (issue #1) is one `system_id` covering A, B, and Proxima. Animations render to `output/animate/alpha_centauri/`:
@@ -327,17 +346,20 @@ Alpha Centauri (issue #1) is one `system_id` covering A, B, and Proxima. Animati
- `alpha_centauri_system_{light,dark}.gif` — wide triple with Proxima (~8.7 kau)
- `proxima_planets_{light,dark}.gif` — confirmed Proxima planets (via shared `exoplanet_system` animator)
-Sol → Alpha Centauri cinematic (issue #10) flies from our solar system to the A–B close-up, zooms back out, then finishes on Proxima and its planets — all in Sol XYZ via `SolCentauriFrameTransform`. Animations render to `output/animate/sol_centauri/`:
+Sol → Alpha Centauri cinematic (issue #10) flies from our solar system to the A–B close-up, zooms back out, then finishes on Proxima and its planets — all in Sol XYZ via `SolCentauriFrameTransform`:
-- `sol_centauri_cinematic_{light,dark}.gif` — Sol → AB → wide → Proxima planets
+- `output/animate/sol_centauri/sol_centauri_cinematic_{light,dark}.gif` — classic scatter-dot bodies
+- `output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_{light,dark}.gif` — Blender spin billboards on Earth/Moon (`--blender-bodies` or `blender --pipeline`)
-Earth Blender close-up (issues #12 / #36) is a body-centered EEVEE view of Earth with NASA Blue Marble texture (light/dark). Render with `render.py blender --body Earth --flyby` to `output/animate/blender/planets/earth/`:
+Earth Blender close-up (issues #12 / #36) is a body-centered EEVEE view of Earth with NASA Blue Marble texture (light/dark). Render with `render.py blender --body Earth --flyby` / `--spin` to `output/animate/blender/planets/earth/`:
- `earth_flyby_{light,dark}.gif` — textured sphere, clouds, thin atmosphere limb, elevated turntable camera + spin
+- `earth_spin_{light,dark}/frame_*.png` — fixed-camera RGBA day/night loop for the cinematic (`--spin`)
Moon Blender close-up (issue #41) uses the shared pack path with an LRO LROC color map (airless — no atmosphere/clouds) under `output/animate/blender/moons/moon/`:
- `moon_flyby_{light,dark}.gif` — textured lunar sphere, elevated turntable camera + spin
+- `moon_spin_{light,dark}/frame_*.png` — fixed-camera RGBA spin loop for the cinematic (`--spin`)
Barnard's Star (issue #15) is a nearby M dwarf (~6 ly) with four confirmed sub-Earth planets. Animations render via `exoplanet_system` to `output/animate/barnards_star/`:
diff --git a/animate/blender_body_sprites.py b/animate/blender_body_sprites.py
new file mode 100644
index 0000000..7fbca83
--- /dev/null
+++ b/animate/blender_body_sprites.py
@@ -0,0 +1,118 @@
+"""Load Blender spin-loop RGBA frames for the Sol→Centauri cinematic.
+
+Assets come from ``render.py blender --spin`` (fixed camera, full rotation,
+transparent PNGs). The cinematic indexes into that loop each ``update()`` —
+rendered together, not GIF-concatenated afterward.
+
+Earth: free spin by animation frame.
+Moon: tidally locked — spin index follows orbital mean anomaly (one turn / orbit).
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+from animate.scenes.blender.flyby_scene import spinFramesDirectory
+
+DEFAULT_DISPLAY_RESOLUTION = 384
+
+
+def loadSpinLoopFrames(
+ bodyName: str,
+ theme: str,
+ *,
+ outputDirectory: Path | str = 'output/animate/blender',
+) -> list[np.ndarray] | None:
+ """Load persistent ``*_spin_/frame_*.png`` as float RGBA arrays."""
+ directory = spinFramesDirectory(bodyName, theme, outputDirectory=outputDirectory)
+ paths = sorted(directory.glob('frame_*.png'))
+ if not paths:
+ return None
+
+ frames: list[np.ndarray] = []
+ for path in paths:
+ with Image.open(path) as image:
+ frames.append(np.asarray(image.convert('RGBA'), dtype=np.float32) / 255.0)
+ return frames
+
+
+def _resizeRgba(rgba: np.ndarray, size: int) -> np.ndarray:
+ if rgba.shape[0] == size and rgba.shape[1] == size:
+ return rgba
+ image = Image.fromarray((np.clip(rgba, 0.0, 1.0) * 255.0).astype(np.uint8), mode='RGBA')
+ image = image.resize((size, size), Image.Resampling.LANCZOS)
+ return np.asarray(image, dtype=np.float32) / 255.0
+
+
+def tidalLockFrameIndex(orbitalPhaseRad: float, frameCount: int) -> int:
+ """Map orbital mean anomaly → spin-loop index (synchronous rotation)."""
+ return int(np.floor(tidalLockFramePosition(orbitalPhaseRad, frameCount))) % frameCount
+
+
+def tidalLockFramePosition(orbitalPhaseRad: float, frameCount: int) -> float:
+ """Fractional spin-loop position for smooth tidally locked blending."""
+ if frameCount <= 0:
+ raise ValueError('frameCount must be positive')
+ turns = float(orbitalPhaseRad) / (2.0 * np.pi)
+ return turns * frameCount
+
+
+def _blendSpinFrames(frames: list[np.ndarray], position: float) -> np.ndarray:
+ """Linear blend between neighboring loop frames (seamless at wrap)."""
+ count = len(frames)
+ wrapped = position % count
+ index0 = int(np.floor(wrapped)) % count
+ index1 = (index0 + 1) % count
+ blend = float(wrapped - np.floor(wrapped))
+ if blend <= 1e-6:
+ return frames[index0]
+ if blend >= 1.0 - 1e-6:
+ return frames[index1]
+ return frames[index0] * (1.0 - blend) + frames[index1] * blend
+
+
+class BlenderBodySpriteAtlas:
+ """Theme-scoped Earth/Moon Blender spin loops for in-animation billboards."""
+
+ def __init__(self, theme: str, *, outputDirectory: Path | str = 'output/animate/blender'):
+ self.theme = theme
+ self.earth = loadSpinLoopFrames('Earth', theme, outputDirectory=outputDirectory)
+ self.moon = loadSpinLoopFrames('Moon', theme, outputDirectory=outputDirectory)
+
+ @property
+ def hasEarth(self) -> bool:
+ return bool(self.earth)
+
+ @property
+ def hasMoon(self) -> bool:
+ return bool(self.moon)
+
+ def earthFrame(
+ self, frame: int, *, resolution: int = DEFAULT_DISPLAY_RESOLUTION
+ ) -> np.ndarray | None:
+ if not self.earth:
+ return None
+ # Fractional position so Earth also eases between PNG samples.
+ position = float(frame) % len(self.earth)
+ rgba = _blendSpinFrames(self.earth, position)
+ return _resizeRgba(rgba, resolution)
+
+ def moonFrame(
+ self,
+ frame: int,
+ *,
+ orbitalPhaseRad: float | None = None,
+ resolution: int = DEFAULT_DISPLAY_RESOLUTION,
+ ) -> np.ndarray | None:
+ """Moon disk. Prefer ``orbitalPhaseRad`` for tidal lock; ``frame`` is fallback."""
+ if not self.moon:
+ return None
+ if orbitalPhaseRad is None:
+ position = float(frame) % len(self.moon)
+ else:
+ position = tidalLockFramePosition(orbitalPhaseRad, len(self.moon))
+ rgba = _blendSpinFrames(self.moon, position)
+ return _resizeRgba(rgba, resolution)
diff --git a/animate/scenes/blender/README.md b/animate/scenes/blender/README.md
index fd3ea15..c349c98 100644
--- a/animate/scenes/blender/README.md
+++ b/animate/scenes/blender/README.md
@@ -50,6 +50,31 @@ Earth uses NASA Blue Marble color + cloud maps under `data/textures/bodies/earth
.venv/bin/python render.py blender --body Moon --flyby
```
+Fixed-camera day/night spin loops (RGBA PNGs for cinematic reuse):
+
+```bash
+.venv/bin/python render.py blender --body Earth --spin
+.venv/bin/python render.py blender --body Moon --spin
+```
+
+End-to-end (Earth + Moon spins, then Sol→Centauri cinematic compositing those frames):
+
+```bash
+.venv/bin/python render.py blender --pipeline
+```
+
+Products land in:
+
+```text
+output/animate/blender/{planets,moons}//
+ *_flyby_{light,dark}.gif # gallery close-ups
+ *_spin_{light,dark}/frame_*.png # cinematic spin loops (transparent)
+ *_spin_{light,dark}.gif # preview of the spin loop
+output/animate/sol_centauri/blender/ # cinematic with Earth/Moon spin billboards
+```
+
+Local debug leftovers (`.blend`, `*_smoke_*`, `overlay_probe.png`) are gitignored / not products.
+
### Debug ingest (orbit view, not the flyby)
```bash
diff --git a/animate/scenes/blender/flyby_camera.py b/animate/scenes/blender/flyby_camera.py
index 441ae92..586e46c 100644
--- a/animate/scenes/blender/flyby_camera.py
+++ b/animate/scenes/blender/flyby_camera.py
@@ -55,3 +55,34 @@ def buildFlybyCameraPath(
)
)
return tuple(samples)
+
+
+def buildSpinCameraPath(
+ displayRadiusAu: float,
+ frameCount: int = 48,
+ *,
+ elevationDeg: float = 18.0,
+ azimuthDeg: float = 35.0,
+) -> tuple[FlybyCameraSample, ...]:
+ """Fixed camera + full 360° body spin (seamless loop for cinematic reuse)."""
+ if frameCount < 2:
+ raise ValueError('frameCount must be >= 2')
+ cameraAu = flybyCameraLocation(
+ 0,
+ 2,
+ displayRadiusAu,
+ elevationDeg=elevationDeg,
+ azimuthStartDeg=azimuthDeg,
+ azimuthSweepDeg=0.0,
+ )
+ samples: list[FlybyCameraSample] = []
+ for frame in range(frameCount):
+ # Omit 360° on the last frame so index 0 and N line up when looping.
+ samples.append(
+ FlybyCameraSample(
+ frame=frame,
+ cameraAu=cameraAu,
+ bodyRotationDeg=(frame / frameCount) * 360.0,
+ )
+ )
+ return tuple(samples)
diff --git a/animate/scenes/blender/flyby_scene.py b/animate/scenes/blender/flyby_scene.py
index 31a1f8a..2c47b9b 100644
--- a/animate/scenes/blender/flyby_scene.py
+++ b/animate/scenes/blender/flyby_scene.py
@@ -13,8 +13,13 @@
from animate.scenes.blender.body_appearance import appearanceForCatalogName
from animate.scenes.blender.body_scene import buildBodyScene
-from animate.scenes.blender.export_body import DEFAULT_OUTPUT_DIRECTORY, bodyStem, exportBodyScene
-from animate.scenes.blender.flyby_camera import buildFlybyCameraPath
+from animate.scenes.blender.export_body import (
+ DEFAULT_OUTPUT_DIRECTORY,
+ bodyOutputDirectory,
+ bodyStem,
+ exportBodyScene,
+)
+from animate.scenes.blender.flyby_camera import buildFlybyCameraPath, buildSpinCameraPath
from animate.scenes.blender.render_flyby import JOB_SCHEMA_ID
FLYBY_EXTENSION_POINT = 'animate.scenes.blender.flyby_scene.renderPlanetFlyby'
@@ -24,6 +29,9 @@
DEFAULT_FLYBY_FRAMES = 72
DEFAULT_FLYBY_RESOLUTION = 640
DEFAULT_FLYBY_FPS = 18
+DEFAULT_SPIN_FRAMES = 48
+DEFAULT_SPIN_RESOLUTION = 512
+DEFAULT_SPIN_FPS = 20
def preparePlanetFlybyExport(
@@ -40,24 +48,26 @@ def preparePlanetFlybyExport(
)
-def buildFlybyJob(
- bodyName: str = 'Earth',
+def _bodyJobSkeleton(
+ bodyName: str,
*,
- theme: Theme = 'dark',
- frameCount: int = DEFAULT_FLYBY_FRAMES,
- resolution: int = DEFAULT_FLYBY_RESOLUTION,
- fps: int = DEFAULT_FLYBY_FPS,
+ theme: Theme,
+ frameCount: int,
+ resolution: int,
+ fps: int,
framesDirectory: Path | str,
+ cameraPath,
+ filmTransparent: bool = False,
+ mode: str = 'flyby',
) -> dict:
- """Build a Blender flyby job dict from PlanetCatalog/MoonCatalog + camera path."""
if theme not in ('light', 'dark'):
raise ValueError(f'theme must be light or dark, got {theme!r}')
bodyScene = buildBodyScene(bodyName, frameCount=max(frameCount, 2))
- cameraPath = buildFlybyCameraPath(bodyScene.body.displayRadiusAu, frameCount=frameCount)
appearance = appearanceForCatalogName(bodyScene.body.name)
job: dict = {
'schema': JOB_SCHEMA_ID,
'theme': theme,
+ 'mode': mode,
'body': {
'name': bodyScene.body.name,
'kind': bodyScene.body.kind,
@@ -78,12 +88,78 @@ def buildFlybyJob(
'outputDirectory': str(Path(framesDirectory)),
'resolution': resolution,
'fps': fps,
+ 'filmTransparent': filmTransparent,
}
if appearance is not None:
job['appearance'] = appearance.toJobDict()
return job
+def buildFlybyJob(
+ bodyName: str = 'Earth',
+ *,
+ theme: Theme = 'dark',
+ frameCount: int = DEFAULT_FLYBY_FRAMES,
+ resolution: int = DEFAULT_FLYBY_RESOLUTION,
+ fps: int = DEFAULT_FLYBY_FPS,
+ framesDirectory: Path | str,
+) -> dict:
+ """Build a Blender flyby job dict from PlanetCatalog/MoonCatalog + camera path."""
+ bodyScene = buildBodyScene(bodyName, frameCount=max(frameCount, 2))
+ cameraPath = buildFlybyCameraPath(bodyScene.body.displayRadiusAu, frameCount=frameCount)
+ return _bodyJobSkeleton(
+ bodyName,
+ theme=theme,
+ frameCount=frameCount,
+ resolution=resolution,
+ fps=fps,
+ framesDirectory=framesDirectory,
+ cameraPath=cameraPath,
+ filmTransparent=False,
+ mode='flyby',
+ )
+
+
+def buildSpinJob(
+ bodyName: str = 'Earth',
+ *,
+ theme: Theme = 'dark',
+ frameCount: int = DEFAULT_SPIN_FRAMES,
+ resolution: int = DEFAULT_SPIN_RESOLUTION,
+ fps: int = DEFAULT_SPIN_FPS,
+ framesDirectory: Path | str,
+) -> dict:
+ """Fixed-camera full-rotation job (transparent PNGs for cinematic reuse)."""
+ bodyScene = buildBodyScene(bodyName, frameCount=max(frameCount, 2))
+ cameraPath = buildSpinCameraPath(bodyScene.body.displayRadiusAu, frameCount=frameCount)
+ return _bodyJobSkeleton(
+ bodyName,
+ theme=theme,
+ frameCount=frameCount,
+ resolution=resolution,
+ fps=fps,
+ framesDirectory=framesDirectory,
+ cameraPath=cameraPath,
+ filmTransparent=True,
+ mode='spin',
+ )
+
+
+def spinFramesDirectory(
+ bodyName: str,
+ theme: Theme,
+ *,
+ outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
+) -> Path:
+ """Persistent PNG loop: ``…/planets/earth/earth_spin_dark/frame_*.png``."""
+ bodyScene = buildBodyScene(bodyName, frameCount=2)
+ stem = bodyStem(bodyName)
+ return (
+ bodyOutputDirectory(bodyScene.body.kind, bodyName, root=outputDirectory)
+ / f'{stem}_spin_{theme}'
+ )
+
+
def writeFlybyJob(job: dict, path: Path | str) -> Path:
outputPath = Path(path)
outputPath.parent.mkdir(parents=True, exist_ok=True)
@@ -185,3 +261,55 @@ def renderPlanetFlyby(
written.append(gifPath)
print(f'Wrote flyby GIF → {gifPath}')
return tuple(written)
+
+
+def renderPlanetSpin(
+ bodyName: str = 'Earth',
+ *,
+ theme: Theme | Literal['all'] = 'all',
+ frameCount: int = DEFAULT_SPIN_FRAMES,
+ resolution: int = DEFAULT_SPIN_RESOLUTION,
+ fps: int = DEFAULT_SPIN_FPS,
+ outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
+) -> tuple[Path, ...]:
+ """Render fixed-camera RGBA spin loops for cinematic body billboards."""
+ themes: tuple[Theme, ...]
+ if theme == 'all':
+ themes = ('light', 'dark')
+ elif theme in ('light', 'dark'):
+ themes = (theme,)
+ else:
+ raise ValueError(f'theme must be light, dark, or all, got {theme!r}')
+
+ outputRoot = Path(outputDirectory)
+ outputRoot.mkdir(parents=True, exist_ok=True)
+ bodyDirectory = preparePlanetFlybyExport(bodyName, outputDirectory=outputRoot).parent
+ stem = bodyStem(bodyName)
+ written: list[Path] = []
+
+ for themeName in themes:
+ framesDirectory = spinFramesDirectory(bodyName, themeName, outputDirectory=outputRoot)
+ if framesDirectory.exists():
+ shutil.rmtree(framesDirectory)
+ framesDirectory.mkdir(parents=True, exist_ok=True)
+ job = buildSpinJob(
+ bodyName,
+ theme=themeName,
+ frameCount=frameCount,
+ resolution=resolution,
+ fps=fps,
+ framesDirectory=framesDirectory,
+ )
+ jobPath = bodyDirectory / f'{stem}_spin_{themeName}_job.json'
+ writeFlybyJob(job, jobPath)
+ _runBlenderFlybyJob(jobPath)
+ framePaths = sorted(framesDirectory.glob('frame_*.png'))
+ if not framePaths:
+ raise RuntimeError(f'No spin frames rendered for theme={themeName}')
+ # Preview GIF (RGB) for the gallery; cinematic loads the RGBA PNGs.
+ gifPath = bodyDirectory / f'{stem}_spin_{themeName}.gif'
+ assembleGifFromPngs(framePaths, gifPath, fps=fps)
+ written.append(framesDirectory)
+ print(f'Wrote spin loop → {framesDirectory} ({len(framePaths)} frames)')
+ print(f'Wrote spin preview GIF → {gifPath}')
+ return tuple(written)
diff --git a/animate/scenes/blender/render_flyby.py b/animate/scenes/blender/render_flyby.py
index cc38949..3d2130c 100644
--- a/animate/scenes/blender/render_flyby.py
+++ b/animate/scenes/blender/render_flyby.py
@@ -62,45 +62,185 @@ def _clearSceneObjects(bpy: Any) -> None:
def _assignSphericalUvs(builder: Any) -> None:
- """Equirectangular UVs from vertex directions (shared by planets/moons/asteroids)."""
+ """Equirectangular UVs from vertex directions, seam-safe per face.
+
+ Loops are per-corner, so faces that straddle U=0/1 get their low-U corners
+ shifted by +1. That stops interpolation from smearing across the whole map
+ (the vertical “date line” artifact on Earth).
+ """
uvLayer = builder.loops.layers.uv.new('UVMap')
for face in builder.faces:
for loop in face.loops:
direction = loop.vert.co.normalized()
- u = 0.5 + math.atan2(direction.y, direction.x) / (2.0 * math.pi)
+ u = (0.5 + math.atan2(direction.y, direction.x) / (2.0 * math.pi)) % 1.0
v = 0.5 + math.asin(max(-1.0, min(1.0, direction.z))) / math.pi
loop[uvLayer].uv = (u, v)
+ us = [loop[uvLayer].uv[0] for loop in face.loops]
+ if max(us) - min(us) > 0.5:
+ for loop in face.loops:
+ u, v = loop[uvLayer].uv
+ if u < 0.5:
+ loop[uvLayer].uv = (u + 1.0, v)
-def _createUvSphere(bpy: Any, name: str, radius: float) -> Any:
+def _createBodySphere(bpy: Any, name: str, radius: float) -> Any:
+ """Icosphere body mesh — UV spheres keep a geometric meridian crease.
+
+ Equirectangular maps are sampled via Environment Texture (object direction),
+ so we do not need UV-sphere topology. UVs are still assigned as a fallback
+ for Image Texture paths.
+ """
import bmesh # type: ignore[import-not-found]
mesh = bpy.data.meshes.new(f'{name}Mesh')
builder = bmesh.new()
- bmesh.ops.create_uvsphere(builder, u_segments=96, v_segments=48, radius=radius)
+ # subdivisions=5 ≈ 20k tris — smooth limb without a date-line edge.
+ bmesh.ops.create_icosphere(builder, subdivisions=5, radius=radius)
bmesh.ops.recalc_face_normals(builder, faces=builder.faces)
_assignSphericalUvs(builder)
builder.to_mesh(mesh)
builder.free()
for polygon in mesh.polygons:
polygon.use_smooth = True
+ if hasattr(mesh, 'use_auto_smooth'):
+ mesh.use_auto_smooth = False
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
return obj
-def _loadImageTexture(bpy: Any, nodeTree: Any, imagePath: Path, *, label: str) -> Any | None:
+def _createUvSphere(bpy: Any, name: str, radius: float) -> Any:
+ """Backward-compatible alias — prefer ``_createBodySphere``."""
+ return _createBodySphere(bpy, name, radius)
+
+
+def _ensureEquirectangularVector(bpy: Any, nodeTree: Any) -> Any:
+ """Per-fragment equirectangular UV from object normals (no mesh UV seam).
+
+ Mesh UV unwraps smear across U=0/1; computing atan2/asin in the shader from
+ the interpolated object normal avoids the date-line streak.
+ """
+ for node in nodeTree.nodes:
+ if node.get('solsys_equirect_uv'):
+ return node.outputs['Vector']
+
+ texCoord = nodeTree.nodes.new('ShaderNodeTexCoord')
+ texCoord.location = (-1600, 200)
+
+ normalize = nodeTree.nodes.new('ShaderNodeVectorMath')
+ normalize.operation = 'NORMALIZE'
+ normalize.location = (-1420, 200)
+ nodeTree.links.new(texCoord.outputs['Normal'], normalize.inputs[0])
+
+ separate = nodeTree.nodes.new('ShaderNodeSeparateXYZ')
+ separate.location = (-1240, 200)
+ nodeTree.links.new(normalize.outputs['Vector'], separate.inputs['Vector'])
+
+ # u = atan2(y, x) / (2π) + 0.5
+ arctan2 = nodeTree.nodes.new('ShaderNodeMath')
+ arctan2.operation = 'ARCTAN2'
+ arctan2.location = (-1060, 260)
+ nodeTree.links.new(separate.outputs['Y'], arctan2.inputs[0])
+ nodeTree.links.new(separate.outputs['X'], arctan2.inputs[1])
+
+ divU = nodeTree.nodes.new('ShaderNodeMath')
+ divU.operation = 'DIVIDE'
+ divU.location = (-880, 260)
+ divU.inputs[1].default_value = 2.0 * math.pi
+ nodeTree.links.new(arctan2.outputs['Value'], divU.inputs[0])
+
+ addU = nodeTree.nodes.new('ShaderNodeMath')
+ addU.operation = 'ADD'
+ addU.location = (-700, 260)
+ addU.inputs[1].default_value = 0.5
+ nodeTree.links.new(divU.outputs['Value'], addU.inputs[0])
+
+ # v = asin(z) / π + 0.5
+ arcsin = nodeTree.nodes.new('ShaderNodeMath')
+ arcsin.operation = 'ARCSINE'
+ arcsin.location = (-1060, 80)
+ nodeTree.links.new(separate.outputs['Z'], arcsin.inputs[0])
+
+ divV = nodeTree.nodes.new('ShaderNodeMath')
+ divV.operation = 'DIVIDE'
+ divV.location = (-880, 80)
+ divV.inputs[1].default_value = math.pi
+ nodeTree.links.new(arcsin.outputs['Value'], divV.inputs[0])
+
+ addV = nodeTree.nodes.new('ShaderNodeMath')
+ addV.operation = 'ADD'
+ addV.location = (-700, 80)
+ addV.inputs[1].default_value = 0.5
+ nodeTree.links.new(divV.outputs['Value'], addV.inputs[0])
+
+ combine = nodeTree.nodes.new('ShaderNodeCombineXYZ')
+ combine.location = (-520, 180)
+ combine['solsys_equirect_uv'] = True
+ nodeTree.links.new(addU.outputs['Value'], combine.inputs['X'])
+ nodeTree.links.new(addV.outputs['Value'], combine.inputs['Y'])
+ return combine.outputs['Vector']
+
+
+def _featherEquirectEdges(imagePath: Path, *, blendPx: int = 8) -> Path:
+ """Cross-fade left/right edges so the 0°/360° wrap is invisible."""
+ seamlessPath = imagePath.with_name(f'{imagePath.stem}_seamless{imagePath.suffix}')
+ if seamlessPath.is_file() and seamlessPath.stat().st_mtime >= imagePath.stat().st_mtime:
+ return seamlessPath
+ try:
+ import numpy as np
+ from PIL import Image
+ except ImportError:
+ return imagePath
+ with Image.open(imagePath) as image:
+ array = np.asarray(image.convert('RGBA'), dtype=np.float32)
+ width = array.shape[1]
+ blendPx = max(1, min(blendPx, width // 4))
+ for index in range(blendPx):
+ blend = (index + 0.5) / blendPx
+ left = array[:, index]
+ right = array[:, width - blendPx + index]
+ mixed = right * (1.0 - blend) + left * blend
+ array[:, index] = mixed
+ array[:, width - blendPx + index] = mixed
+ Image.fromarray(np.clip(array, 0, 255).astype(np.uint8), mode='RGBA').save(seamlessPath)
+ return seamlessPath
+
+
+def _loadImageTexture(
+ bpy: Any,
+ nodeTree: Any,
+ imagePath: Path,
+ *,
+ label: str,
+ equirectangular: bool = False,
+) -> Any | None:
+ """Load a body map. Equirectangular mode uses shader UVs (no mesh seam)."""
if not imagePath.is_file():
print(f'Warning: texture missing ({label}): {imagePath}', file=sys.stderr)
return None
- image = bpy.data.images.load(str(imagePath), check_existing=True)
+ loadPath = _featherEquirectEdges(imagePath) if equirectangular else imagePath
+ image = bpy.data.images.load(str(loadPath), check_existing=True)
textureNode = nodeTree.nodes.new('ShaderNodeTexImage')
textureNode.image = image
textureNode.label = label
- textureNode.interpolation = 'Smart'
+ # Closest avoids mip-seam streaks where atan2 U wraps 0↔1 across neighbors.
+ textureNode.interpolation = 'Closest' if equirectangular else 'Cubic'
+ textureNode.extension = 'REPEAT'
+ if equirectangular:
+ textureNode.projection = 'FLAT'
+ vector = _ensureEquirectangularVector(bpy, nodeTree)
+ nodeTree.links.new(vector, textureNode.inputs['Vector'])
return textureNode
+def _loadBodyColorTexture(bpy: Any, nodeTree: Any, colorPath: Path) -> Any | None:
+ """Seam-safe equirect color map; plain UV image as last resort."""
+ colorNode = _loadImageTexture(bpy, nodeTree, colorPath, label='color', equirectangular=True)
+ if colorNode is not None:
+ return colorNode
+ return _loadImageTexture(bpy, nodeTree, colorPath, label='color', equirectangular=False)
+
+
def _mixCloudLayer(
bpy: Any,
nodeTree: Any,
@@ -111,7 +251,7 @@ def _mixCloudLayer(
Keep coverage modest so continents/oceans stay readable (not a snowball).
"""
- cloudsNode = _loadImageTexture(bpy, nodeTree, cloudsPath, label='clouds')
+ cloudsNode = _loadImageTexture(bpy, nodeTree, cloudsPath, label='clouds', equirectangular=True)
if cloudsNode is None:
return surfaceColorSocket
cloudsNode.location = (-620, -40)
@@ -187,7 +327,7 @@ def _applyBodyMaterial(
if not colorPath:
return
- colorNode = _loadImageTexture(bpy, nodeTree, Path(colorPath), label='color')
+ colorNode = _loadBodyColorTexture(bpy, nodeTree, Path(colorPath))
if colorNode is None:
return
colorNode.location = (-820, 200)
@@ -461,11 +601,12 @@ def applyFlybyJobInBlender(job: dict[str, Any]) -> Path:
scene.render.resolution_y = resolution
scene.render.resolution_percentage = 100
scene.render.fps = int(job.get('fps', 18))
+ filmTransparent = bool(job.get('filmTransparent', False))
scene.render.image_settings.file_format = 'PNG'
- scene.render.image_settings.color_mode = 'RGB'
+ scene.render.image_settings.color_mode = 'RGBA' if filmTransparent else 'RGB'
scene.render.filepath = str(outputDirectory / 'frame_')
scene.render.use_file_extension = True
- scene.render.film_transparent = False
+ scene.render.film_transparent = filmTransparent
# Light theme was reading muddy under Filmic — lift exposure for day-side punch.
viewSettings = getattr(scene, 'view_settings', None)
if viewSettings is not None:
diff --git a/animate/scenes/sol_centauri_cinematic.py b/animate/scenes/sol_centauri_cinematic.py
index bbfedbd..32ca19c 100644
--- a/animate/scenes/sol_centauri_cinematic.py
+++ b/animate/scenes/sol_centauri_cinematic.py
@@ -8,6 +8,7 @@
import numpy as np
import pandas as pd
from matplotlib.animation import FuncAnimation, PillowWriter
+from mpl_toolkits.mplot3d import proj3d
from solsys.motion import AnimatedAsteroidPopulation, AsteroidPopulationCounts
from solsys.motion.mean_anomaly import planetMeanAnomalyRad
from solsys.physics import (
@@ -27,6 +28,7 @@
)
from animate.animation_styles import ASTEROID_RENDER_STYLES
+from animate.blender_body_sprites import BlenderBodySpriteAtlas
from animate.scenes.exoplanet_system import bodyPositionInOrbitalPlane, orbitPathInOrbitalPlane
DEFAULT_FIGURE_SIZE_INCHES = (12.0, 12.0)
@@ -62,8 +64,15 @@
'D': '#D8E6FF', # white dwarfs (class strings often start with D)
}
# Sol opening: Earth+Moon → near-Sun → inner → tighter outer linger, then interstellar pullback.
-# Moon display radius ≈ 0.128 AU (50× exaggerated); keep it nearly filling the frame.
-SOL_EARTH_HALF_AU = 0.14
+# Moon display radius ≈ 0.128 AU (50× exaggerated); leave a little margin for the label.
+SOL_EARTH_HALF_AU = 0.16
+# Blender open: Earth-only frame (Moon orbit ≈ 0.128 AU stays off-screen).
+SOL_EARTH_CLOSE_HALF_AU = 0.042
+# Fixed world radii for textured globes (must NOT scale up with camera half-width).
+EARTH_GLOBE_RADIUS_AU = SOL_EARTH_HALF_AU * 0.18
+# Luna uses bodyScale 0.35 against this base in the billboard path.
+# Hide Luna until the camera is wide enough that its exaggerated orbit fits.
+SOL_MOON_REVEAL_HALF_AU = 0.11
SOL_NEAR_SUN_HALF_AU = 2.4
SOL_INNER_HALF_AU = 6.5
# Linger on the main belt / Jupiter's orbit (same idea as the Kuiper hold).
@@ -118,6 +127,11 @@
PULLBACK_WEIGHTS = (1.4, 2.0, 2.4, 2.8, 3.0, 3.0, 2.6, 2.0, 1.6, 1.3, 1.0)
# Timeline: Earth → belt linger → outer linger → Oort pullback.
SOL_EARTH_DWELL_END = 0.03
+# Blender open: hold Earth-close for ~2 day/night spins (48 PNG samples/turn),
+# then ease out to Earth+Moon before the existing Sol dive.
+SOL_EARTH_SPIN_HOLD_END = 0.055
+SOL_EARTH_MOON_REVEAL_END = 0.085
+SOL_EARTH_BLENDER_DWELL_END = 0.10
SOL_BELT_ARRIVE = 0.16
SOL_BELT_HOLD_END = 0.28
SOL_OUTER_ARRIVE = 0.40
@@ -136,8 +150,11 @@
SOL_ELEVATION_DEG = 28.0
# Top-down open so the Moon orbit fills the viewport (3D foreshortening shrinks it).
EARTH_OPEN_ELEVATION_DEG = 62.0
+# Blender Earth-close: slightly flatter so the globe fills the frame cleanly.
+EARTH_CLOSE_ELEVATION_DEG = 48.0
PROXIMA_ELEVATION_DEG = 58.0
OUTPUT_DIRECTORY = 'output/animate/sol_centauri'
+BLENDER_OUTPUT_DIRECTORY = 'output/animate/sol_centauri/blender'
SOL_PLANET_NAMES = (
'Mercury',
@@ -392,6 +409,8 @@ def __init__(
figureSizeInches: tuple[float, float] = DEFAULT_FIGURE_SIZE_INCHES,
dpi: int = DEFAULT_DPI,
starsCsvPath: str = 'data/nearby_stars_30.csv',
+ *,
+ useBlenderBodies: bool = False,
):
if system.systemId != 'alpha_centauri':
raise ValueError(f'Expected alpha_centauri, got {system.systemId!r}')
@@ -400,6 +419,7 @@ def __init__(
self.figureSizeInches = figureSizeInches
self.dpi = dpi
self.animationFrames = ANIMATION_FRAMES
+ self.useBlenderBodies = useBlenderBodies
self.constants = AstronomicalConstants()
self.orbitCalculator = OrbitCalculator()
self.planetCatalog = PlanetCatalog(self.constants)
@@ -474,6 +494,27 @@ def __init__(
self._viewHalfWidthAu = self.solEarthHalfWidthAu
self.figure = plt.figure(figsize=figureSizeInches, dpi=dpi, layout='none')
self.axes = self.figure.add_axes((0.0, 0.0, 1.0, 1.0), projection='3d')
+ # 2D overlay for Blender spin-loop frames (drawn after the 3D camera is set).
+ self.bodyOverlay = self.figure.add_axes((0.0, 0.0, 1.0, 1.0), facecolor='none', zorder=20)
+ self.bodyOverlay.set_axis_off()
+ self.bodyOverlay.patch.set_alpha(0.0)
+ self.bodyOverlay.set_xlim(0.0, 1.0)
+ self.bodyOverlay.set_ylim(0.0, 1.0)
+ # (name, center, frame, halfWidth, openCloseup, bodyScale, orbitalPhaseRad|None)
+ self._pendingBlenderBodies: list[
+ tuple[str, np.ndarray, int, float, bool, float, float | None]
+ ] = []
+ # (name, center, fontsize, bodyScale)
+ self._pendingBlenderLabels: list[tuple[str, np.ndarray, float, float]] = []
+ self.blenderSprites: BlenderBodySpriteAtlas | None = None
+ if self.useBlenderBodies:
+ themeName = 'dark' if self.isDark else 'light'
+ self.blenderSprites = BlenderBodySpriteAtlas(themeName)
+ print(
+ 'Blender spin loops: '
+ f'Earth={"on" if self.blenderSprites.hasEarth else "missing"} '
+ f'Moon={"on" if self.blenderSprites.hasMoon else "missing"}'
+ )
def _requireOrbit(self, role: str) -> StellarOrbit:
for orbit in self.system.stellarOrbits:
@@ -598,10 +639,27 @@ def _abLegHalfWidthAu(self, focus: np.ndarray, abProgress: float) -> float:
dive = segmentProgress(abProgress, AB_CRUISE_END, 1.0)
return stagedLogDive(cruiseEndHalf, self.abHalfWidthAu, AB_DIVE_WAYPOINTS_AU, dive)
+ def _earthDwellEnd(self) -> float:
+ """Timeline fraction when the Earth(-Moon) open ends and the Sol dive begins."""
+ return SOL_EARTH_BLENDER_DWELL_END if self.useBlenderBodies else SOL_EARTH_DWELL_END
+
def _solOpeningHalfWidthAu(self, linear: float) -> float:
"""Earth → belt/Jupiter linger → outer/Kuiper linger."""
+ dwellEnd = self._earthDwellEnd()
+ if self.useBlenderBodies and linear < dwellEnd:
+ # Earth-only day/night hold → reveal Luna → short Earth+Moon beat.
+ if linear < SOL_EARTH_SPIN_HOLD_END:
+ return SOL_EARTH_CLOSE_HALF_AU
+ if linear < SOL_EARTH_MOON_REVEAL_END:
+ reveal = segmentProgress(linear, SOL_EARTH_SPIN_HOLD_END, SOL_EARTH_MOON_REVEAL_END)
+ return logLerp(
+ SOL_EARTH_CLOSE_HALF_AU,
+ self.solEarthHalfWidthAu,
+ smootherstep(reveal),
+ )
+ return self.solEarthHalfWidthAu
if linear < SOL_BELT_ARRIVE:
- dive = segmentProgress(linear, SOL_EARTH_DWELL_END, SOL_BELT_ARRIVE)
+ dive = segmentProgress(linear, dwellEnd, SOL_BELT_ARRIVE)
if dive >= 1.0 - 1e-9:
return SOL_BELT_LINGER_HALF_AU
return stagedLogDive(
@@ -746,6 +804,13 @@ def _cameraState(self, frame: int) -> tuple[np.ndarray, float]:
def update(self, frame: int):
self.axes.clear()
+ self.bodyOverlay.clear()
+ self.bodyOverlay.set_axis_off()
+ self.bodyOverlay.patch.set_alpha(0.0)
+ self.bodyOverlay.set_xlim(0.0, 1.0)
+ self.bodyOverlay.set_ylim(0.0, 1.0)
+ self._pendingBlenderBodies = []
+ self._pendingBlenderLabels = []
for textArtist in list(self.figure.texts):
textArtist.remove()
focus, halfWidthAu = self._cameraState(frame)
@@ -761,6 +826,9 @@ def update(self, frame: int):
self._drawAlphaCentauri(frame, halfWidthAu, abProgress, linear)
self._drawProxima(frame, halfWidthAu, linear)
self._applyAxes(focus, halfWidthAu, abProgress, proximaProgress, linear)
+ # Globes need a finished 3D projection — paint them into this frame now.
+ self._flushBlenderBodyOverlays(halfWidthAu)
+ self._flushBlenderBodyLabels(halfWidthAu)
return []
def _inView(self, position: np.ndarray, margin: float = 0.95) -> bool:
@@ -778,6 +846,7 @@ def _label3d(
color: str,
fontsize: float = 8,
alpha: float = 1.0,
+ clipOn: bool = True,
) -> None:
if not self._inView(position):
return
@@ -789,7 +858,7 @@ def _label3d(
color=color,
fontsize=fontsize,
alpha=alpha,
- clip_on=True,
+ clip_on=clipOn,
)
def _drawPath(
@@ -930,24 +999,47 @@ def _drawOneSolPlanet(self, name: str, frame: int, halfWidthAu: float) -> None:
return
earthOpen = halfWidthAu <= SOL_EARTH_HALF_AU * 1.5
plutoOuter = name == 'Pluto' and halfWidthAu >= 20.0
- self.axes.scatter(
- [position[0]],
- [position[1]],
- [position[2]],
- color=planet.color,
- s=self._solPlanetMarkerSize(name, halfWidthAu),
- depthshade=True,
- zorder=6 if plutoOuter else 5,
+ queuedBlenderEarth = False
+ blenderEarthAvailable = (
+ name == 'Earth'
+ and self.useBlenderBodies
+ and self.blenderSprites is not None
+ and self.blenderSprites.hasEarth
)
+ if (
+ blenderEarthAvailable
+ and self._blenderBillboardRadiusAu(halfWidthAu, openCloseup=earthOpen, bodyScale=1.0)
+ is not None
+ ):
+ self._pendingBlenderBodies.append(
+ ('Earth', position.copy(), frame, halfWidthAu, earthOpen, 1.0, None)
+ )
+ queuedBlenderEarth = True
+ # Blender mode: never fall back to the catalog-blue Earth scatter marker.
+ if not queuedBlenderEarth and not blenderEarthAvailable:
+ self.axes.scatter(
+ [position[0]],
+ [position[1]],
+ [position[2]],
+ color=planet.color,
+ s=self._solPlanetMarkerSize(name, halfWidthAu),
+ depthshade=True,
+ zorder=6 if plutoOuter else 5,
+ )
if name in LABELED_SOL_PLANETS or (
halfWidthAu < SOL_INNER_HALF_AU and name in ('Mercury', 'Venus', 'Mars')
):
- self._label3d(
- position,
- f' {name}',
- color=self.labelColor,
- fontsize=(12 if earthOpen and name == 'Earth' else (10 if plutoOuter else 8)),
- )
+ fontsize = 12 if earthOpen and name == 'Earth' else (10 if plutoOuter else 8)
+ if name == 'Earth' and queuedBlenderEarth:
+ # Overlay text above the spin billboard — axes labels sit under the disk.
+ self._pendingBlenderLabels.append((name, position.copy(), float(fontsize), 1.0))
+ else:
+ self._label3d(
+ position,
+ f' {name}',
+ color=self.labelColor,
+ fontsize=fontsize,
+ )
def _drawSolMoons(self, frame: int, halfWidthAu: float) -> None:
moonScale = self.moonCatalog.displayScaleForCameraAu(halfWidthAu)
@@ -967,6 +1059,9 @@ def _drawOneSolMoon(
moonScale: float,
halfWidthAu: float,
) -> None:
+ # Earth-only open: keep Luna off-screen until the reveal zoom.
+ if moon.name == 'Moon' and self.useBlenderBodies and halfWidthAu < SOL_MOON_REVEAL_HALF_AU:
+ return
orbitRadiusAu = self.moonCatalog.displayOrbitRadiusAu(moon, moonScale)
ring = np.linspace(0.0, 2.0 * np.pi, 64)
moonOpen = moon.name == 'Moon' and halfWidthAu <= SOL_EARTH_HALF_AU * 2.2
@@ -979,7 +1074,7 @@ def _drawOneSolMoon(
alpha=0.75 if moonOpen else (0.55 if moon.name == 'Moon' else 0.3),
)
moonMeanAnomaly = planetMeanAnomalyRad(
- moon.orbitalPeriodDays, 1, self._solMotionDays(frame)
+ moon.orbitalPeriodDays, 1, self._lunarMotionDays(moon, frame, halfWidthAu)
)
moonX, moonY, moonZ = self.moonCatalog.heliocentricPosition(
planetPosition[0],
@@ -990,23 +1085,214 @@ def _drawOneSolMoon(
moonScale,
)
moonPosition = np.array([float(moonX), float(moonY), float(moonZ)], dtype=float)
- self.axes.scatter(
- [moonPosition[0]],
- [moonPosition[1]],
- [moonPosition[2]],
- color=moon.color,
- s=self.moonCatalog.markerSize3d(
- moon, 600 if moonOpen else (900 if moon.name == 'Moon' else 1400)
- ),
- depthshade=True,
- zorder=6,
+ queuedBlenderMoon = False
+ blenderMoonAvailable = (
+ moon.name == 'Moon'
+ and self.useBlenderBodies
+ and self.blenderSprites is not None
+ and self.blenderSprites.hasMoon
)
- if moon.name == 'Moon' or halfWidthAu < 4.0:
+ if (
+ blenderMoonAvailable
+ and self._blenderBillboardRadiusAu(halfWidthAu, openCloseup=moonOpen, bodyScale=0.35)
+ is not None
+ ):
+ self._pendingBlenderBodies.append(
+ (
+ 'Moon',
+ moonPosition.copy(),
+ frame,
+ halfWidthAu,
+ moonOpen,
+ 0.35,
+ float(moonMeanAnomaly),
+ )
+ )
+ queuedBlenderMoon = True
+ # Blender mode: never fall back to the catalog-gray Luna scatter marker.
+ if not queuedBlenderMoon and not blenderMoonAvailable:
+ self.axes.scatter(
+ [moonPosition[0]],
+ [moonPosition[1]],
+ [moonPosition[2]],
+ color=moon.color,
+ s=self.moonCatalog.markerSize3d(
+ moon, 600 if moonOpen else (900 if moon.name == 'Moon' else 1400)
+ ),
+ depthshade=True,
+ zorder=6,
+ )
+ # Luna: only label in the Earth–Moon open — drop it once the camera
+ # reaches the inner system (Earth keeps its planet label as usual).
+ if moon.name == 'Moon':
+ if moonOpen:
+ if queuedBlenderMoon:
+ # Overlay text above the spin billboard so the leading "M" isn't covered.
+ self._pendingBlenderLabels.append((moon.name, moonPosition.copy(), 11.0, 0.35))
+ else:
+ self._label3d(
+ moonPosition,
+ f' {moon.name}',
+ color=self.labelColor,
+ fontsize=11,
+ )
+ elif halfWidthAu < 4.0:
self._label3d(
moonPosition,
f' {moon.name}',
color=self.labelColor,
- fontsize=11 if moonOpen else (9 if moon.name == 'Moon' else 7),
+ fontsize=7,
+ )
+
+ def _lunarMotionDays(self, moon, frame: int, halfWidthAu: float) -> float:
+ """Sol motion clock is too fast for a readable Earth–Moon opening orbit."""
+ days = self._solMotionDays(frame)
+ if moon.name != 'Moon' or not self.useBlenderBodies:
+ return days
+ # At open (~0.05 d/frame) Luna crawls; blend back to the sol clock by ~2 AU.
+ openHalf = SOL_EARTH_HALF_AU
+ leaveHalf = 2.0
+ if halfWidthAu >= leaveHalf:
+ return days
+ blend = smootherstep((halfWidthAu - openHalf) / max(leaveHalf - openHalf, 1e-6))
+ openScale = 0.015 # ≈0.05 d/frame vs sol's 3.5 d/frame near Earth
+ return days * (openScale + (1.0 - openScale) * blend)
+
+ def _blenderBillboardRadiusAu(
+ self,
+ halfWidthAu: float,
+ *,
+ openCloseup: bool,
+ bodyScale: float,
+ ) -> float | None:
+ """Fixed world-space globe radius; None → omit the body (no catalog-color dot).
+
+ Radius is anchored to the Earth-open framing so zoom-out shrinks the
+ body on screen instead of growing it inside the Moon's orbit.
+ """
+ del openCloseup # size is world-fixed; closeup only affects texture resolution
+ radiusAu = EARTH_GLOBE_RADIUS_AU * bodyScale
+ # Keep textured disks through the Sol zoom-out; drop only once the
+ # camera is well past the outer system (never swap to catalog blue/gray).
+ if halfWidthAu > 100.0:
+ return None
+ return radiusAu
+
+ def _flushBlenderBodyOverlays(self, halfWidthAu: float) -> None:
+ """Project queued bodies and paint Blender spin-loop frames into this frame."""
+ if not self._pendingBlenderBodies or self.blenderSprites is None:
+ return
+ for (
+ catalogName,
+ center,
+ frame,
+ bodyHalfWidth,
+ openCloseup,
+ bodyScale,
+ orbitalPhaseRad,
+ ) in self._pendingBlenderBodies:
+ radiusAu = self._blenderBillboardRadiusAu(
+ bodyHalfWidth, openCloseup=openCloseup, bodyScale=bodyScale
+ )
+ if radiusAu is None:
+ continue
+ fracRadius = self._blenderBillboardFracRadius(halfWidthAu, bodyScale)
+ if fracRadius is None:
+ continue
+ resolution = 384 if openCloseup else (64 if fracRadius <= 0.01 else 128)
+ if catalogName == 'Earth':
+ disk = self.blenderSprites.earthFrame(frame, resolution=resolution)
+ else:
+ disk = self.blenderSprites.moonFrame(
+ frame,
+ orbitalPhaseRad=orbitalPhaseRad,
+ resolution=resolution,
+ )
+ if disk is None:
+ continue
+ x2, y2, _ = proj3d.proj_transform(
+ float(center[0]),
+ float(center[1]),
+ float(center[2]),
+ self.axes.get_proj(),
+ )
+ display = self.axes.transData.transform((x2, y2))
+ frac = self.figure.transFigure.inverted().transform(display)
+ # PillowWriter quantizes better from 8-bit RGBA than float arrays.
+ diskU8 = (np.clip(disk, 0.0, 1.0) * 255.0).astype(np.uint8)
+ self.bodyOverlay.imshow(
+ diskU8,
+ extent=(
+ frac[0] - fracRadius,
+ frac[0] + fracRadius,
+ frac[1] - fracRadius,
+ frac[1] + fracRadius,
+ ),
+ origin='upper',
+ interpolation='bilinear',
+ zorder=5,
+ clip_on=False,
+ )
+ # imshow(extent=...) expands data limits; keep 0–1 so label coords stay figure-like.
+ self.bodyOverlay.set_xlim(0.0, 1.0)
+ self.bodyOverlay.set_ylim(0.0, 1.0)
+
+ def _blenderBillboardFracRadius(self, halfWidthAu: float, bodyScale: float) -> float | None:
+ """On-screen disk radius in figure fraction (matches overlay paint)."""
+ radiusAu = self._blenderBillboardRadiusAu(
+ halfWidthAu, openCloseup=False, bodyScale=bodyScale
+ )
+ if radiusAu is None:
+ return None
+ # Same floor as _flushBlenderBodyOverlays so labels track the painted disk.
+ return max(float(radiusAu / max(2.0 * halfWidthAu, 1e-9)), 0.0035)
+
+ def _blenderBodyLabelPad(self, fracRadius: float) -> float:
+ """Gap from disk edge to label; shrinks with Earth as the camera pulls back."""
+ # Close-up (~0.09): ~0.03. Inner-Sol floor (0.0035): ~0.0045 — not a fixed
+ # 3% figure gap that leaves Earth floating alone on zoom-out.
+ return max(0.004, min(0.03, fracRadius * 0.45 + 0.003))
+
+ def _flushBlenderBodyLabels(self, halfWidthAu: float) -> None:
+ """Draw deferred body labels above spin billboards (figure fraction)."""
+ if not self._pendingBlenderLabels:
+ return
+ self.bodyOverlay.set_xlim(0.0, 1.0)
+ self.bodyOverlay.set_ylim(0.0, 1.0)
+ viewHalf = float(halfWidthAu) if halfWidthAu > 0.0 else self._viewHalfWidthAu
+ for name, center, fontsize, bodyScale in self._pendingBlenderLabels:
+ x2, y2, _ = proj3d.proj_transform(
+ float(center[0]),
+ float(center[1]),
+ float(center[2]),
+ self.axes.get_proj(),
+ )
+ display = self.axes.transData.transform((x2, y2))
+ frac = self.figure.transFigure.inverted().transform(display)
+ fracRadius = self._blenderBillboardFracRadius(viewHalf, bodyScale) or 0.0035
+ pad = self._blenderBodyLabelPad(fracRadius)
+ # Overlay text above the imshow disk. Figure-level text paints under this
+ # axes (zorder 20) and gets covered by the globe.
+ if name == 'Earth':
+ textX = min(frac[0] + fracRadius + pad, 0.94)
+ textY = frac[1]
+ va = 'center'
+ else:
+ # Moon sits near the right edge; lower-right keeps the leading "M".
+ moonPad = max(0.004, min(0.016, pad * 0.55))
+ textX = min(frac[0] + fracRadius + moonPad, 0.90)
+ textY = max(frac[1] - fracRadius - moonPad, 0.08)
+ va = 'top'
+ self.bodyOverlay.text(
+ textX,
+ textY,
+ name,
+ color=self.labelColor,
+ fontsize=fontsize,
+ ha='left',
+ va=va,
+ clip_on=False,
+ zorder=10,
)
def _drawSolAsteroidPopulations(self, frame: int, halfWidthAu: float) -> None:
@@ -1535,6 +1821,8 @@ def _solCaption(self, halfWidthAu: float, linear: float) -> tuple[str, str] | No
if linear >= PULLBACK_END:
return None
if halfWidthAu <= SOL_EARTH_HALF_AU * 2.2:
+ if self.useBlenderBodies and halfWidthAu < SOL_MOON_REVEAL_HALF_AU:
+ return ('Earth', 'A couple of day–night cycles before we find the Moon')
return ('Earth and the Moon', 'Starting close to home before we pull back')
if halfWidthAu <= SOL_NEAR_SUN_HALF_AU:
return ('Inner solar system', 'Pulling back past Venus and Mars')
@@ -1613,14 +1901,24 @@ def _applyAxes(
if linear < PULLBACK_END:
if halfWidthAu <= SOL_NEAR_SUN_HALF_AU:
- # Ease from top-down Earth open down to the usual Sol angle.
- tilt = smootherstep(
- (halfWidthAu - SOL_EARTH_HALF_AU)
- / max(SOL_NEAR_SUN_HALF_AU - SOL_EARTH_HALF_AU, 1e-6)
- )
- elev = EARTH_OPEN_ELEVATION_DEG + tilt * (
- SOL_ELEVATION_DEG - EARTH_OPEN_ELEVATION_DEG
- )
+ if self.useBlenderBodies and halfWidthAu < self.solEarthHalfWidthAu - 1e-9:
+ # Earth-close → Earth+Moon open angle as Luna comes into frame.
+ closeTilt = smootherstep(
+ (halfWidthAu - SOL_EARTH_CLOSE_HALF_AU)
+ / max(self.solEarthHalfWidthAu - SOL_EARTH_CLOSE_HALF_AU, 1e-6)
+ )
+ elev = EARTH_CLOSE_ELEVATION_DEG + closeTilt * (
+ EARTH_OPEN_ELEVATION_DEG - EARTH_CLOSE_ELEVATION_DEG
+ )
+ else:
+ # Ease from top-down Earth open down to the usual Sol angle.
+ tilt = smootherstep(
+ (halfWidthAu - SOL_EARTH_HALF_AU)
+ / max(SOL_NEAR_SUN_HALF_AU - SOL_EARTH_HALF_AU, 1e-6)
+ )
+ elev = EARTH_OPEN_ELEVATION_DEG + tilt * (
+ SOL_ELEVATION_DEG - EARTH_OPEN_ELEVATION_DEG
+ )
else:
elev = SOL_ELEVATION_DEG
azim = self.solAzimuthDeg
@@ -1679,11 +1977,19 @@ def renderSolCentauriCinematicAnimations(
figureSizeInches: tuple[float, float] = DEFAULT_FIGURE_SIZE_INCHES,
dpi: int = DEFAULT_DPI,
starsCsvPath: str = 'data/nearby_stars_30.csv',
+ *,
+ useBlenderBodies: bool = False,
) -> None:
catalog = SystemCatalog(starsCsvPath=starsCsvPath)
system = catalog.load('alpha_centauri')
+ if useBlenderBodies:
+ outputDirectory = BLENDER_OUTPUT_DIRECTORY
+ stem = 'sol_centauri_cinematic_blender'
+ else:
+ outputDirectory = OUTPUT_DIRECTORY
+ stem = 'sol_centauri_cinematic'
for themeName, styleName in (('light', 'default'), ('dark', 'dark_background')):
- outputPath = f'{OUTPUT_DIRECTORY}/sol_centauri_cinematic_{themeName}.gif'
+ outputPath = f'{outputDirectory}/{stem}_{themeName}.gif'
print(f'Rendering {outputPath}...')
animator = SolCentauriCinematicAnimator(
system,
@@ -1691,6 +1997,7 @@ def renderSolCentauriCinematicAnimations(
figureSizeInches=figureSizeInches,
dpi=dpi,
starsCsvPath=starsCsvPath,
+ useBlenderBodies=useBlenderBodies,
)
animator.saveGif(outputPath)
print('Sol ↔ Centauri cinematic animations completed!')
diff --git a/data/textures/bodies/earth/clouds_seamless.png b/data/textures/bodies/earth/clouds_seamless.png
new file mode 100644
index 0000000..c93287a
Binary files /dev/null and b/data/textures/bodies/earth/clouds_seamless.png differ
diff --git a/data/textures/bodies/earth/color_seamless.png b/data/textures/bodies/earth/color_seamless.png
new file mode 100644
index 0000000..2642db0
Binary files /dev/null and b/data/textures/bodies/earth/color_seamless.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark.gif b/output/animate/blender/moons/moon/moon_spin_dark.gif
new file mode 100644
index 0000000..4e05005
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark.gif differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0000.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0000.png
new file mode 100644
index 0000000..27c43c2
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0000.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0001.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0001.png
new file mode 100644
index 0000000..47663bb
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0001.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0002.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0002.png
new file mode 100644
index 0000000..be13dd9
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0002.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0003.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0003.png
new file mode 100644
index 0000000..14dab9e
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0003.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0004.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0004.png
new file mode 100644
index 0000000..62a12d6
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0004.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0005.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0005.png
new file mode 100644
index 0000000..1a2e7b2
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0005.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0006.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0006.png
new file mode 100644
index 0000000..99d030d
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0006.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0007.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0007.png
new file mode 100644
index 0000000..a678a8e
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0007.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0008.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0008.png
new file mode 100644
index 0000000..5acc030
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0008.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0009.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0009.png
new file mode 100644
index 0000000..890c7c2
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0009.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0010.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0010.png
new file mode 100644
index 0000000..93978cf
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0010.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0011.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0011.png
new file mode 100644
index 0000000..bdbd0ad
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0011.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0012.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0012.png
new file mode 100644
index 0000000..7a728c3
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0012.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0013.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0013.png
new file mode 100644
index 0000000..a7c9d61
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0013.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0014.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0014.png
new file mode 100644
index 0000000..b13d3df
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0014.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0015.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0015.png
new file mode 100644
index 0000000..5968818
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0015.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0016.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0016.png
new file mode 100644
index 0000000..dac2a47
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0016.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0017.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0017.png
new file mode 100644
index 0000000..e11508f
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0017.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0018.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0018.png
new file mode 100644
index 0000000..7f2de17
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0018.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0019.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0019.png
new file mode 100644
index 0000000..7049f0b
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0019.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0020.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0020.png
new file mode 100644
index 0000000..47bfeff
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0020.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0021.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0021.png
new file mode 100644
index 0000000..3cd5ee8
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0021.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0022.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0022.png
new file mode 100644
index 0000000..de667c0
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0022.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0023.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0023.png
new file mode 100644
index 0000000..0848338
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0023.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0024.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0024.png
new file mode 100644
index 0000000..127130e
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0024.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0025.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0025.png
new file mode 100644
index 0000000..8cd47eb
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0025.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0026.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0026.png
new file mode 100644
index 0000000..1a04db4
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0026.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0027.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0027.png
new file mode 100644
index 0000000..1ecfb1a
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0027.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0028.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0028.png
new file mode 100644
index 0000000..b324e74
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0028.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0029.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0029.png
new file mode 100644
index 0000000..3c49b41
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0029.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0030.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0030.png
new file mode 100644
index 0000000..6979e6b
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0030.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0031.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0031.png
new file mode 100644
index 0000000..f029896
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0031.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0032.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0032.png
new file mode 100644
index 0000000..c59ba2e
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0032.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0033.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0033.png
new file mode 100644
index 0000000..0686585
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0033.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0034.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0034.png
new file mode 100644
index 0000000..1424a4a
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0034.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0035.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0035.png
new file mode 100644
index 0000000..b9ea315
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0035.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0036.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0036.png
new file mode 100644
index 0000000..b1508c2
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0036.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0037.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0037.png
new file mode 100644
index 0000000..70c2aef
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0037.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0038.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0038.png
new file mode 100644
index 0000000..c851b56
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0038.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0039.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0039.png
new file mode 100644
index 0000000..cc25ce7
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0039.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0040.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0040.png
new file mode 100644
index 0000000..bcaf3c0
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0040.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0041.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0041.png
new file mode 100644
index 0000000..69e8c6c
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0041.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0042.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0042.png
new file mode 100644
index 0000000..1c2b706
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0042.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0043.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0043.png
new file mode 100644
index 0000000..48eae09
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0043.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0044.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0044.png
new file mode 100644
index 0000000..991635a
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0044.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0045.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0045.png
new file mode 100644
index 0000000..440e105
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0045.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0046.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0046.png
new file mode 100644
index 0000000..b576d58
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0046.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark/frame_0047.png b/output/animate/blender/moons/moon/moon_spin_dark/frame_0047.png
new file mode 100644
index 0000000..390a660
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_dark/frame_0047.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_dark_job.json b/output/animate/blender/moons/moon/moon_spin_dark_job.json
new file mode 100644
index 0000000..1c16a62
--- /dev/null
+++ b/output/animate/blender/moons/moon/moon_spin_dark_job.json
@@ -0,0 +1,466 @@
+{
+ "schema": "solsys.blender_flyby_job/v1",
+ "theme": "dark",
+ "mode": "spin",
+ "body": {
+ "name": "Moon",
+ "kind": "moon",
+ "systemId": "sol",
+ "diameterKm": 3474,
+ "color": "lightgray",
+ "colorRgba": [
+ 0.75,
+ 0.75,
+ 0.75,
+ 1.0
+ ],
+ "displayRadiusAu": 0.009288902265104232
+ },
+ "frames": [
+ {
+ "frame": 0,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 0.0
+ },
+ {
+ "frame": 1,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 7.5
+ },
+ {
+ "frame": 2,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 15.0
+ },
+ {
+ "frame": 3,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 22.5
+ },
+ {
+ "frame": 4,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 30.0
+ },
+ {
+ "frame": 5,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 37.5
+ },
+ {
+ "frame": 6,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 45.0
+ },
+ {
+ "frame": 7,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 52.5
+ },
+ {
+ "frame": 8,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 60.0
+ },
+ {
+ "frame": 9,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 67.5
+ },
+ {
+ "frame": 10,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 75.0
+ },
+ {
+ "frame": 11,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 82.5
+ },
+ {
+ "frame": 12,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 90.0
+ },
+ {
+ "frame": 13,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 97.5
+ },
+ {
+ "frame": 14,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 105.0
+ },
+ {
+ "frame": 15,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 112.5
+ },
+ {
+ "frame": 16,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 120.0
+ },
+ {
+ "frame": 17,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 127.5
+ },
+ {
+ "frame": 18,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 135.0
+ },
+ {
+ "frame": 19,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 142.5
+ },
+ {
+ "frame": 20,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 150.0
+ },
+ {
+ "frame": 21,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 157.5
+ },
+ {
+ "frame": 22,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 165.0
+ },
+ {
+ "frame": 23,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 172.5
+ },
+ {
+ "frame": 24,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 180.0
+ },
+ {
+ "frame": 25,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 187.5
+ },
+ {
+ "frame": 26,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 195.0
+ },
+ {
+ "frame": 27,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 202.5
+ },
+ {
+ "frame": 28,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 210.0
+ },
+ {
+ "frame": 29,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 217.5
+ },
+ {
+ "frame": 30,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 225.0
+ },
+ {
+ "frame": 31,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 232.5
+ },
+ {
+ "frame": 32,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 240.0
+ },
+ {
+ "frame": 33,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 247.5
+ },
+ {
+ "frame": 34,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 255.0
+ },
+ {
+ "frame": 35,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 262.5
+ },
+ {
+ "frame": 36,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 270.0
+ },
+ {
+ "frame": 37,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 277.5
+ },
+ {
+ "frame": 38,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 285.0
+ },
+ {
+ "frame": 39,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 292.5
+ },
+ {
+ "frame": 40,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 300.0
+ },
+ {
+ "frame": 41,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 307.5
+ },
+ {
+ "frame": 42,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 315.0
+ },
+ {
+ "frame": 43,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 322.5
+ },
+ {
+ "frame": 44,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 330.0
+ },
+ {
+ "frame": 45,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 337.5
+ },
+ {
+ "frame": 46,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 345.0
+ },
+ {
+ "frame": 47,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 352.5
+ }
+ ],
+ "outputDirectory": "output/animate/blender/moons/moon/moon_spin_dark",
+ "resolution": 512,
+ "fps": 20,
+ "filmTransparent": true,
+ "appearance": {
+ "bodyId": "moon",
+ "kind": "moon",
+ "textures": {
+ "color": "/Users/tom/Desktop/SOLSYS/data/textures/bodies/moon/color.png"
+ },
+ "roughness": 0.82,
+ "specular": 0.04
+ }
+}
diff --git a/output/animate/blender/moons/moon/moon_spin_light.gif b/output/animate/blender/moons/moon/moon_spin_light.gif
new file mode 100644
index 0000000..73f6bb1
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light.gif differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0000.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0000.png
new file mode 100644
index 0000000..f7f0af0
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0000.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0001.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0001.png
new file mode 100644
index 0000000..df770c0
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0001.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0002.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0002.png
new file mode 100644
index 0000000..19d2a4a
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0002.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0003.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0003.png
new file mode 100644
index 0000000..42f7b7a
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0003.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0004.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0004.png
new file mode 100644
index 0000000..a8f4d26
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0004.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0005.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0005.png
new file mode 100644
index 0000000..202a084
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0005.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0006.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0006.png
new file mode 100644
index 0000000..776cc72
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0006.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0007.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0007.png
new file mode 100644
index 0000000..4d08d35
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0007.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0008.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0008.png
new file mode 100644
index 0000000..13de17d
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0008.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0009.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0009.png
new file mode 100644
index 0000000..db8f070
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0009.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0010.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0010.png
new file mode 100644
index 0000000..3f50ff1
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0010.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0011.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0011.png
new file mode 100644
index 0000000..87d7aa1
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0011.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0012.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0012.png
new file mode 100644
index 0000000..2992980
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0012.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0013.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0013.png
new file mode 100644
index 0000000..31bb6c1
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0013.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0014.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0014.png
new file mode 100644
index 0000000..2045934
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0014.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0015.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0015.png
new file mode 100644
index 0000000..e6c44b9
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0015.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0016.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0016.png
new file mode 100644
index 0000000..aab4c24
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0016.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0017.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0017.png
new file mode 100644
index 0000000..979e11b
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0017.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0018.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0018.png
new file mode 100644
index 0000000..6d9597e
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0018.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0019.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0019.png
new file mode 100644
index 0000000..d9ecb1a
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0019.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0020.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0020.png
new file mode 100644
index 0000000..e0a5fc4
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0020.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0021.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0021.png
new file mode 100644
index 0000000..91f2133
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0021.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0022.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0022.png
new file mode 100644
index 0000000..bc15695
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0022.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0023.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0023.png
new file mode 100644
index 0000000..2d70b11
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0023.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0024.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0024.png
new file mode 100644
index 0000000..ae4bcf8
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0024.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0025.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0025.png
new file mode 100644
index 0000000..e369a5d
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0025.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0026.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0026.png
new file mode 100644
index 0000000..c7e7c91
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0026.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0027.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0027.png
new file mode 100644
index 0000000..00b8123
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0027.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0028.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0028.png
new file mode 100644
index 0000000..5e6a950
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0028.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0029.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0029.png
new file mode 100644
index 0000000..99fab76
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0029.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0030.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0030.png
new file mode 100644
index 0000000..45bf72f
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0030.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0031.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0031.png
new file mode 100644
index 0000000..5cd639b
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0031.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0032.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0032.png
new file mode 100644
index 0000000..fce0971
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0032.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0033.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0033.png
new file mode 100644
index 0000000..e4e87ea
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0033.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0034.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0034.png
new file mode 100644
index 0000000..221f6cd
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0034.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0035.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0035.png
new file mode 100644
index 0000000..569ab4d
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0035.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0036.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0036.png
new file mode 100644
index 0000000..0db4349
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0036.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0037.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0037.png
new file mode 100644
index 0000000..6d6220f
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0037.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0038.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0038.png
new file mode 100644
index 0000000..59c5644
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0038.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0039.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0039.png
new file mode 100644
index 0000000..dbc1337
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0039.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0040.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0040.png
new file mode 100644
index 0000000..1775084
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0040.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0041.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0041.png
new file mode 100644
index 0000000..1941b1c
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0041.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0042.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0042.png
new file mode 100644
index 0000000..40e5b50
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0042.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0043.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0043.png
new file mode 100644
index 0000000..7170cb2
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0043.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0044.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0044.png
new file mode 100644
index 0000000..0eb2883
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0044.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0045.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0045.png
new file mode 100644
index 0000000..ba1047f
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0045.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0046.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0046.png
new file mode 100644
index 0000000..beb4cbc
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0046.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light/frame_0047.png b/output/animate/blender/moons/moon/moon_spin_light/frame_0047.png
new file mode 100644
index 0000000..297ae2b
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_spin_light/frame_0047.png differ
diff --git a/output/animate/blender/moons/moon/moon_spin_light_job.json b/output/animate/blender/moons/moon/moon_spin_light_job.json
new file mode 100644
index 0000000..a4128f7
--- /dev/null
+++ b/output/animate/blender/moons/moon/moon_spin_light_job.json
@@ -0,0 +1,466 @@
+{
+ "schema": "solsys.blender_flyby_job/v1",
+ "theme": "light",
+ "mode": "spin",
+ "body": {
+ "name": "Moon",
+ "kind": "moon",
+ "systemId": "sol",
+ "diameterKm": 3474,
+ "color": "lightgray",
+ "colorRgba": [
+ 0.75,
+ 0.75,
+ 0.75,
+ 1.0
+ ],
+ "displayRadiusAu": 0.009288902265104232
+ },
+ "frames": [
+ {
+ "frame": 0,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 0.0
+ },
+ {
+ "frame": 1,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 7.5
+ },
+ {
+ "frame": 2,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 15.0
+ },
+ {
+ "frame": 3,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 22.5
+ },
+ {
+ "frame": 4,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 30.0
+ },
+ {
+ "frame": 5,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 37.5
+ },
+ {
+ "frame": 6,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 45.0
+ },
+ {
+ "frame": 7,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 52.5
+ },
+ {
+ "frame": 8,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 60.0
+ },
+ {
+ "frame": 9,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 67.5
+ },
+ {
+ "frame": 10,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 75.0
+ },
+ {
+ "frame": 11,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 82.5
+ },
+ {
+ "frame": 12,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 90.0
+ },
+ {
+ "frame": 13,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 97.5
+ },
+ {
+ "frame": 14,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 105.0
+ },
+ {
+ "frame": 15,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 112.5
+ },
+ {
+ "frame": 16,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 120.0
+ },
+ {
+ "frame": 17,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 127.5
+ },
+ {
+ "frame": 18,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 135.0
+ },
+ {
+ "frame": 19,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 142.5
+ },
+ {
+ "frame": 20,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 150.0
+ },
+ {
+ "frame": 21,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 157.5
+ },
+ {
+ "frame": 22,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 165.0
+ },
+ {
+ "frame": 23,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 172.5
+ },
+ {
+ "frame": 24,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 180.0
+ },
+ {
+ "frame": 25,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 187.5
+ },
+ {
+ "frame": 26,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 195.0
+ },
+ {
+ "frame": 27,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 202.5
+ },
+ {
+ "frame": 28,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 210.0
+ },
+ {
+ "frame": 29,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 217.5
+ },
+ {
+ "frame": 30,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 225.0
+ },
+ {
+ "frame": 31,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 232.5
+ },
+ {
+ "frame": 32,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 240.0
+ },
+ {
+ "frame": 33,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 247.5
+ },
+ {
+ "frame": 34,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 255.0
+ },
+ {
+ "frame": 35,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 262.5
+ },
+ {
+ "frame": 36,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 270.0
+ },
+ {
+ "frame": 37,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 277.5
+ },
+ {
+ "frame": 38,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 285.0
+ },
+ {
+ "frame": 39,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 292.5
+ },
+ {
+ "frame": 40,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 300.0
+ },
+ {
+ "frame": 41,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 307.5
+ },
+ {
+ "frame": 42,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 315.0
+ },
+ {
+ "frame": 43,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 322.5
+ },
+ {
+ "frame": 44,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 330.0
+ },
+ {
+ "frame": 45,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 337.5
+ },
+ {
+ "frame": 46,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 345.0
+ },
+ {
+ "frame": 47,
+ "cameraAu": [
+ 0.03184108916015677,
+ 0.022295370654749246,
+ 0.012629886099622662
+ ],
+ "bodyRotationDeg": 352.5
+ }
+ ],
+ "outputDirectory": "output/animate/blender/moons/moon/moon_spin_light",
+ "resolution": 512,
+ "fps": 20,
+ "filmTransparent": true,
+ "appearance": {
+ "bodyId": "moon",
+ "kind": "moon",
+ "textures": {
+ "color": "/Users/tom/Desktop/SOLSYS/data/textures/bodies/moon/color.png"
+ },
+ "roughness": 0.82,
+ "specular": 0.04
+ }
+}
diff --git a/output/animate/blender/planets/earth/earth_spin_dark.gif b/output/animate/blender/planets/earth/earth_spin_dark.gif
new file mode 100644
index 0000000..b16af6a
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark.gif differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0000.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0000.png
new file mode 100644
index 0000000..a3c753d
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0000.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0001.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0001.png
new file mode 100644
index 0000000..8326c8e
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0001.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0002.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0002.png
new file mode 100644
index 0000000..1ff9534
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0002.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0003.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0003.png
new file mode 100644
index 0000000..4c5d17e
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0003.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0004.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0004.png
new file mode 100644
index 0000000..5a078d6
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0004.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0005.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0005.png
new file mode 100644
index 0000000..ced927b
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0005.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0006.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0006.png
new file mode 100644
index 0000000..228f631
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0006.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0007.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0007.png
new file mode 100644
index 0000000..8769604
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0007.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0008.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0008.png
new file mode 100644
index 0000000..1211d24
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0008.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0009.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0009.png
new file mode 100644
index 0000000..7b8cec7
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0009.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0010.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0010.png
new file mode 100644
index 0000000..ffc942a
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0010.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0011.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0011.png
new file mode 100644
index 0000000..ec28966
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0011.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0012.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0012.png
new file mode 100644
index 0000000..5e7993e
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0012.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0013.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0013.png
new file mode 100644
index 0000000..c5634fd
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0013.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0014.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0014.png
new file mode 100644
index 0000000..8283440
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0014.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0015.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0015.png
new file mode 100644
index 0000000..f11ed3c
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0015.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0016.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0016.png
new file mode 100644
index 0000000..e7dcb05
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0016.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0017.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0017.png
new file mode 100644
index 0000000..d44438c
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0017.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0018.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0018.png
new file mode 100644
index 0000000..d3495dc
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0018.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0019.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0019.png
new file mode 100644
index 0000000..7978e74
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0019.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0020.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0020.png
new file mode 100644
index 0000000..a5b9436
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0020.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0021.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0021.png
new file mode 100644
index 0000000..4ce6520
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0021.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0022.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0022.png
new file mode 100644
index 0000000..051d069
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0022.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0023.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0023.png
new file mode 100644
index 0000000..c2e33ee
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0023.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0024.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0024.png
new file mode 100644
index 0000000..04073cc
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0024.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0025.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0025.png
new file mode 100644
index 0000000..11d99e7
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0025.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0026.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0026.png
new file mode 100644
index 0000000..d93a5c9
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0026.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0027.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0027.png
new file mode 100644
index 0000000..c6a76e2
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0027.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0028.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0028.png
new file mode 100644
index 0000000..4a82144
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0028.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0029.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0029.png
new file mode 100644
index 0000000..5924993
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0029.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0030.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0030.png
new file mode 100644
index 0000000..705dbf6
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0030.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0031.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0031.png
new file mode 100644
index 0000000..4eafeb9
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0031.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0032.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0032.png
new file mode 100644
index 0000000..4d7984b
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0032.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0033.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0033.png
new file mode 100644
index 0000000..6fe5ba5
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0033.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0034.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0034.png
new file mode 100644
index 0000000..30f9c6e
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0034.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0035.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0035.png
new file mode 100644
index 0000000..f29c2d2
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0035.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0036.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0036.png
new file mode 100644
index 0000000..de5b7f3
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0036.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0037.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0037.png
new file mode 100644
index 0000000..c56fc11
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0037.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0038.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0038.png
new file mode 100644
index 0000000..8ec5b23
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0038.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0039.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0039.png
new file mode 100644
index 0000000..be39766
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0039.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0040.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0040.png
new file mode 100644
index 0000000..226ebe3
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0040.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0041.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0041.png
new file mode 100644
index 0000000..a679083
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0041.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0042.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0042.png
new file mode 100644
index 0000000..1819590
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0042.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0043.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0043.png
new file mode 100644
index 0000000..fcd8569
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0043.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0044.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0044.png
new file mode 100644
index 0000000..8538ef4
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0044.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0045.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0045.png
new file mode 100644
index 0000000..97dbaff
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0045.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0046.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0046.png
new file mode 100644
index 0000000..15a4ef1
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0046.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark/frame_0047.png b/output/animate/blender/planets/earth/earth_spin_dark/frame_0047.png
new file mode 100644
index 0000000..566c7bf
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_dark/frame_0047.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_dark_job.json b/output/animate/blender/planets/earth/earth_spin_dark_job.json
new file mode 100644
index 0000000..03aa9c0
--- /dev/null
+++ b/output/animate/blender/planets/earth/earth_spin_dark_job.json
@@ -0,0 +1,479 @@
+{
+ "schema": "solsys.blender_flyby_job/v1",
+ "theme": "dark",
+ "mode": "spin",
+ "body": {
+ "name": "Earth",
+ "kind": "planet",
+ "systemId": "sol",
+ "diameterKm": 12742,
+ "color": "blue",
+ "colorRgba": [
+ 0.25,
+ 0.45,
+ 0.95,
+ 1.0
+ ],
+ "displayRadiusAu": 0.034070003644777816
+ },
+ "frames": [
+ {
+ "frame": 0,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 0.0
+ },
+ {
+ "frame": 1,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 7.5
+ },
+ {
+ "frame": 2,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 15.0
+ },
+ {
+ "frame": 3,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 22.5
+ },
+ {
+ "frame": 4,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 30.0
+ },
+ {
+ "frame": 5,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 37.5
+ },
+ {
+ "frame": 6,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 45.0
+ },
+ {
+ "frame": 7,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 52.5
+ },
+ {
+ "frame": 8,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 60.0
+ },
+ {
+ "frame": 9,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 67.5
+ },
+ {
+ "frame": 10,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 75.0
+ },
+ {
+ "frame": 11,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 82.5
+ },
+ {
+ "frame": 12,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 90.0
+ },
+ {
+ "frame": 13,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 97.5
+ },
+ {
+ "frame": 14,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 105.0
+ },
+ {
+ "frame": 15,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 112.5
+ },
+ {
+ "frame": 16,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 120.0
+ },
+ {
+ "frame": 17,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 127.5
+ },
+ {
+ "frame": 18,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 135.0
+ },
+ {
+ "frame": 19,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 142.5
+ },
+ {
+ "frame": 20,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 150.0
+ },
+ {
+ "frame": 21,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 157.5
+ },
+ {
+ "frame": 22,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 165.0
+ },
+ {
+ "frame": 23,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 172.5
+ },
+ {
+ "frame": 24,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 180.0
+ },
+ {
+ "frame": 25,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 187.5
+ },
+ {
+ "frame": 26,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 195.0
+ },
+ {
+ "frame": 27,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 202.5
+ },
+ {
+ "frame": 28,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 210.0
+ },
+ {
+ "frame": 29,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 217.5
+ },
+ {
+ "frame": 30,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 225.0
+ },
+ {
+ "frame": 31,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 232.5
+ },
+ {
+ "frame": 32,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 240.0
+ },
+ {
+ "frame": 33,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 247.5
+ },
+ {
+ "frame": 34,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 255.0
+ },
+ {
+ "frame": 35,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 262.5
+ },
+ {
+ "frame": 36,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 270.0
+ },
+ {
+ "frame": 37,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 277.5
+ },
+ {
+ "frame": 38,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 285.0
+ },
+ {
+ "frame": 39,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 292.5
+ },
+ {
+ "frame": 40,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 300.0
+ },
+ {
+ "frame": 41,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 307.5
+ },
+ {
+ "frame": 42,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 315.0
+ },
+ {
+ "frame": 43,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 322.5
+ },
+ {
+ "frame": 44,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 330.0
+ },
+ {
+ "frame": 45,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 337.5
+ },
+ {
+ "frame": 46,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 345.0
+ },
+ {
+ "frame": 47,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 352.5
+ }
+ ],
+ "outputDirectory": "output/animate/blender/planets/earth/earth_spin_dark",
+ "resolution": 512,
+ "fps": 20,
+ "filmTransparent": true,
+ "appearance": {
+ "bodyId": "earth",
+ "kind": "planet",
+ "textures": {
+ "color": "/Users/tom/Desktop/SOLSYS/data/textures/bodies/earth/color.png",
+ "clouds": "/Users/tom/Desktop/SOLSYS/data/textures/bodies/earth/clouds.png"
+ },
+ "roughness": 0.58,
+ "specular": 0.18,
+ "atmosphere": {
+ "enabled": true,
+ "scale": 1.038,
+ "colorRgba": [
+ 0.4,
+ 0.68,
+ 1.0,
+ 1.0
+ ],
+ "strength": 0.85,
+ "fresnelBlend": 0.14
+ }
+ }
+}
diff --git a/output/animate/blender/planets/earth/earth_spin_light.gif b/output/animate/blender/planets/earth/earth_spin_light.gif
new file mode 100644
index 0000000..6d91256
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light.gif differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0000.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0000.png
new file mode 100644
index 0000000..110f16d
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0000.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0001.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0001.png
new file mode 100644
index 0000000..bdaebd9
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0001.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0002.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0002.png
new file mode 100644
index 0000000..842ab3e
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0002.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0003.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0003.png
new file mode 100644
index 0000000..9d1e2a9
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0003.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0004.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0004.png
new file mode 100644
index 0000000..98be32d
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0004.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0005.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0005.png
new file mode 100644
index 0000000..d0d236b
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0005.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0006.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0006.png
new file mode 100644
index 0000000..83bef4a
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0006.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0007.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0007.png
new file mode 100644
index 0000000..ccb85f0
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0007.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0008.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0008.png
new file mode 100644
index 0000000..e734f90
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0008.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0009.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0009.png
new file mode 100644
index 0000000..01d7123
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0009.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0010.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0010.png
new file mode 100644
index 0000000..da623c4
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0010.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0011.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0011.png
new file mode 100644
index 0000000..a2e58d2
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0011.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0012.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0012.png
new file mode 100644
index 0000000..56fd8e2
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0012.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0013.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0013.png
new file mode 100644
index 0000000..44fd12f
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0013.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0014.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0014.png
new file mode 100644
index 0000000..753278c
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0014.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0015.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0015.png
new file mode 100644
index 0000000..b3046ce
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0015.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0016.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0016.png
new file mode 100644
index 0000000..9737abf
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0016.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0017.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0017.png
new file mode 100644
index 0000000..97c4694
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0017.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0018.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0018.png
new file mode 100644
index 0000000..291b1b9
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0018.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0019.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0019.png
new file mode 100644
index 0000000..6d6d3f3
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0019.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0020.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0020.png
new file mode 100644
index 0000000..aafb080
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0020.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0021.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0021.png
new file mode 100644
index 0000000..fbf2d4b
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0021.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0022.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0022.png
new file mode 100644
index 0000000..9f8d243
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0022.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0023.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0023.png
new file mode 100644
index 0000000..e322468
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0023.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0024.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0024.png
new file mode 100644
index 0000000..e9a6892
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0024.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0025.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0025.png
new file mode 100644
index 0000000..7b434e5
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0025.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0026.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0026.png
new file mode 100644
index 0000000..12af1ff
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0026.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0027.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0027.png
new file mode 100644
index 0000000..bb2ecb9
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0027.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0028.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0028.png
new file mode 100644
index 0000000..d36ef2b
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0028.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0029.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0029.png
new file mode 100644
index 0000000..352c402
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0029.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0030.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0030.png
new file mode 100644
index 0000000..b8741e3
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0030.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0031.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0031.png
new file mode 100644
index 0000000..1617c42
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0031.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0032.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0032.png
new file mode 100644
index 0000000..b276a73
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0032.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0033.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0033.png
new file mode 100644
index 0000000..906d4fe
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0033.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0034.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0034.png
new file mode 100644
index 0000000..80f5f15
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0034.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0035.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0035.png
new file mode 100644
index 0000000..0faa556
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0035.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0036.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0036.png
new file mode 100644
index 0000000..7398421
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0036.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0037.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0037.png
new file mode 100644
index 0000000..58e7125
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0037.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0038.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0038.png
new file mode 100644
index 0000000..870e199
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0038.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0039.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0039.png
new file mode 100644
index 0000000..abe8990
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0039.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0040.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0040.png
new file mode 100644
index 0000000..b5fc630
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0040.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0041.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0041.png
new file mode 100644
index 0000000..520f6bb
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0041.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0042.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0042.png
new file mode 100644
index 0000000..cabf10f
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0042.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0043.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0043.png
new file mode 100644
index 0000000..7b54061
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0043.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0044.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0044.png
new file mode 100644
index 0000000..087cc22
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0044.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0045.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0045.png
new file mode 100644
index 0000000..da47ca2
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0045.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0046.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0046.png
new file mode 100644
index 0000000..77ece14
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0046.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light/frame_0047.png b/output/animate/blender/planets/earth/earth_spin_light/frame_0047.png
new file mode 100644
index 0000000..bc9943f
Binary files /dev/null and b/output/animate/blender/planets/earth/earth_spin_light/frame_0047.png differ
diff --git a/output/animate/blender/planets/earth/earth_spin_light_job.json b/output/animate/blender/planets/earth/earth_spin_light_job.json
new file mode 100644
index 0000000..dda8220
--- /dev/null
+++ b/output/animate/blender/planets/earth/earth_spin_light_job.json
@@ -0,0 +1,479 @@
+{
+ "schema": "solsys.blender_flyby_job/v1",
+ "theme": "light",
+ "mode": "spin",
+ "body": {
+ "name": "Earth",
+ "kind": "planet",
+ "systemId": "sol",
+ "diameterKm": 12742,
+ "color": "blue",
+ "colorRgba": [
+ 0.25,
+ 0.45,
+ 0.95,
+ 1.0
+ ],
+ "displayRadiusAu": 0.034070003644777816
+ },
+ "frames": [
+ {
+ "frame": 0,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 0.0
+ },
+ {
+ "frame": 1,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 7.5
+ },
+ {
+ "frame": 2,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 15.0
+ },
+ {
+ "frame": 3,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 22.5
+ },
+ {
+ "frame": 4,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 30.0
+ },
+ {
+ "frame": 5,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 37.5
+ },
+ {
+ "frame": 6,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 45.0
+ },
+ {
+ "frame": 7,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 52.5
+ },
+ {
+ "frame": 8,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 60.0
+ },
+ {
+ "frame": 9,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 67.5
+ },
+ {
+ "frame": 10,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 75.0
+ },
+ {
+ "frame": 11,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 82.5
+ },
+ {
+ "frame": 12,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 90.0
+ },
+ {
+ "frame": 13,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 97.5
+ },
+ {
+ "frame": 14,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 105.0
+ },
+ {
+ "frame": 15,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 112.5
+ },
+ {
+ "frame": 16,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 120.0
+ },
+ {
+ "frame": 17,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 127.5
+ },
+ {
+ "frame": 18,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 135.0
+ },
+ {
+ "frame": 19,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 142.5
+ },
+ {
+ "frame": 20,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 150.0
+ },
+ {
+ "frame": 21,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 157.5
+ },
+ {
+ "frame": 22,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 165.0
+ },
+ {
+ "frame": 23,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 172.5
+ },
+ {
+ "frame": 24,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 180.0
+ },
+ {
+ "frame": 25,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 187.5
+ },
+ {
+ "frame": 26,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 195.0
+ },
+ {
+ "frame": 27,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 202.5
+ },
+ {
+ "frame": 28,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 210.0
+ },
+ {
+ "frame": 29,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 217.5
+ },
+ {
+ "frame": 30,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 225.0
+ },
+ {
+ "frame": 31,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 232.5
+ },
+ {
+ "frame": 32,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 240.0
+ },
+ {
+ "frame": 33,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 247.5
+ },
+ {
+ "frame": 34,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 255.0
+ },
+ {
+ "frame": 35,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 262.5
+ },
+ {
+ "frame": 36,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 270.0
+ },
+ {
+ "frame": 37,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 277.5
+ },
+ {
+ "frame": 38,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 285.0
+ },
+ {
+ "frame": 39,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 292.5
+ },
+ {
+ "frame": 40,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 300.0
+ },
+ {
+ "frame": 41,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 307.5
+ },
+ {
+ "frame": 42,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 315.0
+ },
+ {
+ "frame": 43,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 322.5
+ },
+ {
+ "frame": 44,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 330.0
+ },
+ {
+ "frame": 45,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 337.5
+ },
+ {
+ "frame": 46,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 345.0
+ },
+ {
+ "frame": 47,
+ "cameraAu": [
+ 0.11678732241759286,
+ 0.08177536352412634,
+ 0.046324124548472075
+ ],
+ "bodyRotationDeg": 352.5
+ }
+ ],
+ "outputDirectory": "output/animate/blender/planets/earth/earth_spin_light",
+ "resolution": 512,
+ "fps": 20,
+ "filmTransparent": true,
+ "appearance": {
+ "bodyId": "earth",
+ "kind": "planet",
+ "textures": {
+ "color": "/Users/tom/Desktop/SOLSYS/data/textures/bodies/earth/color.png",
+ "clouds": "/Users/tom/Desktop/SOLSYS/data/textures/bodies/earth/clouds.png"
+ },
+ "roughness": 0.58,
+ "specular": 0.18,
+ "atmosphere": {
+ "enabled": true,
+ "scale": 1.038,
+ "colorRgba": [
+ 0.4,
+ 0.68,
+ 1.0,
+ 1.0
+ ],
+ "strength": 0.85,
+ "fresnelBlend": 0.14
+ }
+ }
+}
diff --git a/output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_dark.gif b/output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_dark.gif
new file mode 100644
index 0000000..e2bb4b6
Binary files /dev/null and b/output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_dark.gif differ
diff --git a/output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_light.gif b/output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_light.gif
new file mode 100644
index 0000000..b472f47
Binary files /dev/null and b/output/animate/sol_centauri/blender/sol_centauri_cinematic_blender_light.gif differ
diff --git a/render.py b/render.py
index 66eec02..f00bb63 100644
--- a/render.py
+++ b/render.py
@@ -13,7 +13,7 @@
from animate.scenes.alpha_centauri import renderAlphaCentauriAnimations
from animate.scenes.barnards_star import renderBarnardsStarAnimations
from animate.scenes.blender.export_body import exportBodyScene
-from animate.scenes.blender.flyby_scene import renderPlanetFlyby
+from animate.scenes.blender.flyby_scene import renderPlanetFlyby, renderPlanetSpin
from animate.scenes.interstellar_objects import renderInterstellarObjectAnimations
from animate.scenes.sol_centauri_cinematic import renderSolCentauriCinematicAnimations
from animate.scenes.tabbys_star import renderTabbysStarAnimations
@@ -132,6 +132,14 @@ def buildParser() -> argparse.ArgumentParser:
default='data/nearby_stars_30.csv',
help='Path to nearby-stars CSV (for multi-star systems)',
)
+ animateParser.add_argument(
+ '--blender-bodies',
+ action='store_true',
+ help=(
+ 'For --system sol_centauri: replace Earth/Moon dots with Blender spin-loop '
+ 'frames drawn each cinematic frame (requires prior: blender --spin)'
+ ),
+ )
allParser = subparsers.add_parser(
'all',
@@ -206,11 +214,27 @@ def buildParser() -> argparse.ArgumentParser:
action='store_true',
help='Render polished light/dark flyby GIFs via Blender (requires blender on PATH)',
)
+ blenderParser.add_argument(
+ '--spin',
+ action='store_true',
+ help=(
+ 'Render fixed-camera RGBA day/night spin loops for cinematic reuse '
+ '(requires blender on PATH)'
+ ),
+ )
blenderParser.add_argument(
'--theme',
choices=('light', 'dark', 'all'),
default='all',
- help='Flyby theme when using --flyby (default: all)',
+ help='Theme when using --flyby / --spin (default: all)',
+ )
+ blenderParser.add_argument(
+ '--pipeline',
+ action='store_true',
+ help=(
+ 'End-to-end: Earth+Moon Blender spin loops, then Sol→Centauri cinematic '
+ 'with --blender-bodies. Ignores --body.'
+ ),
)
return parser
@@ -238,11 +262,43 @@ def _runBlenderLoad(scenePath: Path) -> None:
raise SystemExit(f'Blender ingest failed with exit code {completed.returncode}')
+def _runBlenderCinematicPipeline(
+ *,
+ theme: str,
+ frames: int,
+ outputDirectory: str,
+ starsCsvPath: str,
+) -> None:
+ """Earth+Moon spin loops, then Sol→Centauri cinematic compositing those frames."""
+ spinFrames = 48 if frames == 120 else frames
+ for bodyName in ('Earth', 'Moon'):
+ print(f'[pipeline] Blender spin loop → {bodyName}')
+ paths = renderPlanetSpin(
+ bodyName,
+ theme=theme,
+ frameCount=spinFrames,
+ outputDirectory=outputDirectory,
+ )
+ for path in paths:
+ print(f'Spin ready → {path}')
+
+ print('[pipeline] Sol→Centauri cinematic with Blender spin billboards')
+ renderSolCentauriCinematicAnimations(
+ figureSizeInches=ANIMATE_FIGURE_SIZE_INCHES,
+ dpi=ANIMATE_DPI_CINEMATIC,
+ starsCsvPath=starsCsvPath,
+ useBlenderBodies=True,
+ )
+ print('[pipeline] done')
+
+
def _renderAnimations(
systemChoice: str,
dimensions: tuple[Dimension, ...],
starsCsvPath: str,
objectChoice: str,
+ *,
+ useBlenderBodies: bool = False,
) -> None:
if systemChoice in ('sol', 'all'):
for dimension in dimensions:
@@ -263,6 +319,7 @@ def _renderAnimations(
figureSizeInches=ANIMATE_FIGURE_SIZE_INCHES,
dpi=ANIMATE_DPI_CINEMATIC,
starsCsvPath=starsCsvPath,
+ useBlenderBodies=useBlenderBodies,
)
if systemChoice in ('barnards_star', 'all'):
renderBarnardsStarAnimations(
@@ -297,6 +354,63 @@ def _renderAnimations(
)
+def _handleBlenderCommand(args: argparse.Namespace) -> None:
+ if args.pipeline:
+ _runBlenderCinematicPipeline(
+ theme=args.theme,
+ frames=args.frames,
+ outputDirectory=args.output_dir,
+ starsCsvPath='data/nearby_stars_30.csv',
+ )
+ return
+
+ if args.spin:
+ spinFrames = 48 if args.frames == 120 else args.frames
+ paths = renderPlanetSpin(
+ args.body,
+ theme=args.theme,
+ frameCount=spinFrames,
+ outputDirectory=args.output_dir,
+ )
+ for path in paths:
+ print(f'Spin ready → {path}')
+ return
+
+ if args.flyby:
+ flybyFrames = 72 if args.frames == 120 else args.frames
+ gifPaths = renderPlanetFlyby(
+ args.body,
+ theme=args.theme,
+ frameCount=flybyFrames,
+ outputDirectory=args.output_dir,
+ )
+ for gifPath in gifPaths:
+ print(f'Flyby ready → {gifPath}')
+ return
+
+ scenePath = exportBodyScene(
+ args.body,
+ frameCount=args.frames,
+ outputDirectory=args.output_dir,
+ )
+ print(f'Exported Blender body scene → {scenePath}')
+ if args.load:
+ _runBlenderLoad(scenePath)
+ return
+ print(
+ 'Next: dry-run validate with\n'
+ f' {sys.executable} {BLENDER_LOAD_SCRIPT} {scenePath}\n'
+ 'or ingest with\n'
+ f' blender --background --python {BLENDER_LOAD_SCRIPT} -- {scenePath}\n'
+ 'or render flybys with\n'
+ f' {sys.executable} render.py blender --body {args.body} --flyby\n'
+ 'or render spin loops with\n'
+ f' {sys.executable} render.py blender --body {args.body} --spin\n'
+ 'or run the full Earth/Moon → cinematic pipeline with\n'
+ f' {sys.executable} render.py blender --pipeline'
+ )
+
+
def main(argv: list[str] | None = None) -> None:
parser = buildParser()
args = parser.parse_args(argv)
@@ -313,35 +427,7 @@ def main(argv: list[str] | None = None) -> None:
return
if args.command == 'blender':
- if args.flyby:
- flybyFrames = 72 if args.frames == 120 else args.frames
- gifPaths = renderPlanetFlyby(
- args.body,
- theme=args.theme,
- frameCount=flybyFrames,
- outputDirectory=args.output_dir,
- )
- for gifPath in gifPaths:
- print(f'Flyby ready → {gifPath}')
- return
-
- scenePath = exportBodyScene(
- args.body,
- frameCount=args.frames,
- outputDirectory=args.output_dir,
- )
- print(f'Exported Blender body scene → {scenePath}')
- if args.load:
- _runBlenderLoad(scenePath)
- else:
- print(
- 'Next: dry-run validate with\n'
- f' {sys.executable} {BLENDER_LOAD_SCRIPT} {scenePath}\n'
- 'or ingest with\n'
- f' blender --background --python {BLENDER_LOAD_SCRIPT} -- {scenePath}\n'
- 'or render flybys with\n'
- f' {sys.executable} render.py blender --body {args.body} --flyby'
- )
+ _handleBlenderCommand(args)
return
dimensions = _parseDimensions(args.dimension)
@@ -354,6 +440,7 @@ def main(argv: list[str] | None = None) -> None:
dimensions,
starsCsvPath=getattr(args, 'stars', 'data/nearby_stars_30.csv'),
objectChoice=getattr(args, 'object', 'all'),
+ useBlenderBodies=bool(getattr(args, 'blender_bodies', False)),
)
if args.command in ('static', 'all'):
diff --git a/tests/test_blender_body_sprites.py b/tests/test_blender_body_sprites.py
new file mode 100644
index 0000000..444ece4
--- /dev/null
+++ b/tests/test_blender_body_sprites.py
@@ -0,0 +1,182 @@
+"""Tests for Blender spin-loop frames in the Sol→Centauri cinematic."""
+
+from __future__ import annotations
+
+import tempfile
+import unittest
+from pathlib import Path
+
+import numpy as np
+from animate.blender_body_sprites import (
+ BlenderBodySpriteAtlas,
+ _blendSpinFrames,
+ loadSpinLoopFrames,
+ tidalLockFrameIndex,
+ tidalLockFramePosition,
+)
+from animate.scenes.blender.flyby_camera import buildSpinCameraPath
+from animate.scenes.blender.flyby_scene import buildSpinJob, spinFramesDirectory
+from animate.scenes.sol_centauri_cinematic import SolCentauriCinematicAnimator
+from PIL import Image
+from solsys.physics.catalogs.system_catalog import SystemCatalog, defaultDataPaths
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+
+
+class SpinCameraTests(unittest.TestCase):
+ def test_spin_path_is_fixed_camera_full_turn(self) -> None:
+ path = buildSpinCameraPath(0.01, frameCount=8)
+ self.assertEqual(len(path), 8)
+ self.assertEqual(path[0].cameraAu, path[-1].cameraAu)
+ self.assertAlmostEqual(path[0].bodyRotationDeg, 0.0)
+ self.assertAlmostEqual(path[4].bodyRotationDeg, 180.0)
+ self.assertLess(path[-1].bodyRotationDeg, 360.0)
+
+ def test_spin_job_requests_transparent_film(self) -> None:
+ with tempfile.TemporaryDirectory() as temporaryName:
+ temporary = Path(temporaryName)
+ job = buildSpinJob('Earth', theme='dark', framesDirectory=temporary / 'frames')
+ self.assertEqual(job['mode'], 'spin')
+ self.assertTrue(job['filmTransparent'])
+ self.assertEqual(job['frames'][0]['cameraAu'], job['frames'][-1]['cameraAu'])
+
+
+class TidalLockTests(unittest.TestCase):
+ def test_tidal_lock_index_tracks_orbital_phase(self) -> None:
+ self.assertEqual(tidalLockFrameIndex(0.0, 48), 0)
+ self.assertEqual(tidalLockFrameIndex(np.pi, 48), 24)
+ self.assertEqual(tidalLockFrameIndex(2.0 * np.pi * 0.999, 48), 47)
+ # Full orbit wraps to the same face.
+ self.assertEqual(tidalLockFrameIndex(2.0 * np.pi, 48), 0)
+ self.assertAlmostEqual(tidalLockFramePosition(np.pi, 48), 24.0, places=9)
+
+ def test_blend_spin_frames_is_smooth_between_neighbors(self) -> None:
+ frames = [
+ np.zeros((2, 2, 4), dtype=np.float32),
+ np.ones((2, 2, 4), dtype=np.float32),
+ ]
+ mid = _blendSpinFrames(frames, 0.5)
+ np.testing.assert_allclose(mid, 0.5)
+
+ def test_moon_frame_uses_orbital_phase_not_wall_clock(self) -> None:
+ with tempfile.TemporaryDirectory() as temporaryName:
+ temporary = Path(temporaryName)
+ moonDir = spinFramesDirectory('Moon', 'dark', outputDirectory=temporary)
+ moonDir.mkdir(parents=True)
+ for index in range(8):
+ Image.new('RGBA', (16, 16), (index * 30, 160, 160, 220)).save(
+ moonDir / f'frame_{index:04d}.png'
+ )
+ earthDir = spinFramesDirectory('Earth', 'dark', outputDirectory=temporary)
+ earthDir.mkdir(parents=True)
+ Image.new('RGBA', (16, 16), (20, 90, 180, 230)).save(earthDir / 'frame_0000.png')
+ atlas = BlenderBodySpriteAtlas('dark', outputDirectory=temporary)
+ a = atlas.moonFrame(0, orbitalPhaseRad=0.0, resolution=16)
+ b = atlas.moonFrame(100, orbitalPhaseRad=0.0, resolution=16)
+ c = atlas.moonFrame(0, orbitalPhaseRad=np.pi, resolution=16)
+ assert a is not None and b is not None and c is not None
+ np.testing.assert_allclose(a, b)
+ self.assertFalse(np.allclose(a, c))
+
+
+class SpinAtlasTests(unittest.TestCase):
+ def test_load_spin_frames_from_png_loop(self) -> None:
+ with tempfile.TemporaryDirectory() as temporaryName:
+ temporary = Path(temporaryName)
+ directory = spinFramesDirectory('Earth', 'dark', outputDirectory=temporary)
+ directory.mkdir(parents=True)
+ for index in range(4):
+ Image.new('RGBA', (32, 32), (10, 80, 180, 200)).save(
+ directory / f'frame_{index:04d}.png'
+ )
+ frames = loadSpinLoopFrames('Earth', 'dark', outputDirectory=temporary)
+ assert frames is not None
+ self.assertEqual(len(frames), 4)
+ self.assertEqual(frames[0].shape, (32, 32, 4))
+
+ moonDir = spinFramesDirectory('Moon', 'dark', outputDirectory=temporary)
+ moonDir.mkdir(parents=True)
+ Image.new('RGBA', (32, 32), (160, 160, 160, 220)).save(moonDir / 'frame_0000.png')
+ atlas = BlenderBodySpriteAtlas('dark', outputDirectory=temporary)
+ self.assertTrue(atlas.hasEarth)
+ self.assertTrue(atlas.hasMoon)
+ frame = atlas.earthFrame(5, resolution=16)
+ assert frame is not None
+ self.assertEqual(frame.shape, (16, 16, 4))
+
+
+class BlenderBodyOverlayTests(unittest.TestCase):
+ def setUp(self) -> None:
+ paths = defaultDataPaths(REPO_ROOT)
+ self.system = SystemCatalog(**paths).load('alpha_centauri')
+ self.starsCsvPath = paths['starsCsvPath']
+
+ def tearDown(self) -> None:
+ import matplotlib.pyplot as plt
+
+ for figure in list(plt.get_fignums()):
+ plt.close(figure)
+
+ def test_flag_off_skips_atlas(self) -> None:
+ animator = SolCentauriCinematicAnimator(
+ self.system,
+ starsCsvPath=self.starsCsvPath,
+ useBlenderBodies=False,
+ )
+ self.assertIsNone(animator.blenderSprites)
+
+ def test_billboard_radius_is_world_fixed_then_drops(self) -> None:
+ animator = SolCentauriCinematicAnimator(
+ self.system,
+ starsCsvPath=self.starsCsvPath,
+ useBlenderBodies=False,
+ )
+ close = animator._blenderBillboardRadiusAu(0.14, openCloseup=True, bodyScale=1.0)
+ mid = animator._blenderBillboardRadiusAu(1.5, openCloseup=False, bodyScale=1.0)
+ outerSol = animator._blenderBillboardRadiusAu(42.0, openCloseup=False, bodyScale=1.0)
+ far = animator._blenderBillboardRadiusAu(120.0, openCloseup=False, bodyScale=1.0)
+ self.assertIsNotNone(close)
+ self.assertIsNotNone(mid)
+ self.assertIsNotNone(outerSol)
+ self.assertIsNone(far)
+ assert close is not None and mid is not None and outerSol is not None
+ self.assertAlmostEqual(close, mid, places=9)
+ self.assertAlmostEqual(close, outerSol, places=9)
+
+ def test_earth_label_pad_shrinks_on_inner_sol_zoom_out(self) -> None:
+ animator = SolCentauriCinematicAnimator(
+ self.system,
+ starsCsvPath=self.starsCsvPath,
+ useBlenderBodies=False,
+ )
+ closeFrac = animator._blenderBillboardFracRadius(0.16, 1.0)
+ innerFrac = animator._blenderBillboardFracRadius(6.5, 1.0)
+ self.assertIsNotNone(closeFrac)
+ self.assertIsNotNone(innerFrac)
+ assert closeFrac is not None and innerFrac is not None
+ closePad = animator._blenderBodyLabelPad(closeFrac)
+ innerPad = animator._blenderBodyLabelPad(innerFrac)
+ self.assertAlmostEqual(closePad, 0.03, places=3)
+ self.assertLess(innerPad, 0.01)
+ self.assertLess(innerFrac + innerPad, closeFrac * 0.35)
+
+ def test_update_paints_overlay_when_spin_assets_present(self) -> None:
+ with tempfile.TemporaryDirectory() as temporaryName:
+ temporary = Path(temporaryName)
+ for body in ('Earth', 'Moon'):
+ directory = spinFramesDirectory(body, 'light', outputDirectory=temporary)
+ directory.mkdir(parents=True)
+ Image.new('RGBA', (48, 48), (20, 90, 180, 230)).save(directory / 'frame_0000.png')
+ animator = SolCentauriCinematicAnimator(
+ self.system,
+ style='default',
+ starsCsvPath=self.starsCsvPath,
+ useBlenderBodies=True,
+ )
+ animator.blenderSprites = BlenderBodySpriteAtlas('light', outputDirectory=temporary)
+ animator.update(0)
+ self.assertGreaterEqual(len(animator.bodyOverlay.get_images()), 1)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_sol_centauri_cinematic.py b/tests/test_sol_centauri_cinematic.py
index dadf777..3481131 100644
--- a/tests/test_sol_centauri_cinematic.py
+++ b/tests/test_sol_centauri_cinematic.py
@@ -6,21 +6,25 @@
from pathlib import Path
import numpy as np
+from animate.animation_styles import ASTEROID_RENDER_STYLES
from animate.scenes.sol_centauri_cinematic import (
AB_HOLD_END,
AB_TRAVEL_END,
ANIMATION_SPEED_AB,
ANIMATION_SPEED_SOL_NEAR,
+ PROXIMA_TRAVEL_END,
PULLBACK_END,
SOL_BELT_ARRIVE,
SOL_BELT_HOLD_END,
SOL_BELT_LINGER_HALF_AU,
+ SOL_EARTH_CLOSE_HALF_AU,
SOL_EARTH_HALF_AU,
+ SOL_EARTH_MOON_REVEAL_END,
+ SOL_EARTH_SPIN_HOLD_END,
SOL_HALF_WIDTH_AU,
SOL_HOLD_END,
SOL_INNER_HALF_AU,
SOL_OUTER_ARRIVE,
- PROXIMA_TRAVEL_END,
WIDE_OUT_ARRIVE,
WIDE_OUT_END,
SolCentauriCinematicAnimator,
@@ -31,7 +35,6 @@
spectralClassColor,
travelProgress,
)
-from animate.animation_styles import ASTEROID_RENDER_STYLES
from solsys.physics.catalogs.system_catalog import SystemCatalog, defaultDataPaths
from solsys.physics.frame_transform import SolCentauriFrameTransform
@@ -85,6 +88,29 @@ def test_camera_starts_on_earth_moon(self) -> None:
np.testing.assert_allclose(focus, earth, atol=1e-9)
self.assertAlmostEqual(halfWidth, SOL_EARTH_HALF_AU, places=6)
+ def test_blender_camera_starts_earth_close_then_reveals_moon(self) -> None:
+ paths = defaultDataPaths(REPO_ROOT)
+ animator = SolCentauriCinematicAnimator(
+ self.system,
+ starsCsvPath=paths['starsCsvPath'],
+ useBlenderBodies=True,
+ )
+ try:
+ focus, halfWidth = animator._cameraState(0)
+ earth = animator._planetPositionAu('Earth', 0)
+ np.testing.assert_allclose(focus, earth, atol=1e-9)
+ self.assertAlmostEqual(halfWidth, SOL_EARTH_CLOSE_HALF_AU, places=6)
+ holdFrame = int(SOL_EARTH_SPIN_HOLD_END * (animator.animationFrames - 1)) - 1
+ _, holdHalf = animator._cameraState(holdFrame)
+ self.assertAlmostEqual(holdHalf, SOL_EARTH_CLOSE_HALF_AU, places=6)
+ revealFrame = int(SOL_EARTH_MOON_REVEAL_END * (animator.animationFrames - 1))
+ _, revealHalf = animator._cameraState(revealFrame)
+ self.assertAlmostEqual(revealHalf, SOL_EARTH_HALF_AU, places=6)
+ finally:
+ import matplotlib.pyplot as plt
+
+ plt.close(animator.figure)
+
def test_camera_ends_at_proxima(self) -> None:
from animate.scenes.sol_centauri_cinematic import PROXIMA_INNER_HALF_AU