Skip to content

feat(glyphs): add directional lighting to TexturedGlobeGlyph - #320

Merged
MAfarrag merged 6 commits into
mainfrom
feat/globe-directional-lighting
Aug 25, 2026
Merged

feat(glyphs): add directional lighting to TexturedGlobeGlyph#320
MAfarrag merged 6 commits into
mainfrom
feat/globe-directional-lighting

Conversation

@MAfarrag

Copy link
Copy Markdown
Member

Description

Adds directional lighting to TexturedGlobeGlyph so the sphere can be lit from a direction —
shading a lambertian day/night terminator instead of reading as evenly illuminated. This is the
follow-up #311/0.33.0 did not cover, found while adopting the glyph downstream in earthlens (the
eclipse-geometry notebook had to keep a hand-rolled textured_earth because the terminator is its
actual subject).

  • sun (a world-space (x, y, z) direction, auto-normalised to unit length) and ambient
    (floor so the night side stays legible, default 0.13) on __init__, and overridable per call
    on draw/animate — mirroring how spin works, via a sentinel so an explicit sun=None can turn
    lighting off for one call.
  • Preserves the sample-once / rotate-per-frame design. Lighting is applied per frame from the
    vertices draw already rotates (_spun_mesh(spin), whose unit-sphere positions are the surface
    normals): a copy of the cached facecolors is scaled by
    ambient + (1 - ambient)·clip(dot(normal, sun), 0, 1). The facecolors cache is never mutated and
    the texture is never re-sampled — so a fixed sun with a spinning globe sweeps the terminator across
    the surface.
  • sun=None (the default) is byte-identical to 0.33.0 — every current caller is unchanged.
  • Per-face lighting detail: facecolors are per-face while mesh normals are per-vertex, so each quad's
    four corner normals are averaged to line the lit factor up with the faces.

Frame: sun is in the same world frame the globe is drawn in — +z up/north, +x toward the
viewer at spin=0. Scope: small, backward-compatible enhancement to the already-shipped globe
glyph — NumPy in → matplotlib out, no new dependency (a dot product). No specular/atmosphere/cast
shadows (out of scope, per the issue).

No new runtime dependencies.

Issues

Type of change

Check relevant points.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Dev changes (CI/pyproject.toml/docs/examples/testing)

How Has This Been Tested?

New TestLighting cases in tests/test_textured_globe_glyph.py (60 tests total; 100% line + branch
coverage of the module) covering the issue's Definition of Done:

  • sun=None byte-identical — returns the cached facecolors by identity (unlit regression).
  • Terminator + ambient floor — a uniform texture shows a real day/night range; the darkest lit face
    equals ambient × cache (night side not black).
  • Lit fraction tracks spin — a fixed sun with the globe at spin 0 vs 180 gives different facecolors.
  • No re-sample / no mutation per frame — the facecolors cache is unchanged after a lit draw.
  • sun on __init__ and per-call override; draw(sun=None) disables an instance light; sun
    auto-normalised; animate(sun=...) forwards per frame (rendered via the Pillow writer).
  • Validation — bad sun (wrong length / zero / non-finite) and ambient outside [0, 1] raise.

Reproduce (external uv env, worktree on src):

VIRTUAL_ENV=C:/python-environments/uv/cleopatra PYTHONPATH=src \
  C:/python-environments/uv/cleopatra/Scripts/python.exe -m pytest tests/test_textured_globe_glyph.py -q
  • Test A — tests/test_textured_globe_glyph.py (60 passed, 100% coverage)
  • Test B — full suite pytest tests/ -q → 2532 passed; ruff check + ruff format --check clean;
    module doctests pass

Checklist:

  • updated version number in pyproject.toml
  • added changes to History.rst
  • updated the latest version in README file
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Add a sun unit vector and ambient floor so the globe can be lit from a
direction, shading a lambertian day/night terminator instead of reading as
evenly illuminated. sun=None (the default) renders byte-identical to 0.33.0.

- Accept sun/ambient on __init__ and override per call on draw/animate
  (mirroring spin); sun is normalized to unit length, ambient must be in [0, 1].
- Apply lighting per frame from the already-rotated vertices (one dot product
  over the _spun_mesh output, whose unit-sphere positions are the surface
  normals), scaling a copy of the cached facecolors by
  ambient + (1 - ambient) * clip(dot(normal, sun), 0, 1). The facecolors cache
  is never mutated and the texture is never re-sampled, so the
  sample-once/rotate-per-frame contract holds and a fixed sun sweeps the
  terminator across the surface as the globe spins.
- Add lighting tests and a docs example.

Closes #319
Address round-1 review findings on the new lighting path:
- animate() validates sun/ambient eagerly at the call (matching draw) instead of
  deferring to matplotlib's per-frame render loop, and its Raises docstring now
  lists sun/ambient (M1).
- Type the _INHERIT sentinel as Any and assert the facecolors cache is populated
  in _lit_facecolors, clearing the 7 new mypy errors the feature introduced (M2).
- _normalize_sun rejects non-1-D inputs so a (1,3)/(3,1) array no longer slips
  through, matching the documented length-3 vector contract (L1).
- Clarify ambient is always validated but only affects a lit render (N1).
Add round-1 regression/edge tests: non-1-D sun rejection, NaN ambient, ambient 0
(black night) and 1 (equals cache), animate's eager sun/ambient validation, and
world-space sun honoured under tilt.
…y-space

Assert the geographic north cap is dimmer than the brightest face under a +z sun
with a 45deg tilt (north < peak - 0.1), which a body-space regression -- where
the pole would be fully lit -- would fail. The prior north > south check passed
for both frames.
sun=(1,0,0.3) is nearly parallel to the default camera eye, so it renders a
mostly-lit disc, not the left/right split the comment described. Use
sun=(0,1,0.3) (perpendicular to the view) for a real side-lit terminator.
Replace the two == 0.0 float-equality guards SonarCloud flags as bugs: the sun
zero-vector check uses <= 0.0, and the per-face normal-magnitude guard floors
the magnitude with np.maximum(..., 1e-12) instead of an == mask (same defensive
intent, no float equality, and it doubles as a cleaner div-by-zero floor).
@sonarqubecloud

Copy link
Copy Markdown

@MAfarrag
MAfarrag merged commit 921e83b into main Aug 25, 2026
10 checks passed
@MAfarrag
MAfarrag deleted the feat/globe-directional-lighting branch August 25, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(glyphs): let TexturedGlobeGlyph light the sphere from a direction

1 participant