3D spatial data models explained: 2.5D, meshes and solids

Problem statement

"3D building data" describes at least four different things, and code written for one of them fails on the others. A footprint with a height attribute, a triangulated mesh, a closed solid with labelled surfaces and a point cloud are all called 3D, and only two of them have a volume you can compute.

Choosing the wrong model is expensive in a specific way: you build a pipeline around polygons with a height column, and then a real city model arrives as CityJSON solids with semantic surfaces and none of your code applies. This guide sets out the four models, what each can answer, and which questions force you up to the next one.

Quick answer

model what it is volume? roof shape? typical source
2.5D a 2D footprint plus one height yes, as a prism no OSM, building footprints
surface (2.5D raster) one height per pixel no partly DSM from LiDAR or photogrammetry
mesh triangles in 3D, not necessarily closed only if closed yes photogrammetry, glTF, 3D Tiles
solid a closed shell, oriented, watertight yes yes CityJSON, CityGML LoD2+

Most urban analysis โ€” shadows, solar, volumes, visibility, sky view factor โ€” works on 2.5D. The jump to solids is worth making when the roof shape changes the answer.

Four panels showing a 2.5D prism, a height surface, an open mesh and a closed solid.
Only the first and last have a volume without further work.

Step-by-step solution

1. Start from the question, not the data

  • Shadows, daylight, visibility โ†’ 2.5D is usually enough.
  • Volume, floor area, embodied material โ†’ 2.5D gives a prism volume; solids give the real one.
  • Roof-mounted solar, roof drainage โ†’ you need roof geometry, so LoD2 solids or a DSM.
  • Interior anything โ†’ LoD4 or a BIM model, which is a different world.

2. Understand what 2.5D cannot represent

A footprint with a height is a vertical prism. It cannot represent an overhang, a bridge, an arcade, a balcony or a building with two heights โ€” and it cannot represent the same ground location having two surfaces, which is exactly what a road under a building is. If your study area has any of those, decide explicitly whether the error matters.

3. Know that a mesh is not automatically a solid

A mesh is a set of triangles. Whether it encloses a volume is a separate property โ€” watertightness โ€” that has to be checked, not assumed. On a real LoD2 dataset, 1,985 of 1,990 solids triangulated cleanly to watertight meshes and five did not; those five return a meaningless volume.

4. Treat the surface model as the cheapest 3D there is

A digital surface model is a raster, so every raster tool applies: shadows are a hillshade problem, sky view factor is a horizon-angle problem, and building height is a DSM minus DTM problem. For city-scale analysis it is often the fastest route, and it comes free with LiDAR.

5. Recognise the solid's extra content: semantics

A CityJSON solid carries labelled surfaces. In a real Hague LoD2 tile, 1,990 buildings had 11,215 wall surfaces, 3,004 roof surfaces and 1,990 ground surfaces, each labelled. Those labels are what let you compute roof area without guessing from the normal direction.

6. Expect to move between models

The common path is footprints + heights โ†’ extruded solids โ†’ mesh โ†’ glTF for the browser. Each step is lossy in one direction and fabricated in the other: extruding invents a flat roof, and meshing discards the semantics.

Decision tree from the analytical question to the 3D model it requires.
Most urban questions stop at the first branch.

Code examples

Example 1 โ€” 2.5D: a footprint and a height

import geopandas as gpd

b = gpd.read_file("buildings.gpkg").to_crs(28992)
b["height_m"] = b["levels"] * 3.0

b["footprint_m2"] = b.area
b["prism_volume_m3"] = b.area * b["height_m"]
b["wall_area_m2"] = b.geometry.length * b["height_m"]

print(b[["footprint_m2", "height_m", "prism_volume_m3"]].describe().round(1))

Three useful quantities from two columns. That is why 2.5D survives: the arithmetic is trivial and it is right to within the roof.

Example 2 โ€” a solid, triangulated and measured

import numpy as np, trimesh

def solid_to_mesh(boundaries, vertices):
    """CityJSON Solid โ†’ triangles, fan-triangulating each surface's outer ring."""
    tris = []
    for shell in boundaries:
        for surface in shell:
            ring = surface[0]
            for i in range(1, len(ring) - 1):
                tris.append([ring[0], ring[i], ring[i + 1]])
    return trimesh.Trimesh(vertices=vertices, faces=np.array(tris), process=True)

mesh = solid_to_mesh(geometry["boundaries"], vertices)
print(mesh.is_watertight, round(abs(mesh.volume), 1), round(mesh.area, 1))

On the Hague tile this gave a median building volume of 118.4 mยณ and a total of 683,115 mยณ across 1,990 solids.

Example 3 โ€” comparing the prism against the solid

import numpy as np

prism = footprint_area * height_to_ridge
real = abs(mesh.volume)
print(f"prism {prism:,.0f} mยณ, solid {real:,.0f} mยณ, "
      f"prism overstates by {prism/real - 1:.1%}")

For a flat-roofed building the two agree. For a pitched roof, extruding to the ridge overstates the volume by roughly half the roof volume โ€” which is why the height you extrude to matters more than the model you choose.

Explanation

Why 2.5D is the default and should be

Every input you are likely to have โ€” footprints, DSMs, LiDAR-derived heights โ€” is naturally 2.5D, and the operations people actually run are insensitive to the roof. A shadow cast by a pitched roof differs from the prism's shadow by a metre or two at the edges; a solar analysis does not. Moving to solids costs data you may not have and code you have to write.

Why watertightness is the property that matters for a mesh

Volume, inside/outside tests and boolean operations all require a closed surface with consistent orientation. A mesh from photogrammetry usually has holes where the scanner could not see, and a mesh from a careless triangulation has duplicated or flipped faces. trimesh reports is_watertight and can often repair the simple cases; a mesh that cannot be repaired cannot be measured.

Why the semantics are the real value of a city model

Without labels, separating roof from wall means classifying by surface normal, which fails on steep roofs and on walls that lean. A CityJSON model states which surface is which, so roof area, wall area and ground area are sums rather than estimates. That is usually a bigger practical difference than the geometry itself.

Why the vertical datum is part of the model

A city model's z values are heights in a vertical datum โ€” the Hague tile uses EPSG:7415, which is RD New horizontally and NAP vertically. Mixing a model in NAP with a DTM in ellipsoidal height puts every building about 43 m underground in the Netherlands. The horizontal CRS is checked by everybody; the vertical one is checked by almost nobody.

Triage of four situations that force a move from 2.5D to a mesh or solid model: overhangs, roof solar, stacked surfaces and interior detail.
Four reasons to leave 2.5D; everything else works fine as prisms.

Edge cases or notes

  • Overhangs break 2.5D. Arcades, bridges and cantilevers need a solid or a mesh.
  • A building can have several parts. LoD2 models split them; footprints usually do not.
  • Z-up is not universal. glTF is Y-up, which is a common source of sideways buildings.
  • Units can differ between axes. Rare, and catastrophic when it happens.
  • Point clouds are not a model. They are a measurement you derive a model from.
  • BIM is a different discipline. IFC models carry components, not just surfaces.
  • Terrain is part of the scene. A building model without a ground surface floats or sinks.
  • LoD is about detail, not accuracy. An LoD2 model can be less accurate than an LoD1 one.

FAQ

What is 2.5D data?

A two-dimensional footprint with a single height attached, extruded into a vertical prism. It cannot represent overhangs or two surfaces above the same ground point, and it is enough for most urban analysis.

What is the difference between a mesh and a solid?

A mesh is a set of triangles; a solid is a closed, consistently oriented shell that encloses a volume. A mesh may or may not be watertight, and only a watertight one has a meaningful volume.

Do I need a full city model for shadow analysis?

No. Footprints with heights are enough for shadows, daylight and visibility. Roof geometry matters for roof-mounted solar and drainage.

How do I know if my mesh is watertight?

trimesh.Trimesh(...).is_watertight. On a real LoD2 tile, 1,985 of 1,990 solids were watertight and five were not.

Why are semantic surfaces useful?

They label each face as wall, roof or ground, so roof area is a sum rather than a guess from surface normals. The Hague tile has 11,215 walls, 3,004 roofs and 1,990 grounds, all labelled.

Does the vertical datum matter?

Yes. A model in a national height datum mixed with ellipsoidal heights is offset by tens of metres โ€” about 43 m in the Netherlands.