maplibre / maplibre-gl-js

MapLibre GL JS - Interactive vector tile maps in the browser
https://maplibre.org/maplibre-gl-js/docs/
Other
6.47k stars 694 forks source link

3d models are not correctly hidden using the globe view #4817

Open ibesora opened 6 days ago

ibesora commented 6 days ago

When using a 3d model on the globe view, the model is not correctly occluded when it goes to the back side of the globe.

https://github.com/user-attachments/assets/825a4dd4-0564-4205-8fe8-15c54aa41ee5

maplibre-gl-js version: 5.0.0-pre.1

browser: Chrome

Steps to Trigger Behavior

Using the globe-3d-model example

  1. Move the camera to make the 3d model get behind the horizon

Expected Behavior

The 3d model should be occluded

Actual Behavior

You can see the 3d model right through the globe

HarelM commented 6 days ago

I believe this should be handled by the custom layer code, shouldn't it? Or is this issue about updating the example's code?

kubapelc commented 2 days ago

This is caused by MapLibre in general not having a depth buffer that stores actual depth values. Instead, depth buffer is used to minimize overdraw of transparency: opaque parts of all layers are drawn first, each using a constant depth value, then transparency is drawn while testing against this depth buffer. Layers higher in the stack get depth values closer to the camera, so if a lower transparent layer is covered by a higher opaque layer, the transparent layer's pixels get rejected by depth testing and overdraw is avoided.

These depth values are allocated near 1, from the far part of the depth range, so that drawing fill extrusion and 3D custom models works - these will occlude themselves properly. This also means that extrusion overwrites the depth buffer values the layers used, but this is okay, since fill extrusion will always render on top of the rest of the map.

This stopped being true with globe, since 3D objects can now be occluded by the map itself.

Fill extrusion currently solves this by doing globe occlusion in the pixel shader by computing a ray-sphere collision and discarding the pixel if it is occluded. I don't think this is the best solution, but it works for now.

A better solution would be to draw all 3D objects last, regardless of where they are placed in the layer stack. This would mean that when drawing 3D, the "2D" parts of the map would already be done rendering, and the depth buffer constructed during the rendering process is not needed anymore. We could then overwrite it: clear it, render the globe sphere with its actual depth values, and then get proper occlusion without doing it in the pixel shader. (And for a flat map it would make sense to instead draw all 3D first, so they can occlude the map and prevent overdraw. edit: this would not work, fill-extrusion and 3D is sometimes transparent and thus must be drawn after the main map)

I didn't implement this at first, as I didn't realize it was possible at that time. I was unsure whether a 2D layer could draw on top of a 3D one.