Level of detail explained for 3D city models

On this page

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.

Levels of detail from a footprint to an interior model, with what each answers. the LoD ladderLoD0 footprintno heightarea, densityLoD1 prismone heightvolume, shadow, visibilityLoD2 roof shapesemantic surfacesroof area, solar, drainageLoD3 facade openingswindows, balconiesdaylight, acousticsLoD4 interiorroomsevacuation, indoor routingEach step multiplies the data; only some analyses notice spatialworkflow.io
Each step up multiplies cost; only some analyses can tell the difference.

Match the level to the question, and check the height definition

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 — medians 19% apart purely from the definition, although on the flat roofs that make up two thirds of the tile the two are the same number.

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.

Relative data volume by level of detail, on a log-like scale. relative vertices per buildingLoD0~12LoD1~25LoD2~120LoD3~1,20001400The expensive step is LoD1 to LoD2, because it needs a roof survey spatialworkflow.io
The step that matters is LoD1 to LoD2, because it needs a roof survey rather than a height.

Finding the level you have, and deriving LoD1

The data used on this page

The LoD2 tile of The Hague's 3D city model published as a CityJSON sample dataset (DenHaag_01.city.json, CityJSON 2.0, 3.2 MB), saved as model.city.json. Example 3's study_area.gpkg is a stated 200 m square at the middle of the tile. It covers about 790 × 670 m of the city in EPSG:7415 (the Dutch national grid with NAP heights).

import os

import requests

if not os.path.exists("model.city.json"):
    reply = requests.get("https://3d.bk.tudelft.nl/opendata/cityjson/3dcities/v2.0/DenHaag_01.city.json", timeout=300)
    reply.raise_for_status()
    open("model.city.json", "wb").write(reply.content)
import geopandas as gpd
from shapely.geometry import box

cx, cy = 78642, 457940                              # middle of the tile, EPSG:28992
gpd.GeoDataFrame(geometry=[box(cx - 100, cy - 100, cx + 100, cy + 100)], crs=28992).to_file("study_area.gpkg")
print(os.path.getsize("model.city.json"))
3150451

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

V = np.array(d["vertices"]) * d["transform"]["scale"] + d["transform"]["translate"]   # real coordinates

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))
ridge = lod1_from_lod2(d["CityObjects"], V, height="ridge")
diff = ridge.height_m.values - lod1.height_m.values
print(f"LoD1 volume, eaves {(lod1.area * lod1.height_m).sum():,.0f} m³, ridge {(ridge.area * ridge.height_m).sum():,.0f} m³; "
      f"ridge above eaves on {(diff > 0.5).mean():.1%} of solids, by a median {np.median(diff[diff > 0.5]):.2f} m there")
1990 5.61
LoD1 volume, eaves 627,268 m³, ridge 743,321 m³; ridge above eaves on 34.3% of solids, by a median 3.15 m there

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()
lod2 = lod1[["id", "geometry"]]                     # the LoD2 solids, located by their ground surface
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):,}")
LoD2 within 100 m of the study area: 1,315
LoD1 elsewhere: 675

Why LoD1 to LoD2 is the expensive step

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

On the Hague tile the median eaves height is 5.61 m and the median ridge 6.68 m. Most roofs there are flat, so the two coincide on two thirds of the solids, but on the pitched third the ridge sits a median 3.15 m higher, and the two LoD1 models built in Example 2 differ in total volume by 18.5%: 627,268 m³ from eaves heights against 743,321 m³ from ridges. If your LoD1 model uses ridge height and someone else's uses eaves, your volumes are not comparable. Record which height the model uses, always.

What a level of detail tells you against what it does not. what LoD does not promiseLoD tells you✓which features are modelled✓whether roofs have shape✓whether surfaces are labelled✓roughly how big the file isLoD says nothing about✕positional or height accuracy✕completeness of the building stock✕how current the model is✕which height LoD1 usedA current LoD1 model beats a 2018 LoD2 one for most questions spatialworkflow.io
Accuracy, completeness and currency are separate metadata fields.

Standard versions, terrain and string levels

  • 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 tile the median eaves height was 5.61 m and the median ridge 6.68 m; on its pitched roofs the ridge was a median 3.15 m higher.