Hello everyone,
I recently started to learn how to use ODE with Panda3D. One of my first attempts now was to implement the Roaming Ralph example with ODE for collision detection and physic handling. I got so far, that collisions got caught and Ralph gets moved by applied forces. But now comes the part where I need some help. At the moment I have three problems in that example.
- If Ralph moves forward and I look at him from the side, I can sometimes see how he jitters. This I think comes from the applied force, the friction of the ground and the repositioning in the move task. But i have no idea, how i can set the force, friction or the reposition correctly, that he doesn’t jitter anymore.
- When Ralph moves down a hill, the speed how he moves forward is much to high. Does someone knows how i can set the force correct that he doesn’t move that fast, when he moves downwards?
- Directly at the start of the sample, Ralph falls below the map. I worked around this to test the other things by reset it’s position when hitting the ‘r’ key. This will reset his position to the start position. After that he doesn’t fall off the map anymore.
Thank you for your help,
Wolf
Here is my edited Roaming Ralph example:
# Author: Ryan Myers
# ODE Edit: Wolf
# Models: Jeff Styers, Reagan Heller
# Last Updated: 6/13/2012
#
# This tutorial provides an example of creating a character
# and having it walk around on uneven terrain, as well
# as implementing a fully rotatable camera.
import direct.directbase.DirectStart
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue, CollisionRay
from panda3d.core import AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, TextNode
from panda3d.core import Vec3, Vec4, BitMask32, Mat3, Quat
from panda3d.ode import OdeWorld, OdeSimpleSpace, OdeJointGroup
from panda3d.ode import OdeBody, OdeMass, OdeCappedCylinderGeom, OdeRayGeom
from panda3d.ode import OdeTriMeshData, OdeTriMeshGeom
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
import sys
from OdeHelper import wireGeom
SPEED = 0.5
# Function to put instructions on the screen.
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)
# Function to put title on the screen.
def addTitle(text):
return OnscreenText(text=text, style=1, fg=(1,1,1,1),
pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)
class World(DirectObject):
def __init__(self):
self.keyMap = {"left":0, "right":0, "forward":0, "cam-left":0, "cam-right":0, "reset":0}
base.win.setClearColor(Vec4(0,0,0,1))
# Post the instructions
self.title = addTitle("Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain using ODE)")
self.inst1 = addInstructions(0.95, "[ESC]: Quit")
self.inst2 = addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left")
self.inst3 = addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right")
self.inst4 = addInstructions(0.80, "[Up Arrow]: Run Ralph Forward")
self.inst6 = addInstructions(0.70, "[A]: Rotate Camera Left")
self.inst7 = addInstructions(0.65, "[S]: Rotate Camera Right")
# Set up the environment
#
# This environment model contains collision meshes. If you look
# in the egg file, you will see the following:
#
# <Collide> { Polyset keep descend }
#
# This tag causes the following mesh to be converted to a collision
# mesh -- a mesh which is optimized for collision, not rendering.
# It also keeps the original mesh, so there are now two copies ---
# one optimized for rendering, one for collisions.
self.environ = loader.loadModel("models/world")
self.environ.reparentTo(render)
self.environ.setPos(0,0,0)
# Create the main character, Ralph
self.ralphStartPos = self.environ.find("**/start_point").getPos()
self.ralphStartPos.setZ(1)
self.ralph = Actor("models/ralph",
{"run":"models/ralph-run",
"walk":"models/ralph-walk"})
self.ralph.reparentTo(render)
self.ralph.setScale(.2)
self.ralph.setPos(self.ralphStartPos)
# Create a floater object. We use the "floater" as a temporary
# variable in a variety of calculations.
self.floater = NodePath(PandaNode("floater"))
self.floater.reparentTo(render)
# Accept the control keys for movement and rotation
self.accept("escape", sys.exit)
self.accept("arrow_left", self.setKey, ["left",1])
self.accept("arrow_right", self.setKey, ["right",1])
self.accept("arrow_up", self.setKey, ["forward",1])
self.accept("a", self.setKey, ["cam-left",1])
self.accept("s", self.setKey, ["cam-right",1])
self.accept("arrow_left-up", self.setKey, ["left",0])
self.accept("arrow_right-up", self.setKey, ["right",0])
self.accept("arrow_up-up", self.setKey, ["forward",0])
self.accept("a-up", self.setKey, ["cam-left",0])
self.accept("s-up", self.setKey, ["cam-right",0])
self.accept("r", self.setKey, ["reset", 1])
self.accept("r-up", self.setKey, ["reset", 0])
# Game state variables
self.isMoving = False
# Set up the camera
base.disableMouse()
base.camera.setPos(self.ralph.getX(),self.ralph.getY()+10,2)
# Create the ODE physic world
# init the Worlds physics
self.odeWorld = OdeWorld()
# set normal (Earth) gravity
self.odeWorld.setGravity((0, 0, -9.81))
# initialise 1 Surfaces for the environment
self.odeWorld.initSurfaceTable(1)
# set the surface values
self.odeWorld.setSurfaceEntry(
#id1 id2 mu bounce bounce_vel erp cfm slip dampen
0, 0, 50, 0.0, 9.1, 0.9, 0.00001, 0.0, 0.002)
# set a space for collision detection
self.odeSpace = OdeSimpleSpace()
# and apply it to our physic world
self.odeSpace.setAutoCollideWorld(self.odeWorld)
self.contactgroup = OdeJointGroup()
self.odeSpace.setAutoCollideJointGroup(self.contactgroup)
self.odeSpace.setCollisionEvent("physCollision")
# Create an accumulator to track the time since the sim
# has been running
self.deltaTimeAccumulator = 0.0
# This stepSize makes the simulation run at 90 frames per second
self.stepSize = 1.0 / 90.0
# Create the collision geometry for the environment
self.collideModelTrimesh = OdeTriMeshData(self.environ, True)
self.collisionGeom = OdeTriMeshGeom(self.odeSpace, self.collideModelTrimesh)
self.collisionGeom.setCollideBits(BitMask32(0x00000001))
self.collisionGeom.setCategoryBits(BitMask32(0x00000001))
self.odeSpace.setSurfaceType(self.collisionGeom, 0)
# Here we set up the collision geometry for Ralph. Therfor we will
# use a ray geom, which will be set from little below ralphs feet
# to its center for the ground collision. For the wall collision
# detection we will use a capsule with the height of Ralph.
ralphSize = 0.2
heightMultiplier = 2
footRayLength = 1
# setup the physical body of ralph. This will tell ODE how big
# Ralph is and how much density he has.
self.ralphPhysicBody = OdeBody(self.odeWorld)
ralphMass = OdeMass()
ralphMass.setCapsule(998.1, 1, ralphSize, ralphSize * heightMultiplier)
self.ralphPhysicBody.setMass(ralphMass)
self.ralphPhysicBody.setPosition(self.ralph.getPos(render))
self.ralphPhysicBody.setQuaternion(self.ralph.getQuat(render))
# setup the body capsule. This capsule represents the body
# of Ralph in the ode physics world
self.bodyGeom = OdeCappedCylinderGeom(self.odeSpace,
ralphSize,
ralphSize * heightMultiplier)
pos = Vec3(self.ralph.getPos(render))
pos.setZ(pos.getZ() - 3)
self.bodyGeom.setPosition(pos)
self.bodyGeom.setCollideBits(BitMask32(0x0000001))
self.bodyGeom.setCategoryBits(BitMask32(0x00000001))
self.bodyGeom.setBody(self.ralphPhysicBody)
# now setup the foot ray, which reach from little below ralphs feet
# upwards to his center
self.footGeom = OdeRayGeom(self.odeSpace, footRayLength)
pos = Vec3(self.ralph.getPos(render))
pos.setZ(pos.getZ() - 1)
direction = Vec3(0, 0, -1)
self.footGeom.set(pos, direction)
self.footGeom.setCollideBits(BitMask32(0x00000001))
self.footGeom.setCategoryBits(BitMask32(0x00000001))
self.footGeom.setBody(self.ralphPhysicBody)
# now setup a body and collision geometry for the camera
self.camPhysicBody = OdeBody(self.odeWorld)
camMass = OdeMass()
camMass.setZero()
self.camPhysicBody.setMass(camMass)
self.camPhysicBody.setPosition(base.camera.getPos(render))
self.camPhysicBody.setQuaternion(base.camera.getQuat(render))
self.camRayGeom = OdeRayGeom(self.odeSpace, 2)
pos = Vec3(base.camera.getPos(render))
pos.setZ(pos.getZ() - 0.5)
direction = Vec3(0, 0, 1)
self.camRayGeom.set(pos, direction)
self.camRayGeom.setCollideBits(BitMask32(0x00000001))
self.camRayGeom.setCategoryBits(BitMask32(0x00000001))
self.camRayGeom.setBody(self.camPhysicBody)
# setup some debug geometry for the body capsule
self.capsule = wireGeom().generate(
'capsule',
radius=ralphSize,
length=ralphSize * heightMultiplier)
self.capsule.setPos(self.bodyGeom.getPosition())
self.capsule.setHpr(0, 0, 0)
self.capsule.reparentTo(render)
# setup some debug geometry for the foot ray
self.ray = wireGeom().generate('ray', length=footRayLength)
self.ray.setPos(self.footGeom.getPosition())
self.ray.setHpr(0, 0, 0)
self.ray.reparentTo(render)
# add the move and physic tasks to the manager
taskMgr.add(self.updatePhysics, "physicsUpdaterTask")
taskMgr.add(self.move,"moveTask")
# Create some lighting
ambientLight = AmbientLight("ambientLight")
ambientLight.setColor(Vec4(.3, .3, .3, 1))
directionalLight = DirectionalLight("directionalLight")
directionalLight.setDirection(Vec3(-5, -5, -5))
directionalLight.setColor(Vec4(1, 1, 1, 1))
directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
render.setLight(render.attachNewNode(ambientLight))
render.setLight(render.attachNewNode(directionalLight))
#Records the state of the arrow keys
def setKey(self, key, value):
self.keyMap[key] = value
def updatePhysics(self, task):
'''This task will handle the actualisation of
the physic calculations each frame'''
dt = globalClock.getDt()
# Setup the contact joints
self.odeSpace.autoCollide()
self.deltaTimeAccumulator += dt
while self.deltaTimeAccumulator > self.stepSize:
# Remove a stepSize from the accumulator until
# the accumulated time is less than the stepsize
self.deltaTimeAccumulator -= self.stepSize
# Step the simulation
self.odeWorld.quickStep(self.stepSize)
# Clear the contact joints
self.contactgroup.empty()
return task.cont
# Accepts arrow keys to move either the player or the menu cursor,
# Also deals with grid checking and collision detection
def move(self, task):
# If the camera-left key is pressed, move camera left.
# If the camera-right key is pressed, move camera right.
base.camera.lookAt(self.ralph)
if (self.keyMap["cam-left"]!=0):
base.camera.setX(base.camera, -20 * globalClock.getDt())
if (self.keyMap["cam-right"]!=0):
base.camera.setX(base.camera, +20 * globalClock.getDt())
# actualise the actor model and quaternion with the pos
# and quat of the physic body
pos = self.ralphPhysicBody.getPosition()
pos.setZ(pos.getZ() - 0.5)
self.ralph.setPosQuat(
render,
pos,
Quat(self.ralphPhysicBody.getQuaternion()))
# keep the foot ray below ralph, so set it's offset at the
# size of the rays length
offFoot = Vec3(0, 0, -0.5)
self.footGeom.setOffsetPosition(offFoot)
# set the rotation of ralph to the physic body, so it won't rotate
# ralph back to 0.
charRot = -self.ralph.getH()
rotMat = Mat3.rotateMatNormaxis(charRot, Vec3.up())
self.ralphPhysicBody.setRotation(rotMat)
# set the angular Velocity, so ralph is not
# rotating in a direction we don't want him to rotate
self.ralphPhysicBody.setAngularVel(0, 0, 0)
# If a move-key is pressed, move ralph in the specified direction.
if (self.keyMap["left"]!=0):
turnLeft = -(self.ralph.getH() - globalClock.getDt() * -300)
rotMat = Mat3.rotateMatNormaxis(turnLeft, Vec3.up())
self.ralphPhysicBody.setRotation(rotMat)
if (self.keyMap["right"]!=0):
turnRight = -(self.ralph.getH() - globalClock.getDt() * 300)
rotMat = Mat3.rotateMatNormaxis(turnRight, Vec3.up())
self.ralphPhysicBody.setRotation(rotMat)
if (self.keyMap["forward"]!=0):
self.ralphPhysicBody.addRelForce(0, -10 * 25, 0)
# If ralph is moving, loop the run animation.
# If he is standing still, stop the animation.
if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0):
if self.isMoving is False:
#self.ralph.loop("run")
self.isMoving = True
else:
if self.isMoving:
self.ralph.stop()
self.ralph.pose("walk",5)
self.isMoving = False
# reset ralphs position to his startposition if the reset key is pressed
if self.keyMap["reset"]!=0:
self.ralphPhysicBody.setPosition(self.ralphStartPos)
# If the camera is too far from ralph, move it closer.
# If the camera is too close to ralph, move it farther.
camvec = self.ralph.getPos() - base.camera.getPos()
camvec.setZ(0)
camdist = camvec.length()
camvec.normalize()
if (camdist > 10.0):
base.camera.setPos(base.camera.getPos() + camvec*(camdist-10))
camdist = 10.0
if (camdist < 5.0):
base.camera.setPos(base.camera.getPos() - camvec*(5-camdist))
camdist = 5.0
# Keep the camera at one foot above the terrain,
# or two feet above ralph, whichever is greater.
if (base.camera.getZ() < self.ralph.getZ() + 2.0):
base.camera.setZ(self.ralph.getZ() + 2.0)
# The camera should look in ralph's direction,
# but it should also try to stay horizontal, so look at
# a floater which hovers above ralph's head.
self.floater.setPos(self.ralph.getPos())
self.floater.setZ(self.ralph.getZ() + 2.0)
base.camera.lookAt(self.floater)
# actualise the debug models for the ode geoms
self.ray.setPosQuat(
render,
self.footGeom.getPosition(),
self.footGeom.getQuaternion())
self.capsule.setPosQuat(
render,
self.bodyGeom.getPosition(),
self.bodyGeom.getQuaternion())
return task.cont
w = World()
run()
And here the OdeHelper.py for the debug help of ODE geometry:
#
# Code by FenrirWolf
# https://discourse.panda3d.org/t/representing-ode-geoms-collisions-as-visible-geom/6201/1
#
from pandac.PandaModules import Point3
from pandac.PandaModules import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from pandac.PandaModules import Geom, GeomNode, NodePath, GeomLinestrips
import math, random
"""
Note that wireprims are wire-like representations of geom, in the same manner as Ogre's debug mode. I find this the most useful way to represent
ODE geom structures visually, as you can clearly see the orientation versus a more generic wireframe mesh.
These wireprims are rendered as linestrips. Therefore, only vertices are required and texturing is not supported. You can use standard render attribute changes such
as setColor in order to change the line's color. By default it is green.
This class merely returns a NodePath to a GeomNode that is a representation of what is requested. You can use this outside of ODE geom visualizations, obviously.
Supported are sphere, box, cylinder, capsule (aka capped cylinder), ray, and plane
to use:
sphereNodepath = wireGeom().generate ('sphere', radius=1.0)
boxNodepath = wireGeom().generate ('box', extents=(1, 1, 1))
cylinderNodepath = wireGeom().generate ('cylinder', radius=1.0, length=3.0)
rayNodepath = wireGeom().generate ('ray', length=3.0)
planeNodepath = wireGeom().generate ('plane')
"""
class wireGeom:
def __init__(self):
# GeomNode to hold our individual geoms
self.gnode = GeomNode ('wirePrim')
# How many times to subdivide our spheres/cylinders resulting vertices. Keep low
# because this is supposed to be an approximate representation
self.subdiv = 12
def line(self, start, end):
# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()
# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')
# build a linestrip vertex buffer
lines = GeomLinestrips(Geom.UHStatic)
vertices.addData3f(start[0], start[1], start[2])
vertices.addData3f(end[0], end[1], end[2])
lines.addVertices(0, 1)
lines.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(lines)
# Add our primitive to the geomnode
self.gnode.addGeom(geom)
def circle(self, radius, axis, offset):
# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()
# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')
# build a linestrip vertex buffer
lines = GeomLinestrips (Geom.UHStatic)
for i in range(0, self.subdiv):
angle = i / float(self.subdiv) * 2.0 * math.pi
ca = math.cos(angle)
sa = math.sin(angle)
if axis == "x":
vertices.addData3f(0, radius * ca, radius * sa + offset)
if axis == "y":
vertices.addData3f(radius * ca, 0, radius * sa + offset)
if axis == "z":
vertices.addData3f(radius * ca, radius * sa, offset)
for i in range(1, self.subdiv):
lines.addVertices(i - 1, i)
lines.addVertices(self.subdiv - 1, 0)
lines.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(lines)
# Add our primitive to the geomnode
self.gnode.addGeom(geom)
def capsule(self, radius, length, axis):
# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()
# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')
# build a linestrip vertex buffer
lines = GeomLinestrips(Geom.UHStatic)
# draw upper dome
for i in range (0, self.subdiv / 2 + 1):
angle = i / float(self.subdiv) * 2.0 * math.pi
ca = math.cos (angle)
sa = math.sin (angle)
if axis == "x":
vertices.addData3f(0, radius * ca, radius * sa + (length / 2))
if axis == "y":
vertices.addData3f(radius * ca, 0, radius * sa + (length / 2))
# draw lower dome
for i in range(0, self.subdiv / 2 + 1):
angle = -math.pi + i / float(self.subdiv) * 2.0 * math.pi
ca = math.cos(angle)
sa = math.sin(angle)
if axis == "x":
vertices.addData3f(0, radius * ca, radius * sa - (length / 2))
if axis == "y":
vertices.addData3f(radius * ca, 0, radius * sa - (length / 2))
for i in range(1, self.subdiv + 1):
lines.addVertices(i - 1, i)
lines.addVertices(self.subdiv + 1, 0)
lines.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(lines)
# Add our primitive to the geomnode
self.gnode.addGeom(geom)
def rect(self, width, height, axis):
# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()
# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')
# build a linestrip vertex buffer
lines = GeomLinestrips(Geom.UHStatic)
# draw a box
if axis == "x":
vertices.addData3f(0, -width, -height)
vertices.addData3f(0, width, -height)
vertices.addData3f(0, width, height)
vertices.addData3f(0, -width, height)
if axis == "y":
vertices.addData3f(-width, 0, -height)
vertices.addData3f(width, 0, -height)
vertices.addData3f(width, 0, height)
vertices.addData3f(-width, 0, height)
if axis == "z":
vertices.addData3f(-width, -height, 0)
vertices.addData3f(width, -height, 0)
vertices.addData3f(width, height, 0)
vertices.addData3f(-width, height, 0)
for i in range(1, 3):
lines.addVertices(i - 1, i)
lines.addVertices(3, 0)
lines.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(lines)
# Add our primitive to the geomnode
self.gnode.addGeom(geom)
def generate(self, type, radius=1.0, length=1.0, extents=Point3(1, 1, 1), R=-1, G=-1, B=-1):
if R==-1:
R=random.uniform(0,1)
if G==-1:
G=random.uniform(0,1)
if B==-1:
B=random.uniform(0,1)
if type == 'sphere':
# generate a simple sphere
self.circle(radius, "x", 0)
self.circle(radius, "y", 0)
self.circle(radius, "z", 0)
if type == 'capsule':
# generate a simple capsule
self.capsule(radius, length, "x")
self.capsule(radius, length, "y")
self.circle(radius, "z", -length / 2)
self.circle(radius, "z", length / 2)
if type == 'box':
# generate a simple box
self.rect(extents[1]/2, extents[2]/2, "x")
self.rect(extents[0]/2, extents[2]/2, "y")
self.rect(extents[0]/2, extents[1]/2, "z")
if type == 'cylinder':
# generate a simple cylinder
self.line((0, -radius, -length / 2), (0, -radius, length/2))
self.line((0, radius, -length / 2), (0, radius, length/2))
self.line((-radius, 0, -length / 2), (-radius, 0, length/2))
self.line((radius, 0, -length / 2), (radius, 0, length/2))
self.circle(radius, "z", -length / 2)
self.circle(radius, "z", length / 2)
if type == 'ray':
# generate a ray
self.circle(length / 10, "x", 0)
self.circle(length / 10, "z", 0)
self.line((0, 0, 0), (0, 0, length))
self.line((0, 0, length), (0, -length/10, length*0.9))
self.line((0, 0, length), (0, length/10, length*0.9))
if type == 'plane':
# generate a plane
length = 3.0
self.rect(1.0, 1.0, "z")
self.line((0, 0, 0), (0, 0, length))
self.line((0, 0, length), (0, -length/10, length*0.9))
self.line((0, 0, length), (0, length/10, length*0.9))
# rename ourselves to wirePrimBox, etc.
name = self.gnode.getName()
self.gnode.setName(name + type.capitalize())
NP = NodePath(self.gnode) # Finally, make a nodepath to our geom
NP.setColor(R, G, B) # Set default color
return NP