Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}/<body>/*.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
Expand Down Expand Up @@ -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>/ # 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
Expand Down Expand Up @@ -163,7 +163,13 @@ solsys.motion → moving asteroid fields for animation frames

| Light | Dark |
|-------|------|
| ![Earth close-up light](https://github.com/ThomasAFink/SOLSYS/blob/main/output/animate/blender/earth_flyby_light.gif?raw=true) | ![Earth close-up dark](https://github.com/ThomasAFink/SOLSYS/blob/main/output/animate/blender/earth_flyby_dark.gif?raw=true) |
| ![Earth close-up light](https://github.com/ThomasAFink/SOLSYS/blob/main/output/animate/blender/planets/earth/earth_flyby_light.gif?raw=true) | ![Earth close-up dark](https://github.com/ThomasAFink/SOLSYS/blob/main/output/animate/blender/planets/earth/earth_flyby_dark.gif?raw=true) |

**Moon Blender close-up**

| Light | Dark |
|-------|------|
| ![Moon close-up light](https://github.com/ThomasAFink/SOLSYS/blob/main/output/animate/blender/moons/moon/moon_flyby_light.gif?raw=true) | ![Moon close-up dark](https://github.com/ThomasAFink/SOLSYS/blob/main/output/animate/blender/moons/moon/moon_flyby_dark.gif?raw=true) |

### Static zoom views (side)

Expand Down Expand Up @@ -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/<body>_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}/<body>/` (`*_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/`:

Expand All @@ -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
Expand Down Expand Up @@ -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)
23 changes: 14 additions & 9 deletions animate/scenes/blender/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>_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>/*_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/<id>/` (planets/moons/asteroids) |
| 5. Camera | `flyby_camera.py` | SOLSYS venv | Body-centered elevated turntable orbit + spin samples |
Expand All @@ -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).
15 changes: 11 additions & 4 deletions animate/scenes/blender/body_appearance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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),
),
)


Expand Down
140 changes: 120 additions & 20 deletions animate/scenes/blender/body_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,15 +18,22 @@
# 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),
'orange': (0.95, 0.55, 0.15, 1.0),
'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
Expand Down Expand Up @@ -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',
*,
Expand All @@ -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}'
)
42 changes: 38 additions & 4 deletions animate/scenes/blender/export_body.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -25,9 +58,10 @@ def writeBodyScene(
*,
outputDirectory: Path | str = DEFAULT_OUTPUT_DIRECTORY,
) -> Path:
directory = Path(outputDirectory)
"""Write ``<stem>_body_scene.json`` under ``{root}/{planets|moons}/<stem>/``."""
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
Loading
Loading