Skip to content

feat(globe): TexturedGlobeGlyph drops texture detail finer than one mesh face #325

Description

@MAfarrag

Context

TexturedGlobeGlyph samples its texture once and caches the result as face colours, which is what makes
draw(spin=...) and animate() cheap. The sampling is a point lookup: one texture cell per mesh face centre.

That is the right call for a photographic basemap, where neighbouring cells are similar. It behaves
surprisingly for a texture with sparse or small opaque regions — which is what a geospatial caller
produces when it drapes a regional dataset onto a global canvas and leaves the rest transparent.

Found while adopting the glyph in Digital-Earth (serapeum-org/Digital-Earth#136).

Problem / Current Behaviour

Detail narrower than one mesh face falls between the sample points and disappears completely — however many
texture cells it covers, and however fine the texture is.

At the defaults the gap is large: n_lon=180, n_lat=90 gives faces about 2 degrees across, while a caller
may reasonably supply a 0.125-degree texture. Anything under ~2x2 degrees renders as if it were not there,
with no warning and no way to tell from the returned object.

Because the texture is sampled rather than reduced, the failure is silent and resolution-dependent: raising
the texture resolution — the obvious response to "my data is too small to see" — makes no difference at all.

Affected locations

File Symbol Notes
glyphs/globe/textured_globe_glyph.py _build_facecolors point-samples one texture cell per face centre
glyphs/globe/textured_globe_glyph.py TexturedGlobeGlyph no public accessor for the sampled face colours

Steps to Reproduce

import matplotlib
matplotlib.use("Agg")
import numpy as np
from cleopatra.glyphs.globe.textured_globe_glyph import TexturedGlobeGlyph

# A fine texture with one small opaque patch: 8 cells of a 1440x2880 canvas, about 0.5 degrees across.
texture = np.zeros((1440, 2880, 4), dtype=np.uint8)
texture[700:704, 1300:1304] = (255, 60, 25, 255)

glyph = TexturedGlobeGlyph(texture, n_lon=180, n_lat=90)
glyph.draw()
painted = np.asarray(glyph._facecolors)
print(int((painted[..., 3] > 0).sum()))   # 0 - the patch is not on the globe at all

The patch covers 8 texture cells and renders nothing. Raising the texture to 2880x5760 does not change it;
only raising n_lon/n_lat does.

Proposed Solution

Two independent changes, either useful on its own:

1. Reduce over each face rather than sampling one cell. When the texture is finer than the mesh, take
each face's colour from the texture block it covers instead of from its centre point:

# per face, over the texture rows/cols it spans
block = texture[r0:r1, c0:c1]
opaque = block[..., 3] > 0
face_colour = block[opaque].mean(axis=0) if opaque.any() else TRANSPARENT

Alpha-aware reduction matters more than the exact statistic: a "max alpha wins" or "mean of the opaque cells"
rule keeps a small feature visible, where a plain area-average would fade it toward transparent. A
sampling="point" | "area" switch would keep the current behaviour available and cheap for basemap textures,
where it is the right default.

2. Expose the sampled face colours. There is currently no public way to ask what the glyph will actually
paint, so a caller wanting to check whether its data survived must reach for glyph._facecolors after a
draw(). A read-only face_colors property (or a sampled_texture() that returns the reduced grid without
drawing) would make that a supported question.

Digital-Earth currently reimplements the index arithmetic from _build_facecolors to warn its users before
they render a blank globe. That mirror is exactly the kind of thing that goes stale when the sampling changes;
(2) would let it ask instead.

Out of Scope

  • The sample-once/rotate-per-frame contract — the proposal keeps it; only the reduction changes.
  • Any change to the default mesh resolution.

Effort Estimate

Size: M
Rationale: (2) is small and self-contained. (1) is the real work: a vectorised block reduction over an
irregular row/column partition, plus the option plumbing and tests at several texture:mesh ratios.

Definition of Done

  • A texture feature smaller than one mesh face still paints, under whichever mode is documented to do so
  • The current point-sampling behaviour remains available and remains the cheap path
  • A public accessor reports the colours the glyph will paint, without reaching into private state
  • Tests cover texture-finer-than-mesh, mesh-finer-than-texture, and a fully transparent texture
  • The reproduction above paints a non-zero number of faces

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpythonPull requests that update python code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions