[{"data":1,"prerenderedAt":1469},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fextrude-buildings-in-3d-pyqgis":3},{"id":4,"title":5,"body":6,"description":1458,"extension":1459,"meta":1460,"navigation":223,"path":1465,"seo":1466,"stem":1467,"__hash__":1468},"docs\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fextrude-buildings-in-3d-pyqgis\u002Findex.md","Extrude Buildings in 3D with PyQGIS",{"type":7,"value":8,"toc":1444},"minimark",[9,13,17,26,142,147,169,173,176,289,303,307,416,519,540,544,584,614,618,721,726,737,741,744,856,866,870,873,876,1132,1137,1140,1276,1281,1285,1306,1310,1365,1369,1372,1376,1382,1396,1402,1408,1412,1440],[10,11,5],"h1",{"id":12},"extrude-buildings-in-3d-with-pyqgis",[14,15,16],"p",{},"Extrusion is what turns a footprint layer into a city. The mechanics are one symbol and one property, and the two things that decide whether the result is any good are where the height comes from and how the building sits relative to the ground. Get the second wrong and half your buildings are buried in a hillside.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002F","Temporal & 3D Visualization in PyQGIS",". It covers a constant extrusion, a data-defined one, height reference modes, materials and colour by attribute, and the honest options when the layer carries no height at all.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,82,91,94,97,102,106,110,113,118,122,126,129,136],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 326","img","Height reference modes compared: absolute heights place roofs at a fixed elevation regardless of ground, while terrain-relative heights sit each building on the ground beneath it","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Absolute or relative to terrain",[40,41,42],"desc",{},"On sloping ground, absolute heights place every building base at the same elevation, so buildings uphill are buried and buildings downhill float. Heights measured relative to the terrain place each base on the ground beneath it, which is what a footprint layer with storey heights needs.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","326","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The same heights, two reference modes, one hillside",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","46","344","212","10","#fffdf7","#b91c1c","stroke-width:2.5",[51,70,74],{"x":71,"y":72,"style":73,"fill":67,"textAnchor":57},"196","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","absolute",[76,77],"path",{"d":78,"fill":79,"stroke":80,"style":81},"M46 216 L118 190 L190 164 L262 138 L344 112","none","#92400e","stroke-width:3.4",[44,83],{"x":84,"y":85,"width":86,"height":87,"fill":88,"stroke":89,"style":90},"76","152","42","56","#c9d6cf","#2563eb","stroke-width:2",[44,92],{"x":93,"y":85,"width":86,"height":87,"fill":88,"stroke":89,"style":90},"176",[44,95],{"x":96,"y":85,"width":86,"height":87,"fill":88,"stroke":89,"style":90},"276",[51,98,101],{"x":71,"y":99,"style":100,"fill":67,"textAnchor":57},"238","text-anchor:middle;font-size:10px;font-family:sans-serif","downhill floats, uphill is buried",[44,103],{"x":104,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":105,"style":68},"392","#15803d",[51,107,109],{"x":108,"y":72,"style":73,"fill":105,"textAnchor":57},"564","relative to terrain",[76,111],{"d":112,"fill":79,"stroke":80,"style":81},"M414 216 L486 190 L558 164 L630 138 L712 112",[44,114],{"x":115,"y":116,"width":86,"height":87,"fill":117,"stroke":105,"style":90},"444","146","#e8efe6",[44,119],{"x":120,"y":121,"width":86,"height":87,"fill":117,"stroke":105,"style":90},"544","110",[44,123],{"x":124,"y":125,"width":86,"height":87,"fill":117,"stroke":105,"style":90},"644","78",[51,127,128],{"x":108,"y":99,"style":100,"fill":105,"textAnchor":57},"each base sits on its own ground",[44,130],{"x":131,"y":96,"width":132,"height":86,"rx":133,"fill":134,"stroke":135,"style":90},"80","600","8","#fdf2e2","#b45309",[51,137,141],{"x":53,"y":138,"style":139,"fill":140,"textAnchor":57},"302","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","use absolute only when the attribute is a roof elevation, not a building height",[143,144,146],"h2",{"id":145},"prerequisites","Prerequisites",[148,149,150,158,161],"ul",{},[151,152,153,157],"li",{},[154,155,156],"strong",{},"QGIS 3.34 LTR"," or newer with 3D support.",[151,159,160],{},"A polygon layer of footprints, ideally with a height or storey-count attribute.",[151,162,163,164,168],{},"A configured scene — see ",[21,165,167],{"href":166},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fconfigure-3d-map-view-pyqgis\u002F","configuring a 3D map view",".",[143,170,172],{"id":171},"a-constant-extrusion","A constant extrusion",[14,174,175],{},"Start here to confirm the scene works at all before adding data-defined complexity.",[177,178,183],"pre",{"className":179,"code":180,"language":181,"meta":182,"style":182},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\nfrom qgis._3d import QgsVectorLayer3DRenderer, QgsPolygon3DSymbol\n\nbuildings = QgsProject.instance().mapLayersByName(\"buildings\")[0]\n\nsymbol = QgsPolygon3DSymbol()\nsymbol.setExtrusionHeight(9.0)\n\nbuildings.setRenderer3D(QgsVectorLayer3DRenderer(symbol))\n","python","",[184,185,186,205,218,225,250,255,266,278,283],"code",{"__ignoreMap":182},[187,188,191,195,199,202],"span",{"class":189,"line":190},"line",1,[187,192,194],{"class":193},"snl16","from",[187,196,198],{"class":197},"s95oV"," qgis.core ",[187,200,201],{"class":193},"import",[187,203,204],{"class":197}," QgsProject\n",[187,206,208,210,213,215],{"class":189,"line":207},2,[187,209,194],{"class":193},[187,211,212],{"class":197}," qgis._3d ",[187,214,201],{"class":193},[187,216,217],{"class":197}," QgsVectorLayer3DRenderer, QgsPolygon3DSymbol\n",[187,219,221],{"class":189,"line":220},3,[187,222,224],{"emptyLinePlaceholder":223},true,"\n",[187,226,228,231,234,237,241,244,247],{"class":189,"line":227},4,[187,229,230],{"class":197},"buildings ",[187,232,233],{"class":193},"=",[187,235,236],{"class":197}," QgsProject.instance().mapLayersByName(",[187,238,240],{"class":239},"sU2Wk","\"buildings\"",[187,242,243],{"class":197},")[",[187,245,46],{"class":246},"sDLfK",[187,248,249],{"class":197},"]\n",[187,251,253],{"class":189,"line":252},5,[187,254,224],{"emptyLinePlaceholder":223},[187,256,258,261,263],{"class":189,"line":257},6,[187,259,260],{"class":197},"symbol ",[187,262,233],{"class":193},[187,264,265],{"class":197}," QgsPolygon3DSymbol()\n",[187,267,269,272,275],{"class":189,"line":268},7,[187,270,271],{"class":197},"symbol.setExtrusionHeight(",[187,273,274],{"class":246},"9.0",[187,276,277],{"class":197},")\n",[187,279,281],{"class":189,"line":280},8,[187,282,224],{"emptyLinePlaceholder":223},[187,284,286],{"class":189,"line":285},9,[187,287,288],{"class":197},"buildings.setRenderer3D(QgsVectorLayer3DRenderer(symbol))\n",[14,290,291,294,295,298,299,302],{},[154,292,293],{},"Breakdown:"," ",[184,296,297],{},"setRenderer3D"," is entirely separate from ",[184,300,301],{},"setRenderer"," — a layer's 2D style and 3D style share nothing, and changing one leaves the other alone. The extrusion height is in the layer's map units, so 9.0 on a metric layer is nine metres, roughly three storeys. If nothing appears after this, the problem is upstream in the scene rather than here: no terrain generator, a wrong extent, or the layer missing from the settings' layer list.",[143,304,306],{"id":305},"height-from-an-attribute","Height from an attribute",[14,308,309],{},[29,310,313,316,319,322,325,330,335,340,346,351,355,359,362,368,371,375,379,382,387,390,394,398,401,405,408,412],{"viewBox":311,"role":32,"ariaLabel":312,"xmlns":34},"0 0 760 300","Three sources of building height ranked by quality: a measured height attribute, a storey count multiplied by an assumed storey height, and a LiDAR-derived height from a surface model minus terrain",[36,314,315],{},"Where the height comes from decides the model",[40,317,318],{},"A measured height attribute is best and rarely present. A storey count multiplied by an assumed storey height is common and approximate. A height derived from LiDAR by subtracting terrain from surface within each footprint is accurate and requires the point cloud work first. A constant height for every building is a placeholder, not a model.",[44,320],{"x":46,"y":46,"width":47,"height":321,"fill":49},"300",[51,323,324],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Best available, in order",[44,326],{"x":327,"y":62,"width":328,"height":93,"rx":65,"fill":329,"stroke":105,"style":68},"20","174","#edf8e9",[51,331,334],{"x":332,"y":72,"style":333,"fill":105,"textAnchor":57},"107","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","measured height",[51,336,339],{"x":332,"y":337,"style":338,"fill":140,"textAnchor":57},"100","text-anchor:middle;font-size:10.5px;font-family:monospace","\"height_m\"",[44,341],{"x":342,"y":343,"width":344,"height":345,"fill":117,"stroke":105,"style":90},"70","120","74","58",[51,347,350],{"x":332,"y":348,"style":100,"fill":349,"textAnchor":57},"200","#59645f","rare and excellent",[44,352],{"x":353,"y":62,"width":328,"height":93,"rx":65,"fill":354,"stroke":89,"style":68},"206","#eff3ff",[51,356,358],{"x":357,"y":72,"style":333,"fill":89,"textAnchor":57},"293","storeys × 3",[51,360,361],{"x":357,"y":337,"style":338,"fill":140,"textAnchor":57},"\"levels\" * 3.0",[44,363],{"x":364,"y":365,"width":344,"height":366,"fill":367,"stroke":89,"style":90},"256","126","52","#dbeafe",[51,369,370],{"x":357,"y":348,"style":100,"fill":349,"textAnchor":57},"common, approximate",[44,372],{"x":104,"y":62,"width":328,"height":93,"rx":65,"fill":373,"stroke":374,"style":68},"#eef7f4","#0f766e",[51,376,378],{"x":377,"y":72,"style":333,"fill":374,"textAnchor":57},"479","from LiDAR",[51,380,381],{"x":377,"y":337,"style":338,"fill":140,"textAnchor":57},"DSM − DTM per roof",[44,383],{"x":384,"y":385,"width":344,"height":386,"fill":117,"stroke":374,"style":90},"442","118","60",[51,388,389],{"x":377,"y":348,"style":100,"fill":349,"textAnchor":57},"accurate, more work",[44,391],{"x":392,"y":62,"width":393,"height":93,"rx":65,"fill":66,"stroke":67,"style":68},"578","162",[51,395,397],{"x":396,"y":72,"style":333,"fill":67,"textAnchor":57},"659","one constant",[51,399,400],{"x":396,"y":337,"style":338,"fill":140,"textAnchor":57},"9.0 everywhere",[44,402],{"x":403,"y":343,"width":344,"height":345,"fill":404,"stroke":135,"style":90},"622","#f3e9d8",[51,406,407],{"x":396,"y":348,"style":100,"fill":67,"textAnchor":57},"a placeholder only",[44,409],{"x":343,"y":410,"width":411,"height":86,"rx":133,"fill":134,"stroke":135,"style":90},"248","520",[51,413,415],{"x":53,"y":414,"style":139,"fill":140,"textAnchor":57},"274","a uniform-height city looks convincing and tells the viewer nothing",[177,417,419],{"className":179,"code":418,"language":181,"meta":182,"style":182},"from qgis.core import QgsProperty, QgsAbstract3DSymbol\n\nsymbol = QgsPolygon3DSymbol()\nsymbol.setExtrusionHeight(9.0)   # fallback\n\nproperties = symbol.dataDefinedProperties()\nproperties.setProperty(\n    QgsAbstract3DSymbol.PropertyExtrusionHeight,\n    QgsProperty.fromExpression(\n        'coalesce(\"height_m\", \"levels\" * 3.0, 9.0)'\n    ),\n)\nsymbol.setDataDefinedProperties(properties)\n\nbuildings.setRenderer3D(QgsVectorLayer3DRenderer(symbol))\n",[184,420,421,432,436,444,457,461,471,476,481,486,492,498,503,509,514],{"__ignoreMap":182},[187,422,423,425,427,429],{"class":189,"line":190},[187,424,194],{"class":193},[187,426,198],{"class":197},[187,428,201],{"class":193},[187,430,431],{"class":197}," QgsProperty, QgsAbstract3DSymbol\n",[187,433,434],{"class":189,"line":207},[187,435,224],{"emptyLinePlaceholder":223},[187,437,438,440,442],{"class":189,"line":220},[187,439,260],{"class":197},[187,441,233],{"class":193},[187,443,265],{"class":197},[187,445,446,448,450,453],{"class":189,"line":227},[187,447,271],{"class":197},[187,449,274],{"class":246},[187,451,452],{"class":197},")   ",[187,454,456],{"class":455},"sjoCn","# fallback\n",[187,458,459],{"class":189,"line":252},[187,460,224],{"emptyLinePlaceholder":223},[187,462,463,466,468],{"class":189,"line":257},[187,464,465],{"class":197},"properties ",[187,467,233],{"class":193},[187,469,470],{"class":197}," symbol.dataDefinedProperties()\n",[187,472,473],{"class":189,"line":268},[187,474,475],{"class":197},"properties.setProperty(\n",[187,477,478],{"class":189,"line":280},[187,479,480],{"class":197},"    QgsAbstract3DSymbol.PropertyExtrusionHeight,\n",[187,482,483],{"class":189,"line":285},[187,484,485],{"class":197},"    QgsProperty.fromExpression(\n",[187,487,489],{"class":189,"line":488},10,[187,490,491],{"class":239},"        'coalesce(\"height_m\", \"levels\" * 3.0, 9.0)'\n",[187,493,495],{"class":189,"line":494},11,[187,496,497],{"class":197},"    ),\n",[187,499,501],{"class":189,"line":500},12,[187,502,277],{"class":197},[187,504,506],{"class":189,"line":505},13,[187,507,508],{"class":197},"symbol.setDataDefinedProperties(properties)\n",[187,510,512],{"class":189,"line":511},14,[187,513,224],{"emptyLinePlaceholder":223},[187,515,517],{"class":189,"line":516},15,[187,518,288],{"class":197},[14,520,521,294,523,526,527,530,531,534,535,539],{},[154,522,293],{},[184,524,525],{},"dataDefinedProperties()"," returns a copy, so ",[184,528,529],{},"setDataDefinedProperties()"," afterwards is required — the same read-modify-write pattern that governs most QGIS property collections, and the same silent no-op if you skip it. The ",[184,532,533],{},"coalesce"," expression is the practical heart of this: use a measured height where one exists, fall back to storeys times an assumed storey height, and fall back again to a constant. Three metres per storey is the usual assumption for housing and too low for commercial, which is one more reason to prefer a real height. Because this is an ordinary QGIS expression, everything in ",[21,536,538],{"href":537},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","evaluating expressions"," applies, including aggregates and custom functions.",[143,541,543],{"id":542},"sitting-the-buildings-on-the-ground","Sitting the buildings on the ground",[177,545,547],{"className":179,"code":546,"language":181,"meta":182,"style":182},"from qgis.core import Qgis\n\nsymbol.setAltitudeClamping(Qgis.AltitudeClamping.Terrain)\nsymbol.setAltitudeBinding(Qgis.AltitudeBinding.Centroid)\nsymbol.setOffset(0.0)\n",[184,548,549,560,564,569,574],{"__ignoreMap":182},[187,550,551,553,555,557],{"class":189,"line":190},[187,552,194],{"class":193},[187,554,198],{"class":197},[187,556,201],{"class":193},[187,558,559],{"class":197}," Qgis\n",[187,561,562],{"class":189,"line":207},[187,563,224],{"emptyLinePlaceholder":223},[187,565,566],{"class":189,"line":220},[187,567,568],{"class":197},"symbol.setAltitudeClamping(Qgis.AltitudeClamping.Terrain)\n",[187,570,571],{"class":189,"line":227},[187,572,573],{"class":197},"symbol.setAltitudeBinding(Qgis.AltitudeBinding.Centroid)\n",[187,575,576,579,582],{"class":189,"line":252},[187,577,578],{"class":197},"symbol.setOffset(",[187,580,581],{"class":246},"0.0",[187,583,277],{"class":197},[14,585,586,588,589,592,593,596,597,601,602,605,606,609,610,613],{},[154,587,293],{}," Clamping to terrain places the footprint on the ground; ",[184,590,591],{},"Absolute"," places it at the height in the data regardless of ground, and ",[184,594,595],{},"Relative"," adds the data height to the ground. The binding decides ",[598,599,600],"em",{},"which"," ground height a footprint uses: ",[184,603,604],{},"Centroid"," takes one value at the centre and keeps the base flat, which is right for a building; ",[184,607,608],{},"Vertex"," follows the terrain at each vertex and makes the base drape, which is right for a road or a field boundary and wrong for a wall. A building on a slope with vertex binding has a base that visibly tilts. On QGIS 3.28 and earlier these enums live at ",[184,611,612],{},"QgsPoint3DSymbol.AltitudeClamping"," and similar class-scoped names.",[143,615,617],{"id":616},"colour-and-material","Colour and material",[177,619,621],{"className":179,"code":620,"language":181,"meta":182,"style":182},"from qgis.core import QgsPhongMaterialSettings\nfrom qgis.PyQt.QtGui import QColor\n\nmaterial = QgsPhongMaterialSettings()\nmaterial.setAmbient(QColor(70, 68, 64))\nmaterial.setDiffuse(QColor(206, 200, 190))\nmaterial.setSpecular(QColor(24, 24, 24))\nsymbol.setMaterialSettings(material)\n",[184,622,623,634,646,650,660,681,699,716],{"__ignoreMap":182},[187,624,625,627,629,631],{"class":189,"line":190},[187,626,194],{"class":193},[187,628,198],{"class":197},[187,630,201],{"class":193},[187,632,633],{"class":197}," QgsPhongMaterialSettings\n",[187,635,636,638,641,643],{"class":189,"line":207},[187,637,194],{"class":193},[187,639,640],{"class":197}," qgis.PyQt.QtGui ",[187,642,201],{"class":193},[187,644,645],{"class":197}," QColor\n",[187,647,648],{"class":189,"line":220},[187,649,224],{"emptyLinePlaceholder":223},[187,651,652,655,657],{"class":189,"line":227},[187,653,654],{"class":197},"material ",[187,656,233],{"class":193},[187,658,659],{"class":197}," QgsPhongMaterialSettings()\n",[187,661,662,665,667,670,673,675,678],{"class":189,"line":252},[187,663,664],{"class":197},"material.setAmbient(QColor(",[187,666,342],{"class":246},[187,668,669],{"class":197},", ",[187,671,672],{"class":246},"68",[187,674,669],{"class":197},[187,676,677],{"class":246},"64",[187,679,680],{"class":197},"))\n",[187,682,683,686,688,690,692,694,697],{"class":189,"line":257},[187,684,685],{"class":197},"material.setDiffuse(QColor(",[187,687,353],{"class":246},[187,689,669],{"class":197},[187,691,348],{"class":246},[187,693,669],{"class":197},[187,695,696],{"class":246},"190",[187,698,680],{"class":197},[187,700,701,704,706,708,710,712,714],{"class":189,"line":268},[187,702,703],{"class":197},"material.setSpecular(QColor(",[187,705,61],{"class":246},[187,707,669],{"class":197},[187,709,61],{"class":246},[187,711,669],{"class":197},[187,713,61],{"class":246},[187,715,680],{"class":197},[187,717,718],{"class":189,"line":280},[187,719,720],{"class":197},"symbol.setMaterialSettings(material)\n",[14,722,723,725],{},[154,724,293],{}," Keeping specular near-black is what stops masonry looking like polished plastic — the default in some releases is far too high. Ambient controls how dark the shadowed faces go; too low and one side of every building is a black silhouette, too high and the scene flattens. For colour driven by data, the material's diffuse colour also accepts a data-defined property, so colouring by use class, age or height band works exactly as it does in 2D, and it is usually more informative than the height alone.",[14,727,728,729,732,733,168],{},"Where a layer needs several distinct 3D looks, ",[184,730,731],{},"QgsRuleBased3DRenderer"," takes rules with filter expressions, each carrying its own symbol — the 3D analogue of ",[21,734,736],{"href":735},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","a rule-based renderer",[143,738,740],{"id":739},"deriving-heights-when-there-are-none","Deriving heights when there are none",[14,742,743],{},"If the footprints carry no height and no storey count, the honest options are to state a constant as a placeholder, or to measure the heights from LiDAR. The second is a short chain of algorithms and gives a genuine model.",[177,745,747],{"className":179,"code":746,"language":181,"meta":182,"style":182},"import processing\n\nprocessing.run(\"native:zonalstatisticsfb\", {\n    \"INPUT\": buildings,\n    \"INPUT_RASTER\": \"\u002Fdata\u002Foutput\u002FnDSM.tif\",\n    \"RASTER_BAND\": 1,\n    \"COLUMN_PREFIX\": \"h_\",\n    \"STATISTICS\": [2, 5],          # mean and max\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fbuildings_h.gpkg\",\n})\n",[184,748,749,756,760,771,779,793,805,817,839,851],{"__ignoreMap":182},[187,750,751,753],{"class":189,"line":190},[187,752,201],{"class":193},[187,754,755],{"class":197}," processing\n",[187,757,758],{"class":189,"line":207},[187,759,224],{"emptyLinePlaceholder":223},[187,761,762,765,768],{"class":189,"line":220},[187,763,764],{"class":197},"processing.run(",[187,766,767],{"class":239},"\"native:zonalstatisticsfb\"",[187,769,770],{"class":197},", {\n",[187,772,773,776],{"class":189,"line":227},[187,774,775],{"class":239},"    \"INPUT\"",[187,777,778],{"class":197},": buildings,\n",[187,780,781,784,787,790],{"class":189,"line":252},[187,782,783],{"class":239},"    \"INPUT_RASTER\"",[187,785,786],{"class":197},": ",[187,788,789],{"class":239},"\"\u002Fdata\u002Foutput\u002FnDSM.tif\"",[187,791,792],{"class":197},",\n",[187,794,795,798,800,803],{"class":189,"line":257},[187,796,797],{"class":239},"    \"RASTER_BAND\"",[187,799,786],{"class":197},[187,801,802],{"class":246},"1",[187,804,792],{"class":197},[187,806,807,810,812,815],{"class":189,"line":268},[187,808,809],{"class":239},"    \"COLUMN_PREFIX\"",[187,811,786],{"class":197},[187,813,814],{"class":239},"\"h_\"",[187,816,792],{"class":197},[187,818,819,822,825,828,830,833,836],{"class":189,"line":280},[187,820,821],{"class":239},"    \"STATISTICS\"",[187,823,824],{"class":197},": [",[187,826,827],{"class":246},"2",[187,829,669],{"class":197},[187,831,832],{"class":246},"5",[187,834,835],{"class":197},"],          ",[187,837,838],{"class":455},"# mean and max\n",[187,840,841,844,846,849],{"class":189,"line":285},[187,842,843],{"class":239},"    \"OUTPUT\"",[187,845,786],{"class":197},[187,847,848],{"class":239},"\"\u002Fdata\u002Foutput\u002Fbuildings_h.gpkg\"",[187,850,792],{"class":197},[187,852,853],{"class":189,"line":488},[187,854,855],{"class":197},"})\n",[14,857,858,860,861,865],{},[154,859,293],{}," The input raster is a normalised surface model — the surface model minus the terrain model — so each cell holds height above ground, and its mean inside a footprint is the building's average roof height. Producing that raster is the point-cloud work in ",[21,862,864],{"href":863},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fcreate-dem-from-point-cloud-pyqgis\u002F","creating a DEM from a point cloud",". Taking both mean and maximum is worth it: the mean is the sensible extrusion height, and a maximum far above it flags a footprint that overlaps a tree or a taller neighbour, which is exactly the quality check the result needs.",[143,867,869],{"id":868},"checking-the-model-before-anyone-else-does","Checking the model before anyone else does",[14,871,872],{},"A city model is convincing enough that reviewers rarely question it, so it is worth running two checks yourself.",[14,874,875],{},"The first is a height distribution. Extruded buildings hide their outliers — a thirty-storey error in a suburb looks like a tower block rather than like a mistake — so read the numbers rather than the picture.",[177,877,879],{"className":179,"code":878,"language":181,"meta":182,"style":182},"heights = []\nfor feature in buildings.getFeatures():\n    value = feature[\"height_m\"]\n    if value is not None:\n        heights.append(float(value))\n\nheights.sort()\ncount = len(heights)\nprint(f\"n={count} min={heights[0]:.1f} median={heights[count \u002F\u002F 2]:.1f} \"\n      f\"p99={heights[int(count * 0.99)]:.1f} max={heights[-1]:.1f}\")\nprint(\"above 60 m:\", sum(1 for h in heights if h > 60))\n",[184,880,881,891,905,919,939,950,954,959,972,1036,1090],{"__ignoreMap":182},[187,882,883,886,888],{"class":189,"line":190},[187,884,885],{"class":197},"heights ",[187,887,233],{"class":193},[187,889,890],{"class":197}," []\n",[187,892,893,896,899,902],{"class":189,"line":207},[187,894,895],{"class":193},"for",[187,897,898],{"class":197}," feature ",[187,900,901],{"class":193},"in",[187,903,904],{"class":197}," buildings.getFeatures():\n",[187,906,907,910,912,915,917],{"class":189,"line":220},[187,908,909],{"class":197},"    value ",[187,911,233],{"class":193},[187,913,914],{"class":197}," feature[",[187,916,339],{"class":239},[187,918,249],{"class":197},[187,920,921,924,927,930,933,936],{"class":189,"line":227},[187,922,923],{"class":193},"    if",[187,925,926],{"class":197}," value ",[187,928,929],{"class":193},"is",[187,931,932],{"class":193}," not",[187,934,935],{"class":246}," None",[187,937,938],{"class":197},":\n",[187,940,941,944,947],{"class":189,"line":252},[187,942,943],{"class":197},"        heights.append(",[187,945,946],{"class":246},"float",[187,948,949],{"class":197},"(value))\n",[187,951,952],{"class":189,"line":257},[187,953,224],{"emptyLinePlaceholder":223},[187,955,956],{"class":189,"line":268},[187,957,958],{"class":197},"heights.sort()\n",[187,960,961,964,966,969],{"class":189,"line":280},[187,962,963],{"class":197},"count ",[187,965,233],{"class":193},[187,967,968],{"class":246}," len",[187,970,971],{"class":197},"(heights)\n",[187,973,974,977,980,983,986,989,992,995,998,1000,1003,1005,1008,1011,1013,1016,1018,1021,1024,1027,1029,1031,1033],{"class":189,"line":285},[187,975,976],{"class":246},"print",[187,978,979],{"class":197},"(",[187,981,982],{"class":193},"f",[187,984,985],{"class":239},"\"n=",[187,987,988],{"class":246},"{",[187,990,991],{"class":197},"count",[187,993,994],{"class":246},"}",[187,996,997],{"class":239}," min=",[187,999,988],{"class":246},[187,1001,1002],{"class":197},"heights[",[187,1004,46],{"class":246},[187,1006,1007],{"class":197},"]",[187,1009,1010],{"class":193},":.1f",[187,1012,994],{"class":246},[187,1014,1015],{"class":239}," median=",[187,1017,988],{"class":246},[187,1019,1020],{"class":197},"heights[count ",[187,1022,1023],{"class":193},"\u002F\u002F",[187,1025,1026],{"class":246}," 2",[187,1028,1007],{"class":197},[187,1030,1010],{"class":193},[187,1032,994],{"class":246},[187,1034,1035],{"class":239}," \"\n",[187,1037,1038,1041,1044,1046,1048,1051,1054,1057,1060,1063,1065,1067,1070,1072,1074,1077,1079,1081,1083,1085,1088],{"class":189,"line":488},[187,1039,1040],{"class":193},"      f",[187,1042,1043],{"class":239},"\"p99=",[187,1045,988],{"class":246},[187,1047,1002],{"class":197},[187,1049,1050],{"class":246},"int",[187,1052,1053],{"class":197},"(count ",[187,1055,1056],{"class":193},"*",[187,1058,1059],{"class":246}," 0.99",[187,1061,1062],{"class":197},")]",[187,1064,1010],{"class":193},[187,1066,994],{"class":246},[187,1068,1069],{"class":239}," max=",[187,1071,988],{"class":246},[187,1073,1002],{"class":197},[187,1075,1076],{"class":193},"-",[187,1078,802],{"class":246},[187,1080,1007],{"class":197},[187,1082,1010],{"class":193},[187,1084,994],{"class":246},[187,1086,1087],{"class":239},"\"",[187,1089,277],{"class":197},[187,1091,1092,1094,1096,1099,1101,1104,1106,1108,1111,1114,1116,1119,1122,1124,1127,1130],{"class":189,"line":494},[187,1093,976],{"class":246},[187,1095,979],{"class":197},[187,1097,1098],{"class":239},"\"above 60 m:\"",[187,1100,669],{"class":197},[187,1102,1103],{"class":246},"sum",[187,1105,979],{"class":197},[187,1107,802],{"class":246},[187,1109,1110],{"class":193}," for",[187,1112,1113],{"class":197}," h ",[187,1115,901],{"class":193},[187,1117,1118],{"class":197}," heights ",[187,1120,1121],{"class":193},"if",[187,1123,1113],{"class":197},[187,1125,1126],{"class":193},">",[187,1128,1129],{"class":246}," 60",[187,1131,680],{"class":197},[14,1133,1134,1136],{},[154,1135,293],{}," The gap between the ninety-ninth percentile and the maximum is the useful number: on a real city they are close, and a maximum many times the percentile means a handful of bad records. Counting the buildings above a threshold you know to be implausible for the area turns that into a list you can go and look at. Doing this before rendering saves explaining a skyscraper later.",[14,1138,1139],{},"The second check is coverage. Count how many footprints fell through to the constant fallback, because a model where sixty per cent of buildings are the same placeholder height is not a model of that city.",[177,1141,1143],{"className":179,"code":1142,"language":181,"meta":182,"style":182},"total = buildings.featureCount()\nwith_height = sum(\n    1 for f in buildings.getFeatures()\n    if f[\"height_m\"] is not None or f[\"levels\"] is not None\n)\nprint(f\"{with_height}\u002F{total} have a real height ({100 * with_height \u002F total:.0f}%)\")\n",[184,1144,1145,1155,1168,1183,1218,1222],{"__ignoreMap":182},[187,1146,1147,1150,1152],{"class":189,"line":190},[187,1148,1149],{"class":197},"total ",[187,1151,233],{"class":193},[187,1153,1154],{"class":197}," buildings.featureCount()\n",[187,1156,1157,1160,1162,1165],{"class":189,"line":207},[187,1158,1159],{"class":197},"with_height ",[187,1161,233],{"class":193},[187,1163,1164],{"class":246}," sum",[187,1166,1167],{"class":197},"(\n",[187,1169,1170,1173,1175,1178,1180],{"class":189,"line":220},[187,1171,1172],{"class":246},"    1",[187,1174,1110],{"class":193},[187,1176,1177],{"class":197}," f ",[187,1179,901],{"class":193},[187,1181,1182],{"class":197}," buildings.getFeatures()\n",[187,1184,1185,1187,1190,1192,1195,1197,1199,1201,1204,1206,1209,1211,1213,1215],{"class":189,"line":227},[187,1186,923],{"class":193},[187,1188,1189],{"class":197}," f[",[187,1191,339],{"class":239},[187,1193,1194],{"class":197},"] ",[187,1196,929],{"class":193},[187,1198,932],{"class":193},[187,1200,935],{"class":246},[187,1202,1203],{"class":193}," or",[187,1205,1189],{"class":197},[187,1207,1208],{"class":239},"\"levels\"",[187,1210,1194],{"class":197},[187,1212,929],{"class":193},[187,1214,932],{"class":193},[187,1216,1217],{"class":246}," None\n",[187,1219,1220],{"class":189,"line":252},[187,1221,277],{"class":197},[187,1223,1224,1226,1228,1230,1232,1234,1237,1239,1242,1244,1247,1249,1252,1255,1258,1261,1263,1266,1269,1271,1274],{"class":189,"line":257},[187,1225,976],{"class":246},[187,1227,979],{"class":197},[187,1229,982],{"class":193},[187,1231,1087],{"class":239},[187,1233,988],{"class":246},[187,1235,1236],{"class":197},"with_height",[187,1238,994],{"class":246},[187,1240,1241],{"class":239},"\u002F",[187,1243,988],{"class":246},[187,1245,1246],{"class":197},"total",[187,1248,994],{"class":246},[187,1250,1251],{"class":239}," have a real height (",[187,1253,1254],{"class":246},"{100",[187,1256,1257],{"class":193}," *",[187,1259,1260],{"class":197}," with_height ",[187,1262,1241],{"class":193},[187,1264,1265],{"class":197}," total",[187,1267,1268],{"class":193},":.0f",[187,1270,994],{"class":246},[187,1272,1273],{"class":239},"%)\"",[187,1275,277],{"class":197},[14,1277,1278,1280],{},[154,1279,293],{}," Reporting this alongside the map is the difference between a model and a picture. Where the percentage is low, deriving heights from LiDAR as described below is usually the only way to raise it, and it is worth the effort precisely because the alternative looks the same and means much less.",[143,1282,1284],{"id":1283},"qgis-version-compatibility","QGIS version compatibility",[14,1286,1287,1290,1291,1294,1295,1298,1299,1301,1302,1305],{},[184,1288,1289],{},"QgsPolygon3DSymbol"," and ",[184,1292,1293],{},"QgsVectorLayer3DRenderer"," have been present since QGIS 3.0, with data-defined extrusion arriving in 3.10. The altitude clamping and binding enums moved into the scoped ",[184,1296,1297],{},"Qgis"," namespace in 3.30, with the older class-scoped names still available. ",[184,1300,731],{}," arrived in 3.12. ",[184,1303,1304],{},"QgsPhongMaterialSettings"," gained additional material types alongside it in 3.16, and the Phong settings remain the sensible default for buildings.",[143,1307,1309],{"id":1308},"troubleshooting","Troubleshooting",[148,1311,1312,1321,1333,1344,1353,1359],{},[151,1313,1314,1317,1318,168],{},[154,1315,1316],{},"Buildings are flat."," No extrusion height set, or the data-defined property was built and never assigned back with ",[184,1319,1320],{},"setDataDefinedProperties",[151,1322,1323,1326,1327,1329,1330,168],{},[154,1324,1325],{},"Buildings float or sink on a slope."," Altitude clamping is ",[184,1328,591],{}," where it should be ",[184,1331,1332],{},"Terrain",[151,1334,1335,1338,1339,1341,1342,168],{},[154,1336,1337],{},"A building's base is tilted."," Altitude binding is ",[184,1340,608],{}," rather than ",[184,1343,604],{},[151,1345,1346,1349,1350,1352],{},[154,1347,1348],{},"Every building is the same height despite an attribute."," The expression references a field name that does not exist; ",[184,1351,533],{}," then falls through to the constant silently.",[151,1354,1355,1358],{},[154,1356,1357],{},"The scene is unusably slow."," Too many vertices per footprint; simplify before extruding.",[151,1360,1361,1364],{},[154,1362,1363],{},"Buildings look like plastic."," Specular is too high in the material settings.",[143,1366,1368],{"id":1367},"conclusion","Conclusion",[14,1370,1371],{},"Extrude from the best height available and say which it was, clamp to terrain with centroid binding so buildings sit on the ground rather than through it, and keep the material dull. A city model built from real heights answers questions about shadow, view and massing; one built from a constant answers none of them while looking exactly as convincing.",[143,1373,1375],{"id":1374},"frequently-asked-questions","Frequently Asked Questions",[14,1377,1378,1381],{},[154,1379,1380],{},"Can I extrude down as well as up?","\nYes, with a negative extrusion height plus an offset, which is how basements and cuttings are shown. Combining a negative height with terrain clamping needs care, since the base is the terrain.",[14,1383,1384,1387,1388,1391,1392,1395],{},[154,1385,1386],{},"Does extrusion work on lines and points?","\nLines extrude into walls with ",[184,1389,1390],{},"QgsLine3DSymbol",", which is how fences and retaining walls are modelled. Points take ",[184,1393,1394],{},"QgsPoint3DSymbol",", which places a shape or a loaded 3D model rather than extruding.",[14,1397,1398,1401],{},[154,1399,1400],{},"How do I show roof shapes rather than flat tops?","\nQGIS extrudes to a flat roof. Real roof geometry needs a true 3D model format, imported as a scene model rather than derived from footprints.",[14,1403,1404,1407],{},[154,1405,1406],{},"Can the extrusion height be animated?","\nOnly by changing the expression and re-rendering. There is no interpolation between values across frames.",[143,1409,1411],{"id":1410},"related","Related",[148,1413,1414,1419,1424,1429,1435],{},[151,1415,1416,1418],{},[21,1417,24],{"href":23}," — the guide this recipe belongs to",[151,1420,1421],{},[21,1422,1423],{"href":166},"Configure a 3D Map View in PyQGIS",[151,1425,1426],{},[21,1427,1428],{"href":863},"Create a DEM from a Point Cloud in PyQGIS",[151,1430,1431],{},[21,1432,1434],{"href":1433},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002F","Zonal Statistics in PyQGIS",[151,1436,1437],{},[21,1438,1439],{"href":735},"Rule-Based Renderer in PyQGIS",[1441,1442,1443],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":182,"searchDepth":207,"depth":207,"links":1445},[1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457],{"id":145,"depth":207,"text":146},{"id":171,"depth":207,"text":172},{"id":305,"depth":207,"text":306},{"id":542,"depth":207,"text":543},{"id":616,"depth":207,"text":617},{"id":739,"depth":207,"text":740},{"id":868,"depth":207,"text":869},{"id":1283,"depth":207,"text":1284},{"id":1308,"depth":207,"text":1309},{"id":1367,"depth":207,"text":1368},{"id":1374,"depth":207,"text":1375},{"id":1410,"depth":207,"text":1411},"Give polygons real height in a QGIS 3D scene — data-defined extrusion from an attribute, height above terrain, materials and colour by attribute, and what to do without height data.","md",{"slug":1461,"type":1462,"breadcrumb":1463,"datePublished":1464,"dateModified":1464},"extrude-buildings-in-3d-pyqgis","article","Extrude Buildings in 3D","2026-09-05","\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fextrude-buildings-in-3d-pyqgis",{"title":5,"description":1458},"pyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fextrude-buildings-in-3d-pyqgis\u002Findex","T6CBpTyP1XGI5kfXGx7IH1W59YBzxMGbL7TzVv3zJ3Q",1788563847735]