Using Surface Normals to Set Hpr [SOLVED]

Here’s a brain teaser for all of you. I want to make it possible for my race tracks to defy gravity. Meaning, I want the vehicles to navigate the track even when it turns upside down, or sideways, exactly as if it were upright (as if Z were up). And I want them to transition through different orientations without a hitch.

To try and set this system up, I’m working on a test program that uses surface normals. The basic idea is to have a NodePath set a the collision point between a ray and the track. This NodePath will look straight up at the point that represents a unit surface normal at the collision point. By attaching the vehicle to this NodePath, I can ensure that the vehicle will inherit the correct orientation to always be perpendicular to the track beneath it.

I’ve got this mostly working on my test program, which uses a collision sphere for the track and the panda model packaged with Panda3D as the vehicle. The problem is that when the panda reaches the top or bottom of the sphere, it starts to exhibit erratic orientation changes. I’m guessing this is because of an issue related to +/- signs in the Hpr.

Here’s the test program code:

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.showbase.DirectObject import DirectObject	
	
class Tester(DirectObject):
	def __init__(self):
		base.disableMouse()
		base.camera.setPos(0, -200, 0)
		
		self.bNP1 = render.attachNewNode("Blank")
		self.bNP1.setPos(1,0,25.1)
		self.bNP1.setP(90)
		
		self.bNP2 = render.attachNewNode("Blank")
		
		self.panda = loader.loadModel("panda.egg.pz")
		self.panda.reparentTo(self.bNP1)
		self.panda.setP(-90)
		self.panda.setY(1)
		
		self.setupLight()
		self.setupCols()
		self.setupKeyBrd()
		
		taskMgr.add(self.test, "Test")
		
	def test(self, task):
		colN = self.getCol()
			
		if(self.keyMap["up"] == True):
			self.bNP1.setY(self.panda, -.1)
			
		if(self.keyMap["down"] == True):
			self.bNP1.setY(self.panda, .1)
			
		if(self.keyMap["left"] == True):
			self.panda.setH(self.panda, 1)
			
		if(self.keyMap["right"] == True):
			self.panda.setH(self.panda, -1)
			
		return task.cont
		
	def getCol(self):
		colN = None
		self.cTrav.traverse(render)
				
		self.cHanQ.sortEntries()
		
		if(self.cHanQ.getNumEntries() > 0):
			entry = self.cHanQ.getEntry(0)
			colPoint = entry.getSurfacePoint(render)
			self.bNP1.setPos(colPoint)
			colN = entry.getSurfaceNormal(self.bNP1)
			colN.normalize()
			self.bNP2.setPos(self.bNP1, colN.getX(), colN.getY(), colN.getZ())
			self.bNP1.lookAt(self.bNP2)
			print(self.bNP1.getHpr())
				
	def setupLight(self):
		primeL = DirectionalLight("prime")
		primeL.setColor(VBase4(.6,.6,.6,1))

		self.dirLight = render.attachNewNode(primeL)
		self.dirLight.setHpr(45,-60,0)
		
		render.setLight(self.dirLight)
		
		ambL = AmbientLight("amb")
		ambL.setColor(VBase4(.2,.2,.2,1))
		self.ambLight = render.attachNewNode(ambL)
		
		render.setLight(self.ambLight)
		
	def setupCols(self):
		self.sphereCol = render.attachNewNode(CollisionNode("Sphere Col"))
		CS = CollisionSphere(0,0,0,25)
		self.sphereCol.node().addSolid(CS)
		self.sphereCol.node().setIntoCollideMask(BitMask32.bit(1))
		self.sphereCol.node().setFromCollideMask(BitMask32.allOff())
		self.sphereCol.show()
		
		self.rayCN = CollisionNode("RayCN")
		self.cRay = CollisionRay(Point3(0,0,0), Vec3(0,1,0))
		self.rayCN.addSolid(self.cRay)
		self.rayCN.setFromCollideMask(BitMask32.bit(1))
		self.rayCN.setIntoCollideMask(BitMask32.allOff())
		self.rayCNP = self.panda.attachNewNode(self.rayCN)
		self.rayCNP.setP(-90)
		self.rayCNP.show()
		
		self.cTrav = CollisionTraverser()
		self.cHanQ = CollisionHandlerQueue()		
		self.cTrav.addCollider(self.rayCNP, self.cHanQ)
		
	def setupKeyBrd(self):
		self.keyMap = {"up" : False,
						"down" : False,
						"left" : False,
						"right" : False}
						
		self.accept("w", self.setKey, ["up", True])
		self.accept("s", self.setKey, ["down", True])
		self.accept("a", self.setKey, ["left", True])
		self.accept("d", self.setKey, ["right", True])
		
		self.accept("w-up", self.setKey, ["up", False])
		self.accept("s-up", self.setKey, ["down", False])
		self.accept("a-up", self.setKey, ["left", False])
		self.accept("d-up", self.setKey, ["right", False])
		
	def setKey(self, key, value):
		self.keyMap[key] = value
		return
		
T = Tester()
run()

Any thoughts on how to get what I want? Basically, I need the panda to maintain a constant height and always remain perpendicular to the surface beneath it as it wanders around the sphere.

The lookAt() method takes an optional second parameter, which is the up vector. The default up vector is Z-up, but if the look direction is also very nearly Z-up, then your object will seem to rotate erratically around that axis. This is a form of gimbal lock.

The solution is to supply an up vector that’s suitable for the current orientation of the vehicle. Then the look direction will never be very nearly the same as the up vector (assuming you don’t rotate 90 degrees within one frame), and you will avoid gimbal lock.

David

Okay, I changed the code in getCol() to add an up vector, which is (0,0,1) from self.bNP1 translated into render’s coordinate space. That solved the problem. Here’s the code:

	def getCol(self):
		colN = None
		self.cTrav.traverse(render)
				
		self.cHanQ.sortEntries()
		
		if(self.cHanQ.getNumEntries() > 0):
			entry = self.cHanQ.getEntry(0)
			colPoint = entry.getSurfacePoint(render)
			self.bNP1.setPos(colPoint)
			colN = entry.getSurfaceNormal(self.bNP1)
			colN.normalize()
			self.bNP2.setPos(self.bNP1, colN.getX(), colN.getY(), colN.getZ())
			upVec = render.getRelativeVector(self.bNP1, Vec3(0,0,1))
			self.bNP1.lookAt(self.bNP2.getPos(), upVec)
			print(self.bNP1.getHpr())