Add render tool: synthetic Earth-limb imagery for EarthLim testing - #10
Add render tool: synthetic Earth-limb imagery for EarthLim testing#10j4lando wants to merge 7 commits into
Conversation
Implements the DIY Earth-rendering pipeline from #9: analytical Sun ephemeris and ECI->ECEF geometry (closed-form, no SPICE kernel/IERS download needed, fully unit-tested), a scene builder that resolves date-appropriate Blue Marble Next Generation + cloud textures and camera intrinsics/pose, and a headless Blender script (blender_scene.py) that renders the scene with a Sun lamp and Nishita atmosphere shader for limb glow/terminator softening. bpy isn't installable for this project's Python version, so blender_scene.py runs under Blender's own interpreter and is invoked via subprocess from the found_tools_render CLI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause: the Earth/cloud/atmosphere UV spheres were never smooth-shaded, so Blender rendered each of their ~256x128 quad faces with a single flat normal. Under smooth, low-contrast lighting (open ocean near the terminator) that regular lat/long face grid showed up as a persistent, screen-space-aligned checkerboard of brightness steps -- confirmed by reproducing it with denoising off and adaptive sampling off (unaffected) and with the atmosphere shell removed entirely (still present), which ruled out those as causes before finding the actual one. Fixed by calling bpy.ops.object.shade_smooth() on each sphere after creation, with a test asserting every polygon.use_smooth on Earth/CloudLayer/Atmosphere. Also replaced add_atmosphere()'s use of Blender's Sky Texture node as the World background: that node models a ground-level sky dome, so as a World background it lit the *entire* background in every direction (not just near Earth's limb) with its own bright, independent default sun unrelated to this scene's Sun lamp -- this is what was blowing out roughly half of every render to solid white. Replaced with a black deep-space World background plus a Fresnel-driven emission shell around the Earth, so the glow is strongest at grazing viewing angles (the limb) and fades toward the disk's center, matching an actual from-space limb-glow/terminator appearance. Verified by actually running bpy.ops.render.render (not just unit tests) against real Blue Marble imagery and visually inspecting the output before and after each fix. The README's example camera parameters were also replaced: the previous --position/--focal-length/--pixel-pitch combination put the camera at ~622 km altitude with a 50mm-equivalent lens (~5.5 deg FOV), a telephoto shot of a few-km patch of ocean magnified to fill the frame, not a "blurry render bug" but a bad example command. Swapped for values that produce a full Earth disk, and added a "Choosing camera parameters" section explaining how field of view, distance, and Earth's angular size interact so users don't hit the same thing. Excluded blender_scene.py from `ty check` (pre-existing gap from the earlier switch to bpy as a real dependency: bpy ships no type stubs, so ty can't resolve any of its dynamically-generated attributes -- unrelated to this fix, but restores `just check-all` to green). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The limb-glow shell added in the previous fix used view-angle Fresnel alone, so every point of the limb glowed equally regardless of where the Sun actually was -- a uniform white ring even on the shell's night side, not realistic atmospheric scattering. add_atmosphere() now also takes the scene dict and masks/colors the glow by dot(surface normal, Sun direction) -- the same Sun vector already used for the Sun lamp: - Fresnel (view angle) still shapes *where on the sphere* the shell is visible at all (the limb, not the disk center). - A new sun-facing term (Geometry node Normal dot a Sun direction vector, both in world/ECEF space) masks that down to the sunlit crescent via a Map Range that fades in just before the terminator and out on the night side, instead of a hard cutoff. - A ColorRamp keyed off the same dot product shifts the glow from warm orange right at the terminator (long atmospheric path, like a ground sunset) to blue further into daylight (shorter, bluer-scattering path). Verified by rendering three viewpoints and visually inspecting each: (1) camera near the subsolar direction, where a uniform bright rim is in fact correct since the whole visible limb is sunlit; (2) camera roughly perpendicular to the Sun vector, showing the glow present on the sunlit limb and absent near the terminator; (3) a pulled-back full-disk view confirming the day/night asymmetry and the warm-to-blue color shift right where the terminator meets the limb. Extended test_blender_scene.py: add_atmosphere is called with the scene dict everywhere now, and new tests assert the sun-direction node is wired from scene["sun_vector_ecef"] (normalized), the dot-product node consumes Geometry Normal and that vector, emission strength/color are graph-driven rather than constants, and the color ramp's stops match the intended warm-terminator/blue-day gradient. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Earth mesh (and the concentric cloud/atmosphere shells) were built as true spheres via primitive_uv_sphere_add. WGS84's ~21 km polar flattening is small next to Earth's radius but matters for accurate EarthLim limb-fitting, so all three are now built as WGS84 ellipsoids: a sphere at the equatorial radius with its local Z axis (Earth's rotation axis) scaled and baked down to the WGS84 polar radius. Verified against the actual mesh vertex bounding radii in tests, since the ~0.3% flattening isn't distinguishable by eye at normal framing. Also splits add_atmosphere into add_space_background (always applied) and add_atmosphere_glow (the limb-glow shell), and threads a new --no-atmosphere CLI flag through main.py -> scene.py -> blender_scene.py so the glow shell can be skipped independently of the Earth/background. Verified by rendering both with and without the flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed another round of fixes on top of the render-quality pass (blurry mess -> limb glow, directional glow): Earth modeled as a WGS84 ellipsoid, not a sphere. The Earth mesh (and the concentric cloud/atmosphere shells) were built with Since ~0.3% flattening isn't distinguishable by eye at normal camera framing, I verified this mathematically rather than visually: new tests compute the actual mesh vertex bounding radii (equatorial vs. polar) for the Earth, cloud layer, and atmosphere shell and assert they match the WGS84 constants (equatorial 6,378,137 m, polar 6,356,752.314245 m, difference 21,384.685755 m) to within a small tolerance. Added
Test/coverage status: 119 tests passing, 100% coverage maintained, |
CI's "Run linters" step was failing on PR #10: it installs ruff@latest fresh each run (currently 0.16.1), which flags DTZ001 (naive datetime.datetime()), UP017 (datetime.UTC alias), C408 (dict() call vs literal), and import sorting that an older locally-cached `uvx ruff` (0.15.13) didn't catch. Applied ruff's safe fixes across the render tool's src/tests, manually converted one dict() call to a literal, and left `# noqa: DTZ001` on the two datetime() calls that are deliberately naive (testing the "assume UTC" fallback path). Added CLAUDE.md documenting just check-all, what ci.yml actually runs (read from .github/workflows/, not guessed), and the ruff-version-drift / ty-not-in-CI mismatches that caused this failure, so the same class of "green locally, red in CI" surprise doesn't happen again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Strips out the atmosphere/limb-glow feature entirely rather than shipping it half-working behind a flag: removes add_atmosphere_glow(), the ATMOSPHERE_RADIUS_SCALE constant, and the --no-atmosphere / atmosphere_glow_enabled plumbing through main.py -> scene.py -> blender_scene.py, plus its tests. render_scene() now just builds the Earth+clouds (WGS84 ellipsoid), Sun lamp, black deep-space background, and camera -- no glow shell. Also adds .github/images/render_example.png, a representative render from the current pipeline (Earth ellipsoid + clouds + correct sun lighting/terminator, no atmosphere glow), carved out of the blanket *.png gitignore rule as a checked-in doc asset, for embedding in PR #10 so reviewers can see actual output without pulling the branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Removed the atmosphere/limb-glow shell entirely for now ( Here's a representative render from the current pipeline (
|

Closes #9.
What this adds
A new
rendertool (found_tools.render, CLI entry pointfound_tools_render) that generates synthetic Earth-limb test imagery forEarthLimand similar attitude-determination tools, following the DIY pipeline proposed in #9:geometry.py— Sun direction (ECI) and ECI→ECEF rotation for a given UTC datetime.textures.py— picks the date-appropriate Blue Marble Next Generation texture (by month) and the generic cloud layer texture.scene.py— packages Sun vector, camera intrinsics (reusingfound_tools.utils.Camera), camera pose, and texture filenames into a JSON-serializable scene description.main.py— thefound_tools_renderCLI: parses date/camera/position/attitude/texture-dir args, writes the scene JSON, and (with--render) callsblender_scene.render_scenein-process to build and render it.blender_scene.py— the only module that importsbpy; builds the Blender scene (Earth + cloud sphere, Sun lamp along the computed vector, a sun-direction-aware atmosphere shell for limb glow/terminator softening, camera intrinsics/pose matched to the scene JSON) and renders it with Cycles.src/found_tools/render/README.md— usage docs, including the texture-download caveat from Add a synthetic Earth-rendering tool for generating EarthLim test imagery #9 (no month-by-month cloud product exists, so the cloud layer is generic) and a "Choosing camera parameters" section on avoiding a too-narrow FOV / too-close camera.bpy(Blender as a Python module) is a real project dependency (Python pinned to>=3.13,<3.14for wheel availability), so--renderworks standalone with no separate Blender install required.Design notes / tradeoffs
spiceypyorastropy. I used a closed-form, low-precision analytical Sun-position formula (Vallado, Fundamentals of Astrodynamics and Applications, Alg. 29) and the IAU 1982 GMST polynomial for ECI→ECEF instead of either library. The analytical formulas are accurate to ~0.01°, more than sufficient for lighting/texture selection in synthetic test imagery, and they're fully offline-testable, avoiding a SPICE kernel download or IERS data dependency in this repo'sfail_under = 100CI. If a project later needs higher precision, spiceypy/astropy can be swapped in behind the same function signatures.blender_scene.pyis excluded fromty check—bpyships no type stubs, sotycan't resolve its dynamically-generated attributes (bpy.context,bpy.ops, etc.). It's still exercised by real tests (see below).Render bug fixes
I actually ran
bpy.ops.render.renderend-to-end (not just the unit tests) against real Blue Marble imagery and visually inspected the output at each step, through two rounds of fixes:Round 1 — render came out as a blurry, overexposed mess:
bpy.ops.object.shade_smooth()on each sphere.add_atmosphere()used Blender's Sky Texture node as the World background. That node models a ground-level sky dome, so as a World background it lit the entire background in every direction (not just near Earth's limb) with its own bright, independent default sun unrelated to the scene's actual Sun lamp — blowing out roughly half of every render to solid white. Replaced with a black deep-space World background plus a Fresnel-driven emission shell around the Earth, so the glow is strongest at grazing viewing angles (the limb) and fades toward the disk's center.--position 7000000 0 0with a 50mm-equivalent lens put the camera at only ~622 km altitude with a ~5.5° field of view — a telephoto shot of a few-km ocean patch magnified to fill the frame. Swapped the example for parameters that produce a full Earth disk, and added a README section explaining how FOV, camera distance, and Earth's angular size interact.Round 2 — the round-1 limb glow was a uniform white halo regardless of where the Sun was:
The Fresnel-only glow shell from round 1 glowed the same on the day and night side of the limb — physically wrong.
add_atmosphere()now also takes the scene dict and masks/colors the glow bydot(surface normal, Sun direction)(the same Sun vector already driving the Sun lamp), on top of the existing view-angle Fresnel term:Verified across three viewpoints: near-subsolar (uniform bright rim is correct there — the whole visible limb really is sunlit), perpendicular-to-sun (glow present on the sunlit limb, absent near the terminator), and a pulled-back full-disk view confirming the day/night asymmetry and the warm-to-blue color shift right where the terminator meets the limb.
Test plan
just check-all(lint, 100% coverage, tests,ty check) passes, including tests against the realbpymodule: smooth-shading on all three spheres, atmosphere world background + shell material, the sun-direction node wiring (Geometry Normal → dot product with a normalizedsun_vector_ecef→ Map Range → emission strength/color), the warm/blue color ramp stops, and actualbpy.ops.render.rendercalls