Skip to content

feat: data driven line opacity shading fix - #7514

Closed
CommanderStorm wants to merge 41 commits into
maplibre:mainfrom
CommanderStorm:line-oppacity-shading2
Closed

feat: data driven line opacity shading fix#7514
CommanderStorm wants to merge 41 commits into
maplibre:mainfrom
CommanderStorm:line-oppacity-shading2

Conversation

@CommanderStorm

@CommanderStorm CommanderStorm commented Apr 24, 2026

Copy link
Copy Markdown
Member

Second part of the line-oppacity during overlap fix, this time for data-driven styling.
There are more tradeoffs here of "how should this work", here is the one I came up with:

image image

Opus helped a bit with finding all the places that needed tweaking.

@CommanderStorm CommanderStorm changed the title feat: data driven line oppacity shading o feat: data driven line oppacity shading fix Apr 24, 2026
@CommanderStorm

Copy link
Copy Markdown
Member Author

that is interesting, because running the modified benchmark above only shows 30% (20ms -> 30ms).

I might not entirely understand how to run the benchmarks maybe.

@CommanderStorm

Copy link
Copy Markdown
Member Author

americana

Well.. that seems to be an outlier in terms of how the style is written. CC @1ec5

Of 356 layers, 285 of 301 line layers have zoom-transitionable oppacity.
That is basically the benchmark you wrote..
So the performance impact is because they always do data driven styling..

For the other styles, I would not expect such a large impact given that americana is a bit out there in many metrics (complexity, ...)

@lucaswoj

lucaswoj commented May 3, 2026

Copy link
Copy Markdown
Contributor

Running this benchmark is a little tricky. Since it's not already included on main, we can't use --compare. Here's a little AI slop script that should walk you through it:

#!/usr/bin/env bash
# bench-americana.sh
# Run from inside any maplibre-gl-js git checkout. Stash any uncommitted work
# under test/bench/ first.

set -euo pipefail
cd "$(git rev-parse --show-toplevel)"

ZOOMS=(4 6 8 10 12 14 16 18 20)

if [ -n "$(git status --porcelain -- test/bench/)" ]; then
    echo "Uncommitted changes under test/bench/. Stash them first:"
    echo "    git stash push --include-untracked test/bench/"
    exit 1
fi

ORIG_REF=$(git symbolic-ref -q --short HEAD || git rev-parse HEAD)

echo "Fetching maplibre/maplibre-gl-js@main..."
git fetch --quiet https://github.com/maplibre/maplibre-gl-js.git main
MAIN_REF=$(git rev-parse FETCH_HEAD)

echo "Fetching CommanderStorm/maplibre-gl-js@line-oppacity-shading2 (#7514)..."
git fetch --quiet https://github.com/CommanderStorm/maplibre-gl-js.git line-oppacity-shading2
PR_REF=$(git rev-parse FETCH_HEAD)

echo "  main: $MAIN_REF"
echo "  pr:   $PR_REF"

SERVER_PID=""
if curl -fs --max-time 1 http://localhost:9966 >/dev/null 2>&1; then
    echo "Reusing existing server on :9966"
else
    echo "Starting static file server on :9966..."
    npm run start-server >/tmp/bench-americana-server.log 2>&1 &
    SERVER_PID=$!
    for _ in $(seq 1 60); do
        if curl -fs --max-time 1 http://localhost:9966 >/dev/null 2>&1; then break; fi
        sleep 0.5
    done
fi

cleanup() {
    set +e
    rm -f test/bench/benchmarks/basemap_paint.ts
    git checkout HEAD -- test/bench/versions/index.ts 2>/dev/null
    git checkout --quiet "$ORIG_REF" 2>/dev/null
    [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null && wait "$SERVER_PID" 2>/dev/null
}
trap cleanup EXIT

run_one() {
    local REF="$1" LABEL="$2"
    echo
    echo "===== $LABEL ====="
    git checkout --quiet "$REF"

    if grep -q "from '../lib/benchmark.ts'" test/bench/benchmarks/paint.ts; then EXT=".ts"; else EXT=""; fi

    {
        echo "import Paint from './paint${EXT}';"
        echo "const STYLE = 'https://americanamap.org/style.json';"
        echo "const CENTER: [number, number] = [-73.9857, 40.7484];"
        echo "class BPA extends Paint { constructor(zoom: number) { super(STYLE, [{center: CENTER, zoom}]); } }"
        for Z in "${ZOOMS[@]}"; do
            echo "export class BasemapPaintAmericanaZ${Z} extends BPA { constructor() { super(${Z}); } }"
        done
    } > test/bench/benchmarks/basemap_paint.ts

    EXT="$EXT" ZOOMS="${ZOOMS[*]}" python3 <<'PY'
import os, re
ext = os.environ['EXT']
zooms = os.environ['ZOOMS'].split()
classes = [f'BasemapPaintAmericanaZ{z}' for z in zooms]
imp = f"import {{{', '.join(classes)}}} from '../benchmarks/basemap_paint{ext}';"
regs = '\n'.join(f"register('{c}', new {c}());" for c in classes)
with open('test/bench/versions/index.ts') as f: s = f.read()
s = re.sub(r"(import \{Terrain3DGlobe[^\n]*\n)", lambda m: m.group(1) + imp + '\n', s, count=1)
s = re.sub(r"(register\('Terrain2DMercator'[^\n]*\n)", lambda m: m.group(1) + regs + '\n', s, count=1)
with open('test/bench/versions/index.ts', 'w') as f: f.write(s)
PY

    echo "Building..."
    npm run build-benchmarks >/dev/null 2>&1

    NAMES=()
    for Z in "${ZOOMS[@]}"; do NAMES+=("BasemapPaintAmericanaZ${Z}"); done
    npm run benchmark -- --compare=main "${NAMES[@]}" 2>&1 | tail -$((${#ZOOMS[@]} + 2))

    rm -f test/bench/benchmarks/basemap_paint.ts
    git checkout --quiet HEAD -- test/bench/versions/index.ts
}

run_one "$MAIN_REF" "main"
run_one "$PR_REF" "CommanderStorm:line-oppacity-shading2 (#7514)"

echo
echo "Done."

@CommanderStorm

Copy link
Copy Markdown
Member Author

A new layer-opacity opts into the new compositing behavior, layer-level

Migrating to lumagl is a large body of work and something that I don't think I have the time for.
I think this is what you are asking..
Maybe this is simpler, but as far as I read migrating to the different blending modes like adding or subtracting is a loot of work..

So to maybe ask:
What is the difference between line-opacity and a color with an oppcicity? How SHOULD this work together?
3 different settigns to control the same thing seems very complicated..

@lucaswoj

lucaswoj commented May 3, 2026

Copy link
Copy Markdown
Contributor

Migrating to lumagl is a large body of work and something that I don't think I have the time for.

Sorry for the confusion, layer-opacity (#3062, mapbox/mapbox-gl-js#4090) doesn't require luma.gl. It can be done with WebGL 1 features alone. The approach is the same as today's raster-opacity: render the layer to an offscreen FBO, then composite onto the main framebuffer at the requested opacity. It's also roughly analogous to CSS's opacity.

What is the difference between line-opacity and a color with an opacity? 3 different settings to control the same thing seems very complicated.

There's no difference between line-opacity and the alpha channel of line-color. They've always been equivalent. It's a little redundant, but they're core features nearly every user of GL JS relies on, so we can't change them without breaking a lot of existing styles.

layer-opacity would be a new, opt-in property that works across all layer types. The layer renders as it does today, then the full result gets composited at the requested opacity. Users who want the OSM Carto effect set layer-opacity and pay the offscreen-FBO cost. Users who don't get no behavior change and no perf regression.

One more alternative worth weighing: rewrite the line-join geometry to eliminate overlapping triangles within a feature, while keeping overlap across features identical. That'd address the motivating OSM Carto case directly, with no breaking change, no opt-in, and likely minimal perf impact. I haven't dug into the join code yet so I can't say how hard it'd be.

@lucaswoj

lucaswoj commented May 3, 2026

Copy link
Copy Markdown
Contributor

americana is a bit out there in many metrics

There are other styles that use enough data-driven line-opacity to experience measurable regressions. I've been telling folks for years to use data-driven styling to reduce their layer count for better performance! Another style that comes to mind is Swisstopo (the official Swiss federal cartography product) which has 27 feature-driven line-opacity layers. I'm measuring up to a 2x regression on that style.

@HarelM

HarelM commented May 3, 2026

Copy link
Copy Markdown
Collaborator

To be honest, I would like to avoid adding a new style spec property for this if possible.
@lucaswoj do you think there might be a way to solve this using smart stencil manipulation to improve the performance?

@lucaswoj

lucaswoj commented May 3, 2026

Copy link
Copy Markdown
Contributor

The more fundamental fix I mentioned doesn't involve the stencil buffer. The source of self-intersections is in how we build line geometry: at every corner and miter, we generate overlapping triangles. If we change the way line corners are constructed so the pieces don't overlap, the artifacts disappear with no extra rendering pass and minimal perf cost. I think that's worth investigating, but I'd set expectations: this is a ~10-year-old known issue and nobody's tackled it, so it's probably harder than it sounds.

Even if we fix the geometry, we'd only solve overlap within a single line feature. Overlap between different lines, and the broader "fade a whole layer" effect, still need something else. That's where I think layer-opacity is the right shape:

  • It's a new feature, not a workaround. It's been on the wishlist for years (Add a per-layer opacity property #3062, Add a per-layer opacity property mapbox/mapbox-gl-js#4090) and would enable hover fades, layer toggle animations, atmospheric depth, and the OSM Carto flat-road effect with a single property.
  • It's small to build. We already do the same off-screen rendering for raster-opacity. Extending it to other layer types is mostly plumbing.
  • The perf cost only applies to users who opt in, the same way CSS opacity works.

I hear you on not wanting to grow the spec. But this isn't growing it to patch one bug. It's adding one well-understood property that covers this case plus a long wishlist of others, without breaking existing styles or imposing cost on users who don't opt in.

@CommanderStorm

Copy link
Copy Markdown
Member Author

would be a new, opt-in property that works across all layer types

I can only introduce this one layer at a time, when I need it for capacity reasons.
Sorry.

@lucaswoj

lucaswoj commented May 3, 2026

Copy link
Copy Markdown
Contributor

would be a new, opt-in property that works across all layer types

I can only introduce this one layer at a time, when I need it for capacity reasons. Sorry.

No problem! It's not all on your shoulders. I'm happy to chip in some dev time once we agree on a path forward.

@HarelM

HarelM commented May 4, 2026

Copy link
Copy Markdown
Collaborator

I believe we should start a discussion in the style-spec repo if we want to introduce a new feature to the spec.
From a spec perspective, I think it's a confusion addition. The fact that you need an image to explain the different possibilities is a smell that it's not straight forward. So I'll probably advocate against it. But maybe the community would think otherwise, and that's the beauty of it 😀
I'm sure you know that without a spec change this can't be introduced (it doesn't mean it can't be implemented and tested, just not merged).

@HarelM

HarelM commented May 4, 2026

Copy link
Copy Markdown
Collaborator

As a side-note, while the geometry approach might solve the places where lines change direction and overlap itself I don't think it can solve places where the line "crosses itself".
The main two example in the first PR to address this will not be solved by geometry changes as far as I understand (correct me if I'm wrong):

Smart stencil logic might be an interesting approach to pursue, according to AI.

@lucaswoj

lucaswoj commented May 4, 2026

Copy link
Copy Markdown
Contributor

On the Diagram

image

We're shipping this diagram no matter what!

The diagram shows two "flavors" of opacity:

  • flavor A: (per-feature) opacity, each feature has its own opacity, then they blend
  • flavor B: per-layer opacity, composite the whole layer at one opacity

In this PR, line-color becomes flavor B, line-opacity becomes flavor A. Existing styles will, without warning, find themselves randomly, without warning, in some row of this diagram. Now that's confusing!

If we add layer-opacity, existing styles will be backwards compatible. They can opt into flavor B using a new property.

On Community Signal

mapbox/mapbox-gl-js#4090 is the 5th most-commented issue on the mapbox-gl-js repo. That's the strongest data we have and it's strong. Our community isn't as old or large, so without some maintainer support behind a proposal here I don't think we'd get comparable independent data in our repo before v6 ships.

On Fixing Tessellation

You're right that re-tessellation only fixes overlap within a single feature (corners, miters, U-turns). It doesn't fix a line that crosses itself, and it doesn't fix two different features overlapping. Both of those still need flavor B opacity. I want to push back on the idea that tessellation fixes aren't worth pursuing though. It would fix many real-world cases (the OSM Carto motivating example, road casings with sharp bends) with zero spec change and lower perf cost.

On Stencils

We could use a stencil if and only if we drop support for data-driven "flavor B" opacity. Stencils can make uniform opacity work but it can't compose two different alpha values at one pixel. A stencil-based #7490 would might have somewhat better perf but would stricter (line-opacity must be a constant).

@lucaswoj

lucaswoj commented May 4, 2026

Copy link
Copy Markdown
Contributor

On Community Signal

Per-layer opacity has come up within the Maplibre issue tracker rather often

@HarelM

HarelM commented May 4, 2026

Copy link
Copy Markdown
Collaborator

For upgrading between version 5 and version 6 we started a migration guide so people who are upgrading can read it and do the relevant changes if need be.
For the long run, I think it's better to keep the spec simpler and treat this as a bug fix instead of introducing a new property.

The main issue that I would like see solved is the following:

This is, from my perspective, what users expect when they set line-opacity.

Having said all that, a performance degradation is a problem we can't dismiss.

@CommanderStorm

CommanderStorm commented May 4, 2026

Copy link
Copy Markdown
Member Author

@HarelM lets just discuss this on our TSC.
No spec proposal nessary, the great graphic is fine to have a discussion on.

@1ec5

1ec5 commented May 4, 2026

Copy link
Copy Markdown
Contributor

For the other styles, I would not expect such a large impact given that americana is a bit out there in many metrics (complexity, ...)

osm-americana/openstreetmap-americana#995 (based on this fork) is much more representative of a typical production style, making heavier use of data-driven styling on the road layers for less complexity. We still haven’t optimized the railway layers to take advantage of #5812: osm-americana/openstreetmap-americana#526. But I suspect a significant performance hit there would be indicative of a broader problem.

I had @lucaswoj run the same benchmarks on this branch. He found that the impact was less severe but still notable. Here are the per-frame stats he posted in Slack:

main #7514 slow
Production Americana, z8 1.40 ms 4.86 ms
Production Americana, z14 2.13 ms 8.69 ms
Production Americana, z20 0.90 ms 8.26 ms
Road rewrite (osm-americana/openstreetmap-americana#995), z14 1.68 ms 4.87 ms
Road rewrite (osm-americana/openstreetmap-americana#995), z20 0.73 ms 4.08 ms

Even though OSM Americana’s complexity seems comical, I’ve definitely seen more complex Mapbox styles in the wild and have no reason to believe it doesn’t exist among MapLibre styles too.

Another good data point would be the three MapLibre styles currently deployed on the osm.org homepage: Transport Map, Shortbread (VersaTiles Colorful), and MapTiler OMT (OSM OpenMapTiles). OpenHistoricalMap also has several styles in production. Some of these styles still have a lot of gaps that developers will want to fill in the future, so it’s important to have some slack, performance-wise.

Taking a step back, I hope my comments in #7490 (comment) aren’t taken as a carte blanche on the performance side. I think designers are desperate enough for a solution to that problem to be open to some performance degradation, but the tradeoff will depend on the particular stylesheet and application. Also, note that the problem isn’t just about overlaps; designers care just as much about the spillover effect, which is not addressed by this change.

@xabbu42

xabbu42 commented May 8, 2026

Copy link
Copy Markdown
Collaborator

I'd like to support @lucaswoj that a layer-opacity would be the better fix (but I do not currently have the bandwidth to push this forward myself, so I just add my 2 cents here instead of opening an issue maplibre-style-spec). Consider the following use-case which uses all three properties in a natural way:

Visualizing a large number of gps tracks from different sport activities so that hotspots and mixed-use ways are clearly recognizable but the base map is never completely covered by the data.

A very straightforward style to do that would use:

  • a data-driven line-color without opacity to differentiate between the activities
  • a constant line-opacity to control the range of opacity between rarely used ways and hotspots
  • a constant layer-opacity to control the maximum opacity and therefore how visible the basemap is behind hotspots

@sjg-wdw

sjg-wdw commented May 13, 2026

Copy link
Copy Markdown

Rather than slipping in an offscreen render step behind the scenes, why not make it explicit?
If you introduced the idea of render targets into the style spec, you could get this functionality very cleanly... and a lot more.

In response to a question of @nyurik's a couple years ago, I sketched out a design for doing more complex multi-pass styles. That's basically what you're doing here, but just for transparent lines.

Muti-pass rendering is how most games work, helpful for functionality like shadows, data rendering and definitely where things are going in the future. You could get this improvement and open up a whole new area of cartographic experimentation.

@CommanderStorm

Copy link
Copy Markdown
Member Author

I have no clue what this would mean and I don't want to do render targets.
That seems fairly complicated and I have no clue how to implement that.

I don't want to add targets and operations and this sort of thing for a simple fix like this

@sjg-wdw

sjg-wdw commented May 13, 2026

Copy link
Copy Markdown

I have no clue what this would mean and I don't want to do render targets. That seems fairly complicated and I have no clue how to implement that.

I don't want to add targets and operations and this sort of thing for a simple fix like this

You're already doing them. That's what an offscreen buffer is.
The difference is you're hiding it from the developer.

Ignore the parts of my Stamen Watercolor post that discuss operations on the buffer. That's just what would be necessary for effects. The core idea is simple:

  • Render a layer to an offscreen buffer, as you already are
  • Take that buffer and render it to the screen with a color and opacity

The extra part is making it explicit in the style sheet.

@lucaswoj

lucaswoj commented May 13, 2026

Copy link
Copy Markdown
Contributor

+1 to render targets. They're effectively the same as what we've been calling "blending" or "comp-ops," which is a much larger discussion than line-opacity, but undoubtedly worth considering long term. See #48 #1191 mapbox/mapbox-gl-js#380

@sjg-wdw

sjg-wdw commented May 13, 2026

Copy link
Copy Markdown

+1 to render targets. They're effectively the same as what we've been calling "comp-ops," which is a much larger discussion than line-opacity, but undoubtedly worth considering long term. See #1191 mapbox/mapbox-gl-js#380

One of the reasons I run the Native Development Group for Amazon is I have a non-M*pbox perspective on map rendering. I wasn't aware they'd discussed doing something similar and I'm going to avoid looking at it.

The concept of render targets is something we use for weather rendering, but we didn't invent it. It's a pretty basic concept, not likely to infringe anyone's IP.

@lucaswoj

Copy link
Copy Markdown
Contributor

Fair to keep your perspective independent of Mapbox. That said, there's a substantial MapLibre-side discussion on render targets and per-layer compositing that's worth engaging with before sketching new designs, none of which touches Mapbox prior art:

Whatever shape we land on will be stronger if it builds on the tradeoffs the community has already worked through here.

@CommanderStorm

CommanderStorm commented May 13, 2026

Copy link
Copy Markdown
Member Author

I still don't understand what a render target is for a map designer.
I undestand blending and that sort of stuff though.

I think that this PR seems to not have the support it would need to get merged.
So lets close this PR

@sjg-wdw

sjg-wdw commented May 14, 2026

Copy link
Copy Markdown

I still don't understand what a render target is for a map designer.

Well it's a compromise for real-time rendering. A cartographer might not give it much thought unless they were doing something very experimental (e.g. Stamen Watercolor). It might normally just be an implied operation on a layer that requires a separate render step. Mapnik did stuff like this under the hood if you picked the right operations.

The problem with just doing it by implication is the cost. Setting up a separate rendering step is expensive and you really need to let the map designer control its resolution, how it's blended back in and what goes in to it. If they care about performance at all, that is.

For image based maps, they did things like this quite a bit, I believe. They'd form layers with a variety of content and then composite them after the fact. In a sense, it's a return to older, richer techniques.

@HarelM

HarelM commented May 14, 2026

Copy link
Copy Markdown
Collaborator

I don't think render target is the right approach. You, @sjg-wdw and @lucaswoj, are looking at render target like it's a second nature. Most users won't. I don't think the style spec should be overloaded with graphic engineering concepts.
The target audience are not graphics engineers, the target audience are not even software engineers, so I'm finding this concept far from something that should be embedded in the style spec language.

@1ec5

1ec5 commented May 14, 2026

Copy link
Copy Markdown
Contributor

For what it’s worth, I’ve mainly seen a desire for compositing operations from non-engineers used to similar operations in graphic design. The operations fit more naturally in a raster graphics environment than a vector one, so the requests will naturally come out of things like wanting to implement Watercolor with vector tiles.

Anyways, a general solution is much more substantial than what @CommanderStorm started out with. As far as I could tell from the monthly meeting this morning, a lot of the unease with the proposed change would be addressed by making behavior opt-in somehow. Is there a way to express the one case that he’s improving without necessarily having to implement the whole feature? Or is the whole feature ultimately easier to implement than making the one case intuitive to developers and designers?

@HarelM

HarelM commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Let's keep the conversation in the newly created discussion.

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.

6 participants