Level of detail explained for 3D city models

Problem statement

"LoD2" appears in a dataset description and everyone nods, but the term does two jobs at once. It describes how much geometric detail a model carries โ€” and people also use it to imply accuracy, completeness and fitness for purpose, none of which it guarantees.

The practical consequence is that an LoD2 model can be less accurate than an LoD1 one from a better survey, and an LoD1 model can be perfectly adequate for the analysis you were told needed LoD2. Choosing a level is a cost decision: each step up multiplies the data volume and the acquisition cost, and only some analyses notice.

This guide sets out what each level contains, what it costs, and which questions actually require the next one.

Quick answer

level geometry typical vertices per building answers
LoD0 footprint, no height 5โ€“20 area, density, coverage
LoD1 prism to one height 10โ€“40 volume, shadows, visibility
LoD2 roof shape, semantic surfaces 30โ€“200 roof area, solar, drainage
LoD3 facade openings, balconies 200โ€“2,000 daylight through windows, acoustics
LoD4 interior rooms thousands evacuation, indoor routing

A real LoD2 tile of central The Hague โ€” 2,498 city objects, 1,990 solids โ€” held 22,997 vertices in total, about 11.6 per solid after shared-vertex indexing. The compression comes from the shared vertex list, not from simple geometry.

Five stacked levels of detail from a footprint to an interior model, with what each answers.
Each step up multiplies cost; only some analyses can tell the difference.

Step-by-step solution

1. Match the level to the question, not to the budget

  • Coverage, density, urban form โ†’ LoD0.
  • Volume, shadow, sky view, wind at street level โ†’ LoD1.
  • Roof-mounted solar, roof drainage, roof material โ†’ LoD2.
  • Daylight inside a room, facade-level thermal โ†’ LoD3.
  • Indoor routing, evacuation โ†’ LoD4.

2. Remember that LoD does not promise accuracy

The level describes what is modelled, not how well. A national LoD2 product built from aerial LiDAR has vertical accuracy of a few decimetres; a hand-modelled LoD2 building from a plan may be a metre out. Accuracy is a separate metadata field and has to be stated separately.

3. Check the height definition before comparing

An LoD1 model has one height per building, and which height that is varies: the eaves, the ridge, a percentile of the roof points, or the mean. On the Hague tile, the median eaves height was 5.61 m above ground and the median ridge height 6.68 m โ€” a 19% difference on the same buildings, purely from the definition.

4. Expect building parts

LoD2 models split buildings into parts where the roof changes. The Hague tile has 844 Building objects and 1,653 BuildingPart objects; code that assumes one geometry per building will process fewer than half the solids.

5. Budget for the data volume

Each level multiplies vertices by roughly five to ten. A city-wide LoD2 model is tens of gigabytes; an LoD3 model of the same city is not something you hold in memory.

6. Consider a mixed-level model

Nothing requires one level everywhere. A common pattern is LoD2 for the study area and LoD1 for the surroundings that only cast shadows into it โ€” the shadow of a distant building does not depend on its roof shape.

Bars of relative data volume and acquisition cost across LoD0 to LoD3.
The step that matters is LoD1 to LoD2, because it needs a roof survey rather than a height.

Code examples

Example 1 โ€” find out what level you actually have

import json, collections

d = json.loads(open("model.city.json").read())
objs = d["CityObjects"]

print("object types:", collections.Counter(o["type"] for o in objs.values()).most_common())

lods, gtypes, sem = collections.Counter(), collections.Counter(), collections.Counter()
for o in objs.values():
    for g in o.get("geometry", []):
        lods[str(g.get("lod"))] += 1
        gtypes[g["type"]] += 1
        if g.get("semantics"):
            for s in g["semantics"]["surfaces"]:
                sem[s.get("type")] += 1

print("lod:", lods.most_common())
print("geometry types:", gtypes.most_common())
print("semantic surfaces:", sem.most_common())
object types: [('BuildingPart', 1653), ('Building', 844), ('TINRelief', 1)]
lod: [('2', 1991)]
geometry types: [('Solid', 1990), ('CompositeSurface', 1)]
semantic surfaces: [('WallSurface', 11215), ('RoofSurface', 3004), ('GroundSurface', 1990)]

The presence of RoofSurface labels is what makes this LoD2 rather than an LoD1 model with a lot of triangles.

Example 2 โ€” build LoD1 from LoD2 when you need the simpler thing

import numpy as np, geopandas as gpd
from shapely.geometry import Polygon

def lod1_from_lod2(city_objects, vertices, height="eaves"):
    rows = []
    for oid, o in city_objects.items():
        attrs = o.get("attributes") or {}
        for g in o.get("geometry", []):
            if g["type"] != "Solid" or not g.get("semantics"):
                continue
            values, surfaces = g["semantics"]["values"][0], g["semantics"]["surfaces"]
            for face, si in zip(g["boundaries"][0], values):
                if si is None or surfaces[si].get("type") != "GroundSurface":
                    continue
                ring = [vertices[i][:2] for i in face[0]]
                ground = float(np.mean([vertices[i][2] for i in face[0]]))
                top = attrs["AbsoluteEavesHeight" if height == "eaves" else "AbsoluteRidgeHeight"]
                rows.append({"id": oid, "height_m": top - ground,
                             "geometry": Polygon(ring)})
    return gpd.GeoDataFrame(rows, crs="EPSG:7415")

lod1 = lod1_from_lod2(d["CityObjects"], V, height="eaves")
print(len(lod1), lod1.height_m.median().round(2))

Downgrading is easy and lossless in the sense that matters โ€” you can always go back to the source. Upgrading is fabrication.

Example 3 โ€” mixed level by distance

import geopandas as gpd

study = gpd.read_file("study_area.gpkg").geometry.union_all()
detailed = lod2[lod2.intersects(study.buffer(100))]
simple = lod1[~lod1["id"].isin(detailed["id"])]

print(f"LoD2 within 100 m of the study area: {len(detailed):,}")
print(f"LoD1 elsewhere: {len(simple):,}")

Explanation

Why the LoD1 to LoD2 step is the expensive one

LoD0 and LoD1 need a footprint and a number, both of which can be derived from data you already have โ€” a DSM, a DTM and a building outline. LoD2 needs the roof surface, which means either aerial LiDAR at sufficient density, stereo photogrammetry, or manual modelling. That is a survey, not a processing step, and it is why national LoD2 products exist in some countries and not others.

Why "LoD2" says nothing about completeness

A model is LoD2 if its buildings have roof shapes. Whether it contains all the buildings, whether it is current, and whether the heights are within a decimetre are three separate questions with three separate metadata fields. A 2018 LoD2 model of a city that has been built in since is more misleading than a current LoD1 one.

Why building parts exist and matter

A building with a two-storey extension has two roof planes at different heights, which cannot be one LoD2 solid with a single roof. The standard splits it into BuildingParts under one Building. Any per-building statistic โ€” volume, roof area, height โ€” has to decide whether to aggregate the parts, and the answer is usually yes for reporting and no for geometry.

Why the height definition dominates the LoD choice

Between eaves and ridge on the Hague tile there is a median difference of 1.07 m on a median building height of 6.68 m. If your LoD1 model uses ridge height and someone else's uses eaves, your volumes differ by more than the difference between an LoD1 and an LoD2 calculation. Record which height the model uses, always.

Two panels separating what a level of detail tells you โ€” which features are modelled โ€” from what it does not, including accuracy, completeness and currency.
Accuracy, completeness and currency are separate metadata fields.

Edge cases or notes

  • CityGML and CityJSON define LoD slightly differently across versions; LoD 1.2/1.3/2.2 refinements exist.
  • A TINRelief is the terrain, not a building; the Hague tile has exactly one.
  • lod is a string in CityJSON 2.0, and a number in 1.x.
  • LoD3 is rare outside demonstration datasets. Assume you will not get it.
  • Textures are orthogonal to LoD. An LoD1 model can be textured.
  • Interior models are usually BIM. IFC, not CityGML, is where they live.
  • Mixed-level models are legal and are what most real projects use.
  • Downgrade for analysis, keep the original. Never overwrite the detailed source.

FAQ

What does LoD2 mean?

Buildings modelled with their roof shapes and, usually, semantically labelled wall, roof and ground surfaces. LoD1 is a flat-topped prism; LoD3 adds facade openings.

Does a higher LoD mean a more accurate model?

No. LoD describes what is modelled, not how well. Accuracy is a separate property and must be stated separately in the metadata.

Which level do I need for shadow analysis?

LoD1. The roof shape changes a shadow's edge by a metre or two and changes nothing else.

Why does my model have more parts than buildings?

Because LoD2 splits buildings wherever the roof changes. A real tile had 844 Buildings and 1,653 BuildingParts.

Can I create LoD2 from LoD1?

Not honestly. You can fit a roof shape, but you are inventing geometry. Going the other way โ€” LoD2 down to LoD1 โ€” is exact.

Does LoD1 use eaves or ridge height?

Whichever the producer chose, which is why it has to be documented. On one real dataset the two differed by a median of 1.07 m on a median 6.68 m building.