8
$\begingroup$

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.

$\endgroup$
  • $\begingroup$ Which converter? I'm not willing to pay for Autodesk or 3dvia... $\endgroup$ – Abel Nov 30 '15 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 Nov 30 '15 at 18:27
9
+50
$\begingroup$

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()
            bm.verts.ensure_lookup_table()
            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()
                    bm.faces.ensure_lookup_table()
                    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,
                              verts=verts,
                              dist=0.001)['targetmap']
                    if len(dubs.keys()) > 2:
                        keep_verts.extend(dubs.values())
            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)
            bm.to_mesh(mesh)
            building = bpy.data.objects.new("building", mesh)
            scene.objects.link(building)
            bm.free()

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 = {
           "citygml":"http://www.opengis.net/citygml/1.0",
           "core":"http://www.opengis.net/citygml/base/1.0" ,
           "tex":"http://www.opengis.net/citygml/textures/1.0",
           "gml":"http://www.opengis.net/gml",
           "bldg":"http://www.opengis.net/citygml/building/1.0", 
           "app":"http://www.opengis.net/citygml/appearance/1.0", 
           "dem":"http://www.opengis.net/citygml/relief/1.0",
           "tran":"http://www.opengis.net/citygml/transportation/1.0",
           "gen":"http://www.opengis.net/citygml/generics/1.0",
           "frn":"http://www.opengis.net/citygml/cityfurniture/1.0", 
           "wtr":"http://www.opengis.net/citygml/waterbody/1.0", 
           "luse":"http://www.opengis.net/citygml/landuse/1.0", 
           "veg":"http://www.opengis.net/citygml/vegetation/1.0", 
           "xAL":"urn:oasis:names:tc:ciq:xsdschema:xAL:2.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",
                                namespaces=namespaces)

    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:
                continue
            building = groundsurface
        else:

            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:
                continue
            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)
        city.add_building(building)
    print()
    print("finished importing, building...")
    city.build(scene, directory, scale)
    print("done")
    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(
            default="*.xml",
            options={'HIDDEN'},
            )
    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",
            default=False,
            )
    scale = FloatProperty(
            name="Scale",
            description="Scale the model",
            default=0.05,
            min=0.01,
            max=10.0
            )

    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():
    bpy.utils.register_class(ImportCityGML)
    bpy.types.INFO_MT_file_import.append(menu_func_import)


def unregister():
    bpy.utils.unregister_class(ImportCityGML)
    bpy.types.INFO_MT_file_import.remove(menu_func_import)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.import_dem.city_gml('INVOKE_DEFAULT')

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

Witt Dorp with textures.

EDIT:

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.

$\endgroup$
  • $\begingroup$ You are a genius, thanks a lot for your commitment and dedication. $\endgroup$ – Abel Dec 6 '15 at 13:24
  • $\begingroup$ @Abel changed to a non lxml version. $\endgroup$ – batFINGER Dec 8 '15 at 15:22
  • $\begingroup$ @Abel small fix, as I was losing a couple of triangular roof segments on bospolder. $\endgroup$ – batFINGER Dec 9 '15 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 Feb 3 '16 at 18:19
8
$\begingroup$

enter image description here

io_cityGML_basic

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.

$\endgroup$
  • $\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 Dec 2 '15 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 Dec 2 '15 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 Dec 3 '15 at 21:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.