| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- #!/usr/bin/env python
- # Author: Shao Zhang, Phil Saltzman
- # Last Updated: 2015-03-13
- #
- # This tutorial shows how to detect and respond to collisions. It uses solids
- # create in code and the egg files, how to set up collision masks, a traverser,
- # and a handler, how to detect collisions, and how to dispatch function based
- # on the collisions. All of this is put together to simulate a labyrinth-style
- # game
- from direct.showbase.ShowBase import ShowBase
- from panda3d.core import CollisionTraverser, CollisionNode
- from panda3d.core import CollisionHandlerQueue, CollisionRay
- from panda3d.core import Material, LRotationf, NodePath
- from panda3d.core import AmbientLight, DirectionalLight
- from panda3d.core import TextNode
- from panda3d.core import LVector3, BitMask32
- from direct.gui.OnscreenText import OnscreenText
- from direct.interval.MetaInterval import Sequence, Parallel
- from direct.interval.LerpInterval import LerpFunc
- from direct.interval.FunctionInterval import Func, Wait
- from direct.task.Task import Task
- import sys
- # Some constants for the program
- ACCEL = 70 # Acceleration in ft/sec/sec
- MAX_SPEED = 5 # Max speed in ft/sec
- MAX_SPEED_SQ = MAX_SPEED ** 2 # Squared to make it easier to use lengthSquared
- # Instead of length
- class BallInMazeDemo(ShowBase):
- def __init__(self):
- # Initialize the ShowBase class from which we inherit, which will
- # create a window and set up everything we need for rendering into it.
- ShowBase.__init__(self)
- # This code puts the standard title and instruction text on screen
- self.title = \
- OnscreenText(text="Panda3D: Tutorial - Collision Detection",
- parent=base.a2dBottomRight, align=TextNode.ARight,
- fg=(1, 1, 1, 1), pos=(-0.1, 0.1), scale=.08,
- shadow=(0, 0, 0, 0.5))
- self.instructions = \
- OnscreenText(text="Mouse pointer tilts the board",
- parent=base.a2dTopLeft, align=TextNode.ALeft,
- pos=(0.05, -0.08), fg=(1, 1, 1, 1), scale=.06,
- shadow=(0, 0, 0, 0.5))
- self.accept("escape", sys.exit) # Escape quits
- # Disable default mouse-based camera control. This is a method on the
- # ShowBase class from which we inherit.
- self.disableMouse()
- camera.setPosHpr(0, 0, 25, 0, -90, 0) # Place the camera
- # Load the maze and place it in the scene
- self.maze = loader.loadModel("models/maze")
- self.maze.reparentTo(render)
- # Most times, you want collisions to be tested against invisible geometry
- # rather than every polygon. This is because testing against every polygon
- # in the scene is usually too slow. You can have simplified or approximate
- # geometry for the solids and still get good results.
- #
- # Sometimes you'll want to create and position your own collision solids in
- # code, but it's often easier to have them built automatically. This can be
- # done by adding special tags into an egg file. Check maze.egg and ball.egg
- # and look for lines starting with <Collide>. The part is brackets tells
- # Panda exactly what to do. Polyset means to use the polygons in that group
- # as solids, while Sphere tells panda to make a collision sphere around them
- # Keep means to keep the polygons in the group as visable geometry (good
- # for the ball, not for the triggers), and descend means to make sure that
- # the settings are applied to any subgroups.
- #
- # Once we have the collision tags in the models, we can get to them using
- # NodePath's find command
- # Find the collision node named wall_collide
- self.walls = self.maze.find("**/wall_collide")
- # Collision objects are sorted using BitMasks. BitMasks are ordinary numbers
- # with extra methods for working with them as binary bits. Every collision
- # solid has both a from mask and an into mask. Before Panda tests two
- # objects, it checks to make sure that the from and into collision masks
- # have at least one bit in common. That way things that shouldn't interact
- # won't. Normal model nodes have collision masks as well. By default they
- # are set to bit 20. If you want to collide against actual visable polygons,
- # set a from collide mask to include bit 20
- #
- # For this example, we will make everything we want the ball to collide with
- # include bit 0
- self.walls.node().setIntoCollideMask(BitMask32.bit(0))
- # CollisionNodes are usually invisible but can be shown. Uncomment the next
- # line to see the collision walls
- #self.walls.show()
- # We will now find the triggers for the holes and set their masks to 0 as
- # well. We also set their names to make them easier to identify during
- # collisions
- self.loseTriggers = []
- for i in range(6):
- trigger = self.maze.find("**/hole_collide" + str(i))
- trigger.node().setIntoCollideMask(BitMask32.bit(0))
- trigger.node().setName("loseTrigger")
- self.loseTriggers.append(trigger)
- # Uncomment this line to see the triggers
- # trigger.show()
- # Ground_collide is a single polygon on the same plane as the ground in the
- # maze. We will use a ray to collide with it so that we will know exactly
- # what height to put the ball at every frame. Since this is not something
- # that we want the ball itself to collide with, it has a different
- # bitmask.
- self.mazeGround = self.maze.find("**/ground_collide")
- self.mazeGround.node().setIntoCollideMask(BitMask32.bit(1))
- # Load the ball and attach it to the scene
- # It is on a root dummy node so that we can rotate the ball itself without
- # rotating the ray that will be attached to it
- self.ballRoot = render.attachNewNode("ballRoot")
- self.ball = loader.loadModel("models/ball")
- self.ball.reparentTo(self.ballRoot)
- # Find the collison sphere for the ball which was created in the egg file
- # Notice that it has a from collision mask of bit 0, and an into collison
- # mask of no bits. This means that the ball can only cause collisions, not
- # be collided into
- self.ballSphere = self.ball.find("**/ball")
- self.ballSphere.node().setFromCollideMask(BitMask32.bit(0))
- self.ballSphere.node().setIntoCollideMask(BitMask32.allOff())
- # No we create a ray to start above the ball and cast down. This is to
- # Determine the height the ball should be at and the angle the floor is
- # tilting. We could have used the sphere around the ball itself, but it
- # would not be as reliable
- self.ballGroundRay = CollisionRay() # Create the ray
- self.ballGroundRay.setOrigin(0, 0, 10) # Set its origin
- self.ballGroundRay.setDirection(0, 0, -1) # And its direction
- # Collision solids go in CollisionNode
- # Create and name the node
- self.ballGroundCol = CollisionNode('groundRay')
- self.ballGroundCol.addSolid(self.ballGroundRay) # Add the ray
- self.ballGroundCol.setFromCollideMask(
- BitMask32.bit(1)) # Set its bitmasks
- self.ballGroundCol.setIntoCollideMask(BitMask32.allOff())
- # Attach the node to the ballRoot so that the ray is relative to the ball
- # (it will always be 10 feet over the ball and point down)
- self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol)
- # Uncomment this line to see the ray
- #self.ballGroundColNp.show()
- # Finally, we create a CollisionTraverser. CollisionTraversers are what
- # do the job of walking the scene graph and calculating collisions.
- # For a traverser to actually do collisions, you need to call
- # traverser.traverse() on a part of the scene. Fortunately, ShowBase
- # has a task that does this for the entire scene once a frame. By
- # assigning it to self.cTrav, we designate that this is the one that
- # it should call traverse() on each frame.
- self.cTrav = CollisionTraverser()
- # Collision traversers tell collision handlers about collisions, and then
- # the handler decides what to do with the information. We are using a
- # CollisionHandlerQueue, which simply creates a list of all of the
- # collisions in a given pass. There are more sophisticated handlers like
- # one that sends events and another that tries to keep collided objects
- # apart, but the results are often better with a simple queue
- self.cHandler = CollisionHandlerQueue()
- # Now we add the collision nodes that can create a collision to the
- # traverser. The traverser will compare these to all others nodes in the
- # scene. There is a limit of 32 CollisionNodes per traverser
- # We add the collider, and the handler to use as a pair
- self.cTrav.addCollider(self.ballSphere, self.cHandler)
- self.cTrav.addCollider(self.ballGroundColNp, self.cHandler)
- # Collision traversers have a built in tool to help visualize collisions.
- # Uncomment the next line to see it.
- #self.cTrav.showCollisions(render)
- # This section deals with lighting for the ball. Only the ball was lit
- # because the maze has static lighting pregenerated by the modeler
- ambientLight = AmbientLight("ambientLight")
- ambientLight.setColor((.55, .55, .55, 1))
- directionalLight = DirectionalLight("directionalLight")
- directionalLight.setDirection(LVector3(0, 0, -1))
- directionalLight.setColor((0.375, 0.375, 0.375, 1))
- directionalLight.setSpecularColor((1, 1, 1, 1))
- self.ballRoot.setLight(render.attachNewNode(ambientLight))
- self.ballRoot.setLight(render.attachNewNode(directionalLight))
- # This section deals with adding a specular highlight to the ball to make
- # it look shiny. Normally, this is specified in the .egg file.
- m = Material()
- m.setSpecular((1, 1, 1, 1))
- m.setShininess(96)
- self.ball.setMaterial(m, 1)
- # Finally, we call start for more initialization
- self.start()
- def start(self):
- # The maze model also has a locator in it for where to start the ball
- # To access it we use the find command
- startPos = self.maze.find("**/start").getPos()
- # Set the ball in the starting position
- self.ballRoot.setPos(startPos)
- self.ballV = LVector3(0, 0, 0) # Initial velocity is 0
- self.accelV = LVector3(0, 0, 0) # Initial acceleration is 0
- # Create the movement task, but first make sure it is not already
- # running
- taskMgr.remove("rollTask")
- self.mainLoop = taskMgr.add(self.rollTask, "rollTask")
- # This function handles the collision between the ray and the ground
- # Information about the interaction is passed in colEntry
- def groundCollideHandler(self, colEntry):
- # Set the ball to the appropriate Z value for it to be exactly on the
- # ground
- newZ = colEntry.getSurfacePoint(render).getZ()
- self.ballRoot.setZ(newZ + .4)
- # Find the acceleration direction. First the surface normal is crossed with
- # the up vector to get a vector perpendicular to the slope
- norm = colEntry.getSurfaceNormal(render)
- accelSide = norm.cross(LVector3.up())
- # Then that vector is crossed with the surface normal to get a vector that
- # points down the slope. By getting the acceleration in 3D like this rather
- # than in 2D, we reduce the amount of error per-frame, reducing jitter
- self.accelV = norm.cross(accelSide)
- # This function handles the collision between the ball and a wall
- def wallCollideHandler(self, colEntry):
- # First we calculate some numbers we need to do a reflection
- norm = colEntry.getSurfaceNormal(render) * -1 # The normal of the wall
- curSpeed = self.ballV.length() # The current speed
- inVec = self.ballV / curSpeed # The direction of travel
- velAngle = norm.dot(inVec) # Angle of incidance
- hitDir = colEntry.getSurfacePoint(render) - self.ballRoot.getPos()
- hitDir.normalize()
- # The angle between the ball and the normal
- hitAngle = norm.dot(hitDir)
- # Ignore the collision if the ball is either moving away from the wall
- # already (so that we don't accidentally send it back into the wall)
- # and ignore it if the collision isn't dead-on (to avoid getting caught on
- # corners)
- if velAngle > 0 and hitAngle > .995:
- # Standard reflection equation
- reflectVec = (norm * norm.dot(inVec * -1) * 2) + inVec
- # This makes the velocity half of what it was if the hit was dead-on
- # and nearly exactly what it was if this is a glancing blow
- self.ballV = reflectVec * (curSpeed * (((1 - velAngle) * .5) + .5))
- # Since we have a collision, the ball is already a little bit buried in
- # the wall. This calculates a vector needed to move it so that it is
- # exactly touching the wall
- disp = (colEntry.getSurfacePoint(render) -
- colEntry.getInteriorPoint(render))
- newPos = self.ballRoot.getPos() + disp
- self.ballRoot.setPos(newPos)
- # This is the task that deals with making everything interactive
- def rollTask(self, task):
- # Standard technique for finding the amount of time since the last
- # frame
- dt = base.clock.dt
- # If dt is large, then there has been a # hiccup that could cause the ball
- # to leave the field if this functions runs, so ignore the frame
- if dt > .2:
- return Task.cont
- # The collision handler collects the collisions. We dispatch which function
- # to handle the collision based on the name of what was collided into
- for i in range(self.cHandler.getNumEntries()):
- entry = self.cHandler.getEntry(i)
- name = entry.getIntoNode().getName()
- if name == "wall_collide":
- self.wallCollideHandler(entry)
- elif name == "ground_collide":
- self.groundCollideHandler(entry)
- elif name == "loseTrigger":
- self.loseGame(entry)
- # Read the mouse position and tilt the maze accordingly
- if base.mouseWatcherNode.hasMouse():
- mpos = base.mouseWatcherNode.getMouse() # get the mouse position
- self.maze.setP(mpos.getY() * -10)
- self.maze.setR(mpos.getX() * 10)
- # Finally, we move the ball
- # Update the velocity based on acceleration
- self.ballV += self.accelV * dt * ACCEL
- # Clamp the velocity to the maximum speed
- if self.ballV.lengthSquared() > MAX_SPEED_SQ:
- self.ballV.normalize()
- self.ballV *= MAX_SPEED
- # Update the position based on the velocity
- self.ballRoot.setPos(self.ballRoot.getPos() + (self.ballV * dt))
- # This block of code rotates the ball. It uses something called a quaternion
- # to rotate the ball around an arbitrary axis. That axis perpendicular to
- # the balls rotation, and the amount has to do with the size of the ball
- # This is multiplied on the previous rotation to incrimentally turn it.
- prevRot = LRotationf(self.ball.getQuat())
- axis = LVector3.up().cross(self.ballV)
- newRot = LRotationf(axis, 45.5 * dt * self.ballV.length())
- self.ball.setQuat(prevRot * newRot)
- return Task.cont # Continue the task indefinitely
- # If the ball hits a hole trigger, then it should fall in the hole.
- # This is faked rather than dealing with the actual physics of it.
- def loseGame(self, entry):
- # The triggers are set up so that the center of the ball should move to the
- # collision point to be in the hole
- toPos = entry.getInteriorPoint(render)
- taskMgr.remove('rollTask') # Stop the maze task
- # Move the ball into the hole over a short sequence of time. Then wait a
- # second and call start to reset the game
- Sequence(
- Parallel(
- LerpFunc(self.ballRoot.setX, fromData=self.ballRoot.getX(),
- toData=toPos.getX(), duration=.1),
- LerpFunc(self.ballRoot.setY, fromData=self.ballRoot.getY(),
- toData=toPos.getY(), duration=.1),
- LerpFunc(self.ballRoot.setZ, fromData=self.ballRoot.getZ(),
- toData=self.ballRoot.getZ() - .9, duration=.2)),
- Wait(1),
- Func(self.start)).start()
- # Finally, create an instance of our class and start 3d rendering
- demo = BallInMazeDemo()
- demo.run()
|