Added the gallery

This commit is contained in:
connor committed 2026-10-15 12:33:40 -04:00
1 parent d44f159c98
commit 8861858f0d
8 files changed
+482 -11

No files matched your search

+280 -1
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
"""Writes examples/scenes/<model>.toml for the NASA models fetch-models.sh downloads.
"""Writes examples/scenes/<model>.toml for the NASA models fetch-models.sh downloads,
and two scenes with Earth, the Moon and a spacecraft together (below):
examples/scenes/earthrise.toml from lunar orbit and examples/scenes/moonrise.toml
from geostationary height.
Every scene has the same composition: the spacecraft in an isometric view (the
camera looks along its body (+1, +1, +1) diagonal, body +Z toward image up) with
@@ -174,8 +177,284 @@ attitude = {fmt(q, 9)}
"""
# Earthrise: from 1000 km above the Moon, Earth just above the curved lunar horizon
# through the example camera's 50 mm lens, Landsat 8 standing in for a lunar orbiter
# in the foreground.
RM = 1737400.0
MOON_DISTANCE = 3.844e8
EARTHRISE_ALTITUDE = 1000e3
EARTHRISE_FOCAL = 0.05
EARTHRISE_GROUND = (15.0, 25.0) # Africa, geocentric degrees, on Earth's lit half
# (degrees above the lunar limb, degrees right of Earth) of the image center and of
# the satellite's center, Earth's center above the limb, and the satellite's distance.
EARTHRISE_CENTER = (2.0, -3.0)
EARTHRISE_EARTH = 2.6
EARTHRISE_SATELLITE = (2.4, -6.0)
EARTHRISE_RANGE = 500.0
EARTHRISE_EXPOSURE = 2e-4 # s
def earthrise():
file, scale, center = "landsat8.glb", 1.0, MODELS[0][3]
moon = [0.0, MOON_DISTANCE, 0.0]
to_earth = norm([-x for x in moon])
orbit = RM + EARTHRISE_ALTITUDE
dip = math.acos(RM / orbit)
# The observer over the Moon, tilted from the Earth-facing point along the lunar
# equator (toward +X; toward +Z it would look over the pole, where the map has no
# data) until Earth's center, as seen from there, stands EARTHRISE_EARTH above the
# limb.
want = math.radians(EARTHRISE_EARTH) - dip
theta = want
for _ in range(20):
u = norm([math.sin(theta) * to_earth[i] + math.cos(theta) * (i == 0) for i in range(3)])
camera = [moon[i] + orbit * u[i] for i in range(3)]
earth_dir = norm([-x for x in camera])
theta += want - math.asin(dot(earth_dir, u))
up = u
ahead = norm([earth_dir[i] - dot(earth_dir, up) * up[i] for i in range(3)])
right = cross(ahead, up)
def direction(above_limb, rightward):
el, az = math.radians(above_limb) - dip, math.radians(rightward)
return norm([math.cos(el) * math.cos(az) * ahead[i] + math.sin(el) * up[i]
+ math.cos(el) * math.sin(az) * right[i] for i in range(3)])
boresight = direction(*EARTHRISE_CENTER)
look = [camera[i] + MOON_DISTANCE * boresight[i] for i in range(3)]
to_satellite = direction(*EARTHRISE_SATELLITE)
attitude = triad([1, 1, 1], [0, 0, 1], [-x for x in to_satellite], up)
spot = [camera[i] + EARTHRISE_RANGE * to_satellite[i] for i in range(3)]
offset = mul(attitude, center)
position = [spot[i] - offset[i] for i in range(3)]
# The lunar ground at the limb toward Earth.
reach = math.sqrt(orbit ** 2 - RM ** 2)
limb = direction(0.0, 0.0)
ground = norm([camera[i] + reach * limb[i] - moon[i] for i in range(3)])
# The Sun: Earth and the Moon are the subject, so the direction that best lights
# both, the product of Earth's lit fraction as seen and the Sun's height over the
# lunar ground at the limb, with the satellite out of the Moon's shadow.
to_camera = norm(camera) # from Earth's center
best = None
for az in range(0, 360, 2):
for el in range(-88, 90, 2):
a, e = math.radians(az), math.radians(el)
sun = [math.cos(e) * math.cos(a), math.cos(e) * math.sin(a), math.sin(e)]
phase = math.degrees(math.acos(dot(sun, to_camera)))
if dot(sun, [-x for x in up]) > math.cos(math.asin(RM / orbit)):
continue
score = (1 + math.cos(math.radians(phase))) / 2 * max(dot(ground, sun), 0.0)
if best is None or score > best[0]:
best = (score, sun, phase)
_, sun, phase = best
# Earth turned with its axis along image up, and EARTHRISE_GROUND as near as that
# allows to midway across its lit half as seen.
across = norm([sun[i] - dot(sun, to_camera) * to_camera[i] for i in range(3)])
shown = norm([to_camera[i] + across[i] for i in range(3)])
north = norm([up[i] - dot(up, to_camera) * to_camera[i] for i in range(3)])
lat, lon = (math.radians(x) for x in EARTHRISE_GROUND)
fixed = [math.cos(lat) * math.cos(lon), math.cos(lat) * math.sin(lon), math.sin(lat)]
earth = triad([0, 0, 1], fixed, north, shown)
fmt = lambda v, d=2: "[" + ", ".join(f"{x:.{d}f}" for x in v) + "]"
return f"""# Earthrise from lunar orbit: 1000 km above the Moon, Earth partly lit (phase
# {phase:.0f} degrees) just above the curved lunar horizon, and Landsat 8 standing in
# for a lunar orbiter {EARTHRISE_RANGE:.0f} m off in the foreground. Generated by
# scripts/make-example-scenes.py; the model comes from scripts/fetch-models.sh.
#
# resolved render --scene examples/scenes/earthrise.toml --lighting L1 --preview earthrise.png
#
# At lighting L1 Earth shows Africa on its lit half, the Moon's ground its reflectance
# map, and the satellite is lit by the Sun and, faintly, by Earth and the Moon. The
# Moon has no atmosphere, so its horizon is a sharp edge, as in the Apollo photographs.
sun = {fmt([AU * x for x in sun], 0)}
moon = {fmt(moon)}
# Earth-fixed to inertial: north up, 15 N 25 E on the lit side.
earth_attitude = {fmt(quaternion(earth), 9)}
# Exposed so Earth, the Moon and the satellite stay short of saturation.
[camera]
focal_length = {EARTHRISE_FOCAL}
exposure = {EARTHRISE_EXPOSURE}
[observer]
position = {fmt(camera)}
look_at = {fmt(look)}
up = {fmt(up, 6)}
# At lighting L1, Earth's surface shows July (from scripts/fetch-earth.sh).
[appearance]
earth_month = 7
[[target]]
model = "../models/nasa/{file}"
scale = {scale}
position = {fmt(position, 4)}
attitude = {fmt(quaternion(attitude), 9)}
"""
# Moonrise from geostationary height: looking toward Earth's limb through a 100 mm lens,
# Australia filling the lower half of the frame, the Moon rising over the limb, and the
# SSL-1300 in the foreground.
MOONRISE_ORBIT = GEO
MOONRISE_FOCAL = 0.1
MOONRISE_GROUND = (-25.0, 134.0) # central Australia, geocentric degrees
# The image center's height above the limb (degrees). Then, as (degrees right of the
# image center, degrees above the limb at that column): the ground at the bottom of the
# frame, the Moon's center and the satellite's; and the satellite's distance (m).
MOONRISE_CENTER = 0.15
MOONRISE_SWATH = (0.0, -1.4)
MOONRISE_MOON = (1.7, 0.2)
MOONRISE_SATELLITE = (-1.6, 0.8)
MOONRISE_RANGE = 1100.0
MOONRISE_EXPOSURE = 2e-4 # s
def moonrise():
file, scale, center = "ssl1300.glb", 0.1, MODELS[3][3]
up, east = [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]
b = RE * (1 - 1 / 298.257223563)
lat = math.radians(MOONRISE_GROUND[0])
radius_here = RE * b / math.hypot(b * math.cos(lat), RE * math.sin(lat))
dip = math.acos(radius_here / MOONRISE_ORBIT)
camera = [MOONRISE_ORBIT, 0.0, 0.0]
def hits(d, earth=None):
"""How far along d the line of sight meets Earth, or None: the WGS84 ellipsoid
turned by `earth` (Earth-fixed to inertial), or without it a sphere of the
ellipsoid's radius at MOONRISE_GROUND."""
if earth is None:
o, v, k = camera, d, 1.0
else:
back = transpose(earth)
stretch = [1.0, 1.0, RE / b]
o = [x * f for x, f in zip(mul(back, camera), stretch)]
v = [x * f for x, f in zip(mul(back, d), stretch)]
k = 1.0
r = radius_here if earth is None else RE
aa, bb, cc = dot(v, v), dot(o, v), dot(o, o) - r * r
disc = bb * bb - aa * cc
return (-bb - math.sqrt(disc)) / aa * k if disc >= 0 else None
# The camera's axes: looking east and MOONRISE_CENTER above the limb, up toward the
# local vertical.
el = math.radians(MOONRISE_CENTER) - dip
forward = norm([math.cos(el) * east[i] + math.sin(el) * up[i] for i in range(3)])
image_up = norm([up[i] - dot(up, forward) * forward[i] for i in range(3)])
image_right = cross(forward, image_up)
def ray(x, y):
"""The line of sight x degrees right of the image center and y degrees up."""
tx, ty = math.tan(math.radians(x)), math.tan(math.radians(y))
return norm([forward[i] + tx * image_right[i] + ty * image_up[i] for i in range(3)])
def limb(x, earth=None):
"""Degrees up from the image center to the limb in column x."""
lo, hi = -10.0, 10.0 # on Earth, in space
for _ in range(60):
mid = (lo + hi) / 2
lo, hi = (mid, hi) if hits(ray(x, mid), earth) is not None else (lo, mid)
return lo
def place(x, above, earth=None):
return ray(x, limb(x, earth) + above)
look = [camera[i] + MOON_DISTANCE * forward[i] for i in range(3)]
to_satellite = place(*MOONRISE_SATELLITE)
attitude = triad([1, 1, 1], [0, 0, 1], [-x for x in to_satellite], image_up)
spot = [camera[i] + MOONRISE_RANGE * to_satellite[i] for i in range(3)]
offset = mul(attitude, center)
position = [spot[i] - offset[i] for i in range(3)]
# The ground at the bottom of the frame, turned to MOONRISE_GROUND with north
# along the line of sight across it (toward the limb, image up).
swath = place(*MOONRISE_SWATH)
ground = norm([camera[i] + hits(swath) * swath[i] for i in range(3)])
north = norm([swath[i] - dot(swath, ground) * ground[i] for i in range(3)])
lon = math.radians(MOONRISE_GROUND[1])
fixed = [math.cos(lat) * math.cos(lon), math.cos(lat) * math.sin(lon), math.sin(lat)]
earth = triad(fixed, [0, 0, 1], ground, north)
# The Moon on its line of sight, MOON_DISTANCE from Earth's center, placed against
# the ellipsoid's limb.
to_moon = place(*MOONRISE_MOON, earth)
od = dot(camera, to_moon)
reach = -od + math.sqrt(od * od - dot(camera, camera) + MOON_DISTANCE ** 2)
moon = [camera[i] + reach * to_moon[i] for i in range(3)]
# The Sun: Earth and the Moon are the subject, so the direction that best lights
# both, the product of the Sun's height over that ground and the Moon's lit
# fraction, with the satellite and the Moon out of Earth's shadow.
from_moon = norm([camera[i] - moon[i] for i in range(3)])
moon_dir = norm(moon)
best = None
for az in range(0, 360, 2):
for el in range(-88, 90, 2):
a, e = math.radians(az), math.radians(el)
sun = [math.cos(e) * math.cos(a), math.cos(e) * math.sin(a), math.sin(e)]
phase = math.degrees(math.acos(dot(sun, from_moon)))
if dot(sun, [-x for x in norm(camera)]) > math.cos(math.asin(RE / GEO)):
continue
if dot(sun, [-x for x in moon_dir]) > math.cos(math.radians(3)):
continue
score = (1 + math.cos(math.radians(phase))) / 2 * max(dot(ground, sun), 0.0)
if best is None or score > best[0]:
best = (score, sun, phase)
_, sun, phase = best
fmt = lambda v, d=2: "[" + ", ".join(f"{x:.{d}f}" for x in v) + "]"
return f"""# Moonrise from geostationary height: looking toward Earth's limb through a 100 mm
# lens, Australia filling the lower half of the frame, the Moon (phase {phase:.0f} degrees)
# rising over the limb, and an SSL-1300 communications satellite {MOONRISE_RANGE / 1000:.1f} km off in
# the foreground. Generated by scripts/make-example-scenes.py; the model comes from
# scripts/fetch-models.sh.
#
# resolved render --scene examples/scenes/moonrise.toml --lighting L1 --preview moonrise.png
#
# At lighting L1 Earth's surface and the Moon's come from their maps. There is no
# atmosphere yet, so the limb is a sharp edge rather than a thin glowing band.
sun = {fmt([AU * x for x in sun], 0)}
moon = {fmt(moon)}
# Earth-fixed to inertial: 25 S 134 E at the bottom of the frame, looking north.
earth_attitude = {fmt(quaternion(earth), 9)}
# Exposed so Earth, the Moon and the satellite stay short of saturation.
[camera]
focal_length = {MOONRISE_FOCAL}
exposure = {MOONRISE_EXPOSURE}
[observer]
position = {fmt(camera)}
look_at = {fmt(look)}
up = [1, 0, 0]
# At lighting L1, Earth's surface shows July (from scripts/fetch-earth.sh).
[appearance]
earth_month = 7
[[target]]
model = "../models/nasa/{file}"
scale = {scale}
position = {fmt(position, 4)}
attitude = {fmt(quaternion(attitude), 9)}
"""
out = pathlib.Path(__file__).resolve().parent.parent / "examples" / "scenes"
for model in MODELS:
path = out / f"{model[0]}.toml"
path.write_text(scene(*model))
print(f"wrote {path}")
path = out / "earthrise.toml"
path.write_text(earthrise())
print(f"wrote {path}")
path = out / "moonrise.toml"
path.write_text(moonrise())
print(f"wrote {path}")