├── CMakeLists.txt ├── package.xml ├── scripts └── install ├── CHANGELOG.rst ├── worlds └── actually_empty_world.world ├── README.md └── blender_gazebo └── blender_gazebo.py /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(blender_gazebo) 3 | 4 | find_package(catkin REQUIRED COMPONENTS) 5 | 6 | catkin_package() 7 | include_directories( 8 | ${catkin_INCLUDE_DIRS} 9 | ) 10 | 11 | 12 | install(DIRECTORY blender_gazebo worlds 13 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 14 | ) 15 | 16 | install(PROGRAMS scripts/install 17 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 18 | ) 19 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | blender_gazebo 4 | 0.0.4 5 | The blender_gazebo package 6 | 7 | Dave Niewinski 8 | 9 | BSD 10 | 11 | catkin 12 | gazebo_ros 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$BLENDER_SCRIPT_DIR" ] 4 | then 5 | echo "\$BLENDER_SCRIPT_DIR is UNSET. Please set \$BLENDER_SCRIPT_DIR to the Blender directory containing the scripts folder" 6 | else 7 | if [ -d "$BLENDER_SCRIPT_DIR/scripts/addons" ] 8 | then 9 | echo "Installing blender_gazebo plugin to $BLENDER_SCRIPT_DIR/scripts/addons" 10 | cp -r $(rospack find blender_gazebo)/blender_gazebo/blender_gazebo.py $BLENDER_SCRIPT_DIR/scripts/addons 11 | echo "Done" 12 | else 13 | echo "\$BLENDER_SCRIPT_DIR is INVALID. Please set \$BLENDER_SCRIPT_DIR to the Blender directory containing the scripts folder" 14 | fi 15 | fi 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package blender_gazebo 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 0.0.4 (2019-12-08) 6 | ------------------ 7 | * Fixed install 8 | * Contributors: Dave Niewinski 9 | 10 | 0.0.3 (2019-11-21) 11 | ------------------ 12 | * Fixed deps 13 | * Contributors: Dave Niewinski 14 | 15 | 0.0.2 (2019-11-21) 16 | ------------------ 17 | * Updated installation 18 | * Contributors: Dave Niewinski 19 | 20 | 0.0.1 (2019-11-21) 21 | ------------------ 22 | * Functionality blocked-out. Lots of little things to fix and usability improvements 23 | * Added base launch and urdf files 24 | * Cleaner collision generation, prefixing 25 | * Initial Commit 26 | * Contributors: Dave Niewinski 27 | -------------------------------------------------------------------------------- /worlds/actually_empty_world.world: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 0 0 10 0 -0 0 6 | 0.8 0.8 0.8 1 7 | 0.2 0.2 0.2 1 8 | 9 | 1000 10 | 0.9 11 | 0.01 12 | 0.001 13 | 14 | -0.5 0.1 -0.9 15 | 16 | 17 | 0.001 18 | 1 19 | 1000 20 | 0 0 -9.8 21 | 22 | 23 | 0.4 0.4 0.4 1 24 | 0.7 0.7 0.7 1 25 | 1 26 | 27 | 28 | EARTH_WGS84 29 | 0 30 | 0 31 | 0 32 | 0 33 | 34 | 35 | 35 515000000 36 | 35 905493128 37 | 1495129402 253586127 38 | 39 | 40 | 41 | 39.6939 0.0 20.0 0 0.275643 3.14159 42 | orbit 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender Gazebo 2 | An add-on for Blender to export worlds for Gazebo ROS. It will export all of the models, and create launch and urdf files. 3 | 4 | ## Installation Script 5 | There is an installation script that will install the add-on into Blender. Because you can have multiple versions of Blender, you need to tell the script where to install. Set the environment variable ``BLENDER_SCRIPT_DIR`` to the version number folder inside your Blender installation directory 6 | 7 | ``` 8 | blender-2.80-linux-glibc217-x86_64 9 | ├── 2.80 << This Folder 10 | │   ├── datafiles 11 | │   ├── python 12 | │   └── scripts 13 | ├── icons 14 | │   ├── scalable 15 | │   └── symbolic 16 | └── lib 17 | ``` 18 | 19 | Once that is set, rosrun the script: 20 | 21 | ```rosrun blender_gazebo install``` 22 | 23 | After the script installs the add-on, you will need to enable it inside blender. Edit > Preferences > Add-ons, find the add-on and check it off to enable it. 24 | 25 | ## Manual installation 26 | If the script install doesn't work for you for some reason, you can manually install from inside Blender. 27 | 28 | Under Edit > Preferences > Add-ons, click Install and find the blender_gazebo.py file inside the blender_gazebo directory. Remember to enabled it after it is installed 29 | 30 | ## Usage 31 | To use this add-on: 32 | * Make a new package. Make sure to add blender_gazebo as a dependency. 33 | * Add a launch directory to your package 34 | * In Blender, create your world. Remember to work in meters 35 | * In Blender, go to File > Export > Gazebo Launch (.launch) 36 | * Navigate to the launch folder in your package 37 | * Give your launch file a name. It should use your Blender file name as the default name 38 | * Click Export Gazebo 39 | * Once it is exported, build your package and run your launch file 40 | 41 | ## Known Issues 42 | Blender 2.80 exports standard materials in a way that RVIZ won't work with. Make sure to use Nodes in Blender for your materials to avoid this. 43 | 44 | ## Versions 45 | Currently only tested and supported in 2.80 46 | -------------------------------------------------------------------------------- /blender_gazebo/blender_gazebo.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "ROS Gazebo Exporter", 3 | "author": "Dave Niewinski", 4 | "version": (0,0,1), 5 | "blender": (2,80,0), 6 | "location": "File > Import-Export", 7 | "description": "Export Gazebo", 8 | "category": "Import-Export" 9 | } 10 | 11 | import bpy 12 | import os 13 | import subprocess 14 | from bpy_extras.io_utils import ExportHelper 15 | from bpy.props import StringProperty 16 | import xml.etree.ElementTree as ET 17 | 18 | class InternalData(): 19 | def __init__(self): 20 | self.text = "" 21 | 22 | def getRoot(self): 23 | return ET.fromstring(self.text) 24 | 25 | class BodyLaunch(InternalData): 26 | def __init__(self): 27 | self.text = ''' 28 | 29 | 30 | 31 | 32 | ''' 33 | 34 | class BodyURDF(InternalData): 35 | def __init__(self): 36 | self.text = ''' 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | true 59 | 60 | ''' 61 | 62 | class WorldLaunch(InternalData): 63 | def __init__(self): 64 | self.text = ''' 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ''' 79 | 80 | class GazeboExport(bpy.types.Operator, ExportHelper) : 81 | bl_idname = "object.exportgazebo"; 82 | bl_label = "Export Gazebo"; 83 | bl_options = {'REGISTER', 'UNDO'}; 84 | 85 | filename_ext = ".launch" 86 | filter_glob: StringProperty(default="*.launch", options={'HIDDEN'}) 87 | 88 | def __init__(self): 89 | self.collision_suffix = "_collision" 90 | 91 | def checkDir(self, dir): 92 | if not os.path.exists(dir): 93 | os.makedirs(dir) 94 | 95 | def getPackagePaths(self, launch_file, make_dirs=True): 96 | (dirname, filename) = os.path.split(launch_file) 97 | (shortname, extension) = os.path.splitext(filename) 98 | root_dir = os.path.abspath(os.path.join(dirname, "..")) 99 | mesh_dir = os.path.join(root_dir, "meshes/") 100 | urdf_dir = os.path.join(root_dir, "urdf/") 101 | launch_dir = os.path.join(root_dir, "launch/") 102 | package_name = root_dir.split("/") 103 | package_name = package_name[-1] 104 | 105 | if make_dirs: 106 | self.checkDir(mesh_dir) 107 | self.checkDir(urdf_dir) 108 | self.checkDir(launch_dir) 109 | 110 | return root_dir, mesh_dir, urdf_dir, launch_dir, package_name, shortname 111 | 112 | def writeURDF(self, name, package_name, visual, collision, urdf_dir): 113 | body_urdf = BodyURDF().getRoot() 114 | 115 | for c in body_urdf.findall(".//mesh"): 116 | for k in c.attrib.keys(): 117 | c.set(k, c.attrib[k].replace("$PACKAGE$", package_name).replace("$VISUAL$", visual).replace("$COLLISION$", collision)) 118 | for c in body_urdf.findall("link"): 119 | for k in c.attrib.keys(): 120 | c.set(k, c.attrib[k].replace("$NAME$", name)) 121 | 122 | outFile = open(urdf_dir + name + ".urdf.xacro", 'wb') 123 | outFile.write(ET.tostring(body_urdf)) 124 | outFile.close() 125 | 126 | def writeLaunch(self, name, package_name, launch_dir): 127 | body_launch = BodyLaunch().getRoot() 128 | 129 | for c in body_launch.findall("*"): 130 | for k in c.attrib.keys(): 131 | c.set(k, c.attrib[k].replace("$NAME$", name).replace("$PACKAGE$", package_name)) 132 | 133 | outFile = open(launch_dir + "spawn_" + name + ".launch", 'wb') 134 | outFile.write(ET.tostring(body_launch)) 135 | outFile.close() 136 | 137 | def exportSTL(self, ob, name, dir): 138 | bpy.ops.object.select_all(action='DESELECT') 139 | ob.select_set(True) 140 | bpy.ops.export_mesh.stl(filepath= dir + name, use_selection=True, use_mesh_modifiers=True) 141 | 142 | def exportDAE(self, ob, name, dir): 143 | bpy.ops.object.select_all(action='DESELECT') 144 | ob.select_set(True) 145 | bpy.ops.wm.collada_export(filepath= dir + name, apply_modifiers=True, selected=True) 146 | 147 | def execute(self, context): 148 | root_dir, mesh_dir, urdf_dir, launch_dir, package_name, prefix = self.getPackagePaths(self.filepath) 149 | world_launch = WorldLaunch().getRoot() 150 | 151 | for ob in bpy.data.objects: 152 | if ob.type == 'MESH': 153 | if self.collision_suffix not in ob.name: 154 | print("Working on " + ob.name) 155 | body_name = prefix + "_" + ob.name 156 | visual_name = body_name + ".dae" 157 | self.exportDAE(ob, visual_name, mesh_dir) 158 | 159 | collision_name = ob.name + self.collision_suffix 160 | try: 161 | collision_file = bpy.data.objects[collision_name] 162 | print("Found custom collision mesh") 163 | collision_name = body_name + self.collision_suffix + ".stl" 164 | self.exportSTL(collision_file, collision_name, mesh_dir) 165 | except: 166 | collision_name = visual_name 167 | 168 | self.writeURDF(body_name, package_name, visual_name, collision_name, urdf_dir) 169 | self.writeLaunch(body_name, package_name, launch_dir) 170 | world_launch.append(ET.Element("include", attrib={"file": "$(find " + package_name + ")/launch/spawn_" + body_name + ".launch"})) 171 | 172 | outFile = open(launch_dir + "/" + prefix + ".launch", 'wb') 173 | outFile.write(ET.tostring(world_launch)) 174 | outFile.close() 175 | 176 | return {'FINISHED'} 177 | 178 | def menu_func(self, context): 179 | self.layout.operator(GazeboExport.bl_idname, text="Gazebo Launch (.launch)"); 180 | 181 | def register(): 182 | bpy.utils.register_class(GazeboExport) 183 | bpy.types.TOPBAR_MT_file_export.append(menu_func) 184 | 185 | def unregister(): 186 | bpy.utils.unregister_class(GazeboExport) 187 | bpy.types.TOPBAR_MT_file_export.remove(menu_func); 188 | 189 | if __name__ == "__main__" : 190 | register() 191 | --------------------------------------------------------------------------------