From 020c403ba7dec10f78070e9647eea068f59e1abc Mon Sep 17 00:00:00 2001 From: Delio Vicini Date: Tue, 24 Mar 2020 10:40:39 +0100 Subject: [PATCH] Updated and simplified Blender exporter to work with Blender 2.8 --- ext/plugin/io_nori.py | 379 ++++++++++++------------------------------ ext/plugin/readme.md | 22 +++ ext/plugin/readme.txt | 18 -- 3 files changed, 129 insertions(+), 290 deletions(-) create mode 100644 ext/plugin/readme.md delete mode 100644 ext/plugin/readme.txt diff --git a/ext/plugin/io_nori.py b/ext/plugin/io_nori.py index 50bbda2b..c08f6092 100644 --- a/ext/plugin/io_nori.py +++ b/ext/plugin/io_nori.py @@ -1,305 +1,158 @@ +import math +import os +import shutil +from xml.dom.minidom import Document + +import bpy +import bpy_extras +from bpy.props import BoolProperty, IntProperty, StringProperty +from bpy_extras.io_utils import ExportHelper +from mathutils import Matrix + bl_info = { "name": "Export Nori scenes format", - "author": "Adrien Gruson", + "author": "Adrien Gruson, Delio Vicini, Tizian Zeltner", "version": (0, 1), - "blender": (2, 5, 7), + "blender": (2, 80, 0), "location": "File > Export > Nori exporter (.xml)", - "description": "Export Nori scenes format (.xml)", + "description": "Export Nori scene format (.xml)", "warning": "", "wiki_url": "", "tracker_url": "", "category": "Import-Export"} -import bpy, os, math, shutil -from xml.dom.minidom import Document -# Main class exporter -class NoriWritter: - def verbose(self,text): - print(text) +class NoriWriter: def __init__(self, context, filepath): self.context = context self.filepath = filepath - self.workingDir = os.path.dirname(self.filepath) + self.working_dir = os.path.dirname(self.filepath) - ###################### - # tools private methods - # (xml format) - ###################### - def __createElement(self, name, attr): + def create_xml_element(self, name, attr): el = self.doc.createElement(name) - for k,v in attr.items(): - el.setAttribute(k,v) + for k, v in attr.items(): + el.setAttribute(k, v) return el - def __createEntry(self, t, name, value): - return self.__createElement(t,{"name":name,"value":value}) + def create_xml_entry(self, t, name, value): + return self.create_xml_element(t, {"name": name, "value": value}) - def __createVector(self, t, vec): - return self.__createElement(t, {"value": "%f %f %f" % (vec[0],vec[1],vec[2])}) - - def __createTransform(self, mat, el = None): - transform = self.__createElement("transform",{"name":"toWorld"}) + def create_xml_transform(self, mat, el=None): + transform = self.create_xml_element("transform", {"name": "toWorld"}) if(el): transform.appendChild(el) value = "" for j in range(4): for i in range(4): - value += str(mat[j][i])+"," - transform.appendChild(self.__createElement("matrix",{"value":value[:-1]})) + value += str(mat[j][i]) + "," + transform.appendChild(self.create_xml_element("matrix", {"value": value[:-1]})) return transform - def write(self, exportLight, nbSamples): - """Main method to write the blender scene into Nori format - It will export as follows: - 1) write integrator configuration - 2) write samples information (number, distribution) - 3) export one camera - 4) export all light sources - 5) export all meshes""" + def create_xml_mesh_entry(self, filename): + meshElement = self.create_xml_element("mesh", {"type": "obj"}) + meshElement.appendChild(self.create_xml_element("string", {"name": "filename", "value": "meshes/"+filename})) + return meshElement + + def write(self): + """Main method to write the blender scene into Nori format""" + n_samples = 32 # create xml document self.doc = Document() self.scene = self.doc.createElement("scene") self.doc.appendChild(self.scene) - ###################### # 1) write integrator configuration - ###################### - if(not exportLight): - self.scene.appendChild(self.__createElement("integrator", {"type" : "av" })) - else: - self.scene.appendChild(self.__createElement("integrator", {"type" : "path_mis" })) - - ###################### - # 2) write the number of samples - # and which distribution we will use - ###################### - sampler = self.__createElement("sampler", {"type" : "independent" }) - sampler.appendChild(self.__createElement("integer", {"name":"sampleCount", "value":str(nbSamples)})) + self.scene.appendChild(self.create_xml_element("integrator", {"type": "normals"})) + + # 2) write sampler + sampler = self.create_xml_element("sampler", {"type": "independent"}) + sampler.appendChild(self.create_xml_element("integer", {"name": "sampleCount", "value": str(n_samples)})) self.scene.appendChild(sampler) - ###################### # 3) export one camera - ###################### - # note that we only support one camera cameras = [cam for cam in self.context.scene.objects - if cam.type in {'CAMERA'}] + if cam.type in {'CAMERA'}] if(len(cameras) == 0): - self.verbose("WARN: No camera to export") + print("WARN: No camera to export") else: if(len(cameras) > 1): - self.verbose("WARN: Does not handle multiple camera, only export the first one") - self.scene.appendChild(self.write_camera(cameras[0])) # export the first one - - ###################### - # 4) export all light sources - ###################### - if(exportLight): - sources = [obj for obj in self.context.scene.objects - if obj.type in {'LAMP'}] - for source in sources: - if(source.data.type == "POINT"): - pointLight = self.__createElement("emitter", {"type" : "point" }) - pos = source.location - pointLight.appendChild(self.__createEntry("point", "position", "%f,%f,%f"%(pos.x,pos.y,pos.z))) - self.scene.appendChild(pointLight) - else: - self.verbose("WARN: Light source type (%s) is not supported" % source.data.type) - - ###################### - # 5) export all meshes - ###################### - # create the directory for store the meshes - if not os.path.exists(self.workingDir+"/meshes"): - os.makedirs(self.workingDir+"/meshes") - - # export all of them + print("WARN: Does not handle multiple camera, only export the first one") + self.scene.appendChild(self.write_camera(cameras[0])) # export the first one + + # 4) export all meshes + if not os.path.exists(self.working_dir + "/meshes"): + os.makedirs(self.working_dir + "/meshes") + meshes = [obj for obj in self.context.scene.objects - if obj.type in {'MESH', 'EMPTY'} - and obj.parent is None] + if obj.type in {'MESH', 'EMPTY'} + and obj.parent is None] for mesh in meshes: self.write_mesh(mesh) - ###################### # 6) write the xml file - ###################### - self.doc.writexml(open(self.filepath, "w"), "", "\t","\n") + self.doc.writexml(open(self.filepath, "w"), "", "\t", "\n") def write_camera(self, cam): """convert the selected camera (cam) into xml format""" - camera = self.__createElement("camera",{"type":"perspective"}) - camera.appendChild(self.__createEntry("float","fov",str(cam.data.angle*180/math.pi))) - camera.appendChild(self.__createEntry("float","nearClip",str(cam.data.clip_start))) - camera.appendChild(self.__createEntry("float","farClip",str(cam.data.clip_end))) - percent = self.context.scene.render.resolution_percentage/100.0 - camera.appendChild(self.__createEntry("integer","width",str(int(self.context.scene.render.resolution_x*percent)))) - camera.appendChild(self.__createEntry("integer","height",str(int(self.context.scene.render.resolution_y*percent)))) - trans = self.__createTransform(cam.matrix_world, self.__createVector("scale",(1,1,-1))) + camera = self.create_xml_element("camera", {"type": "perspective"}) + camera.appendChild(self.create_xml_entry("float", "fov", str(cam.data.angle * 180 / math.pi))) + camera.appendChild(self.create_xml_entry("float", "nearClip", str(cam.data.clip_start))) + camera.appendChild(self.create_xml_entry("float", "farClip", str(cam.data.clip_end))) + percent = self.context.scene.render.resolution_percentage / 100.0 + camera.appendChild(self.create_xml_entry("integer", "width", str( + int(self.context.scene.render.resolution_x * percent)))) + camera.appendChild(self.create_xml_entry("integer", "height", str( + int(self.context.scene.render.resolution_y * percent)))) + + mat = cam.matrix_world + + # Conversion to Y-up coordinate system + coord_transf = bpy_extras.io_utils.axis_conversion( + from_forward='Y', from_up='Z', to_forward='-Z', to_up='Y').to_4x4() + mat = coord_transf @ mat + pos = mat.translation + # Nori's camera needs this these coordinates to be flipped + m = Matrix([[-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 0]]) + t = mat.to_3x3() @ m.to_3x3() + mat = Matrix([[t[0][0], t[0][1], t[0][2], pos[0]], + [t[1][0], t[1][1], t[1][2], pos[1]], + [t[2][0], t[2][1], t[2][2], pos[2]], + [0, 0, 0, 1]]) + trans = self.create_xml_transform(mat) camera.appendChild(trans) return camera - ###################### - # meshes related methods - ###################### - def __createMeshEntry(self, filename, matrix): - meshElement = self.__createElement("mesh", {"type" : "obj"}) - meshElement.appendChild(self.__createElement("string", {"name":"filename","value":"meshes/"+filename})) - meshElement.appendChild(self.__createTransform(matrix)) - return meshElement - - def __createBSDFEntry(self, slot): - """method responsible to the auto-conversion - between Blender internal BSDF (not Cycles!) and Nori BSDF - - For more advanced implementation: - http://tinyurl.com/nnhxwuh - """ - if slot.material.raytrace_mirror.use: - return self.__createElement("bsdf", {"type":"mirror"}) - else: - bsdfElement = self.__createElement("bsdf", {"type":"diffuse"}) - c = slot.material.diffuse_color - bsdfElement.appendChild(self.__createEntry("color", "albedo","%f,%f,%f" %(c[0],c[1],c[2]))) - return bsdfElement - - def write_mesh(self,mesh): + def write_mesh(self, mesh): children_mesh = [obj for obj in mesh.children - if obj.type in {'MESH', 'EMPTY'}] + if obj.type in {'MESH', 'EMPTY'}] for child in children_mesh: self.write_mesh(child) - + viewport_selection = self.context.selected_objects + bpy.ops.object.select_all(action='DESELECT') if mesh.type == 'MESH': - for meshEntry in self.write_mesh_objs(mesh): - self.scene.appendChild(meshEntry) - - def write_face(self, prevMesh, fileObj, exportUV, exportNormal, idMat = -1): - for poly in prevMesh.polygons: - if idMat not in [-1, poly.material_index]: - continue - - # Check if it's not a cube - vertFaces = [poly.vertices[:]] - if len(vertFaces[0]) == 4: - vertFaces = [(vertFaces[0][0],vertFaces[0][1], vertFaces[0][2]), - (vertFaces[0][2],vertFaces[0][3], vertFaces[0][0])] - elif len(vertFaces[0]) == 3: - pass # Nothing to do - else: - raise "Exception: Difficult poly, abord" - - for vert in vertFaces: - face = "f" - - # Order checking - if(not exportNormal): - ac = prevMesh.vertices[vert[2]].co - prevMesh.vertices[vert[0]].co - ab = prevMesh.vertices[vert[1]].co - prevMesh.vertices[vert[0]].co - norm = ab.cross(ac) - norm.normalize() - - # Need to inverse order - if(norm.dot(poly.normal) < 0.0): - print("Normal flip: "+str(poly.normal)+" != "+str(norm)) - vert = (vert[2],vert[1],vert[0]) - - for idVert in vert: - face += " "+str(idVert+1) - - # Nothing to do for the export - if((not exportUV) and (not exportNormal)): - continue - - if(exportUV): - face += "/"+str(idVert+1) - else: - face += "/" - - if(exportNormal): - face += "/"+str(idVert+1) - - fileObj.write(face+"\n") - - def write_mesh_objs(self, mesh): - # convert the shape by apply all modifier - prevMesh = mesh.to_mesh(bpy.context.scene, True, "PREVIEW") - - # get useful information of the shape - exportNormal = (prevMesh.polygons[0].use_smooth) - exportUV = (prevMesh.uv_layers.active != None) - haveMaterial = (len(mesh.material_slots) != 0 and mesh.material_slots[0].name != '') - - # export obj file base (vertex pos, normal and uv) - # but not the face data - fileObjPath = mesh.name+".obj" - fileObj = open(self.workingDir+"/meshes/"+fileObjPath, "w") - - # write all vertex informations - for vert in prevMesh.vertices: - fileObj.write('v %f %f %f\n' % (vert.co.x, vert.co.y, vert.co.z)) - - if exportUV: - # By default, UVs are not necessarily ordered like vertices. - # We use `mesh.loops` to recover the right ordering. - # See: https://blender.stackexchange.com/a/3537 - for poly in prevMesh.polygons: - for loop_i in poly.loop_indices: - uv = prevMesh.uv_layers.active.data[loop_i].uv - fileObj.write('vt %f %f \n' % (uv[0], uv[1])) - - if exportNormal: - for vert in prevMesh.vertices: - fileObj.write('vn %f %f %f\n' % (vert.normal.x, vert.normal.y, vert.normal.z)) - - # write all polygones (faces) - listMeshXML = [] - if(not haveMaterial): - self.write_face(prevMesh, fileObj, exportUV, exportNormal) - - # add default BSDF - meshElement = self.__createMeshEntry(fileObjPath, mesh.matrix_world) - bsdfElement = self.__createElement("bsdf", {"type":"diffuse"}) - bsdfElement.appendChild(self.__createEntry("color", "albedo", "0.75,0.75,0.75")) - meshElement.appendChild(bsdfElement) - listMeshXML = [meshElement] - else: - fileObj.close() - for id_mat in range(len(mesh.material_slots)): - slot = mesh.material_slots[id_mat] - self.verbose("MESH: "+mesh.name+" BSDF: "+slot.name) - - # we create an new obj file and concatenate data files - fileObjMatPath = mesh.name+"_"+slot.name+".obj" - fileObjMat = open(self.workingDir+"/meshes/"+fileObjMatPath,"w") - shutil.copyfileobj(open(self.workingDir+"/meshes/"+fileObjPath,"r"), fileObjMat) - - # we write all face material specific - self.write_face(prevMesh, fileObjMat, exportUV, exportNormal, id_mat) - - # We create xml related entry - meshElement = self.__createMeshEntry(fileObjMatPath, mesh.matrix_world) - meshElement.appendChild(self.__createBSDFEntry(slot)) - listMeshXML.append(meshElement) - - fileObjMat.close() - - # Clean temporal obj file - os.remove(self.workingDir+"/meshes/"+fileObjPath) - - # free the memory - bpy.data.meshes.remove(prevMesh) - + obj_name = mesh.name + ".obj" + obj_path = os.path.join(self.working_dir, 'meshes', obj_name) + mesh.select_set(True) + bpy.ops.export_scene.obj(filepath=obj_path, check_existing=False, + use_selection=True, use_edges=False, use_smooth_groups=False, + use_materials=False, use_triangles=True) + mesh.select_set(False) + + # Add the corresponding entry to the xml + mesh_element = self.create_xml_mesh_entry(obj_name) + # We currently just export a default material, a more complex material conversion + # could be implemented following: http://tinyurl.com/nnhxwuh + bsdf_element = self.create_xml_element("bsdf", {"type": "diffuse"}) + bsdf_element.appendChild(self.create_xml_entry("color", "albedo", "0.75,0.75,0.75")) + mesh_element.appendChild(bsdf_element) + self.scene.appendChild(mesh_element) + for ob in viewport_selection: + ob.select_set(True) - return listMeshXML - -###################### -# blender code -###################### -from bpy.props import StringProperty, IntProperty, BoolProperty -from bpy_extras.io_utils import ExportHelper class NoriExporter(bpy.types.Operator, ExportHelper): """Export a blender scene into Nori scene format""" @@ -308,51 +161,33 @@ class NoriExporter(bpy.types.Operator, ExportHelper): bl_idname = "export.nori" bl_label = "Export Nori scene" - # filtering file names filename_ext = ".xml" filter_glob = StringProperty(default="*.xml", options={'HIDDEN'}) - ################### - # other options - ################### - - export_light = BoolProperty( - name="Export light", - description="Export light to Nori", - default=True) - - nb_samples = IntProperty(name="Numbers of camera rays", - description="Number of camera ray", - default=32) - def execute(self, context): - nori = NoriWritter(context, self.filepath) - nori.write(self.export_light, self.nb_samples) + nori = NoriWriter(context, self.filepath) + nori.write() return {'FINISHED'} def invoke(self, context, event): - #self.frame_start = context.scene.frame_start - #self.frame_end = context.scene.frame_end - wm = context.window_manager wm.fileselect_add(self) return {'RUNNING_MODAL'} -def menu_export(self, context): - import os - default_path = os.path.splitext(bpy.data.filepath)[0] + ".xml" - self.layout.operator(NoriExporter.bl_idname, text="Export Nori scenes...").filepath = default_path +def menu_func_export(self, context): + self.layout.operator(NoriExporter.bl_idname, text="Export Nori scene...") -# Register Nori exporter inside blender def register(): - bpy.utils.register_module(__name__) - bpy.types.INFO_MT_file_export.append(menu_export) + bpy.utils.register_class(NoriExporter) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + def unregister(): - bpy.utils.unregister_module(__name__) - bpy.types.INFO_MT_file_export.remove(menu_export) + bpy.utils.unregister_class(NoriExporter) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + if __name__ == "__main__": register() diff --git a/ext/plugin/readme.md b/ext/plugin/readme.md new file mode 100644 index 00000000..9917ec97 --- /dev/null +++ b/ext/plugin/readme.md @@ -0,0 +1,22 @@ +# Nori Exporter for Blender + by Delio Vicini and Tizian Zeltner, based on Adrien Gruson's original exporter. + +## Installation + +First, you must download a fairly recent version of Blender (the plugin is tested for versions >= 2.8). You can download blender from https://www.blender.org or using your system's package manager. + +To install the plugin, open Blender and go to "Edit -> Preferences... -> Add-ons" and click on "Install...". +This should open a file browser in which you can navigate to the `io_nori.py` file and select it. +This will copy the exporter script to Blender's plugin directory. +After the plugin is installed, it has to be activated by clicking the checkbox next to it in the Add-ons menu. + +## Usage + +Once the plugin is installed, scenes can be expored by clicking "File -> Export -> Export Nori Scene..." + +The plugin exports all objects in the scene as separate OBJ FIles. It then generates a Nori XML file with the scene's camera, a basic integrator and XML entries to reference all the exported meshes. + +## Limitations + +The plugin does not support exporting BSDFs and emitters. It will just assign a default BSDF to all shapes. It further exports each mesh as is. +This means that if you have a mesh with multiple materials in Blender, you will have to split it manually into separate submeshes before exporting (one for each material). This can be done by selecting the mesh with multiple materials, going to edit mode (tab) then selecting all vertices (A) and clicking "separate" (P) and then "by material". This will separate the mesh into meshes with one material each. After exporting, each of these meshes will have a separate entry in the scene's XML file and can therefore be assigned a different BSDF. \ No newline at end of file diff --git a/ext/plugin/readme.txt b/ext/plugin/readme.txt deleted file mode 100644 index bb4bc340..00000000 --- a/ext/plugin/readme.txt +++ /dev/null @@ -1,18 +0,0 @@ -Blender Plugin for Nori by Adrien Gruson --> http://people.irisa.fr/Adrien.Gruson/biskra_2012.html - -Installation -First, you must download a fairly recent version of Blender. They can be -downloaded from the official website (https://www.blender.org/). - -To install, you must copy it to the scripts/addons folder of your blender installation. -On Linux, the default location is in "/usr/lib/blender/scripts/addons", and -on MacOS, it is in /Applications/blender.app/Contents/Resources//scripts/addons" - -Adrien Gruson recorded a video with visual instructions on using the plugin. -It is in French, though the main concepts should be understandable even without -narration. - --> http://www.youtube.com/watch?list=UUb_VXTBD3haz-P7P0f_Bi8A&v=RHCwKJPDPjs - -