3rd Person Camera System Tutorial
From PyWiki
The point of this tutorial is to create a camera system consisting of a single camera which can operate in three different modes, described later. The benefit of this system is that the camera is detached from the rest of the scene (other than the root scene node obviously).
The extended camera will consist of two nodes, the first will be the camera handler, the second the target which the camera will be looking at.
This tutorial is based on the C++ code created by Kencho et al. here
Contents |
Camera Types
Chasing 3rd Person Camera
This camera in this setup we have a character (an actor) with a main node (the one on which the entity is positioned) a sight node (where the entity is looking) and finally a chase node (which we will attach the camera to).
Fixed 3rd Person Camera
In this setup the camera will remain at a fixed position and will look constantly at the player entity. Here the cameras position will be a position in the scene and the camera target will be the entity.
1st Person Camera
For this final camera type the entity position is used as the camera position. The entity is hidden to prevent it obstructing our view and the camera target will remain straight ahead of the entity.
Things To Keep In Mind
Coordinate System
Because the camera is detached from the scene and therefore has its own independent coordinate space we will be using world coordinates.
Camera Movement
Essentially what we do here is calculate the difference between the camera position and the desired position giving a displacement vector which we will use to move the camera to the desired position.
Tightness
The movement described above can be too rigid so we use a 'tightness' value to determine how the displacement vector will be applied. This value goes from 0 to 1 depending on how 'tight' you want the camera movement to be
- Tightness of 1 will be rigid movement, the camera moving exactly the same as the entity.
- Tightness of 0 will result in no movement, the displacement vector will not be applied.
The Code
Imports
Nothing outstanding here, just the basic ogre, OIS and SampleFramework. We also include the random modules randrange function which we will use for the random placement of objects in the scene later on.
from random import randrange import ogre.renderer.OGRE as ogre import ogre.io.OIS as OIS import SampleFramework as sf
Abstract Character
This is a simple parent class that will be used by out Character objects in the application. This simply consists of an empty constructor and update methods, along with getSightNode, getCameraNode and getWorldPosition(Note: If you are using the 1.2rc there is an API change in ogre, see the discussion page for more info) methods.
While getters such as this aren't strictly necessary in Python we add them for simplicity.
class Character(object): def __init__(self): pass def update(self, elapsedTime, input): pass def getSightNode(self): return self.sightNode def getCameraNode(self): return self.cameraNode def getWorldPosition(self): return self.mainNode.getWorldPosition()
Concrete Character
This next code is our concrete implementation of the Character base class we defined above. Our character will be a simple floating Ogre head which we will move about in the scene with standard WASD controls.
I'm not going to go over this code as it is outwith the scope of this tutorial and is also covered elsewhere.
class OgreCharacter(Character): def __init__(self, name, sceneManager): self.name = name self.sceneManager = sceneManager self.mainNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(self.name) self.sightNode = self.mainNode.createChildSceneNode(self.name + "_sight", (0, 0, 100)) self.cameraNode = self.mainNode.createChildSceneNode(self.name + "_camera", (0, 50, -100)) self.entity = self.sceneManager.createEntity(self.name, "OgreHead.mesh") self.mainNode.attachObject(self.entity) def __del__(self): self.mainNode.detachAllObjects() del self.entity self.mainNode.removeAndDestroyAllChildren() self.sceneManager.destroySceneNode(self.name) def update(self, elapsedTime, input): if input.isKeyDown(OIS.KC_W): self.mainNode.translate(self.mainNode.getOrientation() * (0, 0, 100 * elapsedTime)) if input.isKeyDown(OIS.KC_S): self.mainNode.translate(self.mainNode.getOrientation() * (0, 0, -50 * elapsedTime)) if input.isKeyDown(OIS.KC_A): self.mainNode.yaw(ogre.Radian(2 * elapsedTime)) if input.isKeyDown(OIS.KC_D): self.mainNode.yaw(ogre.Radian(-2 * elapsedTime)) def setVisible(self, visible): self.mainNode.setVisible(visible)
Extended Camera
The extended camera will act as described above, I will go into a little more detail describing its methods than the original tutorial did.
Constructor
def __init__(self, name, sceneManager, camera = None): self.name = name self.sceneManager = sceneManager self.cameraNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(self.name) self.targetNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(self.name + "_target") self.cameraNode.setAutoTracking(True, self.targetNode) self.cameraNode.setFixedYawAxis(True) if not camera: self.camera = self.sceneManager.createCamera(self.name) self.ownCamera = True else: self.camera = camera self.camera.setPosition(0,0,0) self.ownCamera = False self.cameraNode.attachObject(self.camera) self.tightness = 0.1
This constructor takes in a name, scene manager and optional camera arguments. We use the scene manager to create the target and handler nodes of the camera, called targetNode and cameraNode respectively.
We are setting auto-tracking on the handler node to True and telling it to target the targetNode, naturally. Although we are also using setFixedAxis and setting it to True we are not passing in an axis at this time.
Next section of code determines whether or not a camera was passed into the constructor and handles this appropriately, i.e. seting the camera to the default position (0,0,0) and setting the ownCamera variable to False. If a camera is not passed in, then one is created, this time ownCamera is set to True.
After camera creation/alteration it is attached to the cameraNode and a default tightness of 0.1 is set.
Destructor
The cleanup method basically ensures that when the camera objects are deleted all the required cleanup takes place.
def __del__(self): self.cameraNode.detachAllObjects() if self.ownCamera: del self.camera self.sceneManager.destroySceneNode(self.name) self.sceneManager.destroySceneNode(self.name + "_target")
getCameraPosition
This method just returns the cameraPosition. Again getter methods are not generally required but we add it anyway.
def getCameraPosition(self): return self.cameraNode.getPosition()
instantUpdate
This method is used when we need to make instantaneous updates to the cameras position and target rather than using the displacement vector update we talked about earlier.
def instantUpdate(self, cameraPosition, targetPosition): self.cameraNode.setPosition(cameraPosition) self.targetNode.setPosition(targetPosition)
Update
The final method in our ExtendedCamera class is the standard update method, which will be used to calculate displacements and then translate the cameras target and handler nodes based on these.
The first note here is that although this method takes in an elapsedTime variable, this is never used. (this wasn't a decision I made, the original tutorial was like that)
def update(self, elapsedTime, cameraPosition, targetPosition): displacement = (ogre.Vector3(cameraPosition) - self.cameraNode \ .getPosition()) \ * self.tightness self.cameraNode.translate(displacement) displacement = (ogre.Vector3(targetPosition) - self.targetNode \ .getPosition()) \ * self.tightness self.targetNode.translate(displacement)
SimpleListener
This frame listener will be used during the application, there isn't much to go over here except the frameStarted method where we process the unbuffered input.
class SampleListener(sf.FrameListener): def __init__(self, renderWindow, camera): super(SampleListener, self).__init__(renderWindow, camera) self.char = 0 self.exCamera = 0 self.mode = 0 def setCharacter(self, character): self.char = character def setExtendedCamera(self, cam): self.exCamera = cam def frameStarted(self, evt): self.Keyboard.capture() if self.char: self.char.update(evt.timeSinceLastFrame, self.Keyboard) if self.exCamera: # could use a switch proxy here, but couldn't be bothered if self.mode == 0: self.exCamera.update(evt.timeSinceLastFrame, self.char.getCameraNode() \ .getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) elif self.mode == 1: self.exCamera.update(evt.timeSinceLastFrame, (0, 200, 0), self.char.getSightNode() \ .getWorldPosition()) elif self.mode == 2: self.exCamera.update(evt.timeSinceLastFrame, self.char.getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) if self.Keyboard.isKeyDown(OIS.KC_F1): self.mode = 0 if self.char: self.char.setVisible(True) if self.exCamera: if self.char: self.exCamera.instantUpdate(self.char.getCameraNode() \ .getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) self.exCamera.tightness = 0.01 if self.Keyboard.isKeyDown(OIS.KC_F2): self.mode = 1 if self.char: self.char.setVisible(True) if self.exCamera: if self.char: self.exCamera.instantUpdate((0, 200, 0), self.char.getSightNode() \ .getWorldPosition()) self.exCamera.tightness = 0.01 if self.Keyboard.isKeyDown(OIS.KC_F3): self.mode = 2 if self.char: self.char.setVisible(False) if self.exCamera: if self.char: self.exCamera.instantUpdate(self.char.getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) self.exCamera.tightness = 1.0 if self.Keyboard.isKeyDown(OIS.KC_ESCAPE): return False return True
frameStarted
Lets go over the frameStarted Method. First thing we do here is capture the keyboard input. Then call the update method of our character.
Now we have to update the camera, the way in which we do this is by looking at the current camera mode, then passing in the correct arguments to self.exCamera.update. There are 3 different modes and the updates required by each are shown below:
- Mode 0: This represents the 3rd person chasing camera, in this update we pass the timeSinceLastFrame, the characters cameraNode world position and the characters sightNode world position.
- Mode 1: This represents fixed 3rd person. Here we pass in the timeSinceLastFrame, the fixed position we wish the camera to sit at and the characters sightNode world position.
- Mode 2: This final mode is the 1st person camera mode. Here we pass in the timeSinceLastFrame, the characters world position and the characters sightNode world position.
The next section is the unbuffered input, there shouldn't be anything here that needs much explaining, other than the fact that we are using instantUpdate rather than update, and that in the case of switching to 1st person mode, we are hiding the character with self.char.setVisible(False) and setting the tightness to 1 so that camera movement will reflect character movement properly.
TestApp
The following code is the application itself, there isn't much here that needs explaining other than the fact that we set self.frameListener twice, once in the overridden _createFrameListener method and at the end of the _createScene overridden method. The reason for this is is That the frameListener requires a Character and ExtendedCamera to function properly and these are not created until the end of the _createScene method.
class TestApp(sf.Application): def _createScene(self): self.sceneManager.setAmbientLight((0.2, 0.2, 0.2)) light = self.sceneManager.createLight("MainLight") light.setType(ogre.Light.LT_DIRECTIONAL) light.setDirection(-0.5, -0.5, 0) self.camera.setPosition(0, 0, 0) for i in range(30): knotNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(str(i), (randrange(-1000, 1000), 0, randrange(-1000, 1000) )) knotEntity = self.sceneManager.createEntity(str(i), "knot.mesh") knotNode.attachObject(knotEntity) ogreChar = OgreCharacter("Ogre 1", self.sceneManager) exCamera = ExtendedCamera("ExtendedCamera", self.sceneManager, self.camera) self.frameListener = SampleListener(self.renderWindow, self.camera) self.frameListener.setCharacter(ogreChar) self.frameListener.setExtendedCamera(exCamera) def _createFrameListener(self): self.frameListner = SampleListener(self.renderWindow, self.camera) self.root.addFrameListener(self.frameListener)
Wrapping Up
The final code will allow us to run this tutorial code.
if __name__ == '__main__': app = TestApp() app.go()
I found this tutorial code really useful, its an interesting and reusable way to make a game camera system, thanks to Kencho and everyone else who worked on the original C++ code.
Complete Source
from random import randrange import ogre.renderer.OGRE as ogre import ogre.io.OIS as OIS import SampleFramework as sf class Character(object): def __init__(self): pass def update(self, elapsedTime, input): pass def getSightNode(self): return self.sightNode def getCameraNode(self): return self.cameraNode def getWorldPosition(self): return self.mainNode.getWorldPosition() class OgreCharacter(Character): def __init__(self, name, sceneManager): self.name = name self.sceneManager = sceneManager self.mainNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(self.name) self.sightNode = self.mainNode.createChildSceneNode(self.name + "_sight", (0, 0, 100)) self.cameraNode = self.mainNode.createChildSceneNode(self.name + "_camera", (0, 50, -100)) self.entity = self.sceneManager.createEntity(self.name, "OgreHead.mesh") self.mainNode.attachObject(self.entity) def __del__(self): self.mainNode.detachAllObjects() del self.entity self.mainNode.removeAndDestroyAllChildren() self.sceneManager.destroySceneNode(self.name) def update(self, elapsedTime, input): if input.isKeyDown(OIS.KC_W): self.mainNode.translate(self.mainNode.getOrientation() * (0, 0, 100 * elapsedTime)) if input.isKeyDown(OIS.KC_S): self.mainNode.translate(self.mainNode.getOrientation() * (0, 0, -50 * elapsedTime)) if input.isKeyDown(OIS.KC_A): self.mainNode.yaw(ogre.Radian(2 * elapsedTime)) if input.isKeyDown(OIS.KC_D): self.mainNode.yaw(ogre.Radian(-2 * elapsedTime)) def setVisible(self, visible): self.mainNode.setVisible(visible) class ExtendedCamera(): def __init__(self, name, sceneManager, camera = None): self.name = name self.sceneManager = sceneManager self.cameraNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(self.name) self.targetNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(self.name + "_target") self.cameraNode.setAutoTracking(True, self.targetNode) self.cameraNode.setFixedYawAxis(True) if not camera: self.camera = self.sceneManager.createCamera(self.name) self.ownCamera = True else: self.camera = camera self.camera.setPosition(0,0,0) self.ownCamera = False self.cameraNode.attachObject(self.camera) self.tightness = 0.1 def __del__(self): self.cameraNode.detachAllObjects() if self.ownCamera: del self.camera self.sceneManager.destroySceneNode(self.name) self.sceneManager.destroySceneNode(self.name + "_target") def getCameraPosition(self): return self.cameraNode.getPosition() def instantUpdate(self, cameraPosition, targetPosition): self.cameraNode.setPosition(cameraPosition) self.targetNode.setPosition(targetPosition) def update(self, elapsedTime, cameraPosition, targetPosition): displacement = (ogre.Vector3(cameraPosition) - self.cameraNode \ .getPosition()) \ * self.tightness self.cameraNode.translate(displacement) displacement = (ogre.Vector3(targetPosition) - self.targetNode \ .getPosition()) \ * self.tightness self.targetNode.translate(displacement) class SampleListener(sf.FrameListener): def __init__(self, renderWindow, camera): super(SampleListener, self).__init__(renderWindow, camera) self.char = 0 self.exCamera = 0 self.mode = 0 def setCharacter(self, character): self.char = character def setExtendedCamera(self, cam): self.exCamera = cam def frameStarted(self, evt): self.Keyboard.capture() if self.char: self.char.update(evt.timeSinceLastFrame, self.Keyboard) if self.exCamera: # could use a switch proxy here, but couldn't be bothered if self.mode == 0: self.exCamera.update(evt.timeSinceLastFrame, self.char.getCameraNode() \ .getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) elif self.mode == 1: self.exCamera.update(evt.timeSinceLastFrame, (0, 200, 0), self.char.getSightNode() \ .getWorldPosition()) elif self.mode == 2: self.exCamera.update(evt.timeSinceLastFrame, self.char.getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) if self.Keyboard.isKeyDown(OIS.KC_F1): self.mode = 0 if self.char: self.char.setVisible(True) if self.exCamera: if self.char: self.exCamera.instantUpdate(self.char.getCameraNode() \ .getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) self.exCamera.tightness = 0.01 if self.Keyboard.isKeyDown(OIS.KC_F2): self.mode = 1 if self.char: self.char.setVisible(True) if self.exCamera: if self.char: self.exCamera.instantUpdate((0, 200, 0), self.char.getSightNode() \ .getWorldPosition()) self.exCamera.tightness = 0.01 if self.Keyboard.isKeyDown(OIS.KC_F3): self.mode = 2 if self.char: self.char.setVisible(False) if self.exCamera: if self.char: self.exCamera.instantUpdate(self.char.getWorldPosition(), self.char.getSightNode() \ .getWorldPosition()) self.exCamera.tightness = 1.0 if self.Keyboard.isKeyDown(OIS.KC_ESCAPE): return False return True class TestApp(sf.Application): def _createScene(self): self.sceneManager.setAmbientLight((0.2, 0.2, 0.2)) light = self.sceneManager.createLight("MainLight") light.setType(ogre.Light.LT_DIRECTIONAL) light.setDirection(-0.5, -0.5, 0) self.camera.setPosition(0, 0, 0) for i in range(30): knotNode = self.sceneManager.getRootSceneNode() \ .createChildSceneNode(str(i), (randrange(-1000, 1000), 0, randrange(-1000, 1000) )) knotEntity = self.sceneManager.createEntity(str(i), "knot.mesh") knotNode.attachObject(knotEntity) ogreChar = OgreCharacter("Ogre 1", self.sceneManager) exCamera = ExtendedCamera("ExtendedCamera", self.sceneManager, self.camera) self.frameListener = SampleListener(self.renderWindow, self.camera) self.frameListener.setCharacter(ogreChar) self.frameListener.setExtendedCamera(exCamera) def _createFrameListener(self): self.frameListner = SampleListener(self.renderWindow, self.camera) self.root.addFrameListener(self.frameListener) if __name__ == '__main__': app = TestApp() app.go()
