# Chris Xiong 2018
# Expat (MIT) License
bl_info={
"name":"3D XML (3.0) import",
"description":"Import 3D XML 3.0",
"author":"Chris Xiong",
"version":(0,1),
"blender":(2,79,0),
"category":"Import-Export",
"support":"TESTING"
}
################################################################
# This addon enables blender to import 3D XML 3.0 documents.
# It partially implemented the 3D XML specification 3.0 from
# Dassault Systèmes. Testing was done with files exported
# from 3DVIA Virtools 5.0.
#
# The development took place solely on Linux. It may experience
# problems on other platforms.
#
# This addon started off as a fork of this script:
# https://www.blender.org/forum/viewtopic.php?t=18299
# Later it was rewritten from scratch to better reflect the
# specification.
#
# To report bugs, mail me a minimal 3D XML file that can
# reproduce the issue together with the details of the bug.
#
# References:
# 1) 3D XML Reference Documentation 3.0:
# http://media.3ds.com/3dxml/3dxmldoc/Reference_Guide/3DXML_Reference_Guide.html
# 2) 3D XML 3.0 XSD schema:
# http://media.3ds.com/3dxml/3dxmldoc/3DXML.xsd
# 3) 3D XML User's Guide 3.0:
# http://media.3ds.com/3dxml/3dxmldoc/3DXML_User_Guide.pdf
################################################################
import bpy,bmesh,bpy_extras,mathutils
import xml.etree.ElementTree as etree
import pathlib,zipfile,time,os,tempfile
NS="{http://www.3ds.com/xsd/3DXML}"
meshes=dict()
meshmat=dict()
materials=dict()
textures=dict()
texturefiles=dict()
unitfactor=0.01292
texdir="/Ballance/Textures/"
def load_textures(tree):
txd=pathlib.Path(texdir)
for tex in tree.findall(f".//{NS}Image"):
txname=tex.attrib["name"]
txp=[]
if txd.is_dir():
txp=[i for i in txd.iterdir() if i.stem.lower()==txname.lower()]
if len(txp)<1 and tex.attrib["href"] is not None:
txp=[texturefiles[tex.attrib["href"]]]
tx=bpy.data.textures.new(txname,'IMAGE')
try:
tx.image=bpy.data.images.load(str(txp[0]),True)
except IndexError:
print(txname)
textures[tex.attrib["id"]]=tx
def load_materials(tree):
for mat in tree.findall(f".//{NS}GraphicMaterial"):
mname=mat.attrib["name"]
m=bpy.data.materials.new(mname)
mslot=m.texture_slots.add()
try:
mslot.texture=textures[mat.attrib["texture"].split(":")[-1]]
except KeyError:
pass
m.diffuse_color=[float(mat.find(f"{NS}Diffuse").attrib[i])for i in ["red","green","blue"]]
m.diffuse_intensity=float(mat.attrib["diffuseCoef"])
m.specular_color=[float(mat.find(f"{NS}Specular").attrib[i])for i in ["red","green","blue"]]
m.specular_alpha=float(mat.find(f"{NS}Specular").attrib["alpha"])
m.specular_intensity=float(mat.attrib["specularCoef"])
materials[mat.attrib["id"]]=m
def load_meshes(tree):
for rep in tree.findall(f".//{NS}Representation"):
rid=rep.attrib["id"]
verts=unflatten(rep.find(f".//{NS}Positions").text,float,3,unitfactor)
uvs=unflatten(rep.find(f".//{NS}TextureCoordinates").text,float,2)
faces=[]
facemat=[]
matslots=[]
defmat=-1
facesel=rep.find(f".//{NS}Faces")
if facesel.find(f"{NS}SurfaceAttributes/{NS}MaterialApplication/{NS}MaterialId") is not None:
defmat=0
matslots.append(facesel.find(f"./{NS}SurfaceAttributes/{NS}MaterialApplication/{NS}MaterialId").text)
for face in facesel.findall(f"{NS}Face"):
fmat=defmat
if face.find(f".//{NS}MaterialId") is not None:
matp=-1
try:
matp=matslots.index(face.find(f".//{NS}MaterialId").text)
except ValueError:
matp=len(matslots)
matslots.append(face.find(f".//{NS}MaterialId").text)
fmat=matp
if "triangles" in face.attrib:
faces.extend(unflatten(face.attrib["triangles"],int,3))
facemat.extend([fmat]*len(unflatten(face.attrib["triangles"],int,3)))
if "fans" in face.attrib:
faces.extend(unflatten(face.attrib["fans"],int,4))
facemat.extend([fmat]*len(unflatten(face.attrib["fans"],int,4)))
meshmat[rid]=matslots
create_mesh(verts,faces,facemat,uvs,rid)
def load_objects(tree):
rr=tree.findall(f".//{NS}ReferenceRep[@format='TESSELLATED']")
i3d=tree.findall(f".//{NS}Instance3D")
for ref,i3 in zip(rr,i3d):
meshid=ref.attrib["associatedFile"].split(":")[-1]
objname=ref.attrib["name"]
objname=objname[0:objname.rfind('_')]
mat=list(map(float,i3.find(f"./{NS}RelativeMatrix").text.split(' ')))
obj=bpy.data.objects.new(objname,meshes[meshid])
_wmat=mathutils.Matrix()
for r in range(0,3):
_wmat[r]=[mat[i] for i in range(r,12,3)]
for m in meshmat[meshid]:
obj.data.materials.append(materials[m])
obj.matrix_world=_wmat
scn=bpy.context.scene
scn.objects.link(obj)
scn.objects.active=obj
obj.select=True
bpy.ops.object.shade_smooth()
def create_mesh(verts,faces,facemat,uvs,meshidx):
if len(uvs)>len(verts):
uvs.append([uvs[0]]*(len(verts)-len(uvs)))
meshname=f"Mesh_{meshidx}"
mesh=bmesh.new()
for i in verts:
mesh.verts.new(i)
mesh.verts.ensure_lookup_table()
mesh.verts.index_update()
for i,m in zip(faces,facemat):
f=[mesh.verts[j]for j in i]
try:
nf=mesh.faces.new(f)
if m!=-1:nf.material_index=m
except ValueError:
pass
uv=mesh.loops.layers.uv.new()
for face in mesh.faces:
for lp in face.loops:
lp[uv].uv=mathutils.Vector(uvs[lp.vert.index])
msh=bpy.data.meshes.new(meshname)
mesh.to_mesh(msh)
msh.use_auto_smooth=True
meshes[meshidx]=msh
mesh.free()
def unflatten(seq,func,step,fac=1):
seq=seq.replace(' ',',')
seq=seq.split(',')
seq=list(map(func,seq))
nseq=len(seq)
return [tuple(seq[i+j]*fac for j in range(0,step))for i in range(0,nseq,step)]
def read(filename):
time1=time.time()
if zipfile.is_zipfile(filename):
zf=zipfile.ZipFile(filename,"r")
try:
member=zf.namelist().index("Root.3dxml")
except ValueError:
raise RuntimeError("not a 3D XML 3.0 file!")
filename=zf.open("Root.3dxml")
for im in zf.namelist():
if im.endswith(".tga"):
tf=tempfile.NamedTemporaryFile(suffix=".tga",delete=False)
tf.write(zf.open(im).read())
tf.flush()
texturefiles[im]=tf.name
tf.close()
else:
print("Warning: the file will be treated as a bare XML document.")
tree=etree.parse(filename)
load_textures(tree)
load_materials(tree)
load_meshes(tree)
load_objects(tree)
bpy.ops.file.pack_all()
for fn,tfn in texturefiles.items():
os.remove(tfn)
texturefiles.clear()
time2=time.time()
print("Total import time is: %.2f seconds."%(time2-time1))
class ImportDialog(bpy.types.Operator,bpy_extras.io_utils.ImportHelper):
bl_idname="object.tdxml_import"
bl_label="3DXML Import"
filter_glob=bpy.props.StringProperty(default='*.3dxml',options={'HIDDEN'})
pt=bpy.props.StringProperty(name="Texture path",default=texdir)
uf=bpy.props.FloatProperty(name="Unit factor",default=unitfactor)
def execute(self,context):
global texdir,unitfactor
texdir=self.pt
unitfactor=self.uf
read(self.filepath)
return {'FINISHED'}
class _3DXMLImport(bpy.types.Operator):
"""Import a 3DXML 3.0 file"""
bl_idname="import_mesh.tdxml"
bl_label="Import 3DXML 3.0..."
bl_options={'UNDO'}
filename_ext=".3dxml"
def execute(self,context):
bpy.ops.object.tdxml_import('INVOKE_DEFAULT')
return {'FINISHED'}
def menu_func_import_tdxml(self,context):
self.layout.operator(_3DXMLImport.bl_idname,text="3DXML 3.0 (.3dxml)")
def register():
bpy.utils.register_class(ImportDialog)
bpy.utils.register_class(_3DXMLImport)
bpy.types.INFO_MT_file_import.append(menu_func_import_tdxml)
def unregister():
bpy.utils.unregister_class(_3DXMLImport)
bpy.utils.unregister_class(ImportDialog)
if __name__=="__main__":
register()