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.
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.
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.
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.
lodis 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.
Internal links
- 3D spatial data models explained: 2.5D, meshes and solids โ the models each level uses
- CityJSON and CityGML explained โ where the LoD field lives
- How to read a CityJSON city model in Python โ inspecting the level you have
- Where building heights come from, and how wrong they are โ the eaves-versus-ridge problem
- How to extrude building footprints into 3D in Python โ producing LoD1
- How to calculate building volumes and floor area in Python โ what each level lets you measure
- How to classify roof shapes from LiDAR in Python โ the step from LoD1 to LoD2
- Spatial metadata explained: what a dataset must tell you โ where accuracy is declared
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.