To main page | 3dengine.org

FPS Spectator code (PyOpenGL)


{ Maybe you were looking for frames-per-second? }

This is how to do FPS (First person shooter) spectator code in PyOpenGL, also known as free look or mouselook using only matrix math.

How to use it

The Spectator class is defined below ("Working spectator code in Python using matrix math" + "Full Example"). Then use 'w','s','a','d' and mouse look.
fps = Spectator(w = 640, h = 480, fov = 75)
fps.simple_lights()  # optional
fps.simple_camera_pose() # optional

while fps.loop():
    fps.draw_simple_cube() # optional
    fps.controls_3d(0,'w','s','a','d')
    if fps.keys['q']: break

How does it work (in pseudo code)

Basically (in pseudo code)
m = glGetDoublev(GL_MODELVIEW_MATRIX)
if mouse_drag:
    glTranslate(-camera_center_in_3d)
    glRotate(mouse_dx * look_speed, m[1],m[5],m[9]) # [1]
    glRotate(mouse_dy * look_speed, m[0],m[4],m[8]) # [1]
    # compensate roll - make sure camera's "up" is actually upwards
    glRotate(math.atan2(-m[4],m[5]) * 57.29577,m[2],m[6],m[10])
    glTranslate(camera_center_in_3d)
if key['w' or 's']:
    glTranslate(m[2],m[6],m[10]) # fwd vector [1]
if key['a' or 'd']:
    glTranslate(m[0],m[4],m[8]) # right vector [1]
[1] Right, up, back vectors in modelview matrix


Working spectator code in Python using matrix math

You will need PyGame, PyOpenGL and NumPy. (You'll need those anyway if you want to develop OpenGL in Python)

Full example

This code uses only OpenGL matrices without having to store heading-pitch-roll angles, quaternions or even your position in space - pure matrix math.
from __future__ import division
import pygame, math, numpy
from OpenGL.GL import *
from OpenGL.GLU import *

class Spectator:
    def __init__(self, w=640, h=480, fov=75):
        pygame.init()
        pygame.display.set_mode((640,480), pygame.OPENGL | \
            pygame.DOUBLEBUF)
        glMatrixMode(GL_PROJECTION)
        aspect = w/h
        gluPerspective(fov, aspect, 0.001, 100000.0);
        glMatrixMode(GL_MODELVIEW)

    def simple_lights(self):
        glEnable(GL_LIGHTING)
        glShadeModel(GL_SMOOTH)
        glEnable(GL_LIGHT0)
        glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.9, 0.45, 0.0, 1.0))
        glLightfv(GL_LIGHT0, GL_POSITION, (0.0, 10.0, 10.0, 10.0))
        glEnable(GL_DEPTH_TEST)
        glDepthFunc(GL_LEQUAL)
            
    def simple_camera_pose(self):
        """ Pre-position the camera (optional) """
        glMatrixMode(GL_MODELVIEW)
        glLoadMatrixf(numpy.array([0.741,-0.365,0.563,0,0,0.839,0.544,
            0,-0.671,-0.403,0.622,0,-0.649,1.72,-4.05,1]))

    def draw_simple_cube(self):
        """ Draw a simple object (optional) """
        try:
            glEnableClientState(GL_VERTEX_ARRAY);
            glVertexPointerf( self.points )
            glEnableClientState(GL_NORMAL_ARRAY);
            glNormalPointerf( self.normals )
            glDrawElementsui(GL_TRIANGLES, self.indices)
        except AttributeError:
            # a little hack to initialize points only once
            self.points=numpy.array([2.4,0,0,0,0,2,0,0,0,0,0,2,2.4,0,0,\
                2.4,0,2,0,0,2,0,-1.66,0,0,0,0,0,-1.66,0,2.4,0,0,0,0,0,\
                2.4,0,0,2.4,-1.66,2,2.4,0,2,2.4,-1.66,2,0,0,2,2.4,0,2,0,\
                -1.66,0,0,0,2,0,-1.66,2,2.4,0,0,0,-1.66,0,2.4,-1.66,0,\
                2.4,-1.66,2,2.4,0,0,2.4,-1.66,0,0,0,2,2.4,-1.66,2,0,\
                -1.66,2,2.4,-1.66,2,0,-1.66,0,0,-1.66,2,0,-1.66,0,2.4,
                -1.66,2,2.4,-1.66,0],'f').reshape(-1,3)

            self.normals=numpy.array([0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,\
                1,0,-1,0,0,-1,0,0,-1,0,0,0,0,-1,0,0,-1,0,0,-1,1,0,0,1,\
                0,0,1,0,0,0,0,1,0,0,1,0,0,1,-1,0,0,-1,0,0,-1,0,0,0,0,\
                -1,0,0,-1,0,0,-1,1,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,1,\
                0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0 \
                ],'f').reshape(-1,3)

            self.indices=numpy.array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,\
                14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,\
                32,33,34,35],'i')

    def loop(self):
        pygame.display.flip()
        pygame.event.pump()
        self.keys = dict((chr(i),int(v)) for i,v in \
            enumerate(pygame.key.get_pressed()) if i<256)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        return True

    def controls_3d(self,mouse_button=1,w_key='w',s_key='s',a_key='a',\
                        d_key='d'):
        """ The actual camera setting cycle """
        mouse_dx,mouse_dy = pygame.mouse.get_rel()
        if pygame.mouse.get_pressed()[mouse_button]:
            look_speed = .2
            buffer = glGetDoublev(GL_MODELVIEW_MATRIX)
            c = (-1 * numpy.mat(buffer[:3,:3]) * \
                numpy.mat(buffer[3,:3]).T).reshape(3,1)
            # c is camera center in absolute coordinates, 
            # we need to move it back to (0,0,0) 
            # before rotating the camera
            glTranslate(c[0],c[1],c[2])
            m = buffer.flatten()
            glRotate(mouse_dx * look_speed, m[1],m[5],m[9])
            glRotate(mouse_dy * look_speed, m[0],m[4],m[8])
            
            # compensate roll
            glRotated(-math.atan2(-m[4],m[5]) * \
                57.295779513082320876798154814105 ,m[2],m[6],m[10])
            glTranslate(-c[0],-c[1],-c[2])

        # move forward-back or right-left
        # fwd =   .1 if 'w' is pressed;   -0.1 if 's'
        fwd = .1 * (self.keys[w_key]-self.keys[s_key]) 
        strafe = .1 * (self.keys[a_key]-self.keys[d_key])
        if abs(fwd) or abs(strafe):
            m = glGetDoublev(GL_MODELVIEW_MATRIX).flatten()
            glTranslate(fwd*m[2],fwd*m[6],fwd*m[10])
            glTranslate(strafe*m[0],strafe*m[4],strafe*m[8])


fps = Spectator(w = 640, h = 480, fov = 75)
fps.simple_lights()
fps.simple_camera_pose()

while fps.loop():
    fps.draw_simple_cube()
    fps.controls_3d(0,'w','s','a','d')
    if fps.keys['q']: break

How to find position in space

Just in case you missed this in the code:
buffer = glGetDoublev(GL_MODELVIEW_MATRIX)
c = (-1 * numpy.mat(buffer[:3,:3]) * \
    numpy.mat(buffer[3,:3]).T).reshape(3,1)