Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Line patterns blur on pitched maps: offer anisotropic sampling (`textureGrad`) for patterns

Open
#13,741 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
46/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active

Research direction

Start with src/shaders/_prelude.fragment.glsl and src/shaders/line_pattern.fragment.glsl to trace how pattern coordinates reach textureLodCustom; compare the anisotropic sampling gate in src/render/draw_raster.ts and texture creation in src/render/image_atlas.ts. Decide how to gate textureGrad and set anisotropy consistently for the shared pattern path. Done means pitched patterns gain sharpness while flat maps and devices without the extension retain current behavior, with relevant rendering coverage.

Written by the indexing model from the issue text.

Description

auto-triaged feature :green_apple:

Motivation

On a pitched map, line-pattern images get noticeably blurry, and the steeper the pitch, the blurrier they get. Looking straight down they're as sharp as the image allows.

We draw one-way arrows on ski runs as a line-pattern (a chevron repeated along the run), because a pattern lies on the ground and is hidden by 3D buildings, unlike symbol icons, which draw on top of them. On the pitched 3D views where that matters most, the chevrons blur.

The cause is deliberate. textureLodCustom in _prelude.fragment.glsl, which line_pattern.fragment.glsl uses, picks the mip level from the larger of the two screen-space derivatives and samples with textureLod. The comment there notes that textureLod doesn't support anisotropic filtering, and that textureGrad would, but is discouraged on ARM GPUs for speed. On a pitched view the larger derivative is the foreshortened one, so the pattern is sampled at the blurrier level. Raster layers, by contrast, sample with texture() and get anisotropic filtering beyond 20° pitch (draw_raster.ts).

Measurements

The repro below: one line-pattern line, black chevrons with a white edge, on a white background, at zoom 18 and bearing 60°, on a 2× screen, with the image at the screen's pixel ratio so that a flat map draws it unscaled. "Crisp" is the share of the pixels the pattern changes that it changes strongly (by more than 300 of 765 in summed RGB), so lower means blurrier.

Pitch As shipped Mipmaps disabled on the pattern texture
0° 73.7% 73.7%
45° 64.9% 74.9%
60° 66.2% 75.1%
70° 58.5% 76.6%
  • Looking straight down: mipmaps make no difference. With pitch, the pattern blurs, and disabling mipmaps removes the blur.
  • A line crossing the view: worse still. At 70° pitch and bearing −18°, it's 44.7% as shipped against 69.7% without mipmaps.
  • Anisotropic filtering forced on: setting TEXTURE_MAX_ANISOTROPY_EXT to 16 on the pattern texture (read back as 16) leaves it at 58.5% at 70°, as expected with textureLod.

Mipmaps were disabled through internals (imageAtlasTexture.useMipmap), only to locate the cause; it isn't a fix we'd want, since it would risk the aliasing that mipmaps prevent at a distance.

Design Alternatives

  1. Sample patterns with textureGrad where anisotropic filtering is available, as rasters already get anisotropic filtering, perhaps only beyond some pitch, as draw_raster.ts does.
  2. An opt-in style property, such as line-pattern-filter: "anisotropic", so the cost is paid only by layers that need it.
  3. Pick the LOD from the smaller derivative, or bias it, when pitched. That should be cheaper than textureGrad and sharper on pitched lines, at the risk of some aliasing.
  4. Do nothing. Then patterns stay blurry on pitched and 3D views, and the workarounds are internal patches or bolder artwork.

Design

Option 1, gated like the raster path, beyond 20° pitch and only where EXT_texture_filter_anisotropic is available, would leave flat maps and devices without the extension as they are today. If the cost is a concern, option 2 lets styles opt in.

Implementation

In textureLodCustom, when anisotropic filtering is enabled, sample with textureGrad(image, pos, dFdx(lod_coord), dFdy(lod_coord)), the derivatives of the continuous lod_coord rather than of the discontinuous pos, instead of folding them into a single LOD for textureLod; and set TEXTURE_MAX_ANISOTROPY_EXT on the pattern texture (created with useMipmap: hasPattern in image_atlas.ts). fill-pattern, fill-extrusion-pattern and fill outline patterns sample through the same function, so they would get it too.

Environment and repro

  • mapbox-gl-js 3.32.0
  • Chromium 152 on macOS, ANGLE Metal, Apple M1 Pro, device pixel ratio 2
<div id="map" style="width: 800px; height: 600px"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.32.0/mapbox-gl.js"></script>
<script>
  mapboxgl.accessToken = '<your token>'
  const map = new mapboxgl.Map({
    container: 'map',
    style: { version: 8, sources: { line: { type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [[-0.0012, -0.0004], [0.0012, 0.0004]] } } } }, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#fff' } }] },
    center: [0, 0], zoom: 18, bearing: 60, pitch: 70,
  })
  map.on('load', async () => {
    // A strip 16 times as long as it is tall with one chevron in its middle, so the chevrons repeat along
    // the line, at 2x, drawn 16 px wide: unscaled on a 2x screen when the map is flat.
    const chevron = (stroke, width) => `<path d="M252 7 L261 16 L252 25" fill="none" stroke="${stroke}" stroke-width="${width}" stroke-linecap="round" stroke-linejoin="round"/>`
    const image = new Image(512, 32)
    image.src = 'data:image/svg+xml,' + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="512" height="32" viewBox="0 0 512 32">${chevron('#fff', 9)}${chevron('#000', 4.5)}</svg>`)
    await image.decode()
    map.addImage('chevrons', image, { pixelRatio: 2 })
    map.addLayer({ id: 'line', type: 'line', source: 'line', paint: { 'line-pattern': 'chevrons', 'line-width': 16 } })
  })
  // Compare with pitch: 0, where the chevrons are as sharp as the image.
</script>
Dominant language
TypeScript
Stars
12.5k
Forks
2.4k
PR merge metrics
No merged PRs in 30d

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from mapbox/mapbox-gl-js

All issues in mapbox/mapbox-gl-js

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.