Title pretty much says all. But in detail: This example demonstrates using Panda physics and collisions to make some smiley balls bounce upon a ground plane.
I’ve been learning about Panda physics, so I wrote this to help me test a few things.
I also implemented cheesy shadow blob ground shadows, just so I could judge how far the balls were bouncing.
# by FenrirWolf (David Grace) 7/2009
# Free for use by all under the Panda license
# Purpose: Demonstrates how to use Panda physics with a collisions to generate spheres which bounce on a ground
# plane.
from pandac.PandaModules import loadPrcFileData
loadPrcFileData('', '''
show-frame-rate-meter #t
//want-tk #t
//want-directtools #t''')
from pandac.PandaModules import * # lazy git
import direct.directbase.DirectStart
from random import random
# Set up the collision traverser. If we bind it to base.cTrav, then Panda will handle
# management of this traverser (for example, by calling traverse() automatically for us once per frame)
base.cTrav = CollisionTraverser()
# Turn on particles. (Required to use Panda physics)
base.enableParticles()
# Turn on the traverser debugger if you want to see the collisions
#base.cTrav.showCollisions (base.render)
# Having trouble figuring out what's going on with messages? Turn this on.
#messenger.toggleVerbose()
# Now let's set up some collision bits for our masks
groundBit = 1
smileyBit = 2
# First, we build a card to represent the ground
cm = CardMaker('ground-card')
cm.setFrame(-60, 60, -60, 60)
card = render.attachNewNode(cm.generate())
card.lookAt (0, 0, -1) # align upright
tex = loader.loadTexture('maps/envir-ground.jpg')
card.setTexture(tex)
# Then we build a collisionNode which has a plane solid which will be the ground's collision
# representation
groundColNode = card.attachNewNode (CollisionNode('ground-cnode'))
groundColPlane = CollisionPlane (Plane (Vec3(0, -1, 0), Point3(0, 0, 0)))
groundColNode.node().addSolid (groundColPlane)
# Now, set the ground to the ground mask
groundColNode.setCollideMask (BitMask32().bit (groundBit))
# Why aren't we adding a collider? There is no need to tell the collision traverser about this
# collisionNode, as it will automatically be an Into object during traversal.
# We're going to keep a list of our smileyActorNodes
smileyActors = []
# How many smileys?
maxSmileys = 25
# Our smiley base mass, roughly in kg
baseMass = 100.0
# Let's have some fun and have a PLOP! sound ready for our collisions!
plopSfx = loader.loadSfx ('audio/sfx/GUI_click.wav')
# Create a shadow card for us to use with the smileys
cm = CardMaker ('shadow-card')
cm.setFrame (-1, 1, -1, 1)
shadowCard = render.attachNewNode (cm.generate())
shadowCard.lookAt (0, 0, -1) # align upright
tex = loader.loadTexture ('maps/soft_iris.rgb')
ts = TextureStage ('blended-shadow')
# Using a little trick here... Since the only thing resembling a shadow blob kind of texture is the
# soft_iris image that comes with Panda. Problem is, it's opposite of what we want (ie: ring vs blob)
# So we just invert the alpha values stored in this texture and now it's a blob
ts.setCombineAlpha (TextureStage.CMModulate, TextureStage.CSPrevious, TextureStage.COSrcAlpha,
TextureStage.CSTexture, TextureStage.COOneMinusSrcAlpha)
shadowCard.setTexture (ts, tex)
shadowCard.setTransparency(TransparencyAttrib.MAlpha)
def removeForce (smileyActor, task):
'''Removes a temporary nudge force applied to the smiley'''
smileyActor.node().getPhysical(0).removeLinearForce (nudgeForce)
return (task.done)
# -- end def removeForce
def groundCollisionEventCallback(entry):
'''This is our ground collision message handler. It is called whenever a collision message is triggered'''
# Get our parent actornode
smileyActor = entry.getFromNodePath().getParent()
# Why do we call getParent? Because we are passed the CollisionNode during the event and the
# ActorNode is one level up from there. Our node graph looks like so:
# - ActorNode
# + ModelNode
# + CollisionNode
# Apply the nudge force to bounce us upwards
smileyActor.node().getPhysical(0).addLinearForce (nudgeForce)
# set a task that will clear this force a short moment later
base.taskMgr.doMethodLater (0.1, removeForce, 'removeForceTask', extraArgs=[smileyActor], appendTask=True)
# PLOP!
plopSfx.play()
# -- end def groundCollisionHandler
def updateShadow (gShadow, i, task):
'''Updates the shadow cards that are under each smiley'''
gShadow.setPos (smileyActors[i].getX(), smileyActors[i].getY(), 0.1)
# Shadows range from 0-30 units on Z axis. Above that, they get clamped to certain minimum size
# Unrealistic, but means you never lose your visual cue
dist = 1.0 - smileyActors[i].getZ() / 30.0
z = max (0.25, dist)
z = min (z, 0.9)
gShadow.setScale (1.0 * z)
gShadow.setColor (0, 0, 0, z)
return (task.cont)
# -- end def updateShadow
# Tell the messenger system we're listening for smiley-into-ground messages and invoke our callback
base.accept ('smiley-cnode-into-ground-cnode', groundCollisionEventCallback)
for i in range (0, maxSmileys):
# Create our smiley's physics node
smileyActor = render.attachNewNode (ActorNode("SmileyActorNode"))
# Load the good ole smiley face model
smiley = loader.loadModel('smiley')
smiley.reparentTo (smileyActor)
# Position the smiley faces randomly in the air
smileyActor.setPos(random() * 30 - 15, random() * 30 - 15, 100)
# Associate the default PhysicsManager for this ActorNode
base.physicsMgr.attachPhysicalNode (smileyActor.node())
# Let's set some default body parameters such as mass, and randomize our mass a bit
smileyActor.node().getPhysicsObject().setMass(baseMass + (baseMass * 0.5) * random())
# Build a collisionNode for this smiley which is a sphere of the same diameter as the model
smileyColNode = smileyActor.attachNewNode (CollisionNode ('smiley-cnode'))
smileyColSphere = CollisionSphere (0, 0, 0, 1)
smileyColNode.node().addSolid (smileyColSphere)
# Watch for collisions with our brothers, so we'll push out of each other
smileyColNode.node().setIntoCollideMask (BitMask32().bit (smileyBit))
# we're only interested in colliding with the ground and other smileys
cMask = BitMask32()
cMask.setBit (groundBit)
cMask.setBit (smileyBit)
smileyColNode.node().setFromCollideMask (cMask)
# Now, to keep the spheres out of the ground plane and each other, let's attach a physics handler to them
smileyHandler = PhysicsCollisionHandler()
# Set the physics handler to manipulate the smiley actor's transform.
smileyHandler.addCollider (smileyColNode, smileyActor)
# This call adds the physics handler to the traverser list
# (not related to last call to addCollider!)
base.cTrav.addCollider (smileyColNode, smileyHandler)
# Now, let's set the collision handler so that it will also do a CollisionHandlerEvent callback
# But...wait? Aren't we using a PhysicsCollisionHandler?
# The reason why we can get away with this is that all CollisionHandlerXs are inherited from CollisionHandlerEvent,
# so all the pattern-matching event handling works, too
smileyHandler.addInPattern ('%fn-into-%in')
# Now, add a shadowCard instance to this smiley
gShadow = render.attachNewNode ('shadownode')
gShadow.setPos (smileyActor.getX(), smileyActor.getY(), 0.1)
shadowCard.instanceTo (gShadow)
gShadow.show()
# Start updating the shadow
base.taskMgr.add (updateShadow, 'updateShadowTask', extraArgs=[gShadow, i], appendTask=True)
# Add it to our tracking list
smileyActors.append (smileyActor)
# -- end if
# Now, let's push the smileys downwards so they will impact the ground plane. We do so by building
# a physics force pusher that will act as constant gravity. This is always active.
gravity = ForceNode ('globalGravityForce')
gravityNP = render.attachNewNode (gravity)
gravityForce = LinearVectorForce (0, 0, -9.8) # 9.8 m/s gravity
gravityForce.setMassDependent (False) # constant acceleration (set true if you think Galileo was wrong)
gravity.addForce (gravityForce)
# add it to the built-in physics manager
base.physicsMgr.addLinearForce (gravityForce)
# Let's build a world-based temporary pushing force
nudge = ForceNode ('globalNudgeForce')
nudgeNP = render.attachNewNode (nudge)
nudgeForce = LinearVectorForce (0, 0, baseMass * 150.0) # a sizeable push up into the air
nudgeForce.setMassDependent (True)
nudge.addForce (nudgeForce)
# now, position the camera in a sane spot
base.disableMouse()
base.camera.setPos (-50, -50, 50)
base.camera.lookAt (0, 0, 0)
# doo eet
run()