Marker‑Occlusion in Mapbox GL: HTML‑Marker hinter 3D‑Geschossen, Wänden und Regalen verbergen
HTML‑Marker in Mapbox GL liegen über dem WebGL‑Canvas, Extrusionen verdecken sie nicht. So verbergen wir sie mit einem analytischen Strahl‑Prisma‑Test.


Das Problem in einem Satz
Mapbox GL rendert Geschosse, Wände, Regale und 3D‑Modelle in einem einzigen WebGL‑Canvas, in dem der Depth Buffer entscheidet, welches Pixel gewinnt. Ein `mapboxgl.Marker` ist ein HTML‑Element, das über diesem Canvas liegt. Neigen Sie die Kamera über einem dreigeschossigen Markt, und jeder Erdgeschoss‑Marker schlägt durch die Decken darüber; schauen Sie senkrecht nach unten, sehen Sie die Marker aller Etagen gleichzeitig — ohne sie unterscheiden zu können.
Der naheliegende Ausweg ist die Marker‑Option `occludedOpacity`. Sie hilft hier nicht: Sie blendet Marker hinter 3D‑Terrain aus, und nur hinter Terrain.
Was man üblicherweise versucht (und warum es nicht reicht)
- `queryRenderedFeatures` bei jeder Kamerabewegung. Es sagt, welche Features ein Bildschirmpixel bedecken, nicht ob sie näher an der Kamera liegen als der Marker — und pro Marker und Frame ist es teuer.
- HTML‑Marker durch einen Symbol‑Layer ersetzen. Symbole werden tiefengetestet, aber Sie verlieren DOM‑Inhalte (React‑Icons, Hover, Zieh‑ und Transformationsgriffe) — und wie unten gezeigt ist der Tiefentest selbst für Dinge auf einem Boden unzuverlässig.
- Marker als extrudierte Kreise zeichnen. Sie landen im Depth Buffer, sehen aber aus wie zerdrückte Dosen und sind schwer anzuklicken.
Was wir eigentlich wollten: den DOM‑Marker unverändert lassen und ihn ausblenden, sobald feste Geometrie zwischen ihm und der Kamera liegt.
Die Lösung: ein Strahl, viele Prismen
Alles, was der Editor extrudiert, ist ein Prisma: ein Grundriss‑Ring, zwischen zwei Höhen gezogen. Aus „Liegt dieser Marker hinter etwas?“ wird also „Schneidet das Kamera→Marker‑Segment, beschnitten auf das Höhenband des Prismas, dessen Grundriss?“. Ein einziger Test deckt beide relevanten Fälle ab: Marker unter einer Decke (das Segment kreuzt den Grundriss eines höheren Geschosses) und Marker hinter einer Wand (das Segment kreuzt einen schmalen Grundriss im Höhenband der Kamera). Ein flaches Grundriss‑Raster ist nur das entartete Prisma, dessen Unterseite gleich der Oberseite ist.
Gerechnet wird im Mercator‑Raum, in dem eine konstante Höhe ein konstantes `z` ist; der Schnitt mit dem Höhenband kostet zwei Divisionen. Grundrisse kommen immer aus dem Generator der jeweiligen Form, sodass Drehung, Wandstärke und Bildzuschnitt exakt dem Gezeichneten entsprechen:
- Flächen und Kreise — ein Prisma von Höhe bis Höhe + Höhenmaß (Kreise im Rastermodus: eines pro Zelle).
- Wände — das Wandpolygon mit seiner Stärke; eine flache Polylinie hat kein Volumen und verdeckt nichts.
- Treppen und Aufzüge — ein Prisma pro Stufe und Podest, mit Drehung; Glasflächen werden übersprungen.
- Regale — ein Prisma pro Zelle (Reihe × Spalte × Ebene), damit ein vorderes Regal die Labels des Regals dahinter verdeckt; flache Zellen werden übersprungen.
- Grundrisse — das zugeschnittene Bildviereck (nicht die rohe Bounding Box) auf der Höhe des Plans.
- 1Kamera einmal lesen
`map.getFreeCameraOptions().position` liefert die Kamera in Mercator‑x/y/z.
- 2Segment bilden
Von der Kamera zur Position des Markers auf seiner eigenen Höhe.
- 3Auf das Band schneiden
Für jedes Prisma das Segment auf `[zBottom, zTop]` beschneiden. Schnittpunkte hinter dem Auge oder jenseits des Markers fallen weg — deshalb verdeckt Geometrie unter einem Marker ihn nie, wenn man nach unten schaut.
- 4Grundriss testen
Zuerst Bounding‑Box‑Ausschluss, dann: Liegt ein beschnittener Endpunkt im Ring, oder kreuzt das Segment eine Kante?
- 5Beim ersten Treffer stoppen
Ein Verdecker genügt; die Schleife bricht ab und die Marker‑ID kommt in die verdeckte Menge.
// Simplified from src/lib/marker-occlusion.ts — the real loop also handles a
// camera level with the marker, an AABB reject and the "embedded in a solid" skip.
const camera = map.getFreeCameraOptions().position! // read once
const p = mapboxgl.MercatorCoordinate.fromLngLat(marker.position, altitudeM)
const dx = p.x - camera.x, dy = p.y - camera.y, dz = p.z - camera.z
for (const prism of prisms) {
if (prism.topM <= altitudeM + 0.5) continue // standing on it, not behind it
// Clip the camera→marker segment to the prism's altitude slab: two divisions.
const ta = (prism.zBottom - camera.z) / dz
const tb = (prism.zTop - camera.z) / dz
let tLo = Math.min(ta, tb), tHi = Math.max(ta, tb)
if (tHi <= 0 || tLo >= 1) continue // behind the eye, or past the marker
tLo = Math.max(tLo, 0); tHi = Math.min(tHi, 1)
const ax = camera.x + tLo * dx, ay = camera.y + tLo * dy
const bx = camera.x + tHi * dx, by = camera.y + tHi * dy
if (segmentHitsRing(prism.ring, ax, ay, bx, by)) { // endpoint inside, or an edge crossed
occluded.add(marker.id)
break
}
}Der Durchlauf hängt am `move`‑Event von Mapbox — Verdeckung hängt von der Kamera ab, nicht von den Daten, und `move` feuert höchstens einmal pro Frame. Pro Marker und Prisma ist es ein Bounding‑Box‑Check und ein paar Divisionen, ohne GPU‑Roundtrip.
Das Ausblenden ist ein Data‑Attribut plus eine CSS‑Regel. Sie muss `!important` sein: `mapboxgl.Marker` überschreibt bei jeder Kamerabewegung im Rahmen seiner eigenen Nebel‑/Terrain‑Blende die Inline‑Werte `opacity` und `pointer-events`. Ein einfaches `el.style.opacity = "0"` wirkt kurz und wird dann stillschweigend zurückgesetzt.
/* Outside any Tailwind @layer: the attribute only ever exists at runtime. */
.shape-marker[data-occluded='true'] {
opacity: 0 !important;
pointer-events: none !important;
}

Regeln, die wir auf die harte Tour gelernt haben
- Durchscheinende Geometrie darf nicht verdecken. Ein oberes Geschoss auf 30 % zu stellen, um darunter zu schauen, ist ein echter Arbeitsablauf. Eine Form zählt nur als Verdecker, wenn sie sichtbar und mindestens 50 % deckend ist; ausgeblendete Layer und Etagen verdecken ebenfalls nicht mehr.
- Auf einer Fläche stehen heißt nicht dahinter liegen. Nichts, dessen Oberkante weniger als 0,5 m über der Höhe des Markers liegt, verdeckt ihn, und ein Marker wird nie vom Grundriss verdeckt, zu dem er gehört.
- Ein Marker in einem Körper ignoriert diesen Körper. Sonst flackert er beim Umkreisen. Der Test nutzt bewusst `>=` auf der Unterkante des Prismas: Ein Label auf 0 m in einem ab 0 m extrudierten Raum ist *im* Raum, nicht dahinter.
- Türöffnungen bleiben massiv. Wandprismen werden ohne Türöffnungen gebaut, ein Marker im Türrahmen wird also ausgeblendet statt zu flackern. Zu viel Verdeckung ist die sichere Richtung.
- Nichts nachbauen. Grundrisse mit demselben Generator erzeugen, mit dem der Layer zeichnet. Ein handgebauter Grundriss ist genau der Weg, auf dem ein Verdecker still vom Bildschirm abweicht.
Was wir bewusst nicht verdecken lassen
- GLB‑Modelle und Bildebenen — wir kennen nur eine Bounding Box; sie als Prisma zu behandeln würde Marker meterweit um ein großes Objekt verstecken.
- Aufzugs‑ und Rolltreppenglas — Glas ist durchsichtig und verdeckt nie etwas.
- Alles unter 50 % Deckkraft, ausgeblendet oder auf einer ausgeblendeten Etage — der Durchblick‑Workflow hat Vorrang.
Symbol‑Layer brauchen dieselbe Behandlung
Nicht alles auf der Karte ist ein DOM‑Marker. Textlabels, Regalzellen‑ und Türlabels sowie Live‑Datenpunkte aus Data Links sind native `symbol`‑Layer. Man würde erwarten, dass `text-occlusion-opacity` von Mapbox das erledigt. Zwei Dinge standen im Weg.
Eine fehlende Occlusion‑Opacity ist nicht der Standardwert. Die Style‑Spezifikation nennt `0` („bei Verdeckung ausblenden“) als Default für `text-occlusion-opacity`, aber die nächste Zeile sagt: Ein *fehlender* Wert bedeutet volle Verdeckung nur gegen Terrain. Bleibt die Eigenschaft ungesetzt, ignoriert der Layer Ihre Decken und Wände. Jeder Symbol‑Layer, der sich hinter Geometrie verstecken soll, muss die Eigenschaft ausdrücklich setzen.
Die Tiefenpräzision ist für Dinge auf dem Boden zu grob. Ein Label auf 0 m liegt knapp unter einer 15‑cm‑Flächenplatte oder einem 10‑cm‑Grundriss‑Raster. In der Draufsicht hielt der Tiefentest diese Labels für „hinter 3D‑Inhalt“ und zeichnete sie mit Deckkraft null — beim Neigen tauchten einige wieder auf. Der Textlabel‑Layer setzt deshalb `text-occlusion-opacity: 1` (Röntgenblick) und überlässt die Entscheidung dem analytischen Solver, der `feature-state.occluded` → `text-opacity: 0` schaltet. Regal‑ und Türlabels nutzen denselben Solver.
Live‑Datenpunkte bringen eine weitere Mapbox‑Tatsache mit: Ein `circle`‑Layer lässt sich weder anheben (kein `circle-z-offset`) noch verdecken. Jeder Punkt, jede Scheibe und jeder Ring wird deshalb als `symbol` mit einem `●`‑Glyph gezeichnet. Diese Layer behalten `text-occlusion-opacity: 0` und verrechnen das analytische Ergebnis in `text-opacity` — als Feature‑*Property*, weil `setData` den Feature‑State löscht —, sodass das Ausblenden bei jedem Zoom deterministisch ist.
// Data-link dot: a symbol with a '●' glyph, because a circle layer
// can neither be lifted (no circle-z-offset) nor occluded in Mapbox GL 3.25.
paint: {
'symbol-z-offset': ['coalesce', ['get', 'elevation'], 0],
// Name it explicitly — an ABSENT occlusion opacity only occludes against terrain.
'text-occlusion-opacity': 0,
// The analytic verdict, stamped into feature properties (setData wipes feature-state).
'text-opacity': ['case', ['boolean', ['get', 'occluded'], false], 0, 1],
}Wo Sie es in Spatial Map sehen
Im Viewer öffnet die Schaltfläche Quality den Dialog Graphics quality, in dem Hide items under floors (Elemente unter Geschossen ausblenden) standardmäßig aktiv ist; im Seitenpanel des Editors heißt derselbe Schalter Hide markers under floors. Die Einstellung wird in Ihrem Browser gespeichert. Ausgeschaltet entfällt der Durchlauf für die DOM‑Marker, und das flache Verhalten kehrt zurück, bei dem Marker über allem liegen — praktisch, wenn Sie alle Assets aller Etagen gleichzeitig sehen wollen. Siehe Grafik und Stil im Viewer.


Am wichtigsten ist das bei gestapelten Gebäuden: einem mehrgeschossigen Markt mit Rolltreppen und Aufzügen (siehe Aufzüge und Rolltreppen), einem Einkaufszentrum oder einem mehrgeschossigen Bürogebäude, wo die Draufsicht die betrachtete Etage zeigen soll und nicht die Pins aller Etagen auf einmal. Mit der Etagenauswahl aus Layer und Etagen isolieren Sie eine Ebene.
Stolperfallen
Testen Sie die Plattform, auf die Ihre Daten gehören.
Spatial Map verwandelt Tabellen, Sensor‑Streams und CAD‑Zeichnungen in eine lebende 3D‑Ansicht Ihrer Standorte — nichts zu installieren, sieben Tage kostenfrei.
Weiterlesen
- VertiefungIndoor-Wayfinding-Software: Wegbeschreibung Schritt für Schritt auf Ihrem GrundrissGPS endet an der Eingangstür. So baut Spatial Map Gehwege aus dem Grundriss, den Sie ohnehin pflegen, gibt Besuchern im Viewer Schritt-für-Schritt-Anweisungen – und so ehrlich steht es heute um Etagen, Aufzüge und Barrierefreiheit.
- VertiefungGrundriss zum Digital Twin per KI: was kommt und wie es heute gehtKI, die aus einem Grundriss Räume und Wände macht, ist die meistgewünschte Abkürzung in Digital-Twin-Projekten. Unsere, AI Map, ist in Entwicklung und noch nicht nutzbar. Hier steht, was sie können soll – und wie Sie heute zum selben Ergebnis kommen.
- VertiefungAMR-/AGV-Navigation und Ihre Standortkarte: was Spatial Map einem Robotik-Team liefert – und was nichtEine Roboterflotte braucht keine neue Zeichnung. Sie braucht die Wände, Türen, Regale und Gänge, die Sie schon gezeichnet haben — als GeoJSON exportiert —, einen Routen-Check im Browser und eine Karte, die zeigt, wo die Roboter sind. Steuern bleibt Sache des Flottenmanagers.