Skip to content

fix(3d-buildings): grow tile culling bounds near the horizon#7932

Open
clement-igonet wants to merge 11 commits into
maplibre:mainfrom
clement-igonet:fix/3d-buildings-disappear-7633
Open

fix(3d-buildings): grow tile culling bounds near the horizon#7932
clement-igonet wants to merge 11 commits into
maplibre:mainfrom
clement-igonet:fix/3d-buildings-disappear-7633

Conversation

@clement-igonet

@clement-igonet clement-igonet commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Launch Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) - original work.
  • Briefly describe the changes in this PR.
  • Link to related issues.
  • Include before/after visuals or gifs if this PR includes visual changes. (No visual output change at normal pitch; the fix only affects which tiles stay loaded when pitched steeply upward - see issue video for the bug this fixes.)
  • Write tests for all new functionality. (Updated covering_tiles.test.ts's pitched case, and added dedicated tests for getElevationForTileCulling at pitch 45/60/80/90/100.)
  • Document any changes to public APIs. (None - internal tile-covering logic only.)
  • Post benchmark scores. (Not run - see note below.)
  • Add an entry to CHANGELOG.md under the ## main section.
  • Confirm you have read our AI policy here.

Assisted-By: Claude Sonnet 5 (claude-sonnet-5)

What

Fixes #7633 - 3D buildings disappearing when the camera looks up.

Without terrain, getTileBoundingVolume() hardcoded a tile's bounding box to a flat 0 elevation, so it had no way to represent extruded features (3D buildings) standing on that tile. As the camera pitches up toward the near-horizontal cap, the frustum's bottom edge sweeps past these flat boxes and tiles get culled even though their buildings would still be visibly poking into view.

@NathanMOlson sketched two options in the issue thread:

  1. A global assumed max building height, applied unconditionally - flagged as "definitely not mergeable".
  2. Extend the bounding box using elevation (already a parameter, but only used inside the terrain branch), outside the terrain branch too - "might be" mergeable, but a fixed buffer applied everywhere, even when pitch is low and current culling is already correct.

This PR takes a third approach discussed on the issue: tie the extra bounds to FOV, so they only grow as the frustum's bottom edge - computed from both pitch and fov, not pitch alone - approaches horizontal (maxMercatorHorizonAngle, 89.25°). At normal pitch the buffer is ~0 and culling is unchanged from today; it only kicks in for the steep look-up case this issue is about. See getElevationForTileCulling() in covering_tiles.ts for the implementation and reasoning.

Applies to both the mercator and globe covering-tiles providers. The globe provider caches bounding volumes keyed only by tile z/x/y (plus whether terrain is set) - since elevation now varies continuously with pitch/fov instead of being static, it's added to that cache key too, so frames don't get served a stale, pre-fix bounding volume.

Testing

  • npm run typecheck, npm run lint: clean.
  • npm run test-unit: full suite passing (2820 tests).
  • Updated the existing test that encoded the old (buggy) tile count for a pitch-80 scenario (covering_tiles.test.ts > pitched), and added dedicated tests for getElevationForTileCulling covering pitch 45 (baseline, unaffected), 60 (partial ramp), and 80/90/100 (the range the reported bug was observed across - fully buffered at the default FOV).
  • Visually verified against a live vector-tile city (real liberty-style buildings, walking/pitching 80-100° in first person): nearby buildings that previously vanished around pitch ~90 now stay rendered throughout the range. The exact assumed-height constant (ASSUMED_MAX_FEATURE_HEIGHT_METERS = 500) and onset threshold (TILE_CULLING_HORIZON_ONSET_DEGREES = 15) are the main things worth discussing/tuning if reviewers have thoughts.
  • Benchmark scores not posted - happy to run npm run benchmark if a reviewer wants numbers; expect negligible impact away from the near-horizon case, since the buffer is 0 there and the code path is unchanged.

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.38%. Comparing base (0d0cd8b) to head (55c16ed).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7932      +/-   ##
==========================================
+ Coverage   93.36%   93.38%   +0.02%     
==========================================
  Files         288      288              
  Lines       24422    24429       +7     
  Branches     6454     6454              
==========================================
+ Hits        22801    22813      +12     
+ Misses       1621     1616       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@HarelM

HarelM commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Interesting approach, can you add a render test? I think there are a few tiles with buildings already part of this repo so you can reuse those.

…e#7633)

Without terrain, getTileBoundingVolume() hardcoded a tile's bounding box
to a flat 0 elevation, so it had no way to represent extruded features
(3D buildings) on that tile. As the camera pitches up toward the near-
horizontal cap, the frustum's bottom edge sweeps past these flat boxes
and tiles get culled even though their buildings would still be visibly
poking into view - reported as buildings disappearing when looking up
while walking around a city in first person.

Rather than a flat elevation-based workaround (the maintainer's option 2
in the issue thread - a fixed buffer applied everywhere, even when pitch
is low and current culling is already correct), this ties the extra
bounds to FOV: they only grow as the frustum's bottom edge - computed
from both pitch and fov, not pitch alone - approaches horizontal, via
getElevationForTileCulling() in covering_tiles.ts. At normal pitch the
buffer is ~0 and culling is unchanged; it only kicks in for the steep
look-up case this issue is about.

Applies to both the mercator and globe covering-tiles providers. The
globe provider caches bounding volumes keyed only by tile z/x/y (plus
whether terrain is set) - since elevation now varies continuously with
pitch/fov instead of being static, it's added to that cache key too, so
frames don't get served a stale, pre-fix bounding volume.

Includes dedicated tests for getElevationForTileCulling at pitch
80/90/100 - the range the reported bug was observed across - plus pitch
60 (partial ramp) and 45 (unaffected baseline).
@clement-igonet
clement-igonet force-pushed the fix/3d-buildings-disappear-7633 branch from 2109560 to a42c50e Compare July 13, 2026 11:29
@clement-igonet

clement-igonet commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer verification aid: a standalone repro/test page, walking a real vector-tile city (OpenFreeMap's liberty style) in first person and pitching 80-100°, the range the reported bug was observed across. Not part of the PR itself - just for checking the fix live.

Happy to share the actual file directly if useful - didn't want to clutter the PR with a large inline code dump. Ping me and I'll get it to you however's easiest.

@CommanderStorm

Copy link
Copy Markdown
Member

Reviewer verification aid

First off all, can you please not commuincate trough an LLM? That is a bit rude 😉

Here is how we do render tests -> https://github.com/maplibre/maplibre-gl-js/blob/main/test/integration/README.md

@clement-igonet

Copy link
Copy Markdown
Contributor Author

Fair enough, point taken 😄 Trimmed the earlier comment down too.

Added a render test at test/integration/render/tests/fill-extrusion-height/steep-pitch-near-horizon/, reusing the existing 14-8802-5374.mvt building tile fixture (per HarelM's suggestion) - confirmed it fails against the pre-fix code and passes with the fix.

Reuses the existing 14-8802-5374.mvt building tile fixture at steep pitch
(80°, maxPitch raised to 90 so it isn't clamped) with a tall extrusion
height, so tile-covering bounds that ignore building height would visibly
cut buildings off near the top of frame. Confirmed it fails against the
pre-fix covering_tiles.ts/mercator_covering_tiles_details_provider.ts/
bounding_volume_cache.ts and passes with the fix.
@clement-igonet
clement-igonet marked this pull request as ready for review July 14, 2026 11:22

@CommanderStorm CommanderStorm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promising, but needs cleanup

@CommanderStorm CommanderStorm Jul 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before, for reference:

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that is the pre-fix rendering; the committed expected.png is the after state, with the buildings kept near the frustum edge.

Comment thread src/util/primitives/bounding_volume_cache.ts Outdated
Comment thread src/geo/projection/covering_tiles.ts
Comment thread src/geo/projection/covering_tiles.test.ts
@CommanderStorm
CommanderStorm marked this pull request as draft July 18, 2026 21:36
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +89 to +92
// At pitch 80 (close to the 89.25° horizon cap once fov/2 is
// added), getElevationForTileCulling grows the assumed
// elevation so tiles whose extruded features could still be
// visible near the horizon aren't dropped - see #7633.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// At pitch 80 (close to the 89.25° horizon cap once fov/2 is
// added), getElevationForTileCulling grows the assumed
// elevation so tiles whose extruded features could still be
// visible near the horizon aren't dropped - see #7633.

@CommanderStorm CommanderStorm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM now

@CommanderStorm
CommanderStorm marked this pull request as ready for review July 19, 2026 14:30
HarelM added 2 commits July 23, 2026 12:02
Added contributor information for the bug fix regarding 3D buildings disappearing.
* the frustum's bottom edge approaches the horizon, where a ground-level bounding
* box would cull tiles whose extruded features are still visible.
*/
export function getElevationForTileCulling(transform: IReadonlyTransform): number {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this exported only to be unit tests? If this is the case I think better tests should be used.
Since this is a relatively small method I think it should be easy to cover it using existing tests.

@HarelM

HarelM commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Looks good over all, I've added some comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3D Buildings disappear when tile is no longer in view

3 participants