====== Lab 4 - Events, Collision and Movement ======
==== Before You Start ====
* Download and unzip the [[http://www.ru.is/kennarar/hannes/classes/ve2008/Lab4Assets.zip|Lab 4 Asset File]] into your working directory
==== Making a functional trap door ====
{{public:t-vien-07-1:ve-lab4-screenshot.jpg|}}
- **Creating a New Scene:**\\ Create a new program called "factory.py" and include the following imports at the beginning of the file:
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *
from direct.interval.IntervalGlobal import *
In your code, you should disable the mouse and then place the **camera** at **(1,-8,5)** with a pitch of **-25 degrees**. Then define a class called **World** that derives from **DirectObject** (this allows **World** to receive events). In the constructor of the **World** class load and display two models: **./Models/factoryfloor.egg** and **./Models/box.egg**. Position the box at **(1,0,2)**, scale it down to **30%** and give it a heading of **45 degrees**. Make sure you can see both the floor and the box.
- **Opening and Closing the Door:**\\ The floor model contains a **door** sub-object. Acquire a pathnode to it, using ''.find'', and call it **door**. Create two **[[http://www.panda3d.org/wiki/index.php/Lerp_Intervals|LerpPosInterval]]**s for the **door**, one to open it by moving it to location **(-1,0,0)** and one to close it by moving it to location **(1,0,0)**. You can play with the duration but **2.0 seconds** is a good place to start. Make the **World** accept a 'space' (spacebar key) event and call the associated event handler **self.door_handler**. In that handler, either start the opening interval for the door or the closing interval, depending on whether it's open or closed already (you'll need a new boolean member variable to keep track of that). Make sure you can now open and close the door with the spacebar.\\
- **Using blendType and name to Improve Door Movement:**\\ When you create the opening and closing intervals, give the constructor attribute **blendType** a value of **"easeInOut"** for a bit more realistic trap door motion (you can play with the other values **"easeIn"** and **"easeOut"** as well). Also, give the attribute **name** the same name for both intervals - this ensures that the first interval gets cancelled when the second one gets starts.
- **Creating Collision Solids:**\\ Attach a new **CollisionNode** to the box pathnode and give it the name **"box"** (you pass the name into the constructor of the CollisionNode). To the node of this new pathnode, add a new collision solid of the type sphere and give it a radius of **1.5**.
# HINT - Creating and attaching a CollisionSphere
nodepath = object.attachNewNode(CollisionNode())
nodepath.node().addSolid(CollisionSphere(,,,)) # You typically keep it at (0,0,0)
Similarly, attach a new **CollisionNode** to the door pathnode and call it **door**. This time however, add a **CollisionPolygon** to the node of the new pathnode. The verticies of this polygon are **(-1,-1,0)**, **(1,-1,0)**, **(1,1,0)** and **(-1,1,0)** in that order.
# HINT - The CollisionPolygon
nodepath.node().addSolid(CollisionPolygon(Point3(x1,y1,z1),Point3(x2,y2,z2),Point3(x3,y3,z3),Point3(x4,y4,z4)))
You can call **show()** on your new collision node nodepaths to see your collision solids as semi-transparent objects when you view your scene. Verify that your solids are in the right place and then remove the calls to show().
- **Making the Box Move:**\\ Create a new **LerpPosInterval** for the box and have it move to the location **(1,0,-1)**. Make the **World** accept a 'mouse1' event and call a method that starts this interval playing. A duration of **3.0 seconds** for this movement seems good and you might want to try different blend types. Verify that you can make the box move by clicking the left mouse button. Notice that the box goes right through the door.
- **Making a Collision Happen:**\\ In the **World** constructor, create a new instance of a **CollisionHandlerEvent** and add the event pattern shown below:
collhandler = CollisionHandlerEvent()
collhandler.addInPattern('%fn-into-%in') # The event pattern
Then initialize the global collision traverser as follows:
base.cTrav = CollisionTraverser('world traverser')
and then add the box collision nodepath as a new collider and associate it with the collision handler you just created.
#HINT - Adding a new collider to a traverser
base.cTrav.addCollider(, )
Make the world accept an event called "box-into-door" and call a method named **self.collision** where you can put things that should happen when a collision with this event name occurs. Notice that this method receives a parameter called **collEntry** which contains information about the collision event. The following is a simple collision event receiver:
def collision(self, collEntry):
print collEntry
In addition to printing out some information like is done in this example, you should call the **pause()** method on the LerpPosInterval associated with the box object. This will stop the progress of the box movement. Verify that the box stops now when the door is closed but goes all the way down when the door is open.
- **Adding Sounds:**\\ To add sounds to this scene, you can use **thud.wav** for when the box hits the door and **close.wav** for when the door slams shut. You don't have to localize these sounds, just load them in like this:
thudsound = loader.loadSfx("thud.wav")
closesound = loader.loadSfx("close.wav")
It should be pretty clear where to put the **thudsound**, but the **closesound** needs to be wrapped into a **[[http://www.panda3d.org/wiki/index.php/Sound_Intervals|SoundInterval]]** object and then sequenced with the closing door LerpPosInterval so that it happens at the end of the movement.
# HINT - Sequencing intervals
newinterval = Sequence(, , ... )
- **A Physics Based Version**:\\ In the version of the factory above, you were faking movement in a physical world by using interpolators to move objects around. You had to do everything by hand, and even so, not everything is looking right. For example, if the box hits the edge of the door, the box stops, even if physics tell us that the box should just tip over and fall off the edge. In fact, if it lands on the door, it doesn't even move with the door. All of these details can be taken care of with a good simulation of physics and rigid body dynamics. One such engine is the [[http://www.ode.org/|Open Dynamics Engine (ODE)]]. You can incorporate this engine into your Panda environments using the [[http://pyode.sourceforge.net/|open-source PyODE module]]. Start by downloading and installing this module (remember to get the version for Python 2.4 to match the version that Panda uses). Now all you have to do to use ODE in your Panda program is to add **''import ode''**. Follow these steps to redo the factory scene with ODE, simply by modifying the code you have so far. (This code is based on the excellent PyODE tutorials as well as on several Panda 3D forum posts) \\
- The simulation actually takes place in two separate invisible simulation environments, one for the physics (called ''world'') and another for collision handling (called ''space''). So the first thing you do (you can do all of this in the constructor of your **World** class) is to instantiate and set up these two environments:
self.world = ode.World() # Create a physics/dynamics environment
self.world.setGravity((0,0,-9.81)) # Add earth gravity in the negative z-dimension
self.space = ode.Space() # Create a collision environment
For each object you want to be part of the physics simulation, you have to create a physical body. Here is how the physical body of the box is created (note that you still need your original Panda model of the box, that's the //visible// version of the box):
self.boxbody = ode.Body(self.world) # Instantiate a physical body in the physical environment
self.boxbody.size = (0.3, 0.3, 0.3) # Store the size of the body
self.boxbody.setPosition((1,0,2)) # Place the body in the same location as in the Panda environment
self.boxbody.addForce((0,0,1000)) # This is just for fun: give the box an upward kick at the beginning
self.boxmass = ode.Mass() # Define a physical mass for the box
self.boxmass.setBox(500, *self.boxbody.size) # The mass is defined as density and size
self.boxbody.setMass(self.boxmass) # Add the mass to the physical body of the box
Now that you have a physical representation of the box, you still need to make an ODE collision solid for it and put into the collision environment. This is how:
self.boxsolid = ode.GeomBox(self.space, self.boxbody.size)
self.boxsolid.setBody(self.boxbody)
To see the effect of gravity (and the kicking force) on the box, you will now have to start a task that copies the position of the physical box to the position of the visual box at every frame, and advances the physics simulation world by one step:
taskMgr.add(self.simulate, 'ODE Simulation')
def simulate(self, task):
x,y,z = self.boxbody.getPosition() # Get position of the simulated physical box
self.box.setPos(Vec3(x,y,z)) # Set the position of the visual box
self.world.step(0.04) # Increment time in the physical simulation
return Task.cont # Keep this task running forever
Test this and make sure gravity is working as expected. Make sure to comment out the LerpPosInterval for the box since you no longer need it! You should also change the left mouse button handler so that it sets the position of both the visual box and the physical box back to the original position (to maintain similar functionality as before). You will also want to set the linear velocity of the box to 0 like this **''self.boxbody.setLinearVel( (0,0,0) )''**, otherwise the box will keep going faster and faster\\
- To introduce collision with the door, you will need to create both a physical body for the door and a ODE collision solid for the door, just like you did for the box. Try using **size** of **(2,2,0.1)** and **density** of **1000**. Don't forget to update the door's visual position based on the physical position in the simulation task! In fact, this time, let's update both the position and the rotation of both objects. The problem is that ODE uses a matrix representation for rotation, so it needs to be converted into a format that Panda understands, which happens to be the quaternion format. Here is a function that will set the position and rotation of a Panda model, given position and rotation in ODE format (just add this as a utility function near the top of your program):
def set_model_odeposrot(model, pos, rot):
gquat = Quat ()
gquat.setFromMatrix (Mat3 (*rot))
gpos = VBase3 (pos [0], pos [1], pos [2])
model.setPosQuat (gpos, gquat)
Your **''simulate''** task should now look like this:
def simulate(self, task):
set_model_odeposrot(self.box, self.boxbody.getPosition(), self.boxbody.getRotation())
set_model_odeposrot(self.door, self.doorbody.getPosition(), self.doorbody.getRotation())
self.world.step(0.04)
return Task.cont
You may notice that the door naturally falls down along with the box since there is nothing holding it up! We'll fix that next. \\
- You need to attach the door to the environment so that it doesn't fall. In fact, the door is attached to the environment with a so-called **slider joint** because it can slide from side-to-side. ODE provides a number of [[http://opende.sourceforge.net/mediawiki-1.6.10/index.php/Manual_%28Joint_Types_and_Functions%29|joints]] for attaching objects to each other or to the environment. You create and attach the slider joint like this:
self.doorslider = ode.SliderJoint(self.world) # Instantiating a new slider joint
self.doorslider.attach(self.doorbody, ode.environment) # Using the joint to attach door body to environment
self.doorslider.setAxis((1,0,0)) # Slides along the x-axis
self.doorslider.setParam(ode.paramFMax, 100) # Sets the force of a motor attached to this joint
The last line here actually attaches a little motor to the joint, which we can give a certain velocity whenever we want to move the joint autonomously. Try replacing the LerpPosInterval calls for opening and closing the door, with calls like this: **''self.doorslider.setParam(ode.paramVel, 0.3)''**, where **0.3** is the velocity (can also be negative to go in the other direction). \\
- Finally, let's add actual ODE collision handling. Comment out all of the previous collision handling code, since this will completely replace it. In **''World''** constructor, create the following member variable:
self.contactgroup = ode.JointGroup() # Holds a set of joints
Inside the **''simulate''** task, you now have to ask the collision environment to check for near collisions like this:
self.space.collide((self.world, self.contactgroup), self.near_callback)
Where you then have a new member function called **''near_callback''** that looks like this:
def near_callback(self, args, solid1, solid2):
contacts = ode.collide(solid1, solid2) # Returns the actual collisions between two solids
world, contactgroup = args
for c in contacts:
c.setBounce(0.2) # How much bounce should happen from this collision
c.setMu(5000) # How much friction there should be between the solids
j = ode.ContactJoint(world, contactgroup, c) # A temporary joint joins the solids together
j.attach(solid1.getBody(), solid2.getBody())
The very last thing to do is to call **''self.contactgroup.empty()''** right after you have called **''self.world.step(0.04)''** in order to start looking for fresh collisions after each iteration. Test to see if everything is working. \\
- Play with various simulation values to see what effect they have on the object behavior. You could also try adding more boxes and have them pile on top of each other!
{{:public:t-vien-07-1:lab4solution.zip|.}}