├── 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 |
--------------------------------------------------------------------------------