From 1ea584002435dd5ee36fedca732ba6742c189895 Mon Sep 17 00:00:00 2001 From: ddengster Date: Tue, 29 Jun 2021 15:23:47 +0800 Subject: [PATCH 1/6] add blender tutorial Signed-off-by: ddengster --- examples/scripts/blender/sdf_exporter.py | 204 +++++++++++++++++++++++ tutorials/blender_sdf_exporter.md | 21 +++ 2 files changed, 225 insertions(+) create mode 100644 examples/scripts/blender/sdf_exporter.py create mode 100644 tutorials/blender_sdf_exporter.md diff --git a/examples/scripts/blender/sdf_exporter.py b/examples/scripts/blender/sdf_exporter.py new file mode 100644 index 0000000000..d8931ea0a6 --- /dev/null +++ b/examples/scripts/blender/sdf_exporter.py @@ -0,0 +1,204 @@ +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 xml.dom import minidom + +######################################################################################################################## +### 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' + + # Exports the dae file and its associated textures + bpy.ops.wm.collada_export(filepath=prefix_path+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' ] + + ############################################# + #### export sdf xml based off the scene ##### + ############################################# + sdf = ET.Element('sdf', attrib={'version':'1.8'}) + + # 1 model and 1 link + model = ET.SubElement(sdf, "model", attrib={"name":"test"}) + static = ET.SubElement(sdf, "static") + static.text = "true" + 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}) + + geometry = ET.SubElement(visual, "geometry") + mesh = ET.SubElement(geometry, "mesh") + uri = ET.SubElement(mesh, "uri") + uri.text = 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 + + # setup diffuse/specular color + material = ET.SubElement(visual, "material") + diffuse = ET.SubElement(material, "diffuse") + diffuse.text = "1.0 1.0 1.0 1.0" + specular = ET.SubElement(material, "specular") + specular.text = "0.0 0.0 0.0 1.0" + pbr = ET.SubElement(material, "pbr") + metal = ET.SubElement(pbr, "metal") + if diffuse_map != "": + albedo_map = ET.SubElement(metal, "albedo_map") + albedo_map.text = 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 = 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") + range.text = str(blender_light.cutoff_distance) + linear_attenuation = ET.SubElement(attenuation, "linear") + linear_attenuation.text = str(blender_pointlight.linear_attenuation) + quad_attenuation = ET.SubElement(attenuation, "quadratic") + 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"}) + diffuse = ET.SubElement(light, "diffuse") + diffuse.text = str(blender_light.color.r) + " " + str(blender_light.color.g) + " " + str(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"}) + diffuse = ET.SubElement(light, "diffuse") + diffuse.text = str(blender_light.color.r) + " " + str(blender_light.color.g) + " " + str(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"}) + diffuse = ET.SubElement(light, "diffuse") + diffuse.text = str(blender_light.color.r) + " " + str(blender_light.color.g) + " " + str(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]) + + # 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"}) + + geometry = ET.SubElement(collision, "geometry") + mesh = ET.SubElement(geometry, "mesh") + uri = ET.SubElement(mesh, "uri") + uri.text = dae_filename + + surface = ET.SubElement(collision, "surface") + contact = ET.SubElement(collision, "contact") + collide_bitmask = ET.SubElement(collision, "collide_bitmask") + collide_bitmask.text = "0x01" + + ## sdf write to file + xml_string = ET.tostring(sdf, encoding='unicode') + reparsed = minidom.parseString(xml_string) + + sdf_file = open(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') + name.text = "L1" + version = ET.SubElement(model, 'version') + version.text = "1.0" + sdf_tag = ET.SubElement(model, "sdf", attrib={"sdf":"1.8"}) + sdf_tag.text = "model.sdf" + + author = ET.SubElement(model, 'author') + name = ET.SubElement(author, 'name') + name.text = "Generated by blender sdf tools" + + xml_string = ET.tostring(model, encoding='unicode') + reparsed = minidom.parseString(xml_string) + + config_file = open(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(): + bpy.utils.unregister_class(OT_TestOpenFilebrowser) + +if __name__ == "__main__": + 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/' +# export_sdf(prefix_path) diff --git a/tutorials/blender_sdf_exporter.md b/tutorials/blender_sdf_exporter.md new file mode 100644 index 0000000000..386946d566 --- /dev/null +++ b/tutorials/blender_sdf_exporter.md @@ -0,0 +1,21 @@ +\page blender_sdf_exporter Blender SDF Exporter + +Blender is a DCC tool to create 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 +corresponding SDF file and config file. + +Please note that the SDF format does not have 1 to 1 parity of features with Blender's +mesh/materials/lights feature set. As such feel free to customize the script as needed. + +## Using the Blender SDF Exporter + +1. Download the blender script in [sdf_exporter.py](https://github.com/ignitionrobotics/ign-gazebo/tree/ign-gazebo5/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. + +4. The files `model.dae`, `model.config` and `model.sdf` will be created at the location you specified. From ecd6b4c10c69cee6682935557f2ad98f0eb85834 Mon Sep 17 00:00:00 2001 From: ddengster Date: Tue, 29 Jun 2021 18:06:57 +0800 Subject: [PATCH 2/6] add link to tutorial in main page Signed-off-by: ddengster --- tutorials.md.in | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorials.md.in b/tutorials.md.in index ab7c1c8445..a2651fb4c4 100644 --- a/tutorials.md.in +++ b/tutorials.md.in @@ -30,6 +30,7 @@ Ignition @IGN_DESIGNATION_CAP@ library and how to use the library effectively. * \subpage collada_world_exporter "Collada World Exporter": Export an entire world to a single Collada mesh. * \subpage underwater_vehicles "Underwater Vehicles": Understand how to simulate underwater vehicles. * \subpage particle_mitter "Particle emitter": Using particle emitters in simulation +* \subpage blender_sdf_exporter "Blender SDF Exporter": Use a blender script to export a model to the SDF format. **Migration from Gazebo classic** From b0083fc37d5d2c18e6fff0fd1295284d27c36553 Mon Sep 17 00:00:00 2001 From: ddengster Date: Tue, 29 Jun 2021 18:09:57 +0800 Subject: [PATCH 3/6] add blender version comment Signed-off-by: ddengster --- examples/scripts/blender/sdf_exporter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/scripts/blender/sdf_exporter.py b/examples/scripts/blender/sdf_exporter.py index d8931ea0a6..cb74dc8dc4 100644 --- a/examples/scripts/blender/sdf_exporter.py +++ b/examples/scripts/blender/sdf_exporter.py @@ -7,6 +7,8 @@ import xml.etree.ElementTree as ET from xml.dom import minidom +# Target blender version: 2.82 + ######################################################################################################################## ### Exports model.dae of the scene with textures, its corresponding model.sdf file, and a default model.config file #### ######################################################################################################################## From 3aaabae6989c408d33b8fb223c84a6c52e33ec38 Mon Sep 17 00:00:00 2001 From: ddengster Date: Mon, 12 Jul 2021 17:31:24 +0800 Subject: [PATCH 4/6] text fixes, float model names to top of function Signed-off-by: ddengster --- examples/scripts/blender/sdf_exporter.py | 7 ++++--- tutorials/blender_sdf_exporter.md | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/scripts/blender/sdf_exporter.py b/examples/scripts/blender/sdf_exporter.py index cb74dc8dc4..00c5d2d358 100644 --- a/examples/scripts/blender/sdf_exporter.py +++ b/examples/scripts/blender/sdf_exporter.py @@ -18,6 +18,7 @@ def export_sdf(prefix_path): sdf_filename = 'model.sdf' model_config_filename = 'model.config' lightmap_filename = 'LightmapBaked.png' + model_name = 'model' # Exports the dae file and its associated textures bpy.ops.wm.collada_export(filepath=prefix_path+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) @@ -157,15 +158,15 @@ def add_attenuation_tags(light_tag, blender_light): ############################## model = ET.Element('model') name = ET.SubElement(model, 'name') - name.text = "L1" + name.text = model_name version = ET.SubElement(model, 'version') version.text = "1.0" sdf_tag = ET.SubElement(model, "sdf", attrib={"sdf":"1.8"}) - sdf_tag.text = "model.sdf" + sdf_tag.text = sdf_filename author = ET.SubElement(model, 'author') name = ET.SubElement(author, 'name') - name.text = "Generated by blender sdf tools" + name.text = "Generated by blender SDF tools" xml_string = ET.tostring(model, encoding='unicode') reparsed = minidom.parseString(xml_string) diff --git a/tutorials/blender_sdf_exporter.md b/tutorials/blender_sdf_exporter.md index 386946d566..ad2f023e7f 100644 --- a/tutorials/blender_sdf_exporter.md +++ b/tutorials/blender_sdf_exporter.md @@ -1,7 +1,7 @@ \page blender_sdf_exporter Blender SDF Exporter -Blender is a DCC tool to create 3d models. In some cases you may be using it to bake -lighting and environment maps. +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 From d6222f92a36551ab2df9ed4549c8feaafa110635 Mon Sep 17 00:00:00 2001 From: ddengster Date: Mon, 12 Jul 2021 17:41:29 +0800 Subject: [PATCH 5/6] fix model name, export dae to meshes folder Signed-off-by: ddengster --- examples/scripts/blender/sdf_exporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/scripts/blender/sdf_exporter.py b/examples/scripts/blender/sdf_exporter.py index 00c5d2d358..15a3fdce01 100644 --- a/examples/scripts/blender/sdf_exporter.py +++ b/examples/scripts/blender/sdf_exporter.py @@ -14,11 +14,11 @@ ######################################################################################################################## def export_sdf(prefix_path): - dae_filename = 'model.dae' + dae_filename = 'meshes/model.dae' sdf_filename = 'model.sdf' model_config_filename = 'model.config' lightmap_filename = 'LightmapBaked.png' - model_name = 'model' + model_name = 'my_model' # Exports the dae file and its associated textures bpy.ops.wm.collada_export(filepath=prefix_path+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) From 9f497d96e099b2b867d7ddf15953dfb85b4f42fe Mon Sep 17 00:00:00 2001 From: ddengster Date: Mon, 12 Jul 2021 17:42:40 +0800 Subject: [PATCH 6/6] fix tutorial text Signed-off-by: ddengster --- tutorials/blender_sdf_exporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/blender_sdf_exporter.md b/tutorials/blender_sdf_exporter.md index ad2f023e7f..0d991d3987 100644 --- a/tutorials/blender_sdf_exporter.md +++ b/tutorials/blender_sdf_exporter.md @@ -18,4 +18,4 @@ mesh/materials/lights feature set. As such feel free to customize the script as 3. You will see a file dialog requesting for a location to save the files to. Hit 'Save' when you are done. -4. The files `model.dae`, `model.config` and `model.sdf` will be created at the location you specified. +4. The files `meshes/model.dae`, `model.config` and `model.sdf` will be created at the location you specified.