diff --git a/examples/scripts/blender/sdf_exporter.py b/examples/scripts/blender/sdf_exporter.py index 4e71d6f5be..6450d0a46d 100644 --- a/examples/scripts/blender/sdf_exporter.py +++ b/examples/scripts/blender/sdf_exporter.py @@ -1,67 +1,82 @@ -import bpy -import os.path -from bpy_extras.io_utils import ImportHelper -from bpy.props import StringProperty, BoolProperty -from bpy.types import Operator - import xml.etree.ElementTree as ET +from os import path from xml.dom import minidom -# Target blender version: 2.82 +import bpy +from bpy.props import StringProperty +from bpy.types import Operator +from bpy_extras.io_utils import ImportHelper + +# Tested Blender version: 2.82/3.2 ######################################################################################################################## ### Exports model.dae of the scene with textures, its corresponding model.sdf file, and a default model.config file #### ######################################################################################################################## def export_sdf(prefix_path): - dae_filename = 'model.dae' - sdf_filename = 'model.sdf' - model_config_filename = 'model.config' - lightmap_filename = 'LightmapBaked.png' - model_name = 'my_model' - meshes_folder_prefix = 'meshes/' - + dae_filename = "model.dae" + sdf_filename = "model.sdf" + model_config_filename = "model.config" + lightmap_filename = "LightmapBaked.png" + model_name = "my_model" + meshes_folder_prefix = "meshes/" + # Exports the dae file and its associated textures - bpy.ops.wm.collada_export(filepath=prefix_path+meshes_folder_prefix+dae_filename, check_existing=False, filter_blender=False, filter_image=False, filter_movie=False, filter_python=False, filter_font=False, filter_sound=False, filter_text=False, filter_btx=False, filter_collada=True, filter_folder=True, filemode=8) + bpy.ops.wm.collada_export( + filepath=path.join(prefix_path, meshes_folder_prefix, dae_filename), + check_existing=False, + filter_blender=False, + filter_image=False, + filter_movie=False, + filter_python=False, + filter_font=False, + filter_sound=False, + filter_text=False, + filter_btx=False, + filter_collada=True, + filter_folder=True, + filemode=8, + ) # objects = bpy.context.selected_objects objects = bpy.context.selectable_objects - mesh_objects = [ o for o in objects if o.type == 'MESH' ] - light_objects = [ o for o in objects if o.type == 'LIGHT' ] + mesh_objects = [o for o in objects if o.type == "MESH"] + light_objects = [o for o in objects if o.type == "LIGHT"] ############################################# #### export sdf xml based off the scene ##### ############################################# - sdf = ET.Element('sdf', attrib={'version':'1.8'}) + sdf = ET.Element("sdf", attrib={"version": "1.8"}) # 1 model and 1 link - model = ET.SubElement(sdf, "model", attrib={"name":"test"}) + model = ET.SubElement(sdf, "model", attrib={"name": "test"}) static = ET.SubElement(sdf, "static") static.text = "true" - link = ET.SubElement(model, "link", attrib={"name":"testlink"}) + link = ET.SubElement(model, "link", attrib={"name": "testlink"}) # for each geometry in geometry library add a tag for o in mesh_objects: - visual = ET.SubElement(link, "visual", attrib={"name":o.name}) + visual = ET.SubElement(link, "visual", attrib={"name": o.name}) geometry = ET.SubElement(visual, "geometry") mesh = ET.SubElement(geometry, "mesh") uri = ET.SubElement(mesh, "uri") - uri.text = dae_filename + uri.text = path.join(meshes_folder_prefix, dae_filename) submesh = ET.SubElement(mesh, "submesh") submesh_name = ET.SubElement(submesh, "name") submesh_name.text = o.name - + # grab diffuse/albedo map - diffuse_map = "" - nodes = o.active_material.node_tree.nodes - principled = next(n for n in nodes if n.type == 'BSDF_PRINCIPLED') - if principled is not None: - base_color = principled.inputs['Base Color'] #Or principled.inputs[0] - value = base_color.default_value - if len(base_color.links): - link_node = base_color.links[0].from_node - diffuse_map = link_node.image.name - + diffuse_map = "" + if o.active_material is not None: + nodes = o.active_material.node_tree.nodes + principled = next(n for n in nodes if n.type == "BSDF_PRINCIPLED") + if principled is not None: + base_color = principled.inputs["Base Color"] # Or principled.inputs[0] + value = base_color.default_value + if len(base_color.links): + link_node = base_color.links[0].from_node + diffuse_map = link_node.image.name + # setup diffuse/specular color material = ET.SubElement(visual, "material") diffuse = ET.SubElement(material, "diffuse") @@ -72,16 +87,16 @@ def export_sdf(prefix_path): metal = ET.SubElement(pbr, "metal") if diffuse_map != "": albedo_map = ET.SubElement(metal, "albedo_map") - albedo_map.text = meshes_folder_prefix + diffuse_map - + albedo_map.text = path.join(meshes_folder_prefix, diffuse_map) + # for lightmapping, add the UV and turn off casting of shadows - if os.path.isfile(lightmap_filename): - light_map = ET.SubElement(metal, "light_map", attrib={"uv_set":"1"}) - light_map.text = meshes_folder_prefix + lightmap_filename - + if path.isfile(lightmap_filename): + light_map = ET.SubElement(metal, "light_map", attrib={"uv_set": "1"}) + light_map.text = path.join(meshes_folder_prefix, lightmap_filename) + cast_shadows = ET.SubElement(visual, "cast_shadows") cast_shadows.text = "0" - + def add_attenuation_tags(light_tag, blender_light): attenuation = ET.SubElement(light, "attenuation") range = ET.SubElement(attenuation, "range") @@ -92,54 +107,62 @@ def add_attenuation_tags(light_tag, blender_light): quad_attenuation.text = str(blender_pointlight.quadratic_coefficient) const_attenuation = ET.SubElement(attenuation, "constant") const_attenuation.text = str(blender_pointlight.constant_coefficient) - + # export lights for l in light_objects: blender_light = l.data - + if blender_light.type == "POINT": - light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"point"}) + light = ET.SubElement( + link, "light", attrib={"name": l.name, "type": "point"} + ) diffuse = ET.SubElement(light, "diffuse") - diffuse.text = str(blender_light.color.r) + " " + str(blender_light.color.g) + " " + str(blender_light.color.b) + " 1.0" + diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0" blender_pointlight = bpy.types.PointLight(blender_light) - + add_attenuation_tags(light, blender_pointlight) - + if blender_light.type == "SPOT": - light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"spot"}) + light = ET.SubElement( + link, "light", attrib={"name": l.name, "type": "spot"} + ) diffuse = ET.SubElement(light, "diffuse") - diffuse.text = str(blender_light.color.r) + " " + str(blender_light.color.g) + " " + str(blender_light.color.b) + " 1.0" + diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0" blender_spotlight = bpy.types.SpotLight(blender_light) - + add_attenuation_tags(light, blender_spotlight) # note: unsupported tags in blender - + if blender_light.type == "SUN": - light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"directional"}) + light = ET.SubElement( + link, "light", attrib={"name": l.name, "type": "directional"} + ) diffuse = ET.SubElement(light, "diffuse") - diffuse.text = str(blender_light.color.r) + " " + str(blender_light.color.g) + " " + str(blender_light.color.b) + " 1.0" + diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0" blender_pointlight = bpy.types.SunLight(blender_light) - + if blender_light.type == "SUN" or blender_light.type == "SPOT": direction = ET.SubElement(light, "direction") - direction.text = str(l.matrix_world[0][2]) + " " + str(l.matrix_world[1][2]) + " " + str(l.matrix_world[2][2]) - + direction.text = ( + f"{l.matrix_world[0][2]} {l.matrix_world[1][2]} {l.matrix_world[2][2]}" + ) + # unsupported: AREA lights cast_shadows = ET.SubElement(light, "cast_shadows") cast_shadows.text = "true" - + # todo : bpy.types.light script api lacks an intensity value, possible candidate is energy/power(Watts)? intensity = ET.SubElement(light, "intensity") intensity.text = "1.0" - + ## sdf collision tags - collision = ET.SubElement(link, "collision", attrib={"name":"collision"}) + collision = ET.SubElement(link, "collision", attrib={"name": "collision"}) geometry = ET.SubElement(collision, "geometry") mesh = ET.SubElement(geometry, "mesh") uri = ET.SubElement(mesh, "uri") - uri.text = dae_filename + uri.text = path.join(meshes_folder_prefix, dae_filename) surface = ET.SubElement(collision, "surface") contact = ET.SubElement(collision, "contact") @@ -147,61 +170,65 @@ def add_attenuation_tags(light_tag, blender_light): collide_bitmask.text = "0x01" ## sdf write to file - xml_string = ET.tostring(sdf, encoding='unicode') + xml_string = ET.tostring(sdf, encoding="unicode") reparsed = minidom.parseString(xml_string) - sdf_file = open(prefix_path+sdf_filename, "w") + sdf_file = open(path.join(prefix_path, sdf_filename), "w") sdf_file.write(reparsed.toprettyxml(indent=" ")) sdf_file.close() ############################## ### generate model.config #### ############################## - model = ET.Element('model') - name = ET.SubElement(model, 'name') + model = ET.Element("model") + name = ET.SubElement(model, "name") name.text = model_name - version = ET.SubElement(model, 'version') + version = ET.SubElement(model, "version") version.text = "1.0" - sdf_tag = ET.SubElement(model, "sdf", attrib={"sdf":"1.8"}) + sdf_tag = ET.SubElement(model, "sdf", attrib={"sdf": "1.8"}) sdf_tag.text = sdf_filename - author = ET.SubElement(model, 'author') - name = ET.SubElement(author, 'name') + author = ET.SubElement(model, "author") + name = ET.SubElement(author, "name") name.text = "Generated by blender SDF tools" - xml_string = ET.tostring(model, encoding='unicode') + xml_string = ET.tostring(model, encoding="unicode") reparsed = minidom.parseString(xml_string) - config_file = open(prefix_path+model_config_filename, "w") + config_file = open(path.join(prefix_path, model_config_filename), "w") config_file.write(reparsed.toprettyxml(indent=" ")) config_file.close() #### UI Handling #### class OT_TestOpenFilebrowser(Operator, ImportHelper): - bl_idname = "test.open_filebrowser" - bl_label = "Save" - - directory : bpy.props.StringProperty(name="Outdir Path") - - def execute(self, context): - """Do the export with the selected file.""" - - if not os.path.isdir(self.directory): - print(self.directory + " is not a directory!") - else: - print("exporting to directory: " + self.directory) - export_sdf(self.directory) - return {'FINISHED'} - -def register(): - bpy.utils.register_class(OT_TestOpenFilebrowser) -def unregister(): + bl_idname = "test.open_filebrowser" + bl_label = "Save" + + directory: StringProperty(name="Outdir Path") + + def execute(self, context): + """Do the export with the selected file.""" + + if not path.isdir(self.directory): + print(f"{self.directory} is not a directory!") + else: + print(f"exporting to directory: {self.directory}") + export_sdf(self.directory) + return {"FINISHED"} + + +def register(): + bpy.utils.register_class(OT_TestOpenFilebrowser) + + +def unregister(): bpy.utils.unregister_class(OT_TestOpenFilebrowser) - + + if __name__ == "__main__": - register() - bpy.ops.test.open_filebrowser('INVOKE_DEFAULT') + register() + bpy.ops.test.open_filebrowser("INVOKE_DEFAULT") # alternatively comment the main code block and do a function call without going through all the ui # prefix_path = '/home/ddeng/blender_lightmap/final_office/office/' diff --git a/tutorials/blender_sdf_exporter.md b/tutorials/blender_sdf_exporter.md index bfe60fdaaa..de8b7e7d65 100644 --- a/tutorials/blender_sdf_exporter.md +++ b/tutorials/blender_sdf_exporter.md @@ -4,7 +4,7 @@ Blender is a Digital Content Creation (DCC) tool for working with 3d models. In some cases you may be using it to bake lighting and environment maps. The Blender SDF exporter is a blender script in which you can run within Blender to -export your meshes, their associated textures and lights to a dae file, its +export your meshes, their associated textures and lights to a dae file, its corresponding SDF file and config file. Please note that the SDF format does not have 1 to 1 parity of features with Blender's @@ -12,10 +12,10 @@ mesh/materials/lights feature set. As such feel free to customize the script as ## Using the Blender SDF Exporter -1. Download the blender script in [sdf_exporter.py](https://github.com/gazebosim/gz-sim/tree/ign-gazebo5/examples/scripts/blender/sdf_exporter.py). +1. Download the blender script in [sdf_exporter.py](https://github.com/gazebosim/gz-sim/tree/gz-sim7/examples/scripts/blender/sdf_exporter.py). 2. Open the script under Blender's Scripting tab and run it. -3. You will see a file dialog requesting for a location to save the files to. Hit 'Save' when you are done. +3. You will see a file dialog requesting a location to save the files to. Hit 'Save' when you are done. 4. The files `meshes/model.dae`, `model.config` and `model.sdf` will be created at the location you specified.