1import logging; logger = logging.getLogger("morsebuilder." + __name__) 2import sys 3import math 4from morse.builder.blenderobjects import Cube 5from morse.builder import bpymorse 6from morse.builder.abstractcomponent import AbstractComponent 7from morse.core.exceptions import * 8 9""" 10Morse Builder API 11 12To test this module you can c/p the following code in Blender Python console:: 13 14.. code-block:: python 15 16 import sys 17 sys.path.append("/usr/local/lib/python3/dist-packages") 18 from morse.builder import * 19 atrv=ATRV() 20 21The string passed to the differents Components Classes must be an existing 22.blend file-name, ie. for ``ATRV()`` the file ``atrv.blend`` must exists 23in the folder ``MORSE_COMPONENTS/robots/``. 24""" 25 26# Override the default Python exception handler 27def morse_excepthook(*args, **kwargs): 28 logger.error("[ERROR][MORSE] Uncaught exception, quit Blender.", exc_info = tuple(args)) 29 # call default python exception hook 30 # on Ubuntu/Python3.4 sys.excepthook is overriden by `apport_excepthook` 31 sys.__excepthook__(*args, **kwargs) 32 import os 33 os._exit(-1) 34 35# Uncaught exception quit Blender 36sys.excepthook = morse_excepthook 37# workaround avoid numpy.core.multiarray missmatch ( see #630 ) 38sys.path.insert(0, '%s/lib/python%i.%i/site-packages'%(sys.prefix, 39 sys.version_info.major, sys.version_info.minor)) 40 41class PassiveObject(AbstractComponent): 42 """ Allows to import any Blender object to the scene. 43 """ 44 45 def __init__(self, filename="props/objects", prefix=None, keep_pose=False): 46 """ Initialize a PassiveObject 47 48 :param filename: The Blender file to load. Path can be absolute 49 or if no extension relative to MORSE assets' 50 installation path (typically, $PREFIX/share/morse/data) 51 :param prefix: (optional) the prefix of the objects to load in the 52 Blender file. If not set, all objects present in the file 53 are loaded. If set, all objects **prefixed** by this 54 name are imported. 55 :param keep_pose: If set, the object pose (translation and rotation) 56 in the Blender file is kept. Else, the object 57 own center is placed at origin and all rotation are 58 reset. 59 :return: a new AbstractComponent instance. 60 """ 61 AbstractComponent.__init__(self, filename=filename) 62 63 logger.info("Importing the following passive object(s): %s" % prefix) 64 65 imported_objects = self.append_meshes(prefix=prefix) 66 # Here we use the fact that after appending, Blender select the objects 67 # and the root (parent) object first ( [0] ) 68 self.set_blender_object(imported_objects[0]) 69 70 if not keep_pose: 71 self.location = (0.0, 0.0, 0.0) 72 self.rotation_euler = (0.0, 0.0, 0.0) 73 74 def setgraspable(self): 75 """ 76 Makes an object graspable to the human avatar by adding a NEAR collision 77 sensor to the object. 78 79 This function also set the object to be an active game object (property 80 'Object' set to true), and set the object label to the Blender object 81 name (if not already set). 82 """ 83 obj = self._bpy_object 84 85 if not "Label" in obj.game.properties: 86 self.properties(Object = True, Graspable = True, Label = obj.name) 87 else: 88 self.properties(Object = True, Graspable = True) 89 90 # Add collision sensor for object placement 91 if not 'Collision' in obj.game.sensors: 92 bpymorse.add_sensor(type = 'NEAR') 93 sens = obj.game.sensors[-1] 94 sens.name = 'Collision' 95 sens.distance = 0.05 96 sens.reset_distance = 0.075 97 bpymorse.add_controller() 98 contr = obj.game.controllers[-1] 99 contr.link(sensor = sens) 100 101class Zone(Cube): 102 def __init__(self, type): 103 Cube.__init__(self, 'xxx') 104 # Need to create a new material before calling make_transparent 105 self._bpy_object.active_material = bpymorse.create_new_material() 106 self._make_transparent(self._bpy_object, 1e-6) 107 self.properties(Zone_Tag = True, Type = type) 108 109 @property 110 def size(self): 111 return self._bpy_object.scale 112 @size.setter 113 def size(self, value): 114 self._bpy_object.scale = value 115 116 def rotate(self, x=0.0, y=0.0, z=0.0): 117 logger.warning("rotate is not supported for Zone") 118 119class Component(AbstractComponent): 120 """ Append a morse-component to the scene 121 122 cf. `bpy.ops.wm.link_append` and `bpy.data.libraries.load` 123 """ 124 def __init__(self, category='', filename='',blender_object_name=None, make_morseable=True): 125 """ Initialize a MORSE component 126 127 :param category: The category of the component (folder in 128 MORSE_COMPONENTS) 129 :param filename: The name of the component (file in 130 MORSE_COMPONENTS/category/name.blend) If ends with '.blend', 131 append the objects from the Blender file. 132 :param blender_object_name: If set, use the given Blender object 133 as 'root' for this component. Otherwise, select the first 134 available Blender object (the top parent in case of a hierarchy 135 of objects). 136 :param make_morseable: If the component has no property for the 137 simulation, append default Morse ones. See self.morseable() 138 """ 139 AbstractComponent.__init__(self, filename=filename, category=category) 140 141 142 if blender_object_name is None: 143 imported_objects = self.append_meshes() 144 else: 145 imported_objects = self.append_meshes(objects=[blender_object_name]) 146 if not imported_objects: 147 raise MorseBuilderNoComponentError("No object named <%s> in %s" % (blender_object_name, filename)) 148 149 150 # Here we use the fact that after appending, Blender select the objects 151 # and the root (parent) object first ( [0] ) 152 self.set_blender_object(imported_objects[0]) 153 # If the object has no MORSE logic, add default one 154 if make_morseable and category in ['sensors', 'actuators', 'robots'] \ 155 and not self.is_morseable(): 156 self.morseable() 157 158 159class Robot(Component): 160 def __init__(self, filename='', name=None, blender_object_name=None): 161 """ Initialize a MORSE robot 162 163 :param filename: The name of the component (file in 164 MORSE_COMPONENTS/category/name.blend) If ends with '.blend', 165 append the objects from the Blender file. 166 :param name: Name of the resulting robot in the simulation, default to 'robot'. 167 :param blender_object_name: If set, use the given Blender object 168 in 'filename' as 'root' for this robot. Otherwise, select the first 169 available Blender object (the top parent in case of a hierarchy 170 of objects). 171 """ 172 Component.__init__(self, 'robots', filename, blender_object_name=blender_object_name) 173 self.properties(Robot_Tag = True) 174 self.default_interface = None 175 if name: 176 self.name = name 177 178 def set_friction(self, friction=0.0): 179 """ Set Coulomb friction coefficient 180 181 :param friction: [0, 100], default 0.0 182 :type friction: float 183 """ 184 for slot in self._bpy_object.material_slots: # ['TireMat'] 185 slot.material.physics.friction = friction 186 187 def set_mass(self, mass): 188 """ Set component's mass 189 190 :param mass: The component's mass 191 :type mass: float 192 193 .. note:: 194 The object must have a physics controller for the mass to be 195 applied, otherwise the mass value will be returned as 0.0. 196 """ 197 self._bpy_object.game.mass = mass 198 199 def add_default_interface(self, stream): 200 """ Add a service and stream interface to all components of the robot 201 202 .. note:: 203 If add_stream or add_service is used explicitly for some 204 components and the specified interface is the same it will be 205 added twice. 206 """ 207 self.default_interface = stream 208 209 def make_external(self): 210 self._bpy_object.game.properties['Robot_Tag'].name = 'External_Robot_Tag' 211 212 def make_ghost(self, alpha=0.3): 213 """ Make this robot a ghost 214 215 The robot is made transparent, with no collision. 216 217 .. note:: 218 A ghost robot has no influence on other simulated robots 219 (no collision, invisible to laser sensors) except for video sensors. 220 221 :param alpha: Transparency alpha coefficient (0 for invisible, 1 for opaque, default is 0.3) 222 """ 223 self._make_transparent(self._bpy_object, alpha) 224 225 def set_rigid_body(self): 226 """ Configure this robot to use rigid_body physics """ 227 self._bpy_object.game.use_actor = True 228 self._bpy_object.game.physics_type = 'RIGID_BODY' 229 self._bpy_object.game.use_sleep = True 230 231 def set_no_collision(self): 232 """ Configure this robot to not use physics at all """ 233 self._bpy_object.game.physics_type = 'NO_COLLISION' 234 235 def set_physics_type(self, physics_type='STATIC'): 236 """ Configure this robot physics type """ 237 self._bpy_object.game.physics_type = physics_type 238 239 def set_use_record_animation(self, use_record_animation=True): 240 """ Record animation objects without physics """ 241 self._bpy_object.game.use_record_animation = use_record_animation 242 243 def set_dynamic(self): 244 self._bpy_object.game.physics_type = 'DYNAMIC' 245 self._bpy_object.game.use_actor = True 246 self._bpy_object.game.use_sleep = True 247 248 def set_collision_bounds(self): 249 self._bpy_object.game.use_collision_bounds = True 250 self._bpy_object.game.collision_bounds_type = 'CONVEX_HULL' 251 self._bpy_object.game.use_collision_compound = True 252 253 def make_grasper(self, obj_name): 254 obj = bpymorse.get_object(obj_name) 255 bpymorse.select_only(obj) 256 bpymorse.add_sensor(type = 'NEAR') 257 sens = obj.game.sensors[-1] 258 sens.name = 'Near' 259 sens.distance = 5.0 260 sens.reset_distance = 0.075 261 sens.property = "Graspable" 262 bpymorse.add_controller() 263 contr = obj.game.controllers[-1] 264 contr.link(sensor = sens) 265 266 267class GroundRobot(Robot): 268 def __init__(self, filename, name, blender_object_name=None): 269 Robot.__init__(self, filename, name, blender_object_name=blender_object_name) 270 self.properties(GroundRobot = True) 271 272class WheeledRobot(GroundRobot): 273 def __init__(self, filename, name, blender_object_name=None): 274 Robot.__init__(self, filename, name, blender_object_name=blender_object_name) 275 276 def unparent_wheels(self): 277 """ Make the wheels orphans, but keep the transformation applied to 278 the parent robot """ 279 # Force Blender to update the transformation matrices of objects 280 bpymorse.get_context_scene().update() 281 282 keys = ['WheelFLName', 'WheelFRName', 'WheelRLName', 283 'WheelRRName', 'CasterWheelName'] 284 properties = bpymorse.get_properties(self._bpy_object) 285 for key in keys: 286 expected_wheel = properties.get(key, None) 287 if expected_wheel: 288 wheel = self.get_child(expected_wheel) 289 if wheel: 290 # Make a copy of the current transformation matrix 291 transformation = wheel.matrix_world.copy() 292 wheel.parent = None 293 wheel.matrix_world = transformation 294 else: 295 logger.error('Wheel %s is required but not found' % expected_wheel) 296 297 def after_renaming(self): 298 self.unparent_wheels() 299