I would like to find a way to import 3D XML (zipped file with XML Data and textures) files into Blender or find a workaround without using 3dvia, or AutoDesk to convert it.

I know there is a python script for Blender 2.49. But I can't get it to work (http://kiwi6.com/file/6u3rxha3zw).

Free 3D XML files of Rotterdam are available here: http://rotterdamopendata.nl/dataset/rotterdam-3d-bestanden

It's cool that they have put these models online, but to work with them...

PS: I know XML isn't a 3D format, but a Markup Language.

  • $\begingroup$ Which converter? I'm not willing to pay for Autodesk or 3dvia... $\endgroup$
    – Abel
    Commented Nov 30, 2015 at 16:04
  • $\begingroup$ Thanks for your comment. But they are referring to the tool that made these models into 3D XML. Tool feature: "Converts polygon shape files into solid 3D models in KML/KMZ format (Google Earth 3D format), cityGML format, Shape Multipatch format and OBJ format." I gues for now the only way to get those models is to re-make (Do the same what they did) them and not convert it to a format only for viewing purposes... They also suggest that if people would want to use the models; to use Autocad... I think I'll send them a email, maybe they'll respond in a helpfull way... $\endgroup$
    – Abel
    Commented Nov 30, 2015 at 18:27

2 Answers 2


Import Citygml Model

Zuidplein textured Textures applied to Zuidplein model.

I've merged all the scripts into 1 for convenience, would envisage splitting them up if made into an addon.

import bpy

from mathutils import Vector

##### Refactor to own file citygml.py
class Texture:
    def __init__(self, id=None, path="", map=""):
        def unflatten(coords):
            return [Vector(float(x) for x in coords[i:i+2]) for i in range(0, len(coords), 2)]        
        self.id = id
        self.path = path
        self.map = unflatten(map.split())

class PolyGon:
    def __init__(self, id=None, pts=""):
        def unflatten(coords):
            return [Vector(float(x) for x in coords[i:i+3]) for i in range(0, len(coords), 3)]
        self.id = id
        self.verts = unflatten(pts.split())

class Building:
    def __init__(self, id="", name="", surfaces={}, textures={}):
        self.id = id
        self.name = name
        self.surfaces = {s.id: s for s in surfaces}
        self.textures = {t.id: t for t in textures}

class CityModel:

    def __init__(self, name="", upper_corner="", lower_corner=""):
        self.lower_corner = Vector(float(x) for x in lower_corner.split())
        self.upper_corner = Vector(float(x) for x in upper_corner.split())
        self.name = name
        self.buildings = {}
        self.images = {}

    def add_building(self, building):
        self.buildings[building.id] = building

    def build(self, scene, filepath, scale):
        import bmesh
        import os

        for k, b in self.buildings.items():
            keep_verts = []
            bm = bmesh.new()
            mesh = bpy.data.meshes.new(b.id)

            for sid, s in b.surfaces.items():
                verts = [bm.verts.new(scale * (v - self.lower_corner)) for v in s.verts]
                face = bm.faces.new(verts)
                texture = b.textures.get("#%s" % s.id)

                if texture:                    
                    uv_layer = bm.loops.layers.uv.verify()
                    tex = bm.faces.layers.tex.verify()
                    for i,l in enumerate(bm.faces[-1].loops):
                        uv = l[uv_layer].uv
                        (uv.x, uv.y) = texture.map[i]

                    path = os.path.join(filepath, texture.path).replace("\\", "/")
                    image = bpy.data.images.get(os.path.basename(path))
                    if not image:
                        image = bpy.data.images.load(path)
                        image.use_fake_user = True  

                    bm.faces[-1][tex].image = image  
                    dubs = bmesh.ops.find_doubles(bm,
                    if len(dubs.keys()) > 2:
            verts = list(set(bm.verts) - set(keep_verts))
            bmesh.ops.remove_doubles(bm, verts=verts, dist=0.001)
            #bmesh.ops.automerge(bm, verts=bm.verts, dist=0.000001)
            building = bpy.data.objects.new("building", mesh)

def read_some_data2(context, filepath, directory, use_some_setting, scale):
    scene = context.scene
    from xml.etree import ElementTree as ET
    cityxml = ET.parse(filepath)
    citymodelnode = cityxml.getroot()
    print("Importing citygml city")
    namespaces = {
           "core":"http://www.opengis.net/citygml/base/1.0" ,
    for prefix, uri in namespaces.items():
        ET.register_namespace(prefix, uri)            
    bbox = cityxml.find("gml:boundedBy", namespaces=namespaces)

    name = bbox.find(".//gml:Envelope", namespaces=namespaces).attrib["srsName"]
    print("name: %s" % name)
    uc = bbox.find(".//gml:upperCorner", namespaces=namespaces).text
    lc = bbox.find(".//gml:lowerCorner", namespaces=namespaces).text

    city = CityModel(name=name, upper_corner=uc, lower_corner=lc)

    cityobjects = cityxml.findall("citygml:cityObjectMember",

    print("importing %d city objects (#:buildings, X:non-building)" % len(cityobjects))

    for cityobject in cityobjects:

        building = cityobject.find("bldg:Building", namespaces=namespaces)
        if building is None:
            # ugly hack do for now.
            groundsurface = cityobject.find("bldg:GroundSurface", namespaces=namespaces)
            print("X", end="")
            if groundsurface is None:
            building = groundsurface

            print("#", end="")
        building_id = building.attrib["{%s}id" % namespaces["gml"]]

        rings = cityobject.findall(".//gml:Polygon//gml:LinearRing", namespaces=namespaces)
        surfaces = []
        for ring in rings:
            ring_id = ring.attrib["{%s}id" % namespaces["gml"]]
            poslist = ring.find("gml:posList", namespaces=namespaces).text
            surfaces.append(PolyGon(id=ring_id, pts=poslist))

        textures = []
        sdms = cityobject.findall(".//app:surfaceDataMember", namespaces=namespaces)
        for sdm in sdms:
            coords = sdm.find(".//app:textureCoordinates", namespaces=namespaces)
            if coords is None:
            sdm_ring = coords.attrib["ring"]
            path = r"%s" % sdm.find(".//app:imageURI", namespaces=namespaces).text
            textures.append(Texture(id=sdm_ring, map=coords.text, path=path))

        building = Building(id=building_id, surfaces=surfaces, textures=textures)
    print("finished importing, building...")
    city.build(scene, directory, scale)
    return {'FINISHED'}    

from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty, BoolProperty, FloatProperty
from bpy.types import Operator

class ImportCityGML(Operator, ImportHelper):
    """Import CityGML"""
    bl_idname = "import_dem.city_gml"  # important since its how bpy.ops.import_test.some_data is constructed
    bl_label = "Load City"

    # ImportHelper mixin class uses this
    filename_ext = ".xml"

    filter_glob = StringProperty(
    directory = StringProperty()
    filepath = StringProperty(name="File Path", description="Filepath used for importing txt files", maxlen= 1024, default= "")

    use_setting = BoolProperty(
            name="Save to Text Editor",
            description="Make a copy of generated script in text editor",
    scale = FloatProperty(
            description="Scale the model",

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "scale")

    def execute(self, context):
        return read_some_data2(context, self.filepath, self.directory, self.use_setting, self.scale)

# Only needed if you want to add into a dynamic menu
def menu_func_import(self, context):
    self.layout.operator(ImportCityGML.bl_idname, text="Import CityGML")

def register():

def unregister():

if __name__ == "__main__":

    # test call

3-28-WITTE-DORP.xml enter image description here
enter image description here

Witt Dorp with textures.


This is a non lxml version and works with the xml.etree version from blender. The remove doubles code from Removing doubles on imported 3DXML ngon mesh has been added which greatly reduces the amount of verts. Might post the lxml version on my github page if anyone is interested.

  • $\begingroup$ You are a genius, thanks a lot for your commitment and dedication. $\endgroup$
    – Abel
    Commented Dec 6, 2015 at 13:24
  • $\begingroup$ @Abel changed to a non lxml version. $\endgroup$
    – batFINGER
    Commented Dec 8, 2015 at 15:22
  • $\begingroup$ @Abel small fix, as I was losing a couple of triangular roof segments on bospolder. $\endgroup$
    – batFINGER
    Commented Dec 9, 2015 at 17:08
  • $\begingroup$ @ batFINGER and Zeffli. I'ts been a while since I had the time to stay active at blender stackexchange due to personal reasons and sickness. But again I must thank you; batFINGER and Zeffli for your effort in this Blender "Question"/"Thread". This will definitely help others who are working with Rotterdam 3D as well as people who are working with 3DXML files. $\endgroup$
    – Abel
    Commented Feb 3, 2016 at 18:19
  • $\begingroup$ Searching for import 3dxml to blender got me here. I am new on blender and scripting. Could this be used to import any 3dxml files? $\endgroup$ Commented Jan 17, 2020 at 2:13

enter image description here


Here's a basic importer of this kind of .xml. I think it's fair to say that xml files can be whatever you want them to be, in this case it is a 3d file format which contains coordinates and even texture information. It also seems to contain parametric texturing information (for repeating bricks walls, tiles, trees and foilage..etc)

This is a pretty fast importer because it imports all geometry as one mesh, but it could also be written to import geometry per building/unit.

By the way, the addon doesn't automatically zoom to show the new import, these files of Rotterdam are huge (geometryically speaking) I suggest to:

  • select the imported object from the outliner and press Period, to View Selected .
  • or press Home to view All.

If you want to discuss this add-on further please use the github issue tracker.

  • $\begingroup$ Dude it worked! I downloaded the file of "Zuidplein" from the Rotterdam models link and just by opening the .xml file I got the model in Blender. Next step is texturing, for now manually... Thank you verry much for your help! $\endgroup$
    – Abel
    Commented Dec 2, 2015 at 17:49
  • 3
    $\begingroup$ I didn't spend any time getting textures mapped to polygons, difficult to predict how much effort that would involve... bit like heisenberg's cat, don't know until I attempt to code it. Heisencode :) $\endgroup$
    – zeffii
    Commented Dec 2, 2015 at 23:16
  • $\begingroup$ You are being very helpful, I appreciate that very much. Note that when you google for a 3dxml converter you'll always end up with autodesk, 3dvia or the old blender 2.49 script. I think it could help out a lot of people who are in similar situations. Hope the Bounty will raise some more attention. I'm not that good at programming, so again I thank you for your effort in starting this script with Github. $\endgroup$
    – Abel
    Commented Dec 3, 2015 at 21:08

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .