def createTemplate(): # Create the vertices for a unit cube. vert = defineCube() # Define initial fragment size and position relative to rigid body. scale = 1 pos, rot = (0, 0, 0), (0, 0, 0, 1) # Define the one and only geometry fragment for this template. data_raw = FragRaw(vert, [], []) frags = {'frag_foo': FragMeta('raw', scale, pos, rot, data_raw)} del scale, pos, rot # We will need that collision shape to construct the rigid body below. cs_sphere = CollShapeMeta(cstype='Sphere', position=(0, 0, 0), rotation=(0, 0, 0, 1), csdata=CollShapeSphere(radius=1)) # Create the rigid body. body = azrael.aztypes.RigidBodyData( scale=1, imass=1, restitution=0.9, rotation=(0, 0, 0, 1), position=(0, 0, 0), velocityLin=(0, 0, 0), velocityRot=(0, 0, 0), cshapes={'foo_sphere': cs_sphere}, linFactor=(1, 1, 1), rotFactor=(1, 1, 1), version=0) # Compile and return the template. return Template('my_first_template', body, frags, {}, {})
def cubeGeometry(hlen_x=1.0, hlen_y=1.0, hlen_z=1.0): """ Return the vertices and collision shape for a Box. The parameters ``hlen_*`` are the half lengths of the box in the respective dimension. """ # Vertices that define a Cube. vert = 1 * np.array([ -1.0, -1.0, -1.0, -1.0, -1.0, +1.0, -1.0, +1.0, +1.0, -1.0, -1.0, -1.0, -1.0, +1.0, +1.0, -1.0, +1.0, -1.0, +1.0, -1.0, -1.0, +1.0, +1.0, +1.0, +1.0, -1.0, +1.0, +1.0, -1.0, -1.0, +1.0, +1.0, -1.0, +1.0, +1.0, +1.0, +1.0, -1.0, +1.0, -1.0, -1.0, -1.0, +1.0, -1.0, -1.0, +1.0, -1.0, +1.0, -1.0, -1.0, +1.0, -1.0, -1.0, -1.0, +1.0, +1.0, +1.0, +1.0, +1.0, -1.0, -1.0, +1.0, -1.0, +1.0, +1.0, +1.0, -1.0, +1.0, -1.0, -1.0, +1.0, +1.0, +1.0, +1.0, -1.0, -1.0, -1.0, -1.0, -1.0, +1.0, -1.0, +1.0, +1.0, -1.0, +1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, +1.0, +1.0, -1.0, -1.0, +1.0, +1.0, -1.0, +1.0, +1.0, +1.0, +1.0, -1.0, +1.0, +1.0, +1.0, -1.0, +1.0 ]) # Scale the x/y/z dimensions. vert[0::3] *= hlen_x vert[1::3] *= hlen_y vert[2::3] *= hlen_z # Convenience. box = CollShapeBox(hlen_x, hlen_y, hlen_z) cs = CollShapeMeta('box', (0, 0, 0), (0, 0, 0, 1), box) return vert, cs
def createTemplate(): # Create the vertices for a unit cube. vert = defineCube() # Define initial fragment size and position relative to rigid body. scale = 1 pos, rot = (0, 0, 0), (0, 0, 0, 1) # Define the one and only geometry fragment for this template. data_raw = FragRaw(vert, [], []) frags = { 'body': FragMeta('raw', scale, pos, rot, data_raw), 'satellite': FragMeta('raw', scale, pos, rot, data_raw), } del scale, pos, rot # We will need that collision shape to construct the rigid body below. cs_sphere = CollShapeMeta(cstype='Sphere', position=(0, 0, 0), rotation=(0, 0, 0, 1), csdata=CollShapeSphere(radius=1)) # Create the rigid body. body = RigidBodyData(scale=1, imass=1, restitution=0.9, rotation=(0, 0, 0, 1), position=(0, 0, 0), velocityLin=(0, 0, 0), velocityRot=(0, 0, 0), cshapes={'foo_sphere': cs_sphere}, linFactor=(1, 1, 1), rotFactor=(1, 1, 1), version=0) # Define a booster booster = Booster( position=[0, 0, 0], # Booster is located here and... direction=[1, 0, 0], # points in this direction. force=0 # Initial force. ) boosters = {'booster_foo': booster} return Template('my_first_template', body, frags, boosters, {})
def loadSphere(radius: float=1.0): """ Return the collision shape and vertices for a sphere with ``radius``. Args: radius (float): sphere radius (must be >= 0.01). Returns: (vert, colshapes): the vertices and collision shape for the sphere. """ assert radius >= 0.01 p = os.path.dirname(os.path.abspath(__file__)) fname = os.path.join(p, 'models', 'sphere', 'sphere.obj') vert, uv, rgb = loadModel(fname) vert *= radius cshapes = CollShapeMeta(cstype='Sphere', position=(0, 0, 0), rotation=(0, 0, 0, 1), csdata=CollShapeSphere(radius=radius)) return vert, cshapes
def getRigidBody(scale: (int, float)=1, imass: (int, float)=1, restitution: (int, float)=0.9, com: (tuple, list)=(0, 0, 0), inertia: (tuple, list)=(1, 1, 1), paxis: (tuple, list)=(0, 0, 0, 1), rotation: (tuple, list)=(0, 0, 0, 1), position: (tuple, list, np.ndarray)=(0, 0, 0), velocityLin: (tuple, list, np.ndarray)=(0, 0, 0), velocityRot: (tuple, list, np.ndarray)=(0, 0, 0), cshapes: dict=None, linFactor: (tuple, list, np.ndarray)=(1, 1, 1), rotFactor: (tuple, list, np.ndarray)=(1, 1, 1), version: int=0): if cshapes is None: cshapes = CollShapeMeta(cstype='Sphere', position=(0, 0, 0), rotation=(0, 0, 0, 1), csdata=CollShapeSphere(radius=1)) cshapes = {'Sphere': cshapes} return aztypes.RigidBodyData( scale, imass, restitution, com, inertia, paxis, rotation, position, velocityLin, velocityRot, cshapes, linFactor, rotFactor, version)
def getCSBox(pos=[0, 0, 0], rot=[0, 0, 0, 1], dim=[1, 1, 1]): """ Convenience function: return collision shape for Box. """ return CollShapeMeta('box', pos, rot, CollShapeBox(*dim))
def getCSEmpty(pos=[0, 0, 0], rot=[0, 0, 0, 1]): """ Convenience function to construct an Empty shape. """ return CollShapeMeta('empty', pos, rot, CollShapeEmpty())
def getCSPlane(pos=[0, 0, 0], rot=[0, 0, 0, 1], normal=[0, 0, 1], ofs=0): """ Convenience function: return collision shape for a Plane in the x/y dimension. """ return CollShapeMeta('plane', pos, rot, CollShapePlane(normal, ofs))
def getCSSphere(pos=[0, 0, 0], rot=[0, 0, 0, 1], radius=1): """ Convenience function: return collision shape for Sphere. """ return CollShapeMeta('sphere', pos, rot, CollShapeSphere(radius))
def _compileCollisionShape(self, rbState: _RigidBodyData): """ Return the correct Bullet collision shape based on ``rbState``. Bullet does *not* use inertia tensors. It also does not have (much of) a concept of centre of mass. Instead it *assumes* that the principal axis of inertia of the (unrotated) collision shape is *literally* the x/y/z axis of the world coordinate system. It *assumes* further that the centre of mass is at the center of that collision shape. This is fine for axis symmetric objects (eg spheres, boxes, capsules), but not much else. We can get around this restriction with a compound shape. The basic idea is that with a compound shape bullet does not care about the child shapes, only the compound shape. We may thus apply a particular transform (translate/rotate) to all child shapes and the inverse transform to the compound shape. If we do this correctly we can make the position of the compound shape coincide with its centre of mass, and have its principal axis aligned with x/y/z in world coordinates. This is explained next. Before we start recall that the centre of mass and axis of inertia for the overall rigid body were specified by the user (available in the `rbState` argument). Azrael *never* computes or modifies inertia tensors or centre of masses - the user has to do that explicitly. Now, to get around the aforementioned problem: first we put the collision shape(s) into a compound shape. However, we do not put them at their absolute positions but at their respective position/rotation relative to the centre of mass and principal axis orientation. The compound shape's centre of mass thus coincides with the position of the compound shape. The compound's principal axis of inertia also coincides with the x/y/z axis of the world coordinates. Together this satisfies Bullet's assumption stated in the second paragraph of this doc string. The position/rotation of the child shapes in world coordinates is still *wrong*, however, because their position/rotation was relative to centre of mass and principal axis. To correct it we move and rotate the compound shape in the exact opposite way. The net effect is that the child shapes are now in the correct location (as we want it) *and* the centre of mass coincides with the position of the compound shape (as Bullet wants it) *and* the principal axis of the compound shape aligns with the x/y/z axis of the world coordinate system (as Bullet wants it as well). Remarks: * The mass and inertia of the child shapes is irrelevant; Bullet only looks at the mass/inertia of the compound. * Bullet does not compute the mass/inertia of the compound based on the children; it offers convenience methods for doing so, but the user must explicitly use them. * The one and only purpose of the child shapes inside a compound is to define where (and how) the rigid body can be "collided with". :param _RigidBodyData rbState: meta data to describe the body. :return: compound shape with all the individual shapes. :rtype: ``CompoundShape`` """ # Create the compound shape that will hold all other shapes. compound = azBullet.CompoundShape() # Compute the inverse paComT. paxis = Quaternion(*rbState.paxis).normalized() # Determine the transform with respect to the principal axis # orientation and the centre of mass. i_paComT = Transform(paxis, Vec3(*rbState.com)).inverse() # Create the collision shapes one by one. scale = rbState.scale for cs in rbState.cshapes.values(): # Convert the input data (usually a list of values) to a # proper CollShapeMeta tuple (sanity checks included). cs = CollShapeMeta(*cs) # Instantiate the specified collision shape. cstype = cs.cstype.upper() if cstype == 'SPHERE': sphere = CollShapeSphere(*cs.csdata) child = azBullet.SphereShape(scale * sphere.radius) elif cstype == 'BOX': box = CollShapeBox(*cs.csdata) hl = Vec3(scale * box.x, scale * box.y, scale * box.z) child = azBullet.BoxShape(hl) elif cstype == 'EMPTY': child = azBullet.EmptyShape() elif cstype == 'PLANE': # Planes are always static. rbState_mass = 0 plane = CollShapePlane(*cs.csdata) normal = Vec3(*plane.normal) child = azBullet.StaticPlaneShape(normal, plane.ofs) else: child = azBullet.EmptyShape() msg = 'Unrecognised collision shape <{}>'.format(cstype) self.logit.warning(msg) # Specify the position/rotation of the child shape. Then adjust # them to be relative to the principal axis orientation and centre # of mass location (setRigidBodyData will compensate for this with # the inverse transform on the compound shape). t = Transform(Quaternion(*cs.rotation), Vec3(*cs.position)) t = i_paComT * t # Add the child with the correct transform. compound.addChildShape(t, child) return compound
def addBoosterCubeTemplate(scale, vert, uv, rgb): # Get a Client instance. client = pyazrael.AzraelClient() # Ensure the data has the correct format. vert = scale * np.array(vert) uv = np.array(uv, np.float32) rgb = np.array(rgb, np.uint8) print('done') # Attach four boosters: left (points down), front (points back), right # (points up), and back (point forward). dir_up = np.array([0, +1, 0]) dir_forward = np.array([0, 0, -1]) pos_left = np.array([-1.5, 0, 0]) pos_center = np.zeros(3) boosters = { '0': aztypes.Booster(position=pos_left, direction=-dir_up, force=0), '1': aztypes.Booster(position=pos_center, direction=dir_forward, force=0), '2': aztypes.Booster(position=-pos_left, direction=dir_up, force=0), '3': aztypes.Booster(position=pos_center, direction=-dir_forward, force=0) } del dir_up, dir_forward, pos_left, pos_center # Construct a Tetrahedron (triangular Pyramid). This is going to be the # (super simple) "flame" that comes out of the (still invisible) boosters. y = 0.5 * np.arctan(np.pi / 6) a = (-0.5, -y, 1) b = (0.5, -y, 1) c = (0, 3 / 4 - y, 1) d = (0, 0, 0) vert_b = [(a + b + c) + (a + b + d) + (a + c + d) + (b + c + d)] vert_b = np.array(vert_b[0], np.float64) del a, b, c, d, y # Add the template to Azrael. print(' Adding template to Azrael... ', end='', flush=True) tID = 'ground' cs = CollShapeBox(1, 1, 1) cs = CollShapeMeta('box', (0, 0, 0), (0, 0, 0, 1), cs) z = np.array([]) frags = { 'frag_1': demolib.getFragMetaRaw(vert, uv, rgb), 'b_left': demolib.getFragMetaRaw(vert_b, z, z), 'b_right': demolib.getFragMetaRaw(vert_b, z, z), } body = demolib.getRigidBody() temp = Template(tID, body, frags, boosters, {}) assert client.addTemplates([temp]).ok del cs, frags, temp, z print('done') # Spawn the template near the center. print(' Spawning object... ', end='', flush=True) pos, orient = [0, 0, -10], [0, 1, 0, 0] new_obj = { 'templateID': tID, 'rbs': { 'scale': scale, 'imass': 0.1, 'position': pos, 'rotation': orient, 'linFactor': [1, 1, 1], 'rotFactor': [1, 1, 1]} } ret = client.spawn([new_obj]) objID = ret.data[0] print('done (ID=<{}>)'.format(objID)) # Disable the booster fragments by settings their scale to Zero. newStates = {objID: { 'b_left': {'op': 'mod', 'scale': 0}, 'b_right': {'op': 'mod', 'scale': 0}, }} assert client.setFragments(newStates).ok
def computeAABBs(cshapes: dict): """ Return a dictionary of AABBs that correspond to the ``cshapes``. This function returns an error as soon as it encounters an unknown collision shape. If ``cshapes`` contains a Plane shape then it must be the only collision shape (ie len(cshapes) == 1). Furthermore, planes must have default values for position and rotation. ..note:: The bounding boxes are large enough to accommodate all possible rotations of the collision shape. This makes them larger than necessary yet avoids recomputing them whenever the rotation of the body changes. :param dict[name: CollShapeMeta] cshapes: collision shapes :return: AABBs for the ``cshapes``. """ # Convenience. s3 = np.sqrt(3.1) # Compute the AABBs for each shape. aabbs = {} try: # Wrap the inputs into CollShapeMeta structures. cshapes = {k: CollShapeMeta(*v) for (k, v) in cshapes.items()} if 'PLANE' in [_.cstype.upper() for _ in cshapes.values()]: if len(cshapes) > 1: msg = 'Plane must be the only collision shape' return RetVal(False, msg, None) name, cshape = cshapes.popitem() # Planes must have defaule values for position and rotation, or # Azrael considers them invalid. pos, rot = tuple(cshape.position), tuple(cshape.rotation) if (pos == (0, 0, 0)) and (rot == (0, 0, 0, 1)): aabbs[name] = (0, 0, 0, 0, 0, 0) return RetVal(True, None, aabbs) else: msg = 'Planes must have default position and rotation' return RetVal(False, msg, None) for name, cs in cshapes.items(): # Move the origin of the collision shape according to its rotation. quat = util.Quaternion(*cs.rotation) pos = tuple(quat * cs.position) # Determine the AABBs based on the collision shape type. ctype = cs.cstype.upper() if ctype == 'SPHERE': # All half lengths have the same length (equal to radius). r = CollShapeSphere(*cs.csdata).radius aabbs[name] = pos + (r, r, r) elif ctype == 'BOX': # All AABBs half lengths are equal. The value equals the # largest extent times sqrt(3) to accommodate all possible # rotations. tmp = s3 * max(CollShapeBox(*cs.csdata)) aabbs[name] = pos + (tmp, tmp, tmp) elif ctype == 'EMPTY': # Empty shapes do not have an AABB. continue else: # Error. msg = 'Unknown collision shape <{}>'.format(ctype) return RetVal(False, msg, None) except TypeError: # Error: probably because 'CollShapeMeta' or one of its sub-shapes, # could not be constructed. msg = 'Encountered invalid collision shape data' return RetVal(False, msg, None) return RetVal(True, None, aabbs)