182 lines
6.8 KiB
Python
Executable File
182 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Writes examples/scenes/<model>.toml for the NASA models fetch-models.sh downloads.
|
|
|
|
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
|
|
Earth's limb crossing the image 2 degrees above center, Earth below and space
|
|
above. The camera distance makes the model's bounding sphere span about 5.6
|
|
degrees, half the example camera's width.
|
|
|
|
Earth is turned so the ground behind the spacecraft, at the image center, is
|
|
25 N 33 E (Egypt, the Nile and the Red Sea), with the camera looking north
|
|
toward the Mediterranean on the limb. The Sun is the direction, on a 2-degree
|
|
grid, that puts it at least 58 degrees above that ground (cos i >= 0.85) and
|
|
best lights the spacecraft's three visible faces, without putting the
|
|
spacecraft in Earth's shadow. With the Sun that high over the ground, one face
|
|
is lit at a slant.
|
|
|
|
Model centers and radii were measured from the decoded files at the listed
|
|
scales (the bounding box of every vertex); remeasure if a model changes.
|
|
"""
|
|
|
|
import math
|
|
import pathlib
|
|
import textwrap
|
|
|
|
RE = 6378137.0
|
|
# The ground at the image center, geocentric latitude and longitude in degrees.
|
|
GROUND = (25.0, 33.0)
|
|
# The least cos(incidence) the Sun may have on that ground.
|
|
GROUND_SUN = 0.85
|
|
AU = 1.495978707e11
|
|
LEO = 7.0e6
|
|
GEO = 42_164_000.0
|
|
|
|
# name, file, scale, bounding-box center (model frame, scaled, m), radius (m),
|
|
# orbit radius (m), description
|
|
MODELS = [
|
|
("landsat8", "landsat8.glb", 1.0, (0.0001, -3.6797, 3.6108), 8.3027, RE + 705e3,
|
|
"Landsat 8 (85k triangles) in its 705 km orbit"),
|
|
("icesat2", "icesat2.glb", 1.0, (1.8979, -0.0013, 4.1380), 5.5159, RE + 496e3,
|
|
"ICESat-2 (752k triangles) in its 496 km orbit; the heavy model for benchmarks"),
|
|
("ostm", "ostm.glb", 3.5, (0.0, -0.1750, 0.0), 4.3988, RE + 1336e3,
|
|
"OSTM / Jason-2 (65k triangles) in its 1336 km orbit"),
|
|
("ssl1300", "ssl1300.glb", 0.1, (0.0, 0.5135, 0.0), 14.3366, GEO,
|
|
"an SSL-1300 communications satellite (182k triangles) in geostationary orbit"),
|
|
("ace", "ace.glb", 0.00485, (0.0, 0.0, 0.0), 4.0114, LEO,
|
|
"the Advanced Composition Explorer (26k triangles). The real ACE sits at the "
|
|
"Sun-Earth L1 point; here it is put in low orbit"),
|
|
]
|
|
|
|
|
|
def norm(v):
|
|
n = math.sqrt(sum(x * x for x in v))
|
|
return [x / n for x in v]
|
|
|
|
|
|
def cross(a, b):
|
|
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
|
|
|
|
|
def dot(a, b):
|
|
return sum(x * y for x, y in zip(a, b))
|
|
|
|
|
|
def mul(m, v):
|
|
return [dot(row, v) for row in m]
|
|
|
|
|
|
def transpose(m):
|
|
return [[m[j][i] for j in range(3)] for i in range(3)]
|
|
|
|
|
|
def matmul(a, b):
|
|
return [[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)]
|
|
|
|
|
|
def triad(b1, b2, i1, i2):
|
|
"""The rotation taking body b1 onto inertial i1, with b2 as close to i2 as it can."""
|
|
def frame(v1, v2):
|
|
t1 = norm(v1)
|
|
t2 = norm(cross(v1, v2))
|
|
t3 = cross(t1, t2)
|
|
return [[t1[i], t2[i], t3[i]] for i in range(3)]
|
|
return matmul(frame(i1, i2), transpose(frame(b1, b2)))
|
|
|
|
|
|
def quaternion(r):
|
|
"""[w, x, y, z] of rotation matrix r, with w >= 0."""
|
|
tr = r[0][0] + r[1][1] + r[2][2]
|
|
if tr > 0:
|
|
s = math.sqrt(tr + 1) * 2
|
|
q = [0.25 * s, (r[2][1] - r[1][2]) / s, (r[0][2] - r[2][0]) / s, (r[1][0] - r[0][1]) / s]
|
|
else:
|
|
i = max(range(3), key=lambda k: r[k][k])
|
|
j, k = (i + 1) % 3, (i + 2) % 3
|
|
s = math.sqrt(1 + r[i][i] - r[j][j] - r[k][k]) * 2
|
|
v = [0.0, 0.0, 0.0]
|
|
v[i] = 0.25 * s
|
|
v[j] = (r[j][i] + r[i][j]) / s
|
|
v[k] = (r[k][i] + r[i][k]) / s
|
|
q = [(r[k][j] - r[j][k]) / s] + v
|
|
return [-x for x in q] if q[0] < 0 else q
|
|
|
|
|
|
def scene(name, file, scale, center, radius, orbit, description):
|
|
limb = math.asin(RE / orbit)
|
|
theta = limb - math.radians(2.0)
|
|
zenith = [1.0, 0.0, 0.0]
|
|
look = [orbit, 0.0, 0.0]
|
|
boresight = [-math.cos(theta), math.sin(theta), 0.0]
|
|
attitude = triad([1, 1, 1], [0, 0, 1], [-x for x in boresight], zenith)
|
|
distance = radius / math.tan(math.radians(2.8))
|
|
camera = [look[i] - distance * boresight[i] for i in range(3)]
|
|
# Place the model so its bounding-box center is at the look point.
|
|
offset = mul(attitude, center)
|
|
position = [look[i] - offset[i] for i in range(3)]
|
|
|
|
# The ground behind the spacecraft at the image center: where the boresight,
|
|
# continued past the spacecraft, meets Earth.
|
|
b = dot(look, boresight)
|
|
reach = -b - math.sqrt(b * b - (dot(look, look) - RE * RE))
|
|
ground = norm([look[i] + reach * boresight[i] for i in range(3)])
|
|
# Turn Earth so GROUND is there, with north along the camera's view across it.
|
|
lat, lon = (math.radians(x) for x in GROUND)
|
|
fixed = [math.cos(lat) * math.cos(lon), math.cos(lat) * math.sin(lon), math.sin(lat)]
|
|
north = norm([boresight[i] - dot(boresight, ground) * ground[i] for i in range(3)])
|
|
earth = triad(fixed, [0, 0, 1], ground, north)
|
|
|
|
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)]
|
|
if dot(sun, [-1.0, 0.0, 0.0]) > math.cos(limb):
|
|
continue # behind Earth: the spacecraft would be in shadow
|
|
if dot(ground, sun) < GROUND_SUN:
|
|
continue
|
|
faces = [dot(mul(attitude, n), sun) for n in ([1, 0, 0], [0, 1, 0], [0, 0, 1])]
|
|
if best is None or min(faces) > best[0]:
|
|
best = (min(faces), sun)
|
|
sun = best[1]
|
|
|
|
fmt = lambda v, d=2: "[" + ", ".join(f"{x:.{d}f}" for x in v) + "]"
|
|
q = quaternion(attitude)
|
|
header = textwrap.fill(
|
|
f"{description[0].upper() + description[1:]}, in an isometric view with Earth's limb "
|
|
"behind it. Generated by scripts/make-example-scenes.py; the model comes from "
|
|
"scripts/fetch-models.sh.",
|
|
width=80, initial_indent="# ", subsequent_indent="# ", break_on_hyphens=False)
|
|
return f"""{header}
|
|
#
|
|
# resolved render --scene examples/scenes/{name}.toml --preview {name}.png
|
|
# resolved render --scene examples/scenes/{name}.toml --lighting L1 --preview {name}.png
|
|
|
|
sun = {fmt([AU * x for x in sun], 0)}
|
|
moon = [0, 0, -3.844e8]
|
|
# Earth-fixed to inertial: 25 N 33 E behind the spacecraft, looking north.
|
|
earth_attitude = {fmt(quaternion(earth), 9)}
|
|
|
|
[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(q, 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}")
|