
Making the camera smoothly follow an object
In this recipe you will learn how to program a simple camera system that follows an object smoothly, without giving the impression of being glued to the back of the target.
Getting ready
See the recipe Setting up the game structure to create the basic application framework for the following steps.
How to do it...
Let's build a third person camera system:
- Add this code to
Application.py
:from direct.showbase.ShowBase import ShowBase from direct.actor.Actor import Actor from panda3d.core import Vec3 from direct.interval.IntervalGlobal import * from FollowCam import FollowCam class Application(ShowBase): def __init__(self): ShowBase.__init__(self) self.world = loader.loadModel("environment") self.world.reparentTo(render) self.world.setScale(0.5) self.world.setPos(-8, 80, 0) self.panda = Actor("panda", {"walk": "panda-walk"}) self.panda.reparentTo(render) self.panda.setHpr(270, 0, 0) self.panda.loop("walk") self.walkIval1 = self.panda.posInterval(2, Vec3(-8, -8, 0), startPos = Vec3(8, -8, 0)) self.walkIval2 = self.panda.posInterval(2, Vec3(-8, 8, 0), startPos = Vec3(-8, -8, 0)) self.walkIval3 = self.panda.posInterval(2, Vec3(8, 8, 0), startPos = Vec3(-8, 8, 0)) self.walkIval4 = self.panda.posInterval(2, Vec3(8, -8, 0), startPos = Vec3(8, 8, 0)) self.turnIval1 = self.panda.hprInterval(0.5, Vec3(180, 0, 0), startHpr = Vec3(270, 0, 0)) self.turnIval2 = self.panda.hprInterval(0.5, Vec3(90, 0, 0), startHpr = Vec3(180, 0, 0)) self.turnIval3 = self.panda.hprInterval(0.5, Vec3(0, 0, 0), startHpr = Vec3(90, 0, 0)) self.turnIval4 = self.panda.hprInterval(0.5, Vec3(-90, 0, 0), startHpr = Vec3(0, 0, 0)) self.pandaWalk = Sequence(self.walkIval1, self.turnIval1, self.walkIval2, self.turnIval2, self.walkIval3, self.turnIval3, self.walkIval4, self.turnIval4) self.pandaWalk.loop() self.followCam = FollowCam(self.cam, self.panda)
- Add a new file to the project. Call it
FollowCam.py
. - Copy the following code to the file you just created:
from direct.showbase.ShowBase import ShowBase from panda3d.core import Vec3 class FollowCam(): def __init__(self, camera, target): self.dummy = render.attachNewNode("cam" + target.getName()) self.turnRate = 2.2 self.camera = camera self.target = target taskMgr.add(self.updateCamera, "updateCamera" + target.getName()) def updateCamera(self, task): self.dummy.setPos(self.target.getPos()) heading = self.clampAngle(self.dummy.getH()) turnDiff = self.target.getH() - heading turnDiff = self.clampAngle(turnDiff) dt = globalClock.getDt() turn = turnDiff * dt self.dummy.setH(heading + turn * self.turnRate) self.camera.setPos(self.dummy.getPos()) self.camera.setY(self.dummy, 40) self.camera.setZ(self.dummy, 10) self.camera.lookAt(self.target.getPos() + Vec3(0, 0, 7)) return task.cont def clampAngle(self, angle): while angle < -180: angle = angle + 360 while angle > 180: angle = angle - 360 return angle
- Press F6 to start the application. You should be able to see a panda walking in circles while the camera follows it:
How it works...
We use the constructor of our Application
class to set up the scene containing the walking panda and the background scenery. In the last line we create a new instance of our FollowCam
, which contains the camera tracking code that is the core of this recipe.
To make the FollowCam
work correctly and to be able to have multiple cameras follow different objects, we have to pass the camera we want to be updated and its target to the constructor, where we set up a few things we need for updating the camera. For example, we add a task that will call the updateCamera()
method each frame. Additionally, the target's name is appended to both the dummy object's and the task's names to avoid name clashes in the case where we need to use more than one FollowCam
instance. The dummy object is an invisible helper object that will help us to position the camera, as you will see in the following paragraphs.
The updateCamera()
method is where all the work is happening: We move the dummy to our target's current position and get its current heading. The heading angle (in degrees) is clamped to the range of values from 180 to 180. We do this to avoid the camera getting stuck or continuing to turn because of the ambiguous nature of angles.
In the next steps, we calculate the difference between the target's heading and that of our dummy object, which is also clamped to avoid the undesired results described in the previous paragraph. In the following lines we can find the explanation for the camera's smooth turning—every frame, the dummy object's heading converges towards the heading of the camera target just a little bit. This is intentional; as it is multiplied by the time it took to complete the last frame. Additionally, we can also influence how fast the camera turns by adjusting turnRate
.
In the final steps, the camera is first moved to the position of the dummy and then pushed away again along the dummy's local axis to its final position. After setting the camera's lookAt()
target, we are done!
There's more...
In this version, the camera only supports smooth turning for objects that only change their heading. Other rotation axes can be added rather easily, as they work exactly the same as the one presented in this recipe!