Analyse approfondiemapbox-gl3Docclusion

Occlusion des marqueurs Mapbox GL : cacher les marqueurs HTML derrière étages, murs et rayonnages 3D

Les marqueurs HTML de Mapbox GL passent au‑dessus du canvas WebGL : les extrusions ne les cachent pas. Notre test analytique rayon caméra contre prismes.

9 min de lecturePublié Mis à jour Par Spatial Map team
Retail Mart Store en 3D vue en biais : rayonnages, murs et escalators avec pulsations des lecteurs RFID, badges de stock et marqueurs posés à l’altitude de leur étage

Le problème en une phrase

Mapbox GL dessine étages, murs, rayonnages et modèles 3D dans un seul canvas WebGL, où le depth buffer décide quel pixel l’emporte. Un `mapboxgl.Marker` est un élément HTML posé par‑dessus ce canvas. Inclinez la caméra au‑dessus d’un magasin de trois niveaux : chaque marqueur du rez‑de‑chaussée traverse les dalles du dessus. Regardez à la verticale : vous voyez les marqueurs de tous les étages à la fois, sans moyen de les distinguer.

L’échappatoire tentante est l’`occludedOpacity` du Marker lui‑même. Elle n’aide pas ici : elle estompe les marqueurs derrière le terrain 3D, et seulement le terrain.

Ce qu’on essaie d’habitude (et pourquoi ça ne suffit pas)

  • `queryRenderedFeatures` à chaque mouvement de caméra. Il indique quelles features couvrent un pixel, pas si elles sont plus proches de la caméra que le marqueur — et l’appeler par marqueur et par frame coûte cher.
  • Remplacer les marqueurs HTML par une couche symbol. Les symbols passent le test de profondeur, mais vous perdez le contenu DOM (icônes React, survol, poignées de déplacement et de transformation) — et, comme on le verra, le test de profondeur lui‑même n’est pas fiable pour ce qui est posé sur un sol.
  • Dessiner les marqueurs en cercles extrudés. Ils entrent dans le depth buffer, mais ressemblent à des canettes écrasées et sont difficiles à cliquer.

Ce que nous voulions vraiment : garder le marqueur DOM tel quel et le masquer dès qu’une géométrie solide se trouve entre lui et la caméra.

La solution : un rayon, beaucoup de prismes

Tout ce que l’éditeur extrude est un prisme : un anneau d’empreinte balayé entre deux altitudes. « Ce marqueur est‑il derrière quelque chose ? » devient donc « le segment caméra→marqueur, découpé à la tranche d’altitude du prisme, traverse‑t‑il son empreinte ? ». Un seul test couvre les deux cas utiles : un marqueur sous une dalle (le segment traverse l’empreinte d’un étage supérieur) et un marqueur derrière un mur (le segment traverse une empreinte fine couvrant la tranche d’altitude de la caméra). Un plan raster plat n’est que le prisme dégénéré dont la base égale le sommet.

Tous les calculs se font en espace Mercator, où une altitude constante est un `z` constant : le découpage à la tranche tient en deux divisions. Les empreintes viennent toujours du générateur propre à chaque type, si bien que rotation, épaisseur de mur et recadrage d’image correspondent à ce qui est réellement peint :

  • Zones et cercles — un prisme de l’élévation à élévation + hauteur (cercles en mode grille : un par cellule).
  • Murs — le polygone du mur avec son épaisseur ; une polyligne plate n’a pas de volume et ne cache rien.
  • Escaliers et ascenseurs — un prisme par marche et par palier, rotation appliquée ; les vitres sont ignorées.
  • Rayonnages — un prisme par cellule (rangée × colonne × niveau), si bien qu’un rayonnage avant cache les étiquettes de celui de derrière ; les cellules plates sont ignorées.
  • Plans — le quadrilatère recadré de l’image (pas sa bounding box brute) à l’élévation du plan.
  1. 1
    Lire la caméra une fois

    `map.getFreeCameraOptions().position` donne la caméra en x/y/z Mercator.

  2. 2
    Construire le segment

    De la caméra jusqu’à la position du marqueur, à sa propre altitude.

  3. 3
    Découper à la tranche

    Pour chaque prisme, découpez le segment à `[zBottom, zTop]`. Les intersections derrière l’œil ou au‑delà du marqueur sont écartées — c’est pourquoi la géométrie située sous un marqueur ne le cache jamais quand on regarde vers le bas.

  4. 4
    Tester l’empreinte

    Rejet par bounding box d’abord, puis : une extrémité découpée est‑elle dans l’anneau, ou le segment croise‑t‑il une arête ?

  5. 5
    S’arrêter au premier impact

    Un seul occulteur suffit : la boucle s’interrompt et l’id du marqueur rejoint l’ensemble occulté.

ts·Le découpage à la tranche (simplifié)
// 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
  }
}

Le calcul tourne sur l’événement `move` de Mapbox : l’occlusion dépend de la caméra, pas des données, et `move` se déclenche au plus une fois par frame. Par marqueur et par prisme, c’est un test de bounding box et quelques divisions, sans aller‑retour GPU.

Masquer, c’est un attribut data et une règle CSS. Elle doit être `!important` : `mapboxgl.Marker` réécrit l’`opacity` et le `pointer-events` en ligne de l’élément pendant son propre fondu brouillard/terrain à chaque mouvement de caméra. Un simple `el.style.opacity = "0"` fonctionne un instant puis revient silencieusement.

css·index.css
/* Outside any Tailwind @layer: the attribute only ever exists at runtime. */
.shape-marker[data-occluded='true'] {
  opacity: 0 !important;
  pointer-events: none !important;
}
Retail Mart Store, trois niveaux vus en biais — marqueurs, badges de rayonnage et pulsations des lecteurs RFID à l’altitude de leur étage, entre murs, rayonnages et escalators
Aperçu Spatial Map de Retail Mart Store en 3D : zones de rayon rouges, orange et vertes avec rayonnages, escalators au centre, pulsations violettes des lecteurs RFID et badges de stock verts au‑dessus des rayonnages
Aperçu Spatial Map de Retail Mart Store en 3D : zones de rayon rouges, orange et vertes avec rayonnages, escalators au centre, pulsations violettes des lecteurs RFID et badges de stock verts au‑dessus des rayonnages

Les règles apprises à nos dépens

  • Une géométrie translucide ne doit pas occulter. Passer un étage supérieur à 30 % pour regarder celui du dessous est un vrai usage. Une forme ne compte comme occulteur que si elle est visible et opaque à au moins 50 % ; les calques et étages masqués n’occultent plus.
  • Être posé sur une surface, ce n’est pas être derrière. Rien dont le sommet est à moins de 0,5 m de l’altitude du marqueur ne le cache, et un marqueur n’est jamais caché par le plan auquel il appartient.
  • Un marqueur dans un solide ignore ce solide. Sinon il clignote quand la caméra tourne. Le test utilise volontairement `>=` sur la base du prisme : une étiquette à 0 m dans une pièce extrudée depuis 0 m est *dans* la pièce, pas derrière.
  • Les portes restent pleines. Les prismes de mur sont construits sans leurs ouvertures : un marqueur cadré dans une porte est masqué au lieu de clignoter. Sur‑occulter est la direction sûre.
  • Ne rien redériver. Construisez les empreintes avec le même générateur que celui qui peint le calque. Une empreinte faite à la main, c’est l’occulteur qui dérive en silence de ce qui est à l’écran.

Ce que nous n’occultons volontairement pas

  • Modèles GLB et plans d’image — nous n’en connaissons qu’une bounding box, et la traiter comme un prisme cacherait des marqueurs à des mètres d’un gros objet.
  • Vitrages d’ascenseurs et d’escalators — le verre est transparent, il ne cache jamais rien.
  • Tout ce qui est sous 50 % d’opacité, masqué ou sur un étage masqué — l’usage translucide l’emporte.

Les couches symbol ont besoin du même traitement

Tout n’est pas un marqueur DOM sur la carte. Les étiquettes de texte, celles des cellules de rayonnage et des portes, et les points de données en direct des data links sont des couches `symbol` natives. On s’attendrait à ce que `text-occlusion-opacity` de Mapbox s’en charge. Deux choses s’y sont opposées.

Une opacité d’occlusion absente n’est pas la valeur par défaut. La spécification de style donne `0` (« masquer si occulté ») comme défaut de `text-occlusion-opacity`, mais la ligne suivante précise qu’une valeur *absente* signifie une occlusion complète contre le terrain uniquement. Sans la définir, la couche ignore vos dalles et vos murs. Toute couche symbol qui doit se cacher derrière la géométrie doit nommer la propriété explicitement.

La précision de profondeur est trop grossière pour ce qui est posé au sol. Une étiquette à 0 m se trouve juste sous une dalle de zone de 15 cm ou un raster de plan de 10 cm. En vue de dessus, le test de profondeur jugeait ces étiquettes « derrière du contenu 3D » et les peignait à opacité nulle, puis en laissait réapparaître certaines à l’inclinaison. La couche d’étiquettes fixe donc `text-occlusion-opacity: 1` (rayons X) et laisse décider le solveur analytique, qui bascule `feature-state.occluded` → `text-opacity: 0`. Les étiquettes de rayonnages et de portes utilisent le même solveur.

Les points de données en direct ajoutent un fait Mapbox : une couche `circle` ne peut ni être élevée (pas de `circle-z-offset`) ni occultée. Chaque point, disque et anneau est donc dessiné en `symbol` avec un glyphe `●`. Ces couches gardent `text-occlusion-opacity: 0` et intègrent le verdict analytique dans `text-opacity` — sous forme de *propriété* de feature, car `setData` efface le feature‑state — pour un masquage déterministe à tous les niveaux de zoom.

ts·Un point de data link capable de se cacher sous un étage
// 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],
}

Où le voir dans Spatial Map

Dans la visionneuse, le bouton Quality ouvre Graphics quality, où Hide items under floors (masquer les éléments sous les étages) est activé par défaut ; le panneau latéral de l’éditeur appelle le même interrupteur Hide markers under floors. Le réglage est enregistré dans votre navigateur. Le désactiver coupe le calcul pour les marqueurs DOM et rétablit le comportement à plat où les marqueurs passent au‑dessus de tout — pratique pour voir tous les équipements de tous les étages d’un coup. Voir Graphismes et style dans la visionneuse.

La boîte de dialogue Graphics quality de la visionneuse — 3D shadows, Ambient occlusion et Hide items under floors (activé)
Boîte de dialogue Graphics quality sur une carte 3D floutée : 3D shadows désactivé, Ambient occlusion désactivé, Hide items under floors activé, bouton Done
Boîte de dialogue Graphics quality sur une carte 3D floutée : 3D shadows désactivé, Ambient occlusion désactivé, Hide items under floors activé, bouton Done

C’est sur les bâtiments empilés que cela compte le plus : un magasin sur plusieurs niveaux avec escalators et ascenseurs (voir ascenseurs et escalators), un centre commercial ou un immeuble de bureaux sur plusieurs niveaux, où la vue de dessus doit montrer l’étage regardé et non les épingles de tous les étages. Utilisez le sélecteur d’étages décrit dans calques et étages pour isoler un niveau.

Pièges

Essayez la plateforme que méritent vos données.

Spatial Map transforme les tableurs, les capteurs et les plans CAO en une vue 3D en direct de vos sites — rien à installer, essai gratuit de sept jours.

À lire ensuite