Tuesday, January 10, 2012

The Scene Graph and First Person Perspective

No post yesterday, so this is a double post. Yesterday I looked at some of the more theoretical aspects of the panda3D engine, specifically the scene graph. Basically, the scene graph is a construct that the engine keeps while a program is running of all the active objects in the scene. So in the flight simulator I already made, the scene graph would contain things like the player's plane model, the enemy plane models and the terrain model. In order to put a model into the game, you first have to insert it into the scene graph. It has a tree structure to it, so you have to specify where in the graph you want to insert it. For simple programs, like mine, the graph just has a linear structure where everything is on one level. There are two large branches of the scene graph that everything is under, render and render2d. Render holds all of the 3d models used in the game; all of the models in my game were placed under the render branch. Render2d holds 2d elements like a user interface or heads-up display. The radar in my game was placed into the render2d branch. Everything in the tree is referred to as a node, so once an object has been placed into the tree as a node, it is rendered onscreen.

Today, I applied some of my new knowledge to another simple program. This time, instead of starting from scratch with my flight simulator, I modified an existing program. The first video below shows the Roaming Ralph application that is included in the panda3d package as a sample program. It's a simple program that demonstrates third-person perspective camera along with using animated characters. I wanted to modify that program to have a first-person perspective and get rid of the Ralph character altogether. Overall, this wasn't extremely difficult. I had to rewrite most of the camera move functions and remap the keys that were used to move Ralph onto controlling the camera instead. I deleted or rewrote all of the code that referenced Ralph. So really all you are doing in the program is controlling the camera. I also had to do some modifying of the collision detection so that it would work on the camera, instead of on Ralph. The camera is set to stay a set distance above the terrain, so you can move across the uneven terrain realistically with no clipping issues. The second video below demonstrates my new version of the Roaming Ralph program. Below the videos, I've put the complete source code for the program, so you can look through it if you want. As you can see, it's not a lot of code for a program like this. Panda3D makes programming the game that much easier. If you are unfamiliar with the python language, note that any line that starts with # is a comment. Comments are completely ignored by the interpreter and are only there to explain what is going on for someone looking at the code. I have a tendency to leave most of my code uncommented, so many of the comments are from the original program.






# Author: Ryan Myers
# Models: Jeff Styers, Reagan Heller
# Modified by Jesse Dixon

import direct.directbase.DirectStart
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay
from panda3d.core import Filename,AmbientLight,DirectionalLight
from panda3d.core import PandaNode,NodePath,Camera,TextNode
from panda3d.core import Vec3,Vec4,BitMask32
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
import random, sys, os, math

SPEED = 0.5

class World(DirectObject):

    def __init__(self):
       
        self.keyMap = {"left":0, "right":0, "forward":0}
        base.win.setClearColor(Vec4(0,0,0,1))

        # 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 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("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])

        taskMgr.add(self.move,"moveTask")

        # Game state variables
        self.isMoving = False

        # Set up the camera
       
        base.disableMouse()
       
        base.camera.setPos(10,10,2)
       
        # We will detect the height of the terrain by creating a collision
        # ray and casting it downward toward the terrain.  One ray will
        # start above ralph's head, and the other will start above the camera.
        # A ray may hit the terrain, or it may hit a rock or a tree.  If it
        # hits the terrain, we can detect the height.  If it hits anything
        # else, we rule that the move is illegal.

        self.cTrav = CollisionTraverser()

        self.camGroundRay = CollisionRay()
        self.camGroundRay.setOrigin(0,0,1000)
        self.camGroundRay.setDirection(0,0,-1)
        self.camGroundCol = CollisionNode('camRay')
        self.camGroundCol.addSolid(self.camGroundRay)
        self.camGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.camGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol)
        self.camGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)
     
        # Uncomment this line to show a visual representation of the
        # collisions occuring
        #self.cTrav.showCollisions(render)
       
        # 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
   

    # Accepts arrow keys to move either the player or the menu cursor,
    # Also deals with grid checking and collision detection
    def move(self, task):

        base.camera

        # save the camera's initial position, so that we can restore it if necessary
        startpos = base.camera.getPos()

        # If a move-key is pressed, move ralph in the specified direction.

        if (self.keyMap["left"]!=0):
            base.camera.setH(base.camera.getH() + 150 * globalClock.getDt())
        if (self.keyMap["right"]!=0):
            base.camera.setH(base.camera.getH() - 150 * globalClock.getDt())
        if (self.keyMap["forward"]!=0):
            base.camera.setY(base.camera, 25 * globalClock.getDt())


        if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0):
            if self.isMoving is False:
                self.isMoving = True
        else:
            if self.isMoving:
                self.isMoving = False

        # Now check for collisions.

        self.cTrav.traverse(render)
       
        # Keep the camera at one foot above the terrain.
       
        entries = []
        for i in range(self.camGroundHandler.getNumEntries()):
            entry = self.camGroundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+2.0)
        else:
            base.camera.setPos(startpos)

        return task.cont


w = World()
run()

No comments:

Post a Comment