diff --git a/README.md b/README.md
index 12c91a0..7874b7f 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ SOLSYS/
│ ├── tabbys_star_lightcurve.csv # Downsampled Kepler LC for Tabby's Star inset
│ └── textures/ # Blender body maps (earth/…; moons/asteroids later)
│ ├── README.md # Attribution + pack layout
-│ └── bodies/earth/{color,clouds}.png # NASA Blue Marble + clouds
+│ └── bodies/{earth,moon}/… # NASA Blue Marble / LROC packs
│
├── animate/ # MAIN PRODUCT — GIF animations
│ ├── __init__.py
@@ -67,7 +67,7 @@ SOLSYS/
│ └── blender/ # Blender close-up pipeline (catalog → JSON → bpy)
│ ├── README.md # Pipeline docs + CLI examples
│ ├── body_scene.py # Versioned body-scene schema + PlanetCatalog build
-│ ├── export_body.py # Write output/animate/blender/*_body_scene.json
+│ ├── export_body.py # Write blender/{planets,moons}/
/*.json
│ ├── load_body.py # Blender ingest (stdlib + optional bpy)
│ ├── body_appearance.py # Shared texture packs (planets/moons/asteroids)
│ ├── flyby_camera.py # Body-centered close-up camera path
@@ -116,7 +116,7 @@ SOLSYS/
│ ├── trappist_1/ # trappist_1_planets_{light,dark}.gif
│ ├── tabbys_star/ # tabbys_star_dust_{light,dark}.gif
│ ├── interstellar_objects/ # {oumuamua,borisov,atlas}_{side,oblique}_{light,dark}.gif
-│ └── blender/ # body JSON + earth_flyby_{light,dark}.gif
+│ └── blender/{planets,moons}// # body JSON + *_flyby_{light,dark}.gif
├── 2d/ # Static top-down zoom JPGs (*_{light,dark}.jpg)
├── 3d/ # Static perspective zoom JPGs (*_{light,dark}.jpg)
└── neighborhood/ # interstellar_neighborhood_*ly.jpg
@@ -163,7 +163,13 @@ solsys.motion → moving asteroid fields for animation frames
| Light | Dark |
|-------|------|
-|  |  |
+|  |  |
+
+**Moon Blender close-up**
+
+| Light | Dark |
+|-------|------|
+|  |  |
### Static zoom views (side)
@@ -308,11 +314,12 @@ 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
```
-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`) to `output/animate/blender/_flyby_{light,dark}.gif`. Earth uses a NASA Blue Marble color map from `data/textures/bodies/`; moons/asteroids can share the same pack layout later.
+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/`.
Alpha Centauri (issue #1) is one `system_id` covering A, B, and Proxima. Animations render to `output/animate/alpha_centauri/`:
@@ -324,10 +331,14 @@ Sol → Alpha Centauri cinematic (issue #10) flies from our solar system to the
- `sol_centauri_cinematic_{light,dark}.gif` — Sol → AB → wide → Proxima planets
-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/`:
+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_flyby_{light,dark}.gif` — textured sphere, clouds, thin atmosphere limb, elevated turntable camera + 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
+
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/`:
- `barnards_star_planets_{light,dark}.gif` — compact planets d, b, c, e
@@ -361,4 +372,5 @@ CLI: `render.py animate --system interstellar` (all) or `--object oumuamua|boris
## Roadmap
- Additional star systems via `SystemCatalog` / `data/systems.csv` (`exoplanet_system.py` for planet disks; dedicated scenes for dust / other phenomena)
-- More Blender close-ups (moons, asteroids, additional planets) via shared `data/textures/bodies/` packs
+- More Blender close-ups (additional moons, asteroids, planets) via shared `data/textures/bodies/` packs
+- Splice Earth/Moon Blender opens into the Sol→α Cen cinematic (#42)
diff --git a/animate/scenes/blender/README.md b/animate/scenes/blender/README.md
index b763fce..fd3ea15 100644
--- a/animate/scenes/blender/README.md
+++ b/animate/scenes/blender/README.md
@@ -8,8 +8,8 @@ Pipeline for Blender-based planet flybys / zoom-ins that plug into the SOLSYS an
| Stage | Module | Runs in | Role |
|-------|--------|---------|------|
-| 1. Build | `body_scene.py` | SOLSYS venv | Load one `PlanetCatalog` body; sample Keplerian positions into `solsys.blender_body_scene/v1` |
-| 2. Export | `export_body.py` | SOLSYS venv | Write `output/animate/blender/_body_scene.json` |
+| 1. Build | `body_scene.py` | SOLSYS venv | Load one PlanetCatalog / MoonCatalog body into `solsys.blender_body_scene/v1` |
+| 2. Export | `export_body.py` | SOLSYS venv | Write `output/animate/blender/{planets,moons}//*_body_scene.json` |
| 3. Ingest | `load_body.py` | Blender (`bpy`) or host dry-run | Debug UV sphere + orbit keyframes (**stdlib-only**) |
| 4. Appearance | `body_appearance.py` | SOLSYS venv | Shared texture packs under `data/textures/bodies//` (planets/moons/asteroids) |
| 5. Camera | `flyby_camera.py` | SOLSYS venv | Body-centered elevated turntable orbit + spin samples |
@@ -36,20 +36,25 @@ Render the first polished Earth flyby (light + dark GIFs; needs `blender` on `PA
.venv/bin/python render.py blender --body Earth --flyby --theme dark
```
-Outputs:
+Outputs (organized by kind):
-- `output/animate/blender/earth_flyby_light.gif`
-- `output/animate/blender/earth_flyby_dark.gif`
-- `output/animate/blender/earth_body_scene.json` (catalog export still written)
-- `output/animate/blender/earth_flyby_{light,dark}_job.json` (last close-up jobs)
+```text
+output/animate/blender/
+├── planets/earth/ # *_body_scene.json, *_flyby_{light,dark}.gif (+ jobs)
+└── moons/moon/
+```
+
+Earth uses NASA Blue Marble color + cloud maps under `data/textures/bodies/earth/` (see `data/textures/README.md`) plus a shared fresnel **atmosphere** shell from `BodyAtmosphere`. Moon uses an LRO LROC color pack under `data/textures/bodies/moon/` (airless). Future Jupiter / asteroid packs drop into the same folder layout and register in `body_appearance.py` (clouds/atmosphere optional per body).
-Earth uses NASA Blue Marble color + cloud maps under `data/textures/bodies/earth/` (see `data/textures/README.md`) plus a shared fresnel **atmosphere** shell from `BodyAtmosphere`. Future Moon / Jupiter / asteroid packs drop into the same folder layout and register in `body_appearance.py` (clouds/atmosphere optional per body).
+```bash
+.venv/bin/python render.py blender --body Moon --flyby
+```
### Debug ingest (orbit view, not the flyby)
```bash
blender --python animate/scenes/blender/load_body.py -- \
- output/animate/blender/earth_body_scene.json
+ output/animate/blender/planets/earth/earth_body_scene.json
```
That also writes `earth_body_scene.blend` (Blender may keep `*.blend1` as a save backup).
diff --git a/animate/scenes/blender/body_appearance.py b/animate/scenes/blender/body_appearance.py
index ef8225e..ccb5d21 100644
--- a/animate/scenes/blender/body_appearance.py
+++ b/animate/scenes/blender/body_appearance.py
@@ -103,7 +103,7 @@ def _mapsForBodyId(bodyId: str) -> BodyTextureMaps:
)
-# Registry: add Moon / Jupiter / Ceres here as packs land under data/textures/bodies/.
+# Registry: add Jupiter / Ceres / other moons here as packs land under data/textures/bodies/.
_BODY_APPEARANCES: tuple[BodyAppearance, ...] = (
BodyAppearance(
bodyId='earth',
@@ -121,9 +121,16 @@ def _mapsForBodyId(bodyId: str) -> BodyTextureMaps:
fresnelBlend=0.14,
),
),
- # Future examples (no files yet → colorRgba fallback until packs exist):
- # BodyAppearance(..., atmosphere=BodyAtmosphere(enabled=True)) # thin-air moons
- # BodyAppearance('ceres', 'asteroid', ..., atmosphere=BodyAtmosphere(enabled=False))
+ BodyAppearance(
+ bodyId='moon',
+ kind='moon',
+ catalogNames=('Moon',),
+ textures=_mapsForBodyId('moon'),
+ # Airless regolith: matte, no atmosphere / clouds.
+ roughness=0.82,
+ specular=0.04,
+ atmosphere=BodyAtmosphere(enabled=False),
+ ),
)
diff --git a/animate/scenes/blender/body_scene.py b/animate/scenes/blender/body_scene.py
index 296f670..42f54f5 100644
--- a/animate/scenes/blender/body_scene.py
+++ b/animate/scenes/blender/body_scene.py
@@ -9,6 +9,7 @@
import numpy as np
from solsys.physics.astronomical_constants import AstronomicalConstants
+from solsys.physics.catalogs.moon_catalog import MoonCatalog, MoonOrbit
from solsys.physics.catalogs.planet_catalog import PlanetCatalog, PlanetOrbit
from solsys.physics.orbit_calculator import OrbitCalculator
@@ -17,6 +18,8 @@
# Matplotlib / catalog color names → RGBA for Blender materials.
_COLOR_RGBA: dict[str, tuple[float, float, float, float]] = {
'gray': (0.55, 0.55, 0.55, 1.0),
+ 'lightgray': (0.75, 0.75, 0.75, 1.0),
+ 'darkgray': (0.35, 0.35, 0.35, 1.0),
'yellow': (0.95, 0.85, 0.2, 1.0),
'blue': (0.25, 0.45, 0.95, 1.0),
'red': (0.9, 0.25, 0.2, 1.0),
@@ -24,8 +27,13 @@
'gold': (0.9, 0.75, 0.25, 1.0),
'lightblue': (0.55, 0.8, 0.95, 1.0),
'brown': (0.55, 0.35, 0.2, 1.0),
+ 'tan': (0.82, 0.71, 0.55, 1.0),
+ 'wheat': (0.90, 0.85, 0.70, 1.0),
+ 'silver': (0.75, 0.75, 0.78, 1.0),
+ 'whitesmoke': (0.90, 0.90, 0.90, 1.0),
+ 'white': (0.95, 0.95, 0.95, 1.0),
+ 'gainsboro': (0.86, 0.86, 0.86, 1.0),
}
-
# Physical planet radii are ~1e-5 AU; exaggerate so a UV sphere is visible near the body.
_DISPLAY_RADIUS_EXAGGERATION = 800.0
_MIN_DISPLAY_RADIUS_AU = 0.002
@@ -147,6 +155,55 @@ def _bodyFromPlanet(planet: PlanetOrbit, constants: AstronomicalConstants) -> Bo
)
+def _bodyFromMoon(
+ moon: MoonOrbit,
+ constants: AstronomicalConstants,
+ moonCatalog: MoonCatalog,
+) -> BodySceneBody:
+ return BodySceneBody(
+ name=moon.name,
+ kind='moon',
+ systemId='sol',
+ semiMajorAxisAu=moonCatalog.semiMajorAxisAu(moon),
+ eccentricity=0.0,
+ inclinationDeg=moon.inclinationDeg,
+ orbitalPeriodDays=moon.orbitalPeriodDays,
+ diameterKm=moon.diameterKm,
+ color=moon.color,
+ colorRgba=colorRgbaForName(moon.color),
+ displayRadiusAu=displayRadiusAu(moon.diameterKm, constants),
+ )
+
+
+def _orbitKeyframes(
+ *,
+ semiMajorAxisAu: float,
+ eccentricity: float,
+ inclinationDeg: float,
+ orbitalPeriodDays: float,
+ frameCount: int,
+) -> tuple[BodyKeyframe, ...]:
+ calculator = OrbitCalculator()
+ trueAnomalies = np.linspace(0.0, 2.0 * np.pi, frameCount, endpoint=False)
+ daysPerFrame = orbitalPeriodDays / frameCount
+ keyframes: list[BodyKeyframe] = []
+ for frame, trueAnomaly in enumerate(trueAnomalies):
+ positionX, positionY, positionZ = calculator.ellipticalPosition(
+ semiMajorAxisAu,
+ eccentricity,
+ inclinationDeg,
+ float(trueAnomaly),
+ )
+ keyframes.append(
+ BodyKeyframe(
+ frame=frame,
+ day=frame * daysPerFrame,
+ positionAu=(float(positionX), float(positionY), float(positionZ)),
+ )
+ )
+ return tuple(keyframes)
+
+
def buildPlanetBodyScene(
planetName: str = 'Earth',
*,
@@ -166,30 +223,73 @@ def buildPlanetBodyScene(
raise ValueError(f'Unknown planet {planetName!r}. Known: {known}') from error
body = _bodyFromPlanet(planet, constants)
- calculator = OrbitCalculator()
- trueAnomalies = np.linspace(0.0, 2.0 * np.pi, frameCount, endpoint=False)
- daysPerFrame = planet.orbitalPeriodDays / frameCount
+ keyframes = _orbitKeyframes(
+ semiMajorAxisAu=planet.semiMajorAxisAu,
+ eccentricity=planet.eccentricity,
+ inclinationDeg=planet.inclinationDeg,
+ orbitalPeriodDays=planet.orbitalPeriodDays,
+ frameCount=frameCount,
+ )
+ cameraHintDistanceAu = max(body.displayRadiusAu * 8.0, 0.05)
+ return BodyScene(
+ schema=SCHEMA_ID,
+ body=body,
+ keyframes=keyframes,
+ cameraHintDistanceAu=cameraHintDistanceAu,
+ )
- keyframes: list[BodyKeyframe] = []
- for frame, trueAnomaly in enumerate(trueAnomalies):
- positionX, positionY, positionZ = calculator.ellipticalPosition(
- planet.semiMajorAxisAu,
- planet.eccentricity,
- planet.inclinationDeg,
- float(trueAnomaly),
- )
- keyframes.append(
- BodyKeyframe(
- frame=frame,
- day=frame * daysPerFrame,
- positionAu=(float(positionX), float(positionY), float(positionZ)),
- )
- )
+def buildMoonBodyScene(
+ moonName: str = 'Moon',
+ *,
+ frameCount: int = 120,
+ constants: AstronomicalConstants | None = None,
+) -> BodyScene:
+ """Build a Blender-ingestible scene for one Sol moon from ``MoonCatalog``."""
+ if frameCount < 2:
+ raise ValueError('frameCount must be >= 2')
+
+ constants = constants or AstronomicalConstants()
+ catalog = MoonCatalog()
+ try:
+ moon = catalog.moons[moonName]
+ except KeyError as error:
+ known = ', '.join(sorted(catalog.moons))
+ raise ValueError(f'Unknown moon {moonName!r}. Known: {known}') from error
+
+ body = _bodyFromMoon(moon, constants, catalog)
+ keyframes = _orbitKeyframes(
+ semiMajorAxisAu=body.semiMajorAxisAu,
+ eccentricity=body.eccentricity,
+ inclinationDeg=body.inclinationDeg,
+ orbitalPeriodDays=body.orbitalPeriodDays,
+ frameCount=frameCount,
+ )
cameraHintDistanceAu = max(body.displayRadiusAu * 8.0, 0.05)
return BodyScene(
schema=SCHEMA_ID,
body=body,
- keyframes=tuple(keyframes),
+ keyframes=keyframes,
cameraHintDistanceAu=cameraHintDistanceAu,
)
+
+
+def buildBodyScene(
+ bodyName: str = 'Earth',
+ *,
+ frameCount: int = 120,
+ constants: AstronomicalConstants | None = None,
+) -> BodyScene:
+ """Build a body scene from PlanetCatalog or MoonCatalog by name."""
+ constants = constants or AstronomicalConstants()
+ planets = PlanetCatalog(constants).planets
+ moons = MoonCatalog().moons
+ if bodyName in planets:
+ return buildPlanetBodyScene(bodyName, frameCount=frameCount, constants=constants)
+ if bodyName in moons:
+ return buildMoonBodyScene(bodyName, frameCount=frameCount, constants=constants)
+ knownPlanets = ', '.join(sorted(planets))
+ knownMoons = ', '.join(sorted(moons))
+ raise ValueError(
+ f'Unknown body {bodyName!r}. Known planets: {knownPlanets}. Known moons: {knownMoons}'
+ )
diff --git a/animate/scenes/blender/export_body.py b/animate/scenes/blender/export_body.py
index 15141dc..3210cb8 100644
--- a/animate/scenes/blender/export_body.py
+++ b/animate/scenes/blender/export_body.py
@@ -1,13 +1,46 @@
-"""Write Blender body-scene JSON under output/animate/blender/."""
+"""Write Blender body-scene JSON under output/animate/blender/{planets,moons}/…"""
from __future__ import annotations
from pathlib import Path
-from animate.scenes.blender.body_scene import BodyScene, buildPlanetBodyScene
+from animate.scenes.blender.body_scene import BodyScene, buildBodyScene, buildPlanetBodyScene
DEFAULT_OUTPUT_DIRECTORY = Path('output/animate/blender')
+_KIND_DIRECTORY: dict[str, str] = {
+ 'planet': 'planets',
+ 'moon': 'moons',
+ 'asteroid': 'asteroids',
+ 'dwarf_planet': 'dwarf_planets',
+}
+
+
+def bodyStem(bodyName: str) -> str:
+ return bodyName.lower().replace(' ', '_')
+
+
+def bodyOutputDirectory(
+ kind: str,
+ bodyName: str,
+ *,
+ root: Path | str = DEFAULT_OUTPUT_DIRECTORY,
+) -> Path:
+ """``output/animate/blender/planets/earth`` / ``…/moons/moon`` (etc.)."""
+ kindDirectory = _KIND_DIRECTORY.get(kind, 'bodies')
+ return Path(root) / kindDirectory / bodyStem(bodyName)
+
+
+def exportBodyScene(
+ bodyName: str = 'Earth',
+ *,
+ frameCount: int = 120,
+ outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
+) -> Path:
+ """Export one catalog planet or moon to JSON for Blender ingest."""
+ scene = buildBodyScene(bodyName, frameCount=frameCount)
+ return writeBodyScene(scene, outputDirectory=outputDirectory)
+
def exportPlanetBodyScene(
planetName: str = 'Earth',
@@ -25,9 +58,10 @@ def writeBodyScene(
*,
outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
) -> Path:
- directory = Path(outputDirectory)
+ """Write ``_body_scene.json`` under ``{root}/{planets|moons}//``."""
+ directory = bodyOutputDirectory(scene.body.kind, scene.body.name, root=outputDirectory)
directory.mkdir(parents=True, exist_ok=True)
- stem = scene.body.name.lower().replace(' ', '_')
+ stem = bodyStem(scene.body.name)
outputPath = directory / f'{stem}_body_scene.json'
outputPath.write_text(scene.toJson(), encoding='utf-8')
return outputPath
diff --git a/animate/scenes/blender/flyby_scene.py b/animate/scenes/blender/flyby_scene.py
index 104fb44..31a1f8a 100644
--- a/animate/scenes/blender/flyby_scene.py
+++ b/animate/scenes/blender/flyby_scene.py
@@ -12,8 +12,8 @@
from PIL import Image
from animate.scenes.blender.body_appearance import appearanceForCatalogName
-from animate.scenes.blender.body_scene import buildPlanetBodyScene
-from animate.scenes.blender.export_body import DEFAULT_OUTPUT_DIRECTORY, exportPlanetBodyScene
+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.render_flyby import JOB_SCHEMA_ID
@@ -27,21 +27,21 @@
def preparePlanetFlybyExport(
- planetName: str = 'Earth',
+ bodyName: str = 'Earth',
*,
frameCount: int = 120,
outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
) -> Path:
"""Export catalog body-scene JSON used as flyby input metadata."""
- return exportPlanetBodyScene(
- planetName,
+ return exportBodyScene(
+ bodyName,
frameCount=frameCount,
outputDirectory=outputDirectory,
)
def buildFlybyJob(
- planetName: str = 'Earth',
+ bodyName: str = 'Earth',
*,
theme: Theme = 'dark',
frameCount: int = DEFAULT_FLYBY_FRAMES,
@@ -49,10 +49,10 @@ def buildFlybyJob(
fps: int = DEFAULT_FLYBY_FPS,
framesDirectory: Path | str,
) -> dict:
- """Build a Blender flyby job dict from PlanetCatalog + camera path."""
+ """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 = buildPlanetBodyScene(planetName, frameCount=max(frameCount, 2))
+ bodyScene = buildBodyScene(bodyName, frameCount=max(frameCount, 2))
cameraPath = buildFlybyCameraPath(bodyScene.body.displayRadiusAu, frameCount=frameCount)
appearance = appearanceForCatalogName(bodyScene.body.name)
job: dict = {
@@ -138,7 +138,7 @@ def _runBlenderFlybyJob(jobPath: Path) -> None:
def renderPlanetFlyby(
- planetName: str = 'Earth',
+ bodyName: str = 'Earth',
*,
theme: Theme | Literal['all'] = 'all',
frameCount: int = DEFAULT_FLYBY_FRAMES,
@@ -146,7 +146,7 @@ def renderPlanetFlyby(
fps: int = DEFAULT_FLYBY_FPS,
outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
) -> tuple[Path, ...]:
- """Render light/dark planet flyby GIFs via Blender; returns written GIF paths."""
+ """Render light/dark body flyby GIFs via Blender; returns written GIF paths."""
themes: tuple[Theme, ...]
if theme == 'all':
themes = ('light', 'dark')
@@ -158,28 +158,29 @@ def renderPlanetFlyby(
outputRoot = Path(outputDirectory)
outputRoot.mkdir(parents=True, exist_ok=True)
# Keep catalog export in the product tree so the #11 pipeline stays exercised.
- preparePlanetFlybyExport(planetName, outputDirectory=outputRoot)
+ # Export lands under blender/{planets|moons}// — GIFs/jobs join it.
+ bodyDirectory = preparePlanetFlybyExport(bodyName, outputDirectory=outputRoot).parent
written: list[Path] = []
- stem = planetName.lower().replace(' ', '_')
+ stem = bodyStem(bodyName)
for themeName in themes:
with tempfile.TemporaryDirectory(prefix=f'solsys_flyby_{stem}_{themeName}_') as temporary:
framesDirectory = Path(temporary) / 'frames'
job = buildFlybyJob(
- planetName,
+ bodyName,
theme=themeName,
frameCount=frameCount,
resolution=resolution,
fps=fps,
framesDirectory=framesDirectory,
)
- jobPath = outputRoot / f'{stem}_flyby_{themeName}_job.json'
+ jobPath = bodyDirectory / f'{stem}_flyby_{themeName}_job.json'
writeFlybyJob(job, jobPath)
_runBlenderFlybyJob(jobPath)
framePaths = sorted(framesDirectory.glob('frame_*.png'))
if not framePaths:
raise RuntimeError(f'No frames rendered for theme={themeName}')
- gifPath = outputRoot / f'{stem}_flyby_{themeName}.gif'
+ gifPath = bodyDirectory / f'{stem}_flyby_{themeName}.gif'
assembleGifFromPngs(framePaths, gifPath, fps=fps)
written.append(gifPath)
print(f'Wrote flyby GIF → {gifPath}')
diff --git a/animate/scenes/blender/load_body.py b/animate/scenes/blender/load_body.py
index f34ee3b..04b4aa9 100644
--- a/animate/scenes/blender/load_body.py
+++ b/animate/scenes/blender/load_body.py
@@ -4,12 +4,13 @@
1. Host dry-run (no bpy)::
- python animate/scenes/blender/load_body.py output/animate/blender/earth_body_scene.json
+ python animate/scenes/blender/load_body.py \\
+ output/animate/blender/planets/earth/earth_body_scene.json
2. Inside Blender::
blender --background --python animate/scenes/blender/load_body.py -- \\
- output/animate/blender/earth_body_scene.json
+ output/animate/blender/planets/earth/earth_body_scene.json
Keep this module stdlib-only so Blender's bundled Python can import it.
"""
diff --git a/animate/scenes/blender/render_flyby.py b/animate/scenes/blender/render_flyby.py
index 52a4a49..cc38949 100644
--- a/animate/scenes/blender/render_flyby.py
+++ b/animate/scenes/blender/render_flyby.py
@@ -4,7 +4,7 @@
close-up and writes PNG frames::
blender --background --python animate/scenes/blender/render_flyby.py -- \\
- output/animate/blender/earth_flyby_dark_job.json
+ output/animate/blender/planets/earth/earth_flyby_dark_job.json
"""
from __future__ import annotations
diff --git a/data/textures/README.md b/data/textures/README.md
index 4dd3370..a1d0446 100644
--- a/data/textures/README.md
+++ b/data/textures/README.md
@@ -10,7 +10,8 @@ data/textures/
├── earth/
│ ├── color.png # equirectangular color map
│ └── clouds.png # cloud layer (RGBA; alpha = coverage)
- ├── moon/ # future
+ ├── moon/
+ │ └── color.png # equirectangular color map (airless)
├── jupiter/ # future
└── ceres/ # future (asteroids use the same layout)
```
@@ -38,4 +39,12 @@ Atmosphere is not a texture file: enable a fresnel limb-haze shell per body in `
- **Credit:** Matt Eason / Live Cloud Maps; underlying imagery from NASA & NOAA sources
- **License:** NASA media generally in the U.S. public domain ([NASA image use policy](https://www.nasa.gov/nasa-brand-center/images-and-media/))
+## Moon
+
+- **Color:** `bodies/moon/color.png` (2048×1024)
+ - **Source:** NASA SVS — [CGI Moon Kit / LRO LROC WAC color (SVS 4720)](https://svs.gsfc.nasa.gov/4720) (`lroc_color_2k.jpg`)
+ - **Credit:** NASA/GSFC Scientific Visualization Studio; LRO / LROC
+- **Atmosphere / clouds:** none (airless pack in `body_appearance.py`)
+- **License:** NASA media generally in the U.S. public domain ([NASA image use policy](https://www.nasa.gov/nasa-brand-center/images-and-media/))
+
Do not replace vendor textures without updating this README attribution.
\ No newline at end of file
diff --git a/data/textures/bodies/moon/color.png b/data/textures/bodies/moon/color.png
new file mode 100644
index 0000000..a95d7b0
Binary files /dev/null and b/data/textures/bodies/moon/color.png differ
diff --git a/output/animate/blender/moons/moon/moon_body_scene.json b/output/animate/blender/moons/moon/moon_body_scene.json
new file mode 100644
index 0000000..12b2aa0
--- /dev/null
+++ b/output/animate/blender/moons/moon/moon_body_scene.json
@@ -0,0 +1,1104 @@
+{
+ "schema": "solsys.blender_body_scene/v1",
+ "body": {
+ "name": "Moon",
+ "kind": "moon",
+ "systemId": "sol",
+ "semiMajorAxisAu": 0.0025695552897999907,
+ "eccentricity": 0.0,
+ "inclinationDeg": 0.0,
+ "orbitalPeriodDays": 27.3,
+ "diameterKm": 3474,
+ "color": "lightgray",
+ "colorRgba": [
+ 0.75,
+ 0.75,
+ 0.75,
+ 1.0
+ ],
+ "displayRadiusAu": 0.009288902265104232
+ },
+ "keyframes": [
+ {
+ "frame": 0,
+ "day": 0.0,
+ "positionAu": [
+ 0.0025695552897999907,
+ 0.0,
+ 0.0
+ ]
+ },
+ {
+ "frame": 1,
+ "day": 0.2275,
+ "positionAu": [
+ 0.0025660338035791187,
+ 0.00013448013321079716,
+ 0.0
+ ]
+ },
+ {
+ "frame": 2,
+ "day": 0.455,
+ "positionAu": [
+ 0.0025554789970654596,
+ 0.00026859166572406294,
+ 0.0
+ ]
+ },
+ {
+ "frame": 3,
+ "day": 0.6825,
+ "positionAu": [
+ 0.0025379198002500112,
+ 0.00040196700715115693,
+ 0.0
+ ]
+ },
+ {
+ "frame": 4,
+ "day": 0.91,
+ "positionAu": [
+ 0.00251340434167072,
+ 0.0005342405849520336,
+ 0.0
+ ]
+ },
+ {
+ "frame": 5,
+ "day": 1.1375,
+ "positionAu": [
+ 0.002481999816495502,
+ 0.0006650498464441646,
+ 0.0
+ ]
+ },
+ {
+ "frame": 6,
+ "day": 1.365,
+ "positionAu": [
+ 0.0024437923023449626,
+ 0.0007940362525342401,
+ 0.0
+ ]
+ },
+ {
+ "frame": 7,
+ "day": 1.5925,
+ "positionAu": [
+ 0.0023988865233596163,
+ 0.0009208462604489024,
+ 0.0
+ ]
+ },
+ {
+ "frame": 8,
+ "day": 1.82,
+ "positionAu": [
+ 0.0023474055631582985,
+ 0.001045132292770913,
+ 0.0
+ ]
+ },
+ {
+ "frame": 9,
+ "day": 2.0475,
+ "positionAu": [
+ 0.002289490527474524,
+ 0.0011665536901246938,
+ 0.0
+ ]
+ },
+ {
+ "frame": 10,
+ "day": 2.275,
+ "positionAu": [
+ 0.0022253001573954772,
+ 0.0012847776448999951,
+ 0.0
+ ]
+ },
+ {
+ "frame": 11,
+ "day": 2.5025,
+ "positionAu": [
+ 0.002155010394263727,
+ 0.001399480113454425,
+ 0.0
+ ]
+ },
+ {
+ "frame": 12,
+ "day": 2.73,
+ "positionAu": [
+ 0.0020788138974342355,
+ 0.0015103467042945464,
+ 0.0
+ ]
+ },
+ {
+ "frame": 13,
+ "day": 2.9575,
+ "positionAu": [
+ 0.0019969195162084594,
+ 0.0016170735398011084,
+ 0.0
+ ]
+ },
+ {
+ "frame": 14,
+ "day": 3.185,
+ "positionAu": [
+ 0.0019095517173929296,
+ 0.0017193680891364792,
+ 0.0
+ ]
+ },
+ {
+ "frame": 15,
+ "day": 3.4125,
+ "positionAu": [
+ 0.0018169499700513379,
+ 0.0018169499700513376,
+ 0.0
+ ]
+ },
+ {
+ "frame": 16,
+ "day": 3.64,
+ "positionAu": [
+ 0.0017193680891364794,
+ 0.0019095517173929293,
+ 0.0
+ ]
+ },
+ {
+ "frame": 17,
+ "day": 3.8675,
+ "positionAu": [
+ 0.0016170735398011087,
+ 0.001996919516208459,
+ 0.0
+ ]
+ },
+ {
+ "frame": 18,
+ "day": 4.095,
+ "positionAu": [
+ 0.0015103467042945464,
+ 0.0020788138974342355,
+ 0.0
+ ]
+ },
+ {
+ "frame": 19,
+ "day": 4.3225,
+ "positionAu": [
+ 0.0013994801134544251,
+ 0.0021550103942637264,
+ 0.0
+ ]
+ },
+ {
+ "frame": 20,
+ "day": 4.55,
+ "positionAu": [
+ 0.0012847776448999956,
+ 0.0022253001573954772,
+ 0.0
+ ]
+ },
+ {
+ "frame": 21,
+ "day": 4.7775,
+ "positionAu": [
+ 0.001166553690124694,
+ 0.002289490527474524,
+ 0.0
+ ]
+ },
+ {
+ "frame": 22,
+ "day": 5.005,
+ "positionAu": [
+ 0.0010451322927709137,
+ 0.002347405563158298,
+ 0.0
+ ]
+ },
+ {
+ "frame": 23,
+ "day": 5.2325,
+ "positionAu": [
+ 0.0009208462604489028,
+ 0.0023988865233596163,
+ 0.0
+ ]
+ },
+ {
+ "frame": 24,
+ "day": 5.46,
+ "positionAu": [
+ 0.0007940362525342402,
+ 0.0024437923023449626,
+ 0.0
+ ]
+ },
+ {
+ "frame": 25,
+ "day": 5.6875,
+ "positionAu": [
+ 0.0006650498464441651,
+ 0.002481999816495502,
+ 0.0
+ ]
+ },
+ {
+ "frame": 26,
+ "day": 5.915,
+ "positionAu": [
+ 0.0005342405849520339,
+ 0.0025134043416707194,
+ 0.0
+ ]
+ },
+ {
+ "frame": 27,
+ "day": 6.1425,
+ "positionAu": [
+ 0.0004019670071511571,
+ 0.0025379198002500112,
+ 0.0
+ ]
+ },
+ {
+ "frame": 28,
+ "day": 6.37,
+ "positionAu": [
+ 0.00026859166572406354,
+ 0.0025554789970654596,
+ 0.0
+ ]
+ },
+ {
+ "frame": 29,
+ "day": 6.5975,
+ "positionAu": [
+ 0.0001344801332107975,
+ 0.0025660338035791187,
+ 0.0
+ ]
+ },
+ {
+ "frame": 30,
+ "day": 6.825,
+ "positionAu": [
+ 1.5733988304428542e-19,
+ 0.0025695552897999907,
+ 0.0
+ ]
+ },
+ {
+ "frame": 31,
+ "day": 7.0525,
+ "positionAu": [
+ -0.00013448013321079661,
+ 0.0025660338035791187,
+ 0.0
+ ]
+ },
+ {
+ "frame": 32,
+ "day": 7.28,
+ "positionAu": [
+ -0.0002685916657240626,
+ 0.0025554789970654596,
+ 0.0
+ ]
+ },
+ {
+ "frame": 33,
+ "day": 7.5075,
+ "positionAu": [
+ -0.0004019670071511568,
+ 0.0025379198002500112,
+ 0.0
+ ]
+ },
+ {
+ "frame": 34,
+ "day": 7.735,
+ "positionAu": [
+ -0.000534240584952033,
+ 0.00251340434167072,
+ 0.0
+ ]
+ },
+ {
+ "frame": 35,
+ "day": 7.9625,
+ "positionAu": [
+ -0.0006650498464441642,
+ 0.002481999816495502,
+ 0.0
+ ]
+ },
+ {
+ "frame": 36,
+ "day": 8.19,
+ "positionAu": [
+ -0.00079403625253424,
+ 0.002443792302344963,
+ 0.0
+ ]
+ },
+ {
+ "frame": 37,
+ "day": 8.4175,
+ "positionAu": [
+ -0.0009208462604489019,
+ 0.0023988865233596163,
+ 0.0
+ ]
+ },
+ {
+ "frame": 38,
+ "day": 8.645,
+ "positionAu": [
+ -0.0010451322927709128,
+ 0.0023474055631582985,
+ 0.0
+ ]
+ },
+ {
+ "frame": 39,
+ "day": 8.8725,
+ "positionAu": [
+ -0.0011665536901246936,
+ 0.002289490527474524,
+ 0.0
+ ]
+ },
+ {
+ "frame": 40,
+ "day": 9.1,
+ "positionAu": [
+ -0.001284777644899995,
+ 0.0022253001573954772,
+ 0.0
+ ]
+ },
+ {
+ "frame": 41,
+ "day": 9.3275,
+ "positionAu": [
+ -0.0013994801134544243,
+ 0.0021550103942637273,
+ 0.0
+ ]
+ },
+ {
+ "frame": 42,
+ "day": 9.555,
+ "positionAu": [
+ -0.0015103467042945462,
+ 0.0020788138974342355,
+ 0.0
+ ]
+ },
+ {
+ "frame": 43,
+ "day": 9.7825,
+ "positionAu": [
+ -0.001617073539801108,
+ 0.0019969195162084594,
+ 0.0
+ ]
+ },
+ {
+ "frame": 44,
+ "day": 10.01,
+ "positionAu": [
+ -0.0017193680891364786,
+ 0.0019095517173929298,
+ 0.0
+ ]
+ },
+ {
+ "frame": 45,
+ "day": 10.2375,
+ "positionAu": [
+ -0.0018169499700513376,
+ 0.0018169499700513379,
+ 0.0
+ ]
+ },
+ {
+ "frame": 46,
+ "day": 10.465,
+ "positionAu": [
+ -0.0019095517173929287,
+ 0.00171936808913648,
+ 0.0
+ ]
+ },
+ {
+ "frame": 47,
+ "day": 10.6925,
+ "positionAu": [
+ -0.0019969195162084586,
+ 0.0016170735398011093,
+ 0.0
+ ]
+ },
+ {
+ "frame": 48,
+ "day": 10.92,
+ "positionAu": [
+ -0.002078813897434235,
+ 0.0015103467042945468,
+ 0.0
+ ]
+ },
+ {
+ "frame": 49,
+ "day": 11.1475,
+ "positionAu": [
+ -0.0021550103942637264,
+ 0.0013994801134544258,
+ 0.0
+ ]
+ },
+ {
+ "frame": 50,
+ "day": 11.375,
+ "positionAu": [
+ -0.002225300157395477,
+ 0.0012847776448999962,
+ 0.0
+ ]
+ },
+ {
+ "frame": 51,
+ "day": 11.602500000000001,
+ "positionAu": [
+ -0.0022894905274745236,
+ 0.001166553690124694,
+ 0.0
+ ]
+ },
+ {
+ "frame": 52,
+ "day": 11.83,
+ "positionAu": [
+ -0.002347405563158298,
+ 0.0010451322927709137,
+ 0.0
+ ]
+ },
+ {
+ "frame": 53,
+ "day": 12.057500000000001,
+ "positionAu": [
+ -0.002398886523359616,
+ 0.0009208462604489035,
+ 0.0
+ ]
+ },
+ {
+ "frame": 54,
+ "day": 12.285,
+ "positionAu": [
+ -0.0024437923023449626,
+ 0.0007940362525342403,
+ 0.0
+ ]
+ },
+ {
+ "frame": 55,
+ "day": 12.512500000000001,
+ "positionAu": [
+ -0.002481999816495502,
+ 0.0006650498464441652,
+ 0.0
+ ]
+ },
+ {
+ "frame": 56,
+ "day": 12.74,
+ "positionAu": [
+ -0.0025134043416707194,
+ 0.0005342405849520347,
+ 0.0
+ ]
+ },
+ {
+ "frame": 57,
+ "day": 12.967500000000001,
+ "positionAu": [
+ -0.002537919800250011,
+ 0.00040196700715115725,
+ 0.0
+ ]
+ },
+ {
+ "frame": 58,
+ "day": 13.195,
+ "positionAu": [
+ -0.0025554789970654596,
+ 0.0002685916657240637,
+ 0.0
+ ]
+ },
+ {
+ "frame": 59,
+ "day": 13.422500000000001,
+ "positionAu": [
+ -0.0025660338035791187,
+ 0.00013448013321079824,
+ 0.0
+ ]
+ },
+ {
+ "frame": 60,
+ "day": 13.65,
+ "positionAu": [
+ -0.0025695552897999907,
+ 3.1467976608857084e-19,
+ 0.0
+ ]
+ },
+ {
+ "frame": 61,
+ "day": 13.877500000000001,
+ "positionAu": [
+ -0.0025660338035791187,
+ -0.00013448013321079648,
+ -0.0
+ ]
+ },
+ {
+ "frame": 62,
+ "day": 14.105,
+ "positionAu": [
+ -0.0025554789970654596,
+ -0.0002685916657240619,
+ -0.0
+ ]
+ },
+ {
+ "frame": 63,
+ "day": 14.332500000000001,
+ "positionAu": [
+ -0.0025379198002500112,
+ -0.0004019670071511566,
+ -0.0
+ ]
+ },
+ {
+ "frame": 64,
+ "day": 14.56,
+ "positionAu": [
+ -0.00251340434167072,
+ -0.0005342405849520329,
+ -0.0
+ ]
+ },
+ {
+ "frame": 65,
+ "day": 14.7875,
+ "positionAu": [
+ -0.0024819998164955026,
+ -0.0006650498464441636,
+ -0.0
+ ]
+ },
+ {
+ "frame": 66,
+ "day": 15.015,
+ "positionAu": [
+ -0.002443792302344963,
+ -0.0007940362525342397,
+ -0.0
+ ]
+ },
+ {
+ "frame": 67,
+ "day": 15.2425,
+ "positionAu": [
+ -0.0023988865233596163,
+ -0.0009208462604489017,
+ -0.0
+ ]
+ },
+ {
+ "frame": 68,
+ "day": 15.47,
+ "positionAu": [
+ -0.002347405563158299,
+ -0.0010451322927709122,
+ -0.0
+ ]
+ },
+ {
+ "frame": 69,
+ "day": 15.6975,
+ "positionAu": [
+ -0.002289490527474524,
+ -0.0011665536901246936,
+ -0.0
+ ]
+ },
+ {
+ "frame": 70,
+ "day": 15.925,
+ "positionAu": [
+ -0.0022253001573954777,
+ -0.0012847776448999947,
+ -0.0
+ ]
+ },
+ {
+ "frame": 71,
+ "day": 16.1525,
+ "positionAu": [
+ -0.0021550103942637273,
+ -0.0013994801134544243,
+ -0.0
+ ]
+ },
+ {
+ "frame": 72,
+ "day": 16.38,
+ "positionAu": [
+ -0.0020788138974342355,
+ -0.0015103467042945462,
+ -0.0
+ ]
+ },
+ {
+ "frame": 73,
+ "day": 16.6075,
+ "positionAu": [
+ -0.00199691951620846,
+ -0.001617073539801108,
+ -0.0
+ ]
+ },
+ {
+ "frame": 74,
+ "day": 16.835,
+ "positionAu": [
+ -0.0019095517173929302,
+ -0.0017193680891364786,
+ -0.0
+ ]
+ },
+ {
+ "frame": 75,
+ "day": 17.0625,
+ "positionAu": [
+ -0.001816949970051338,
+ -0.0018169499700513376,
+ -0.0
+ ]
+ },
+ {
+ "frame": 76,
+ "day": 17.29,
+ "positionAu": [
+ -0.00171936808913648,
+ -0.0019095517173929287,
+ -0.0
+ ]
+ },
+ {
+ "frame": 77,
+ "day": 17.517500000000002,
+ "positionAu": [
+ -0.0016170735398011095,
+ -0.0019969195162084586,
+ -0.0
+ ]
+ },
+ {
+ "frame": 78,
+ "day": 17.745,
+ "positionAu": [
+ -0.0015103467042945468,
+ -0.002078813897434235,
+ -0.0
+ ]
+ },
+ {
+ "frame": 79,
+ "day": 17.9725,
+ "positionAu": [
+ -0.0013994801134544269,
+ -0.0021550103942637256,
+ -0.0
+ ]
+ },
+ {
+ "frame": 80,
+ "day": 18.2,
+ "positionAu": [
+ -0.0012847776448999964,
+ -0.0022253001573954764,
+ -0.0
+ ]
+ },
+ {
+ "frame": 81,
+ "day": 18.427500000000002,
+ "positionAu": [
+ -0.0011665536901246942,
+ -0.0022894905274745236,
+ -0.0
+ ]
+ },
+ {
+ "frame": 82,
+ "day": 18.655,
+ "positionAu": [
+ -0.001045132292770915,
+ -0.0023474055631582977,
+ -0.0
+ ]
+ },
+ {
+ "frame": 83,
+ "day": 18.8825,
+ "positionAu": [
+ -0.0009208462604489037,
+ -0.0023988865233596154,
+ -0.0
+ ]
+ },
+ {
+ "frame": 84,
+ "day": 19.11,
+ "positionAu": [
+ -0.0007940362525342405,
+ -0.0024437923023449626,
+ -0.0
+ ]
+ },
+ {
+ "frame": 85,
+ "day": 19.337500000000002,
+ "positionAu": [
+ -0.0006650498464441665,
+ -0.0024819998164955018,
+ -0.0
+ ]
+ },
+ {
+ "frame": 86,
+ "day": 19.565,
+ "positionAu": [
+ -0.0005342405849520348,
+ -0.0025134043416707194,
+ -0.0
+ ]
+ },
+ {
+ "frame": 87,
+ "day": 19.7925,
+ "positionAu": [
+ -0.00040196700715115736,
+ -0.002537919800250011,
+ -0.0
+ ]
+ },
+ {
+ "frame": 88,
+ "day": 20.02,
+ "positionAu": [
+ -0.00026859166572406495,
+ -0.0025554789970654596,
+ -0.0
+ ]
+ },
+ {
+ "frame": 89,
+ "day": 20.247500000000002,
+ "positionAu": [
+ -0.0001344801332107984,
+ -0.0025660338035791187,
+ -0.0
+ ]
+ },
+ {
+ "frame": 90,
+ "day": 20.475,
+ "positionAu": [
+ -4.720196491328562e-19,
+ -0.0025695552897999907,
+ -0.0
+ ]
+ },
+ {
+ "frame": 91,
+ "day": 20.7025,
+ "positionAu": [
+ 0.00013448013321079518,
+ -0.002566033803579119,
+ -0.0
+ ]
+ },
+ {
+ "frame": 92,
+ "day": 20.93,
+ "positionAu": [
+ 0.00026859166572406175,
+ -0.0025554789970654596,
+ -0.0
+ ]
+ },
+ {
+ "frame": 93,
+ "day": 21.157500000000002,
+ "positionAu": [
+ 0.00040196700715115644,
+ -0.0025379198002500112,
+ -0.0
+ ]
+ },
+ {
+ "frame": 94,
+ "day": 21.385,
+ "positionAu": [
+ 0.0005342405849520316,
+ -0.00251340434167072,
+ -0.0
+ ]
+ },
+ {
+ "frame": 95,
+ "day": 21.6125,
+ "positionAu": [
+ 0.0006650498464441634,
+ -0.0024819998164955026,
+ -0.0
+ ]
+ },
+ {
+ "frame": 96,
+ "day": 21.84,
+ "positionAu": [
+ 0.0007940362525342396,
+ -0.002443792302344963,
+ -0.0
+ ]
+ },
+ {
+ "frame": 97,
+ "day": 22.0675,
+ "positionAu": [
+ 0.0009208462604489006,
+ -0.002398886523359617,
+ -0.0
+ ]
+ },
+ {
+ "frame": 98,
+ "day": 22.295,
+ "positionAu": [
+ 0.001045132292770912,
+ -0.002347405563158299,
+ -0.0
+ ]
+ },
+ {
+ "frame": 99,
+ "day": 22.5225,
+ "positionAu": [
+ 0.0011665536901246934,
+ -0.002289490527474524,
+ -0.0
+ ]
+ },
+ {
+ "frame": 100,
+ "day": 22.75,
+ "positionAu": [
+ 0.0012847776448999936,
+ -0.002225300157395478,
+ -0.0
+ ]
+ },
+ {
+ "frame": 101,
+ "day": 22.9775,
+ "positionAu": [
+ 0.001399480113454424,
+ -0.0021550103942637273,
+ -0.0
+ ]
+ },
+ {
+ "frame": 102,
+ "day": 23.205000000000002,
+ "positionAu": [
+ 0.001510346704294546,
+ -0.002078813897434236,
+ -0.0
+ ]
+ },
+ {
+ "frame": 103,
+ "day": 23.4325,
+ "positionAu": [
+ 0.001617073539801107,
+ -0.0019969195162084603,
+ -0.0
+ ]
+ },
+ {
+ "frame": 104,
+ "day": 23.66,
+ "positionAu": [
+ 0.0017193680891364783,
+ -0.0019095517173929302,
+ -0.0
+ ]
+ },
+ {
+ "frame": 105,
+ "day": 23.8875,
+ "positionAu": [
+ 0.0018169499700513372,
+ -0.001816949970051338,
+ -0.0
+ ]
+ },
+ {
+ "frame": 106,
+ "day": 24.115000000000002,
+ "positionAu": [
+ 0.0019095517173929278,
+ -0.001719368089136481,
+ -0.0
+ ]
+ },
+ {
+ "frame": 107,
+ "day": 24.3425,
+ "positionAu": [
+ 0.0019969195162084586,
+ -0.0016170735398011095,
+ -0.0
+ ]
+ },
+ {
+ "frame": 108,
+ "day": 24.57,
+ "positionAu": [
+ 0.002078813897434235,
+ -0.001510346704294547,
+ -0.0
+ ]
+ },
+ {
+ "frame": 109,
+ "day": 24.7975,
+ "positionAu": [
+ 0.0021550103942637256,
+ -0.0013994801134544269,
+ -0.0
+ ]
+ },
+ {
+ "frame": 110,
+ "day": 25.025000000000002,
+ "positionAu": [
+ 0.0022253001573954764,
+ -0.0012847776448999964,
+ -0.0
+ ]
+ },
+ {
+ "frame": 111,
+ "day": 25.2525,
+ "positionAu": [
+ 0.0022894905274745236,
+ -0.0011665536901246942,
+ -0.0
+ ]
+ },
+ {
+ "frame": 112,
+ "day": 25.48,
+ "positionAu": [
+ 0.0023474055631582977,
+ -0.0010451322927709152,
+ -0.0
+ ]
+ },
+ {
+ "frame": 113,
+ "day": 25.7075,
+ "positionAu": [
+ 0.0023988865233596154,
+ -0.0009208462604489038,
+ -0.0
+ ]
+ },
+ {
+ "frame": 114,
+ "day": 25.935000000000002,
+ "positionAu": [
+ 0.0024437923023449626,
+ -0.0007940362525342407,
+ -0.0
+ ]
+ },
+ {
+ "frame": 115,
+ "day": 26.1625,
+ "positionAu": [
+ 0.0024819998164955018,
+ -0.0006650498464441667,
+ -0.0
+ ]
+ },
+ {
+ "frame": 116,
+ "day": 26.39,
+ "positionAu": [
+ 0.0025134043416707194,
+ -0.000534240584952035,
+ -0.0
+ ]
+ },
+ {
+ "frame": 117,
+ "day": 26.6175,
+ "positionAu": [
+ 0.002537919800250011,
+ -0.0004019670071511576,
+ -0.0
+ ]
+ },
+ {
+ "frame": 118,
+ "day": 26.845000000000002,
+ "positionAu": [
+ 0.0025554789970654596,
+ -0.0002685916657240651,
+ -0.0
+ ]
+ },
+ {
+ "frame": 119,
+ "day": 27.0725,
+ "positionAu": [
+ 0.0025660338035791187,
+ -0.00013448013321079854,
+ -0.0
+ ]
+ }
+ ],
+ "cameraHintDistanceAu": 0.07431121812083386
+}
diff --git a/output/animate/blender/moons/moon/moon_flyby_dark.gif b/output/animate/blender/moons/moon/moon_flyby_dark.gif
new file mode 100644
index 0000000..3abb7e6
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_flyby_dark.gif differ
diff --git a/output/animate/blender/moons/moon/moon_flyby_dark_job.json b/output/animate/blender/moons/moon/moon_flyby_dark_job.json
new file mode 100644
index 0000000..7e39dfc
--- /dev/null
+++ b/output/animate/blender/moons/moon/moon_flyby_dark_job.json
@@ -0,0 +1,680 @@
+{
+ "schema": "solsys.blender_flyby_job/v1",
+ "theme": "dark",
+ "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.032182755537859827,
+ 0.02253460802796973,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 0.0
+ },
+ {
+ "frame": 1,
+ "cameraAu": [
+ 0.03091761261437237,
+ 0.024241277672834977,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 1.971830985915493
+ },
+ {
+ "frame": 2,
+ "cameraAu": [
+ 0.029562066554216046,
+ 0.025877065792473192,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 3.943661971830986
+ },
+ {
+ "frame": 3,
+ "cameraAu": [
+ 0.028120080975709828,
+ 0.02743718934044461,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 5.915492957746479
+ },
+ {
+ "frame": 4,
+ "cameraAu": [
+ 0.026595872246428863,
+ 0.028917086513608522,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 7.887323943661972
+ },
+ {
+ "frame": 5,
+ "cameraAu": [
+ 0.024993897154540586,
+ 0.03031243009084117,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 9.859154929577466
+ },
+ {
+ "frame": 6,
+ "cameraAu": [
+ 0.02331883987715072,
+ 0.031619140085835516,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 11.830985915492958
+ },
+ {
+ "frame": 7,
+ "cameraAu": [
+ 0.02157559828376379,
+ 0.03283339567698616,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 13.80281690140845
+ },
+ {
+ "frame": 8,
+ "cameraAu": [
+ 0.01976926961490671,
+ 0.033951646379476376,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 15.774647887323944
+ },
+ {
+ "frame": 9,
+ "cameraAu": [
+ 0.017905135577791362,
+ 0.03497062242689991,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 17.746478873239436
+ },
+ {
+ "frame": 10,
+ "cameraAu": [
+ 0.015988646902596323,
+ 0.03588734433206199,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 19.71830985915493
+ },
+ {
+ "frame": 11,
+ "cameraAu": [
+ 0.014025407404525202,
+ 0.03669913159900347,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 21.69014084507042
+ },
+ {
+ "frame": 12,
+ "cameraAu": [
+ 0.012021157598244235,
+ 0.03740361056077421,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 23.661971830985916
+ },
+ {
+ "frame": 13,
+ "cameraAu": [
+ 0.009981757912610464,
+ 0.03799872132003817,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 25.633802816901408
+ },
+ {
+ "frame": 14,
+ "cameraAu": [
+ 0.007913171554770653,
+ 0.03848272377221568,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 27.6056338028169
+ },
+ {
+ "frame": 15,
+ "cameraAu": [
+ 0.00582144707373637,
+ 0.038854202693551325,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 29.577464788732392
+ },
+ {
+ "frame": 16,
+ "cameraAu": [
+ 0.0037127006744193994,
+ 0.03911207187922996,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 31.549295774647888
+ },
+ {
+ "frame": 17,
+ "cameraAu": [
+ 0.0015930983338411646,
+ 0.03925557731944083,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 33.52112676056338
+ },
+ {
+ "frame": 18,
+ "cameraAu": [
+ -0.0005311622281915372,
+ 0.03928429940410326,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 35.49295774647887
+ },
+ {
+ "frame": 19,
+ "cameraAu": [
+ -0.0026538696712267453,
+ 0.03919815414980695,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 37.46478873239436
+ },
+ {
+ "frame": 20,
+ "cameraAu": [
+ -0.004768817196134325,
+ 0.038997393445379634,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 39.43661971830986
+ },
+ {
+ "frame": 21,
+ "cameraAu": [
+ -0.006869820693794128,
+ 0.038682604315363814,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 41.40845070422535
+ },
+ {
+ "frame": 22,
+ "cameraAu": [
+ -0.008950736827438549,
+ 0.038254707203556275,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 43.38028169014084
+ },
+ {
+ "frame": 23,
+ "cameraAu": [
+ -0.01100548099577663,
+ 0.03771495328162935,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 45.352112676056336
+ },
+ {
+ "frame": 24,
+ "cameraAu": [
+ -0.013028045124375635,
+ 0.0370649207907034,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 47.32394366197183
+ },
+ {
+ "frame": 25,
+ "cameraAu": [
+ -0.015012515233277932,
+ 0.03630651042656789,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 49.29577464788732
+ },
+ {
+ "frame": 26,
+ "cameraAu": [
+ -0.01695308872948554,
+ 0.035441939782044594,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 51.267605633802816
+ },
+ {
+ "frame": 27,
+ "cameraAu": [
+ -0.01884409137374908,
+ 0.034473736862743506,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 53.23943661971831
+ },
+ {
+ "frame": 28,
+ "cameraAu": [
+ -0.02067999387205019,
+ 0.03340473269517138,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 55.2112676056338
+ },
+ {
+ "frame": 29,
+ "cameraAu": [
+ -0.022455428043263937,
+ 0.03223805304880679,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 57.183098591549296
+ },
+ {
+ "frame": 30,
+ "cameraAu": [
+ -0.0241652025157269,
+ 0.030977109296346415,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 59.154929577464785
+ },
+ {
+ "frame": 31,
+ "cameraAu": [
+ -0.025804317906814197,
+ 0.029625588438847137,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 61.12676056338028
+ },
+ {
+ "frame": 32,
+ "cameraAu": [
+ -0.027367981441140322,
+ 0.02818744232493051,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 63.098591549295776
+ },
+ {
+ "frame": 33,
+ "cameraAu": [
+ -0.028851620964640216,
+ 0.026666876095572535,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 65.07042253521126
+ },
+ {
+ "frame": 34,
+ "cameraAu": [
+ -0.030250898313553157,
+ 0.025068335888266527,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 67.04225352112675
+ },
+ {
+ "frame": 35,
+ "cameraAu": [
+ -0.03156172199921903,
+ 0.02339649583651147,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 69.01408450704226
+ },
+ {
+ "frame": 36,
+ "cameraAu": [
+ -0.03278025917159587,
+ 0.021656244402640315,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 70.98591549295774
+ },
+ {
+ "frame": 37,
+ "cameraAu": [
+ -0.03390294682651829,
+ 0.019852670083949885,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 72.95774647887323
+ },
+ {
+ "frame": 38,
+ "cameraAu": [
+ -0.034926502223925765,
+ 0.017991046533928863,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 74.92957746478872
+ },
+ {
+ "frame": 39,
+ "cameraAu": [
+ -0.03584793248659864,
+ 0.016076817142088504,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 76.90140845070424
+ },
+ {
+ "frame": 40,
+ "cameraAu": [
+ -0.036664543351334684,
+ 0.014115579117485212,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 78.87323943661973
+ },
+ {
+ "frame": 41,
+ "cameraAu": [
+ -0.037373947046978016,
+ 0.012113067122474319,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 80.84507042253522
+ },
+ {
+ "frame": 42,
+ "cameraAu": [
+ -0.03797406927626477,
+ 0.010075136504550667,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 82.8169014084507
+ },
+ {
+ "frame": 43,
+ "cameraAu": [
+ -0.03846315528107087,
+ 0.008007746175305207,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 84.78873239436619
+ },
+ {
+ "frame": 44,
+ "cameraAu": [
+ -0.038839774973326885,
+ 0.005916941186560482,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 86.76056338028168
+ },
+ {
+ "frame": 45,
+ "cameraAu": [
+ -0.03910282711659726,
+ 0.003808835054631705,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 88.73239436619718
+ },
+ {
+ "frame": 46,
+ "cameraAu": [
+ -0.039251542546096994,
+ 0.0016895918843976814,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 90.70422535211267
+ },
+ {
+ "frame": 47,
+ "cameraAu": [
+ -0.03928548641773042,
+ -0.0004345916545493137,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 92.67605633802818
+ },
+ {
+ "frame": 48,
+ "cameraAu": [
+ -0.03920455947957582,
+ -0.002557504446972914,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 94.64788732394366
+ },
+ {
+ "frame": 49,
+ "cameraAu": [
+ -0.039008998362098236,
+ -0.004672939093301126,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 96.61971830985915
+ },
+ {
+ "frame": 50,
+ "cameraAu": [
+ -0.03869937488624162,
+ -0.006774710060070178,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 98.59154929577464
+ },
+ {
+ "frame": 51,
+ "cameraAu": [
+ -0.03827659439142377,
+ -0.008856671766431852,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 100.56338028169014
+ },
+ {
+ "frame": 52,
+ "cameraAu": [
+ -0.03774189308832274,
+ -0.010912736553839337,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 102.53521126760563
+ },
+ {
+ "frame": 53,
+ "cameraAu": [
+ -0.037096834444195335,
+ -0.012936892486368094,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 104.50704225352112
+ },
+ {
+ "frame": 54,
+ "cameraAu": [
+ -0.036343304611296985,
+ -0.014923220929623896,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 106.47887323943662
+ },
+ {
+ "frame": 55,
+ "cameraAu": [
+ -0.0354835069117702,
+ -0.01686591385683685,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 108.45070422535211
+ },
+ {
+ "frame": 56,
+ "cameraAu": [
+ -0.034519955395128046,
+ -0.01875929083153878,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 110.4225352112676
+ },
+ {
+ "frame": 57,
+ "cameraAu": [
+ -0.033455467487170265,
+ -0.020597815617166264,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 112.3943661971831
+ },
+ {
+ "frame": 58,
+ "cameraAu": [
+ -0.032293155751826876,
+ -0.022376112365023015,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 114.36619718309859
+ },
+ {
+ "frame": 59,
+ "cameraAu": [
+ -0.031036418790017447,
+ -0.02408898133326806,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 116.33802816901408
+ },
+ {
+ "frame": 60,
+ "cameraAu": [
+ -0.029688931302137914,
+ -0.025731414090967106,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 118.30985915492957
+ },
+ {
+ "frame": 61,
+ "cameraAu": [
+ -0.028254633343232156,
+ -0.027298608162750693,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 120.28169014084507
+ },
+ {
+ "frame": 62,
+ "cameraAu": [
+ -0.02673771880226632,
+ -0.028785981071257922,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 122.25352112676056
+ },
+ {
+ "frame": 63,
+ "cameraAu": [
+ -0.025142623139192372,
+ -0.030189183736305844,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 124.22535211267606
+ },
+ {
+ "frame": 64,
+ "cameraAu": [
+ -0.02347401041565791,
+ -0.031504113191605135,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 126.19718309859155
+ },
+ {
+ "frame": 65,
+ "cameraAu": [
+ -0.021736759657284128,
+ -0.032726924581838575,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 128.16901408450704
+ },
+ {
+ "frame": 66,
+ "cameraAu": [
+ -0.019935950587389183,
+ -0.0338540424050225,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 130.14084507042253
+ },
+ {
+ "frame": 67,
+ "cameraAu": [
+ -0.018076848773870863,
+ -0.034882170967279094,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 132.11267605633802
+ },
+ {
+ "frame": 68,
+ "cameraAu": [
+ -0.01616489023267991,
+ -0.0358083040194491,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 134.0845070422535
+ },
+ {
+ "frame": 69,
+ "cameraAu": [
+ -0.014205665532902577,
+ -0.03662973354736817,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 136.05633802816902
+ },
+ {
+ "frame": 70,
+ "cameraAu": [
+ -0.012204903449930096,
+ -0.03734405769010338,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 138.0281690140845
+ },
+ {
+ "frame": 71,
+ "cameraAu": [
+ -0.010168454214512266,
+ -0.03794918776299768,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 140.0
+ }
+ ],
+ "outputDirectory": "/var/folders/nx/wwmkd72x7jn3p284yz62yz980000gn/T/solsys_flyby_moon_dark_7xpu_sma/frames",
+ "resolution": 640,
+ "fps": 18,
+ "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_flyby_light.gif b/output/animate/blender/moons/moon/moon_flyby_light.gif
new file mode 100644
index 0000000..726b34d
Binary files /dev/null and b/output/animate/blender/moons/moon/moon_flyby_light.gif differ
diff --git a/output/animate/blender/moons/moon/moon_flyby_light_job.json b/output/animate/blender/moons/moon/moon_flyby_light_job.json
new file mode 100644
index 0000000..b6926e8
--- /dev/null
+++ b/output/animate/blender/moons/moon/moon_flyby_light_job.json
@@ -0,0 +1,680 @@
+{
+ "schema": "solsys.blender_flyby_job/v1",
+ "theme": "light",
+ "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.032182755537859827,
+ 0.02253460802796973,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 0.0
+ },
+ {
+ "frame": 1,
+ "cameraAu": [
+ 0.03091761261437237,
+ 0.024241277672834977,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 1.971830985915493
+ },
+ {
+ "frame": 2,
+ "cameraAu": [
+ 0.029562066554216046,
+ 0.025877065792473192,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 3.943661971830986
+ },
+ {
+ "frame": 3,
+ "cameraAu": [
+ 0.028120080975709828,
+ 0.02743718934044461,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 5.915492957746479
+ },
+ {
+ "frame": 4,
+ "cameraAu": [
+ 0.026595872246428863,
+ 0.028917086513608522,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 7.887323943661972
+ },
+ {
+ "frame": 5,
+ "cameraAu": [
+ 0.024993897154540586,
+ 0.03031243009084117,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 9.859154929577466
+ },
+ {
+ "frame": 6,
+ "cameraAu": [
+ 0.02331883987715072,
+ 0.031619140085835516,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 11.830985915492958
+ },
+ {
+ "frame": 7,
+ "cameraAu": [
+ 0.02157559828376379,
+ 0.03283339567698616,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 13.80281690140845
+ },
+ {
+ "frame": 8,
+ "cameraAu": [
+ 0.01976926961490671,
+ 0.033951646379476376,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 15.774647887323944
+ },
+ {
+ "frame": 9,
+ "cameraAu": [
+ 0.017905135577791362,
+ 0.03497062242689991,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 17.746478873239436
+ },
+ {
+ "frame": 10,
+ "cameraAu": [
+ 0.015988646902596323,
+ 0.03588734433206199,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 19.71830985915493
+ },
+ {
+ "frame": 11,
+ "cameraAu": [
+ 0.014025407404525202,
+ 0.03669913159900347,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 21.69014084507042
+ },
+ {
+ "frame": 12,
+ "cameraAu": [
+ 0.012021157598244235,
+ 0.03740361056077421,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 23.661971830985916
+ },
+ {
+ "frame": 13,
+ "cameraAu": [
+ 0.009981757912610464,
+ 0.03799872132003817,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 25.633802816901408
+ },
+ {
+ "frame": 14,
+ "cameraAu": [
+ 0.007913171554770653,
+ 0.03848272377221568,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 27.6056338028169
+ },
+ {
+ "frame": 15,
+ "cameraAu": [
+ 0.00582144707373637,
+ 0.038854202693551325,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 29.577464788732392
+ },
+ {
+ "frame": 16,
+ "cameraAu": [
+ 0.0037127006744193994,
+ 0.03911207187922996,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 31.549295774647888
+ },
+ {
+ "frame": 17,
+ "cameraAu": [
+ 0.0015930983338411646,
+ 0.03925557731944083,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 33.52112676056338
+ },
+ {
+ "frame": 18,
+ "cameraAu": [
+ -0.0005311622281915372,
+ 0.03928429940410326,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 35.49295774647887
+ },
+ {
+ "frame": 19,
+ "cameraAu": [
+ -0.0026538696712267453,
+ 0.03919815414980695,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 37.46478873239436
+ },
+ {
+ "frame": 20,
+ "cameraAu": [
+ -0.004768817196134325,
+ 0.038997393445379634,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 39.43661971830986
+ },
+ {
+ "frame": 21,
+ "cameraAu": [
+ -0.006869820693794128,
+ 0.038682604315363814,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 41.40845070422535
+ },
+ {
+ "frame": 22,
+ "cameraAu": [
+ -0.008950736827438549,
+ 0.038254707203556275,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 43.38028169014084
+ },
+ {
+ "frame": 23,
+ "cameraAu": [
+ -0.01100548099577663,
+ 0.03771495328162935,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 45.352112676056336
+ },
+ {
+ "frame": 24,
+ "cameraAu": [
+ -0.013028045124375635,
+ 0.0370649207907034,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 47.32394366197183
+ },
+ {
+ "frame": 25,
+ "cameraAu": [
+ -0.015012515233277932,
+ 0.03630651042656789,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 49.29577464788732
+ },
+ {
+ "frame": 26,
+ "cameraAu": [
+ -0.01695308872948554,
+ 0.035441939782044594,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 51.267605633802816
+ },
+ {
+ "frame": 27,
+ "cameraAu": [
+ -0.01884409137374908,
+ 0.034473736862743506,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 53.23943661971831
+ },
+ {
+ "frame": 28,
+ "cameraAu": [
+ -0.02067999387205019,
+ 0.03340473269517138,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 55.2112676056338
+ },
+ {
+ "frame": 29,
+ "cameraAu": [
+ -0.022455428043263937,
+ 0.03223805304880679,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 57.183098591549296
+ },
+ {
+ "frame": 30,
+ "cameraAu": [
+ -0.0241652025157269,
+ 0.030977109296346415,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 59.154929577464785
+ },
+ {
+ "frame": 31,
+ "cameraAu": [
+ -0.025804317906814197,
+ 0.029625588438847137,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 61.12676056338028
+ },
+ {
+ "frame": 32,
+ "cameraAu": [
+ -0.027367981441140322,
+ 0.02818744232493051,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 63.098591549295776
+ },
+ {
+ "frame": 33,
+ "cameraAu": [
+ -0.028851620964640216,
+ 0.026666876095572535,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 65.07042253521126
+ },
+ {
+ "frame": 34,
+ "cameraAu": [
+ -0.030250898313553157,
+ 0.025068335888266527,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 67.04225352112675
+ },
+ {
+ "frame": 35,
+ "cameraAu": [
+ -0.03156172199921903,
+ 0.02339649583651147,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 69.01408450704226
+ },
+ {
+ "frame": 36,
+ "cameraAu": [
+ -0.03278025917159587,
+ 0.021656244402640315,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 70.98591549295774
+ },
+ {
+ "frame": 37,
+ "cameraAu": [
+ -0.03390294682651829,
+ 0.019852670083949885,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 72.95774647887323
+ },
+ {
+ "frame": 38,
+ "cameraAu": [
+ -0.034926502223925765,
+ 0.017991046533928863,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 74.92957746478872
+ },
+ {
+ "frame": 39,
+ "cameraAu": [
+ -0.03584793248659864,
+ 0.016076817142088504,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 76.90140845070424
+ },
+ {
+ "frame": 40,
+ "cameraAu": [
+ -0.036664543351334684,
+ 0.014115579117485212,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 78.87323943661973
+ },
+ {
+ "frame": 41,
+ "cameraAu": [
+ -0.037373947046978016,
+ 0.012113067122474319,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 80.84507042253522
+ },
+ {
+ "frame": 42,
+ "cameraAu": [
+ -0.03797406927626477,
+ 0.010075136504550667,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 82.8169014084507
+ },
+ {
+ "frame": 43,
+ "cameraAu": [
+ -0.03846315528107087,
+ 0.008007746175305207,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 84.78873239436619
+ },
+ {
+ "frame": 44,
+ "cameraAu": [
+ -0.038839774973326885,
+ 0.005916941186560482,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 86.76056338028168
+ },
+ {
+ "frame": 45,
+ "cameraAu": [
+ -0.03910282711659726,
+ 0.003808835054631705,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 88.73239436619718
+ },
+ {
+ "frame": 46,
+ "cameraAu": [
+ -0.039251542546096994,
+ 0.0016895918843976814,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 90.70422535211267
+ },
+ {
+ "frame": 47,
+ "cameraAu": [
+ -0.03928548641773042,
+ -0.0004345916545493137,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 92.67605633802818
+ },
+ {
+ "frame": 48,
+ "cameraAu": [
+ -0.03920455947957582,
+ -0.002557504446972914,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 94.64788732394366
+ },
+ {
+ "frame": 49,
+ "cameraAu": [
+ -0.039008998362098236,
+ -0.004672939093301126,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 96.61971830985915
+ },
+ {
+ "frame": 50,
+ "cameraAu": [
+ -0.03869937488624162,
+ -0.006774710060070178,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 98.59154929577464
+ },
+ {
+ "frame": 51,
+ "cameraAu": [
+ -0.03827659439142377,
+ -0.008856671766431852,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 100.56338028169014
+ },
+ {
+ "frame": 52,
+ "cameraAu": [
+ -0.03774189308832274,
+ -0.010912736553839337,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 102.53521126760563
+ },
+ {
+ "frame": 53,
+ "cameraAu": [
+ -0.037096834444195335,
+ -0.012936892486368094,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 104.50704225352112
+ },
+ {
+ "frame": 54,
+ "cameraAu": [
+ -0.036343304611296985,
+ -0.014923220929623896,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 106.47887323943662
+ },
+ {
+ "frame": 55,
+ "cameraAu": [
+ -0.0354835069117702,
+ -0.01686591385683685,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 108.45070422535211
+ },
+ {
+ "frame": 56,
+ "cameraAu": [
+ -0.034519955395128046,
+ -0.01875929083153878,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 110.4225352112676
+ },
+ {
+ "frame": 57,
+ "cameraAu": [
+ -0.033455467487170265,
+ -0.020597815617166264,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 112.3943661971831
+ },
+ {
+ "frame": 58,
+ "cameraAu": [
+ -0.032293155751826876,
+ -0.022376112365023015,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 114.36619718309859
+ },
+ {
+ "frame": 59,
+ "cameraAu": [
+ -0.031036418790017447,
+ -0.02408898133326806,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 116.33802816901408
+ },
+ {
+ "frame": 60,
+ "cameraAu": [
+ -0.029688931302137914,
+ -0.025731414090967106,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 118.30985915492957
+ },
+ {
+ "frame": 61,
+ "cameraAu": [
+ -0.028254633343232156,
+ -0.027298608162750693,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 120.28169014084507
+ },
+ {
+ "frame": 62,
+ "cameraAu": [
+ -0.02673771880226632,
+ -0.028785981071257922,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 122.25352112676056
+ },
+ {
+ "frame": 63,
+ "cameraAu": [
+ -0.025142623139192372,
+ -0.030189183736305844,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 124.22535211267606
+ },
+ {
+ "frame": 64,
+ "cameraAu": [
+ -0.02347401041565791,
+ -0.031504113191605135,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 126.19718309859155
+ },
+ {
+ "frame": 65,
+ "cameraAu": [
+ -0.021736759657284128,
+ -0.032726924581838575,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 128.16901408450704
+ },
+ {
+ "frame": 66,
+ "cameraAu": [
+ -0.019935950587389183,
+ -0.0338540424050225,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 130.14084507042253
+ },
+ {
+ "frame": 67,
+ "cameraAu": [
+ -0.018076848773870863,
+ -0.034882170967279094,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 132.11267605633802
+ },
+ {
+ "frame": 68,
+ "cameraAu": [
+ -0.01616489023267991,
+ -0.0358083040194491,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 134.0845070422535
+ },
+ {
+ "frame": 69,
+ "cameraAu": [
+ -0.014205665532902577,
+ -0.03662973354736817,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 136.05633802816902
+ },
+ {
+ "frame": 70,
+ "cameraAu": [
+ -0.012204903449930096,
+ -0.03734405769010338,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 138.0281690140845
+ },
+ {
+ "frame": 71,
+ "cameraAu": [
+ -0.010168454214512266,
+ -0.03794918776299768,
+ 0.011265621218701805
+ ],
+ "bodyRotationDeg": 140.0
+ }
+ ],
+ "outputDirectory": "/var/folders/nx/wwmkd72x7jn3p284yz62yz980000gn/T/solsys_flyby_moon_light_7ium9erm/frames",
+ "resolution": 640,
+ "fps": 18,
+ "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/earth_body_scene.json b/output/animate/blender/planets/earth/earth_body_scene.json
similarity index 100%
rename from output/animate/blender/earth_body_scene.json
rename to output/animate/blender/planets/earth/earth_body_scene.json
diff --git a/output/animate/blender/earth_flyby_dark.gif b/output/animate/blender/planets/earth/earth_flyby_dark.gif
similarity index 100%
rename from output/animate/blender/earth_flyby_dark.gif
rename to output/animate/blender/planets/earth/earth_flyby_dark.gif
diff --git a/output/animate/blender/earth_flyby_dark_job.json b/output/animate/blender/planets/earth/earth_flyby_dark_job.json
similarity index 100%
rename from output/animate/blender/earth_flyby_dark_job.json
rename to output/animate/blender/planets/earth/earth_flyby_dark_job.json
diff --git a/output/animate/blender/earth_flyby_light.gif b/output/animate/blender/planets/earth/earth_flyby_light.gif
similarity index 100%
rename from output/animate/blender/earth_flyby_light.gif
rename to output/animate/blender/planets/earth/earth_flyby_light.gif
diff --git a/output/animate/blender/earth_flyby_light_job.json b/output/animate/blender/planets/earth/earth_flyby_light_job.json
similarity index 100%
rename from output/animate/blender/earth_flyby_light_job.json
rename to output/animate/blender/planets/earth/earth_flyby_light_job.json
diff --git a/render.py b/render.py
index 4f0a2bb..66eec02 100644
--- a/render.py
+++ b/render.py
@@ -12,7 +12,7 @@
from animate import renderAllAnimations
from animate.scenes.alpha_centauri import renderAlphaCentauriAnimations
from animate.scenes.barnards_star import renderBarnardsStarAnimations
-from animate.scenes.blender.export_body import exportPlanetBodyScene
+from animate.scenes.blender.export_body import exportBodyScene
from animate.scenes.blender.flyby_scene import renderPlanetFlyby
from animate.scenes.interstellar_objects import renderInterstellarObjectAnimations
from animate.scenes.sol_centauri_cinematic import renderSolCentauriCinematicAnimations
@@ -178,12 +178,12 @@ def buildParser() -> argparse.ArgumentParser:
blenderParser = subparsers.add_parser(
'blender',
- help='Export Sol planet catalog state or render Blender planet flybys',
+ help='Export Sol planet/moon catalog state or render Blender close-up flybys',
)
blenderParser.add_argument(
'--body',
default='Earth',
- help='PlanetCatalog name to export / flyby (default: Earth)',
+ help='PlanetCatalog or MoonCatalog name to export / flyby (default: Earth)',
)
blenderParser.add_argument(
'--frames',
@@ -325,7 +325,7 @@ def main(argv: list[str] | None = None) -> None:
print(f'Flyby ready → {gifPath}')
return
- scenePath = exportPlanetBodyScene(
+ scenePath = exportBodyScene(
args.body,
frameCount=args.frames,
outputDirectory=args.output_dir,
diff --git a/tests/test_blender_pipeline.py b/tests/test_blender_pipeline.py
index d1ab729..b99ce9d 100644
--- a/tests/test_blender_pipeline.py
+++ b/tests/test_blender_pipeline.py
@@ -17,10 +17,12 @@
from animate.scenes.blender.body_scene import (
SCHEMA_ID,
BodyScene,
+ buildBodyScene,
+ buildMoonBodyScene,
buildPlanetBodyScene,
loadBodyScene,
)
-from animate.scenes.blender.export_body import exportPlanetBodyScene
+from animate.scenes.blender.export_body import exportBodyScene, exportPlanetBodyScene
from animate.scenes.blender.flyby_camera import buildFlybyCameraPath, flybyCameraLocation
from animate.scenes.blender.flyby_scene import (
assembleGifFromPngs,
@@ -53,6 +55,21 @@ def test_unknown_planet_raises(self) -> None:
with self.assertRaises(ValueError):
buildPlanetBodyScene('Nibiru')
+ def test_build_moon_scene(self) -> None:
+ scene = buildMoonBodyScene('Moon', frameCount=16)
+ self.assertEqual(scene.body.name, 'Moon')
+ self.assertEqual(scene.body.kind, 'moon')
+ self.assertEqual(scene.body.systemId, 'sol')
+ self.assertEqual(len(scene.keyframes), 16)
+ self.assertGreater(scene.body.displayRadiusAu, 0.0)
+ viaDispatch = buildBodyScene('Moon', frameCount=8)
+ self.assertEqual(viaDispatch.body.kind, 'moon')
+ self.assertEqual(len(viaDispatch.keyframes), 8)
+
+ def test_unknown_body_raises(self) -> None:
+ with self.assertRaises(ValueError):
+ buildBodyScene('Nibiru')
+
def test_round_trip_json(self) -> None:
scene = buildPlanetBodyScene('Jupiter', frameCount=8)
restored = BodyScene.fromDict(json.loads(scene.toJson()))
@@ -68,6 +85,7 @@ def test_export_and_host_load(self) -> None:
path = exportPlanetBodyScene('Earth', frameCount=12, outputDirectory=outputDirectory)
self.assertTrue(path.is_file())
self.assertEqual(path.name, 'earth_body_scene.json')
+ self.assertEqual(path.parent.as_posix().endswith('planets/earth'), True)
scene = loadBodyScene(path)
self.assertEqual(scene.body.name, 'Earth')
@@ -112,6 +130,22 @@ def test_earth_pack_resolves_color_map(self) -> None:
self.assertTrue((TEXTURE_BODIES_ROOT / 'earth' / 'color.png').is_file())
self.assertTrue((TEXTURE_BODIES_ROOT / 'earth' / 'clouds.png').is_file())
+ def test_moon_pack_resolves_color_map_without_atmosphere(self) -> None:
+ appearance = appearanceForCatalogName('Moon')
+ self.assertIsNotNone(appearance)
+ assert appearance is not None
+ self.assertEqual(appearance.bodyId, 'moon')
+ self.assertEqual(appearance.kind, 'moon')
+ maps = appearance.textures.existingMaps()
+ self.assertIn('color', maps)
+ self.assertNotIn('clouds', maps)
+ self.assertFalse(appearance.atmosphere.enabled)
+ jobAppearance = appearance.toJobDict()
+ self.assertNotIn('atmosphere', jobAppearance)
+ self.assertIn('color', jobAppearance['textures'])
+ self.assertIn('Moon', registeredCatalogNames())
+ self.assertTrue((TEXTURE_BODIES_ROOT / 'moon' / 'color.png').is_file())
+
def test_unknown_catalog_name_has_no_pack(self) -> None:
self.assertIsNone(appearanceForCatalogName('Nibiru'))
@@ -124,6 +158,7 @@ def test_prepare_export(self) -> None:
)
self.assertTrue(path.is_file())
self.assertEqual(path.name, 'mars_body_scene.json')
+ self.assertTrue(path.parent.as_posix().endswith('planets/mars'))
def test_camera_path_moves_around_body(self) -> None:
path = buildFlybyCameraPath(0.03, frameCount=12)
@@ -165,6 +200,23 @@ def test_build_and_validate_flyby_job(self) -> None:
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn('dry-run', completed.stdout.lower())
+ def test_build_moon_flyby_job(self) -> None:
+ with tempfile.TemporaryDirectory() as temporaryDirectory:
+ path = exportBodyScene('Moon', frameCount=6, outputDirectory=temporaryDirectory)
+ self.assertEqual(path.name, 'moon_body_scene.json')
+ self.assertTrue(path.parent.as_posix().endswith('moons/moon'))
+ job = buildFlybyJob(
+ 'Moon',
+ theme='light',
+ frameCount=6,
+ framesDirectory=Path(temporaryDirectory) / 'frames',
+ )
+ self.assertEqual(job['body']['name'], 'Moon')
+ self.assertEqual(job['body']['kind'], 'moon')
+ self.assertEqual(job['appearance']['bodyId'], 'moon')
+ self.assertIn('color', job['appearance']['textures'])
+ self.assertNotIn('atmosphere', job['appearance'])
+
def test_assemble_gif_from_pngs(self) -> None:
with tempfile.TemporaryDirectory() as temporaryDirectory:
root = Path(temporaryDirectory)