class BulletBase(object): """ Manages Panda3d Bullet physics resources and convenience methods.""" # Bitmasks for each object type. By setting ghost and static # objects to different masks, we can filter ghost-to-static # collisions. ghost_bit = BitMask32.bit(1) static_bit = BitMask32.bit(2) dynamic_bit = ghost_bit | static_bit bw_types = (BulletBaseCharacterControllerNode, BulletBodyNode, BulletConstraint, BulletVehicle) def __init__(self): self.world = None # Parameters for simulation. self.sim_par = {"size": 1. / 100, "n_subs": 10, "size_sub": 1. / 1000} # Initialize axis constraint so that there aren't constraints. self.axis_constraint_fac = Vec3(1, 1, 1) self.axis_constraint_disp = Vec3(nan, nan, nan) # Attribute names of destructable objects. self._destructables = () def init(self): """ Initialize world and resources. """ # Initialize world. self.world = BulletWorld() def destroy(self): """ Destroy all resources.""" for key in self._destructables: getattr(self, key).destroy() def setup_debug(self): debug_node = BulletDebugNode('Debug') debug_node.showWireframe(True) debug_node.showConstraints(True) debug_node.showBoundingBoxes(True) debug_node.showNormals(True) self.world.setDebugNode(debug_node) return debug_node @property def bodies(self): """ Return all bodies (rigid, soft, and ghost) in self.world.""" bodies = (self.world.getRigidBodies() + self.world.getSoftBodies() + self.world.getGhosts()) return bodies def _constrain_axis(self, body): """ Apply existing axis constraints to a body.""" # Set displacements. for axis, (f, d) in enumerate(zip(self.axis_constraint_fac, self.axis_constraint_disp)): if not f and not isnan(d): nodep = NodePath(body) pos = nodep.getPos() pos[axis] = d nodep.setPos(pos) try: # Linear and angular factors of 0 mean forces in the # corresponding axis are scaled to 0. body.setLinearFactor(self.axis_constraint_fac) # Angular factors restrict rotation about an axis, so the # following logic selects the appropriate axes to # constrain. s = sum(self.axis_constraint_fac) if s == 3.: v = self.axis_constraint_fac elif s == 2.: v = -self.axis_constraint_fac + 1 else: v = Vec3.zero() body.setAngularFactor(v) except AttributeError: # The body was not a rigid body (it didn't have # setLinearFactor method). pass def set_axis_constraint(self, axis, on, disp=None): """ Sets an axis constraint, so that bodies can/cannot move in that direction.""" # Create the constraint vector. self.axis_constraint_fac[axis] = int(not on) self.axis_constraint_disp[axis] = disp if disp is not None else nan # Iterate over bodies, applying the constraint. for body in self.bodies: self._constrain_axis(body) def attach(self, objs, suppress_deact_warn=False): """ Attach Bullet objects to the world.""" if not self.world: raise BulletBaseError("No BulletWorld initialized.") # Make sure they're iterable. if not isinstance(objs, Iterable): objs = [objs] elif isinstance(objs, dict): objs = objs.itervalues() bw_objs = [] for obj in objs: if isinstance(obj, NodePath): obj = obj.node() if isinstance(obj, self.bw_types): bw_objs.append(obj) # Don't attach ones that are already attached. bw_objs = set(bw_objs) - set(self.bodies) # Attach them. for obj in bw_objs: # Warn about deactivation being enabled. if (not suppress_deact_warn and getattr(obj, "isDeactivationEnabled", lambda: True)()): msg = "Deactivation is enabled on object: %s" % obj warn(msg, DeactivationEnabledWarning) # Apply existing axis constraints to the objects. self._constrain_axis(obj) # Attach the objects to the world. try: self.world.attach(obj) except AttributeError: DeprecationWarning("Upgrade to Panda3d 1.9.") for attr in ("attachRigidBody", "attachConstraint", "attachGhost"): attach = getattr(self.world, attr) try: attach(obj) except TypeError: pass else: break def remove(self, objs): """ Remove Bullet objects to the world.""" if not self.world: raise BulletBaseError("No BulletWorld initialized.") # Make sure they're iterable. if not isinstance(objs, Iterable): objs = [objs] elif isinstance(objs, dict): objs = objs.itervalues() bw_objs = [] for obj in objs: if isinstance(obj, NodePath): obj = obj.node() if isinstance(obj, self.bw_types): bw_objs.append(obj) # Remove them. for obj in bw_objs: # Remove the objects from the world. try: self.world.remove(obj) except AttributeError: DeprecationWarning("Upgrade to Panda3d 1.9.") for attr in ("removeRigidBody", "removeConstraint", "removeGhost"): remove = getattr(self.world, attr) try: remove(obj) except TypeError: pass else: break def remove_all(self): """ Remove all objects from world.""" objs = (self.world.getCharacters() + self.world.getConstraints() + self.world.getGhosts() + self.world.getRigidBodies() + self.world.getSoftBodies() + self.world.getVehicles()) self.remove(objs) @property def gravity(self): """ Get gravity on self.world. """ return self.world.getGravity() @gravity.setter def gravity(self, gravity): """ Set gravity on self.world. """ self.world.setGravity(Vec3(*gravity)) def step(self, *args, **kwargs): """ Wrapper for BulletWorld.doPhysics.""" # Defaults. dt = args[0] if len(args) > 0 else self.sim_par["size"] n_subs = args[1] if len(args) > 1 else self.sim_par["n_subs"] size_sub = args[2] if len(args) > 2 else self.sim_par["size_sub"] force = kwargs.get("force", None) if force: bodies, vecpos, dur = force dt0 = np.clip(dur, 0., dt) n_subs0 = int(np.ceil(n_subs * dt0 / dt)) dt1 = dt - dt0 n_subs1 = n_subs - n_subs0 + 1 for body in bodies: body.applyForce(Vec3(*vecpos[0]), Point3(*vecpos[1])) # With force. self.world.doPhysics(dt0, n_subs0, size_sub) for body in bodies: body.clearForces() else: dt1 = dt n_subs1 = n_subs # With no force. self.world.doPhysics(dt1, n_subs1, size_sub) @staticmethod def attenuate_velocities(bodies, linvelfac=0., angvelfac=0.): """ Zeros out the bodies' linear and angular velocities.""" # Iterate through bodies, re-scaling their velocity vectors for body in bodies: linvel0 = body.getLinearVelocity() angvel0 = body.getAngularVelocity() # .normalize() returns True if the length is > 0 if linvel0.normalize(): linvel = linvel0 * linvelfac body.setLinearVelocity(linvel) if angvel0.normalize(): angvel = angvel0 * angvelfac body.setAngularVelocity(angvel) def repel(self, n_steps=1000, thresh=10, step_size=0.01): """ Performs n_steps physical "repel" steps. """ @contextmanager def repel_context(world): """ Sets up a repel context. Gets the bodies, turns off gravity, rescales the masses, sets up the collision notification callback. """ def change_contact_thresh(bodies, thresh=0.001): """ Adjust the contact processing threshold. This is used to make the objects not trigger collisions when just barely touching.""" if isinstance(thresh, Iterable): it = izip(bodies, thresh) else: it = ((body, thresh) for body in bodies) thresh0 = [] for body, th in it: thresh0.append(body.getContactProcessingThreshold()) body.setContactProcessingThreshold(th) return thresh0 def rescale_masses(bodies): """ Rescale the masses so they are proportional to the volume.""" masses, inertias = zip(*[(body.getMass(), body.getInertia()) for body in bodies]) volumefac = 1. for body, mass, inertia in zip(bodies, masses, inertias): # Compute the mass-normalized diagonal elements of the # inertia tensor. if mass > 0.: it = inertia / mass * 12 # Calculate volume from the mass-normalized # inertia tensor (from wikipedia). h = sqrt((it[0] - it[1] + it[2]) / 2) w = sqrt(it[2] - h ** 2) d = sqrt(it[1] - w ** 2) volume = h * w * d # Change the mass. body.setMass(volume * volumefac) return masses # Get the bodies. bodies = world.getRigidBodies() # Turn gravity off. gravity = world.getGravity() world.setGravity(Vec3.zero()) # Tighten the contact processing threshold slightly. delta = -0.001 cp_thresh = change_contact_thresh(bodies, thresh=delta) # Adjust masses. masses = rescale_masses(bodies) # Adjust sleep thresholds. deactivations = [b.isDeactivationEnabled() for b in bodies] for body in bodies: body.setDeactivationEnabled(False) # Zero out velocities. self.attenuate_velocities(bodies) # Collisions monitor. collisions = CollisionMonitor(world) collisions.push_notifiers(bodies) ## Finish __enter__. yield bodies, collisions ## Start __exit__. collisions.pop_notifiers() # Zero out velocities. self.attenuate_velocities(bodies) # Restore the contact processing threshold. change_contact_thresh(bodies, thresh=cp_thresh) # Set masses back. for body, mass in zip(bodies, masses): body.setMass(mass) # Turn gravity back on. world.setGravity(gravity) for body, d in zip(bodies, deactivations): body.setDeactivationEnabled(d) # Operate in a context that changes the masses, turns off # gravity, adds collision monitoring callback, etc. with repel_context(self.world) as (bodies, collisions): # Loop through the repel simulation. done_count = 0 for istep in xrange(n_steps): # Take one step. self.world.doPhysics(step_size, 1, step_size) # HACK: The following can be removed once Panda3d 1.9 # is installed (and the method can be removed from # CollisionMonitor). collisions.detect18() # Increment done_count, only if there are no contacts. if collisions: done_count = 0 else: done_count += 1 if any(body.getMass() > 0. and not body.isActive() for body in bodies): BP() # Stop criterion. if done_count >= thresh: break # Zero-out/re-scale velocities. linvelfac = bool(collisions) * 0.001 angvelfac = bool(collisions) * 0.001 self.attenuate_velocities(bodies, linvelfac, angvelfac) # Reset collisions. collisions.reset() return istep @classmethod def add_collide_mask(cls, func0): """ Decorator. Initializes ghost, static, and dynamic nodes with the appropriate collide masks.""" def func(*args, **kwargs): # Create node using func0. node = func0(*args, **kwargs) # Determine collide mask. if isinstance(node, BulletGhostNode): bit = cls.ghost_bit elif node.getMass() == 0.: bit = cls.static_bit else: bit = cls.dynamic_bit # Set collide mask. node.setCollideMask(bit) return node return update_wrapper(func0, func) @staticmethod def add_ghostnode(node): """ Adds a child ghostnode to a node as a workaround for the ghost-static node collision detection problem.""" name = "%s-ghost" % node.getName() ghost = NodePath(BulletGhostNode(name)) ghost.reparentTo(node) return ghost
class Balls(ShowBase): def __init__(self): ShowBase.__init__(self) self.title = OnscreenText(text="0 balls", parent=base.a2dBottomRight, align=TextNode.ARight, fg=(1, 1, 1, 1), pos=(-0.1, 0.1), scale=.08, shadow=(0, 0, 0, 0.5)) # exit on esc self.accept('escape', sys.exit) # disable standart mouse based camera control self.disableMouse() # set camera position self.camera.setPos(0, -30, 25) self.camera.lookAt(0, 0, 0) # self.world = BulletWorld() self.world.setGravity(Vec3(0, 0, -9.81)) self.taskMgr.add(self.updateWorld, 'updateWorld') self.setupLight() # down self.makePlane(0, Vec3(0, 0, 1), (0, 0, 0), (0, 0, 0)) # up self.makePlane(1, Vec3(0, 0, -1), (0, 0, 10), (0, 0, 0)) # left self.makePlane(2, Vec3(1, 0, 0), (-5, 0, 5), (0, 0, 90)) # right self.makePlane(3, Vec3(-1, 0, 0), (5, 0, 5), (0, 0, -90)) # top self.makePlane(4, Vec3(0, 1, 0), (0, -5, 5), (0, 90, 0)) # buttom self.makePlane(5, Vec3(0, -1, 0), (0, 5, 5), (0, -90, 0)) self.accept('mouse1', self.pickBall) self.accept('mouse3', self.releaseBall) self.accept('arrow_up', partial(self.rotateCube, hpr = (0, ROTATE_SPEED, 0))) self.accept('arrow_down', partial(self.rotateCube, hpr = (0, -ROTATE_SPEED, 0))) self.accept('arrow_left', partial(self.rotateCube, hpr = (0, 0, -ROTATE_SPEED))) self.accept('arrow_right', partial(self.rotateCube, hpr = (0, 0, ROTATE_SPEED))) self.accept('space', self.shakeBalls) self.accept('page_up', self.addRandomBall) self.accept('page_down', self.rmBall) self.ballCnt = 0 self.ballColors = {} for num in xrange(DEFAULT_BALLS): self.addRandomBall() self.picked = set([]) def setupLight(self): ambientLight = AmbientLight("ambientLight") ambientLight.setColor((.8, .8, .8, 1)) directionalLight = DirectionalLight("directionalLight") directionalLight.setDirection(LVector3(0, 45, -45)) directionalLight.setColor((0.2, 0.2, 0.2, 1)) render.setLight(render.attachNewNode(directionalLight)) render.setLight(render.attachNewNode(ambientLight)) def updateWorld(self, task): dt = globalClock.getDt() # bug #1455084, simple doPhysics(dt) does nothing # looks like fixed already self.world.doPhysics(dt, 1, 1. / 60.) return task.cont def rayCollision(self): if self.mouseWatcherNode.hasMouse(): mouse = self.mouseWatcherNode.getMouse() pointFrom, pointTo = Point3(), Point3() self.camLens.extrude(mouse, pointFrom, pointTo) pointFrom = render.getRelativePoint(self.cam, pointFrom) pointTo = render.getRelativePoint(self.cam, pointTo) hits = self.world.rayTestAll(pointFrom, pointTo).getHits() return sorted(hits, key = lambda x: (x.getHitPos() - pointFrom).length()) return [] def pickBall(self): hits = self.rayCollision() for hit in hits: hit_node = hit.getNode() if 'ball' in hit_node.getName(): self.picked.add(hit_node.getName()) NodePath(hit_node.getChild(0).getChild(0)).setColor(HIGHLIGHT) def releaseBall(self): hits = self.rayCollision() if hits: foundBall = False for picked in hits: hit_node = picked.getNode() if 'ball' in hit_node.getName(): foundBall = True x, y, z = picked.getHitPos() bodies = self.world.getRigidBodies() for elem in bodies: name = elem.getName() if name in self.picked: # apply some physics node = NodePath(elem.getChild(0).getChild(0)) node_x, node_y, node_z = node.getPos(render) ix = (x - node_x) iy = (y - node_y) dir = atan2(iy, ix) dx, dy = SPEED * cos(dir), SPEED * sin(dir) elem.applyCentralImpulse(LVector3(dx, dy, z)) node.setColor(self.ballColors[elem.getName()]) if foundBall: self.picked = set([]) def rotateCube(self, hpr = (0, 0, 0)): # h, p, r = z, x, y # FIXME: something completely wrong goes here # need some time to figure it out planes = render.findAllMatches('**/plane*') for plane in planes: oldParent = plane.getParent() pivot = render.attachNewNode('pivot') pivot.setPos(render, 0, 0, 5) plane.wrtReparentTo(pivot) pivot.setHpr(render, Vec3(hpr)) if oldParent.getName() != 'render': oldParent.removeNode() def shakeBalls(self): balls = filter(lambda x: 'ball' in x.getName(), self.world.getRigidBodies()) for ball in balls: dx = uniform(-SHAKE_SPEED, SHAKE_SPEED) dy = uniform(-SHAKE_SPEED, SHAKE_SPEED) dz = uniform(-SHAKE_SPEED, SHAKE_SPEED) ball.applyCentralImpulse(LVector3(dx, dy, dz)) def updateBallsCounter(self, num): self.ballCnt += num self.title.setText('%d balls' % (self.ballCnt)) def addRandomBall(self): planes = render.findAllMatches('**/plane*') x, y, z = zip(*[tuple(plane.getPos()) for plane in planes]) xPos = uniform(min(x), max(x)) yPos = uniform(min(y), max(y)) zPos = uniform(min(z), max(z)) self.makeBall(self.ballCnt, (xPos, yPos, zPos)) self.updateBallsCounter(1) def rmBall(self): if self.ballCnt != 0: ball = render.find('**/ball_' + str(self.ballCnt - 1)) self.ballColors.pop(ball.getName()) ball.removeNode() self.updateBallsCounter(-1) def makePlane(self, num, norm, pos, hpr): shape = BulletPlaneShape(norm, 0) node = BulletRigidBodyNode('plane_' + str(num)) node.addShape(shape) physics = render.attachNewNode(node) physics.setPos(*pos) self.world.attachRigidBody(node) model = loader.loadModel('models/square') model.setScale(10, 10, 10) model.setHpr(*hpr) model.setTransparency(TransparencyAttrib.MAlpha) model.setColor(1, 1, 1, 0.25) model.reparentTo(physics) def makeColor(self): return (random(), random(), random(), 1) def makeBall(self, num, pos = (0, 0, 0)): shape = BulletSphereShape(0.5) node = BulletRigidBodyNode('ball_' + str(num)) node.setMass(1.0) node.setRestitution(.9) node.setDeactivationEnabled(False) node.addShape(shape) physics = render.attachNewNode(node) physics.setPos(*pos) self.world.attachRigidBody(node) model = loader.loadModel('models/ball') color = self.makeColor() model.setColor(color) self.ballColors['ball_' + str(num)] = color model.reparentTo(physics)
class PhysicsManager(object): def __init__(self, gravity=(0,0,-9.81) ): self.world = BulletWorld() self.world.setGravity(Vec3(gravity) ) self.addShape = {} self.addShape['plane'] = self.__addTriangleMesh self.addShape['sphere'] = self.__addSphere self.addShape['box'] = self.__addBox self.addShape['cylinder'] = self.__addCylinder self.addShape['capsule'] = self.__addCapsule self.addShape['cone'] = self.__addCone self.addShape['hull'] = self.__addConvexHull self.addShape['trimesh'] = self.__addTriangleMesh self.addShape['compound'] = self.__addCompound self.num_rigidBodies = 0 self.num_ghosts = 0 def getRigidBodyDefaultProps(self): props = {} props['mass'] = .0 props['friction'] = .5 props['restitution'] = .0 props['adamping'] = .0 props['ldamping'] = .0 props['asleep'] = .08 props['lsleep'] = 1. props['deactivation'] = True props['kinematic'] = False return props def getRigidBody(self, name=None): self.num_rigidBodies+=1 return BulletRigidBodyNode(name or 'rigidbody'+str(self.num_rigidBodies) ) def createRigidBody(self, shapetype, model, props={}, name=None): body = self.getRigidBody(name) self.addShape[shapetype](body, model) props = dict(self.getRigidBodyDefaultProps().items() + props.items() ) self.setBodyProps(body, props) self.world.attachRigidBody( body ) return body def getGhost(self, name=None): self.num_ghosts+=1 return BulletGhostNode(name or 'ghost'+str(self.num_ghosts) ) def createGhost(self, shapetype, size, name=None): ghost = self.getGhost(name) self.addShape[shapetype](ghost, size) self.world.attachGhost(ghost) return ghost def attachRigidBody(self, body, props): self.setBodyProps(body, props) self.world.attachRigidBody(body) def __addCompound(self, body, model): self.createCompound(model,body) def __addSphere(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): size = self.getSize(model) shape = BulletSphereShape( max(size)/2 ) body.addShape(shape, TransformState.makePosHprScale(pos,hpr,scale) ) def __addBox(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): size = self.getSize(model) shape = BulletBoxShape( size/2 ) body.addShape(shape, TransformState.makePosHprScale(pos,hpr,scale) ) def __addCylinder(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): size = self.getSize(model) shape = BulletCylinderShape(max(size.x,size.y)/2, size.z, ZUp) body.addShape( shape, TransformState.makePosHprScale(pos,hpr,scale) ) def __addCapsule(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): size = self.getSize(model) diam = max(size.x,size.y) shape = BulletCapsuleShape(diam/2, size.z-diam, ZUp) body.addShape( shape, TransformState.makePosHprScale(pos,hpr,scale) ) def __addCone(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): size = self.getSize(model) shape = BulletConeShape(max(size.x,size.y)/2, size.z, ZUp) body.addShape( shape, TransformState.makePosHprScale(pos,hpr,scale) ) def __addConvexHull(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): def one(): geom = model.node().getGeom(0) shape = BulletConvexHullShape() shape.addGeom(geom) body.addShape( shape, TransformState.makePosHprScale(pos,hpr,scale) ) return [] children = model.findAllMatches('**/+GeomNode') or one() model.flattenLight() for piece in children: shape = BulletConvexHullShape() geom = piece.node().getGeom(0) shape.addGeom(geom) body.addShape( shape, TransformState.makePosHprScale(pos,hpr,scale) ) def __addTriangleMesh(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1), dynamic=True): mesh = BulletTriangleMesh() def one(): geom = model.node().getGeom(0) mesh.addGeom(geom) return [] children = model.findAllMatches('**/+GeomNode') or one() model.flattenLight() for piece in children: geom = piece.node().getGeom(0) mesh.addGeom(geom) shape = BulletTriangleMeshShape(mesh, dynamic=dynamic ) body.addShape( shape, TransformState.makePosHprScale(pos,hpr,scale) ) # def __addPlane(self, body, model, pos=(0,0,0), hpr=(0,0,0), scale=(1,1,1) ): # shape = BulletPlaneShape(Vec3(0, 0, 1), 1) # node = BulletRigidBodyNode(model.getName()) # node.addShape(shape) # np = render.attachNewNode(node) # np.setPos( pos ) # model.reparentTo(np) # self.world.attachRigidBody(node) def setBodyProps(self, body, props): body.setMass( props['mass'] ) body.setFriction( props['friction'] ) body.setRestitution( props['restitution'] ) body.setAngularDamping( props['adamping'] ) body.setLinearDamping( props['ldamping'] ) body.setAngularSleepThreshold( props['asleep'] ) body.setLinearSleepThreshold( props['lsleep'] ) if not props['deactivation']: body.setDeactivationEnabled( False ) body.setKinematic( props['kinematic'] ) def characterController(self, name, height, mass, radius, step_height): shape = BulletCapsuleShape(radius, height - 2*radius, ZUp) body = BulletRigidBodyNode(name) body.setMass(mass) body.addShape(shape) return BulletCharacterControllerNode(shape, step_height, name) def debug(self): from panda3d.bullet import BulletDebugNode debugNP = render.attachNewNode(BulletDebugNode('Debug') ) #debugNP.show() debugNP.node().showWireframe(True) debugNP.node().showConstraints(True) debugNP.node().showBoundingBoxes(False) debugNP.node().showNormals(False) self.world.setDebugNode(debugNP.node()) return debugNP def getSize(self,model): hpr = model.getHpr() model.setHpr(0,0,0) minLimit, maxLimit = model.getTightBounds() model.setHpr(hpr) return maxLimit - minLimit def setShape(self, model, body): #To Re-Do name = model.getName() pos,hpr,scale = model.getPos(), model.getHpr(), model.getScale() shapetype = re.findall("[-a-z]+",name.lower())[0].split('-') shapetype = filter(None,shapetype) if shapetype[0] in self.addShape.keys(): self.addShape[shapetype[0]](body,model,pos,hpr,scale) not '-' in name or model.remove() def createCompound(self, model, body): children = model.find('**/*').getChildren() or model.getChildren() [self.setShape(child,body) for child in children] def cleanup(self): rbs = self.world.getRigidBodies() for body in rbs: self.world.removeRigidBody(body) print 'PhysicsManager: removing rigidbody' def destroy(self): self.cleanup() del self.addShape del self.world print 'destroy PhysicsManager' def __del__(self): print 'delete PhysicsManager'