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
Context
TexturedGlobeGlyphsamples its texture once and caches the result as face colours, which is what makesdraw(spin=...)andanimate()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=90gives faces about 2 degrees across, while a callermay 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
glyphs/globe/textured_globe_glyph.py_build_facecolorsglyphs/globe/textured_globe_glyph.pyTexturedGlobeGlyphSteps to Reproduce
The patch covers 8 texture cells and renders nothing. Raising the texture to 2880x5760 does not change it;
only raising
n_lon/n_latdoes.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:
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._facecolorsafter adraw(). A read-onlyface_colorsproperty (or asampled_texture()that returns the reduced grid withoutdrawing) would make that a supported question.
Digital-Earth currently reimplements the index arithmetic from
_build_facecolorsto warn its users beforethey 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
Effort Estimate
Size:
MRationale: (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